transcript/scroll is called on every publish and from the scroll DOM event — the follow decision is data, not a scroll handler.
Quick Start
1
Seed the follow state
2
Update on scroll
3
React to new content
ScrollMetrics is the three numbers every platform reports — scrollTop, scrollHeight, clientHeight — so scroll.ts needs no DOM and the same rule serves a React Native port.The follow state machine
Following is on by default. A user scroll off the bottom turns it off; a tap on jump-to-latest turns it back on.“At the bottom” is a threshold (
FOLLOW_THRESHOLD_PX = 48, roughly one line), not an equality. Fractional device pixels, a rubber-band overscroll, and a settling keyboard all mean the exact equality never holds on a real device, so following would never engage without the tolerance.The affordance
The jump-to-latest button lives inside the chat screen,hidden by default. Its visibility is driven entirely by shouldShowJumpToLatest(follow).
shouldShowJumpToLatest shows the button exactly when following is off — the user has scrolled up off the bottom of a streaming transcript.
The tap is a scroll decision, not an intent
The delegatedclick listener on root special-cases jump-latest before intentFrom, because it is a scroll decision, not an engine call — it never reaches the intent router.
How It Works
The subtle part is that our own auto-scroll comes back as ascroll event a frame later. Counting outstanding auto-scrolls is what stops that correction — caught mid-flight at a position that is not yet the bottom — from being read as “the user scrolled up”.
Best Practices
Let scroll.ts own the follow decision
Let scroll.ts own the follow decision
Do not read
scrollTop and decide in the renderer. Feed ScrollMetrics to onScroll / onContentChanged and apply the returned action — the pure function is where the auto-scroll counting lives.Count your own auto-scrolls
Count your own auto-scrolls
Every
scrollTo you apply comes back as a scroll event. Without pendingAutoScrolls, that echo unsticks the view and following works for two seconds, then stops for the rest of the answer.Handle jump-to-latest before the intent walk
Handle jump-to-latest before the intent walk
It is a scroll decision, not an engine call. Special-case
data-action="jump-latest" ahead of intentFrom so it never reaches the intent router.Related
Composer Behavior
Draft persistence, autosize, and the Enter policy.
History & Reopen
Restored messages painted through the reconciler.
Overview
The retained chat screen and its transcript.
Shell & Adapters
Keyboard height and safe-area geometry.

