Repository navigation
Conversation
…following While a turn streams, the timeline follows the end with a native smooth scroll. If the zoom changes mid-animation, Chromium lands that scroll at its target scaled by old/new zoom, so zooming in leaves the view far above the end. The upward-scroll detector reads the jump as the user scrolling away and releases follow. When the devicePixelRatio changes while the list maintains the end, write the end immediately. The listener runs before the animation's next frame, and the write also cancels it. Fixes pingdotgg#25692 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused web bug fix that preserves live chat following when a device zoom change interrupts an existing smooth scroll. The production change is localized to the timeline component and is accompanied by a browser regression test, with no changes to defaults, interfaces, infrastructure, or sensitive code. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Observe zoom changes and pin the timeline apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/MessagesTimeline.browser.test.tsx |
MessagesTimeline observes resolution changes and pins the scroll node to the bottom when end maintenance is active. The browser test changes the device scale factor during streaming and checks the timeline position. |
Priority:
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix · Severity of issue fixed: Medium
Suggested labels: size:S
Suggested reviewers: juliusmarminge
Merge Risk: 
Minimal · up to 3f32d
-
The reviewed change has no established merge-blocking issue; normal checks can proceed.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description clearly explains the problem, cause, fix, scope, verification results, test evidence, and unverified scenarios. It also explains the small-fix exception and identifies the affected surfaces.
Full details: Linked Issues check
-
Explanation
Directly linked issue
#25692requires that, while the user follows the end, a zoom change keeps the newest streamed output visible unless the user scrolls away. The change implements this for the zoom case. In MessagesTimeline.tsx, observeDevicePixelRatio re-arms a (resolution: …dppx) media query after each change and calls pinEndAcrossZoom. pinEndAcrossZoom writes scrollNode.scrollTop = scrollNode.scrollHeight only when maintainScrollAtEnd is active. The observer is attached when the list attaches and removed on detach. maintainScrollAtEnd is now computed before render with the same conditions as before, so anchored, citation, find, fullscreen, released-follow and reduced-motion cases still return false and are unchanged. The browser test in MessagesTimeline.browser.test.tsx streams content during a smooth follow, changes the device scale factor through CDP, and asserts the list is within 1 px of the end when the resolution query fires. The issue's own verification requests (full Tasks/Goal/run workflow in the desktop app, Actual Size and zoom-step comparisons, 840×620 minimum window) are manual checks, not coding requirements, so they do not apply to this assessment. The issue does not establish a specific internal cause, so the chosen mechanism is not required to match one. Residual uncertainty: the mechanism rests on the author's Electron measurements, which I did not re-run. Window resize without a devicePixelRatio change is not handled or tested by this PR, and the issue gives no evidence that resize alone breaks follow.
Full details: Out of Scope Changes check
-
Explanation
All changed hunks relate to the objective of
#25692. In MessagesTimeline.tsx: (1) observeDevicePixelRatio adds the zoom-change observer that drives the fix; (2) the maintainScrollAtEnd expression is hoisted into a const and reused by both LegendList and pinEndAcrossZoom, so the fix uses the same follow conditions and the JSX value is unchanged; (3) pinEndAcrossZoom and the observer wiring in setTimelineList are the fix itself, and stopObservingZoom is cleaned up on detach. In MessagesTimeline.browser.test.tsx: the cdp import and the new live-follow test support the zoom-during-follow requirement. No unrelated refactors, contract, server, or documentation changes appear in the reviewed diffs.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
|
I reported #25692. My local v6 workaround is working in normal use, but the full fix still needs to be added to this PR. At the supported 840×620 window size, I tested the current PR head ( The empty header can be mistaken for scrollbar input, and zoom or viewport changes can be mistaken for me scrolling up. My patch handles those cases while keeping real scrollbar, wheel and keyboard navigation working. It also preserves following when a run finishes or stops and I resume from its notification. Web and desktop share this code; there are no server, provider or mobile changes. Checks
The replay used the full source-built client and an isolated copy of the Nightly 3783 backend with synthetic streaming. It was not a real provider run. Focus was emulated, and I have not verified a physical move between displays. My normal-use confirmation applies to the local v6 workaround only. Patch and screenshots Complete source patch · Code, test logs and measurements Before — current PR: After — with my patch: To apply the patch on the current PR branch: git apply --check t3-scroll-follow-25738.patch
git apply t3-scroll-follow-25738.patch
vp run check:changed HEADI cannot push to this fork's branch, so the branch is unchanged. The patch is ready for the PR author to apply and verify. |
|
closing at @StiensWout's request under Provide evidence for the changed behavior. Changes chat follow behavior during zoom while a smooth streaming scroll is active, but supplies no recording of that timing-dependent UI behavior. The two third-party screenshots were opened: they compare the current PR with a separate uncommitted patch, not main with the PR, as that comment explicitly states. Hidden-window measurements and tests do not replace visual evidence for this motion/interaction fix. submission evidence: "The patch is ready for the PR author to apply and verify." Attach clear before/after captures and a short recording of zoom during streaming on main and the assessed PR head. A dev client with synthetic/replay streaming is sufficient; a packaged build or paid provider is not required. thanks for trying to help improve t3 code. if you think this closure was a mistake, please open a fresh pr addressing the requirement above, and link back to this one. |


Fixes #25692.
Zooming in (View → Zoom In, or ⌘+ in a browser) while an agent streams makes the chat stop following new output. The view jumps up, "Scroll to end" appears, and every later chunk lands further below the view.
Cause
While a turn is working, the timeline follows the end with LegendList's animated
maintainScrollAtEnd, which is a nativescrollTo({ behavior: "smooth" }). If the zoom changes while that animation is running, Chromium finishes it at the old target scaled by old/new zoom. At 110% the view lands about 9% of the scroll height above the end. The viewport's upward-scroll detector (timelineViewport.ts) reads that jump as the user scrolling away and releases follow. Zooming out is harmless because the scaled target clamps to the end. Following an idle thread is not affected, because that follow scroll is instant.Fix
MessagesTimelinewatchesdevicePixelRatiowith a re-arming(resolution: …dppx)media query, the same pattern the terminal uses. When it changes while the list maintains the end, it writesscrollTop = scrollHeight. The listener runs before the animation's next frame, and the write also cancels the animation. Nothing changes when follow is released, or for anchored turns, find, citations or fullscreen apps, becausemaintainScrollAtEndis alreadyfalsein all of them.This is a small, focused fix (about 25 production lines in one component) for a defect that is established below, so I'm filing it under the small-fix exception rather than waiting for triage of #25692.
Evidence
Measured with throwaway Electron 44.9.3 scripts (the reporter's Electron version) using real
webContents.setZoomFactor. Nothing from these scripts is committed.Chromium behaviour. Top-level page, nested scroller, smooth scroll in flight to 19352:
scrollTopStatic scrollers keep their CSS
scrollTopacross zoom, both at top level and in an iframe. AmatchMediaresolution listener that writes the end lands exactly at the end at 1.1, 1.25, 1.5 and 0.9.Real
MessagesTimelinein Electron. I ran the browser test harness in Electron through vitest's preview provider. Each step is a real zoom while a turn streams, with chunks every 300 ms:mainscrollTop12278 → 11181.8 (= 12300 × 2/2.2), follow released by a{source:"scroll"}navigationfromEnd0.7), no navigationfromEndgrows 1190 → 4224 px, never recoversfromEndstays within the usual smooth-follow trailRegression test. I added one test to
MessagesTimeline.browser.test.tsx: during a streamed smooth follow, a device scale change through CDP leaves the list at its end when the resolution change fires. It fails onmain(expected 618 to be less than or equal to 1, 3 of 3 runs) and passes with this change. The file passes 30/30. Headless Chromium does not reproduce the mis-landing itself, because its animation finishes at the end. So the test asserts the timeline's contract rather than the browser bug. It goes red only if the scale change lands while the animation is running, which it did on every run here. With the fix it passes regardless of timing.vp run check:changedpasses: format, lint, knip, typecheck for 2 packages, and tests in 3 files.Surfaces
Not checked
devicePixelRatio, and the fix pins the end only while the list follows it.Council review
Claude models only; GPT Sol was not available.
Rounds: 2
Created with Claude Opus 5.5 in T3 Code (Claude Code harness).
🤖 Generated with Claude Code