Skip to content

fix(web): zooming in while an agent streams no longer stops the chat following - #25738

Closed
tim-d0gan wants to merge 1 commit into
pingdotgg:mainfrom
tim-d0gan:fix/zoom-follow-25692
Closed

tim-d0gan wants to merge 1 commit into
pingdotgg:mainfrom
tim-d0gan:fix/zoom-follow-25692

Conversation

@tim-d0gan

Copy link
Copy Markdown

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 native scrollTo({ 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

MessagesTimeline watches devicePixelRatio with a re-arming (resolution: …dppx) media query, the same pattern the terminal uses. When it changes while the list maintains the end, it writes scrollTop = 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, because maintainScrollAtEnd is already false in 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:

Zoom during the animation Final scrollTop 19352 ÷ ratio
1 → 1.1 17592.7 17592.7
1 → 1.25 15481.6 15481.6
1 → 1.5 12901.3 12901.3
1 → 0.9 19266.7 (end) clamped
Control: zoom after the animation finished 19351.8 (end)
Control: no zoom 19352 (end)

Static scrollers keep their CSS scrollTop across zoom, both at top level and in an iframe. A matchMedia resolution listener that writes the end lands exactly at the end at 1.1, 1.25, 1.5 and 0.9.

Real MessagesTimeline in 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:

main This PR
Zoom 1 → 1.1 scrollTop 12278 → 11181.8 (= 12300 × 2/2.2), follow released by a {source:"scroll"} navigation stays at the end (fromEnd 0.7), no navigation
After 5 chunks and steps to 1.25 and 1.5 fromEnd grows 1190 → 4224 px, never recovers keeps following; fromEnd stays within the usual smooth-follow trail
Same zoom, idle thread (instant follow) stays pinned stays pinned

Regression 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 on main (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:changed passes: format, lint, knip, typecheck for 2 packages, and tests in 3 files.

Surfaces

  • Web and desktop share this component. Browser zoom in Chrome on app.t3.codes hits the same Chromium path.
  • Mobile uses its own React Native follow logic and has no page zoom, so it's unchanged.
  • No contract, server or docs changes.

Not checked

  • No before/after recording in the packaged desktop app with a real provider turn and Tasks open. All runs used hidden Electron windows with synthetic streaming.
  • Moving the window between displays with different scale factors. That also changes 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

Delegate Round 1 Round 2
Claude Fable 5.1 9/10 9/10
Claude Opus 5.5 8.5/10 9/10
Claude Sonnet 5.5 8.5/10 9/10

Created with Claude Opus 5.5 in T3 Code (Claude Code harness).

🤖 Generated with Claude Code

…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>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 27, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 3f32d98

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.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between d145077 and 3f32d98.

Files selected for processing: 2
  • apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

MessagesTimeline now observes device-pixel-ratio changes and, while end-scroll maintenance is active, resets the scroll position to the timeline bottom. A browser test changes the device scale factor during streamed output and checks that the timeline remains within one pixel of the end.

Changes: Timeline end-follow

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: Normal

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: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main fix: preserving chat follow behavior during zoom while an agent streams.
Description check Passed 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 sur…
Linked Issues check Passed Directly linked issue #25692 requires 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…
Out of Scope Changes check Passed 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 h…
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 #25692 requires 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.

@juliusmarminge juliusmarminge added the bugfix Fix PR opened by an issue lead; enters the PR loop label Oct 28, 2026

tverma101 commented Oct 28, 2026 •

Copy link
Copy Markdown

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 (3f32d98776a56a8de5f484d164aaffa7e3395623). I zoomed to 144%, clicked the empty timeline header away from the scrollbar, and kept the response streaming. Following stopped and the output ended 1,281.36 CSS pixels above the bottom. With my attached patch, the same replay kept following and ended within 0.14 CSS pixels.

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 new empty-padding and viewport-resize regressions fail on the current PR and pass with my patch.
  • vp run check:changed HEAD passes formatting, lint, both knip checks, typechecking, and 147 tests in 5 files.
  • 49 full-app synthetic replay cases pass, covering zoom and window sizes, focus, background clicks, app switches, short threads, finish/stop/notification resume, and deliberate navigation. Follow cases end within 0.5 CSS px; deliberate navigation stays released.
  • I tested the supported 840×620 minimum window and zoom values up to 172.8%.

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:

Before: exact PR loses following at 144%

After — with my patch:

After: source patch keeps the latest output in view

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 HEAD

I cannot push to this fork's branch, so the branch is unchanged. The patch is ready for the PR author to apply and verify.

@t3-code-bot

t3-code-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

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.

@t3-code-bot t3-code-bot Bot closed this Oct 30, 2026
@juliusmarminge juliusmarminge removed the pr:gate-failed A land gate failed label Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Zooming in can break streaming chat scroll-follow on macOS

3 participants