Fix chat scroll jitter during streaming; respect reduced motion - #11
Merged
Conversation
The message timeline had three independent systems writing scrollTop to the bottom on the same content update, reading scroll geometry at slightly different instants from a virtualizer whose item heights are estimates. When virtua corrected a measured height a frame later, the writers had already landed on slightly different positions -> visible bounce. - session.tsx: gate createAutoScroll on the session's real working state instead of a hard-coded `true`. The auto-scroller's ResizeObserver was force-following the bottom on *any* reflow (image load, accordion expand, font swap), which read as the chat jumping on its own. Send-to-bottom and the jump button are unaffected (they use the force path). - message-timeline.tsx: only realign via virtua's estimate-based scrollToIndex(align:"end") when the row set changes or status flips, not on every streamed token. Pure intra-row growth is left to the measured-bottom rAF lock, which pins against the real DOM height, so the two mechanisms stop disagreeing frame-to-frame. Safe because timelineRowKeys is memoized with `equals: sameKeys`. - message-part.tsx: honor prefers-reduced-motion in the imperative ShellSubmessage reveal (the CSS already does; this JS animate() did not, and its initial render collapses width to 0 / blurs the value). Verified: app + ui typecheck clean; message-part, scroll-view, layout-scroll, file-tab-scroll, use-session-hash-scroll unit tests pass (16/16). Perceptual smoothness during live streaming still wants a visual pass against a real model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Aug 7, 2026
jeonghun-jj-lee
added a commit
that referenced
this pull request
Aug 7, 2026
…les (#115) (#126) Extends the global editing fallback (patch #11) to cover select-all, undo, and redo — the same class of bug where native browser actions are suppressed by the VS Code/Electron platform layer inside the sandboxed iframe. - Cmd+A: selectAll via .select() (form fields) or Range.selectNodeContents (contenteditables) - Cmd+Z: document.execCommand('undo') - Cmd+Shift+Z / Cmd+Y: document.execCommand('redo') - Framed-context guard preserved (unframed windows keep native behavior) - Existing Cmd+V/C/X behavior unchanged - Shift modifier now allowed specifically for Cmd+Shift+Z (redo chord) Closes #115
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.
Why
The chat visibly jitters/bounces while a response streams, and occasionally jumps on its own even when idle.
Root cause: the message timeline had three independent systems all writing
scrollTopto the bottom on the same content update, each reading scroll geometry at a slightly different instant from avirtualist whose item heights are estimates. When virtua corrects a measured height a frame later, the writers have already landed on slightly different positions → visible bounce.What changed (3 files)
1. Gate auto-scroll on real streaming state —
packages/app/src/pages/session.tsxcreateAutoScroll({ working: () => true })→ realsync.data.session_working(id). The auto-scroller'sResizeObserverwas force-following the bottom on any reflow (image load, tool accordion expand, font swap, layout settle), which read as the chat "jumping" on its own. Now it only follows while the model is actually streaming.Send-to-bottom and the jump-to-bottom button are unaffected — they use the
forcepath, which bypasses this gate.2. Stop the estimate-based realign from fighting the real-DOM lock —
packages/app/src/pages/session/message-timeline.tsxvirtua.scrollToIndex(align:"end")(which uses estimated offsets) was called on every streamed token, landing a hair off from the rAF loop that pins against the real DOM height — that disagreement was the frame-to-frame bounce. NowscrollToIndexonly runs when the row set changes (new message/tool row) or session status flips; pure intra-row text growth is left entirely to the measured-bottom rAF lock.Safe because
timelineRowKeysis memoized withequals: sameKeys, so its reference only changes on a real row change.3. Respect
prefers-reduced-motionin JS —packages/ui/src/components/message-part.tsxThe CSS honors reduced motion everywhere, but the imperative
animate()reveal inShellSubmessagedidn't — and its initial render collapses width to 0 / blurs the value. Under reduced motion it now snaps to the resting state instead of animating (and can't get stuck hidden).Verification
bun run typecheck— app and ui both clean.message-part,scroll-view,layout-scroll,file-tab-scroll,use-session-hash-scroll: 16 pass / 0 fail.Deliberately deferred (happy to follow up)
overflow-anchor+ flex bottom-anchoring, delete the rAF workaround for virtua #301). Higher impact but needs the visual pass to tune.width/marginanimation to a compositor-only transform.🤖 Generated with Claude Code