fix(web): transcript ordering, scroll anchoring, and viewport fill on thread activation - #1761
Merged
Merged
Conversation
…eading row Cold activation committed a narrative-free tail page before the richer conversation page resolved, so the response painted first and reasoning popped in above it. Fetch the page concurrently and commit once; the tail remains a failure fallback. Keep loaded narrative resident instead of evicting on row unmount, and warm the tail's narrative on activation. Also pin the reading row's screen position captured at user-scroll time. Resolving the anchor through provisional heights drifted on every measurement pass after a prepend, sliding the viewport upward.
…lier The two-message tail paints fast but leaves most of the screen empty. Top up with older pages after each commit until the transcript covers 1.5x the viewport (bounded at three pages). Page size drops to 25 and the boundary trigger moves out to one viewport height so the next page is resident before the edge. The pagination spinner now only appears when a fetch exceeds 300ms.
…ead switches A warm thread switch remounted the whole virtualized transcript: the projection caches died with the component and vlist rebuilt ~15 row components before the transcript could paint, so revisiting a thread cost 173-630ms even though the record commit was ~3ms. Keep the five most recently selected transcripts mounted, hidden with visibility:hidden and inert instead of display:none (which would collapse the virtualizer to zero height and destroy every row). Hidden transcripts hold a residency display lease so their records stay resident and self-heal after cache eviction; eviction past the LRU cap releases the lease and falls back to the existing cache path. A resident target record is now paintable immediately instead of waiting for the hydration commit. Measured on a production build with 150-message threads: warm switches drop to ~70-135ms; memory stays bounded at ~2MB per retained transcript and flat across churn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Four related transcript fixes/UX changes:
Narrative ordering on cold thread activation. The tail fast-path committed a narrative-free
conversation.tailpayload first, so the final response painted and reasoning popped in above it. Activation now fetches the tail and the narrative page concurrently and commits once; the tail stays as a failure fallback. Loaded narrative is no longer evicted when virtual rows unmount, and the last 10 assistant messages' narrative warms on activation.Scroll anchoring during history prepends. The reading anchor resolved through provisional row heights (128px) while real narrative rows measure ~300-500px, so each measurement pass after a prepend drifted the viewport upward. The anchored row's screen position is now captured at user-scroll time and held against real DOM rects as measurements settle.
Viewport fill + earlier pagination. The two-message tail left most of the screen empty on open. A UI effect now tops up with older pages until the transcript covers 1.5x the viewport (bounded at 3 pages). History page size drops 50 to 25 and the boundary trigger moves from 200px to one viewport height, so the next page is resident before the edge. The pagination spinner only appears when a fetch exceeds 300ms.
Keep-alive transcript switching. A warm switch remounted the whole virtualized transcript: projection caches died with the component and vlist rebuilt ~15 row components before paint, costing 173-630ms against a ~3ms record commit. The five most recently selected transcripts now stay mounted, hidden with
visibility:hidden+inert(notdisplay:none, which would collapse the virtualizer to zero height and destroy every row). Hidden transcripts hold a residency display lease so records stay resident and self-heal after cache eviction; eviction past the LRU cap releases the lease back to the existing cache path. A resident target record is paintable immediately instead of waiting for the hydration commit.Verified with unit tests (49/49 ChatView, 650+ across the conversation suites), typecheck, lint, and live Electron runs over CDP (videos attached).
Why
Thread switches visibly flashed the response before its reasoning; scrolling up through older history jumped toward the first message instead of staying on the reading row; the initial two-message paint left the transcript mostly empty; and warm thread switches rebuilt the entire transcript DOM, making the app feel sluggish.
UI Changes
thread-switch-ordering-2.mp4: cold/warm thread switches — narrative lands with or before the responsescroll-anchor.mp4: reading row stays pinned through history prependsfill-viewport.mp4: viewport fill on open + seamless scroll-up paginationkeepalive-switch.mp4: warm thread switches now swap visibility instead of remounting (~70-135ms click-to-visible vs 173-630ms before)Config Changes
None.
Test plan
transcript-viewport.test.ts16/16 (new pinned-anchor regression test)MessageList.thread-switch.test.tsx34/34 (new viewport-fill test)ChatView.test.tsx49/49 (kept-alive retention, lease, LRU eviction, hold/transition stages)thread-hydrator.test.ts73/73, pagination, message-cap, message-cache greentsc --noEmitand oxlint cleanthread-switch-ordering-2.mp4
scroll-anchor.mp4
fill-viewport.mp4
keepalive-switch.mp4