Skip to content

[Bug]: Scrolling over an inline HTML render doesn't stop live-follow, so the thread snaps back to the bottom #17056

Description

@josephv123

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Summary

When you scroll up with the pointer over an agent's inline HTML render (html_render), the thread keeps following the end and snaps back to the bottom at the next layout change. Scrolling over plain chat text in the same spot works. It is intermittent because a single wheel, click, or key press anywhere outside the frame turns live-follow off for good.

It shows up most on threads that end with a tall render. The frame fills the viewport, so the pointer is almost always over it. It is easiest to hit right after opening such a thread, while the frame, its CDN scripts and fonts, and images are still settling and causing layout changes.

Steps to reproduce

  1. Open a thread whose last assistant reply includes an html_render page taller than the viewport, for example a KaTeX derivation about 1,700 px tall. Opening the thread puts the timeline in live-follow at the end.
  2. With the pointer over the rendered page, scroll up with the mouse wheel or trackpad. The timeline scrolls, because the frame passes the scroll up to it.
  3. Cause any layout change: wait for the frame or images to finish loading on first open, resize the window, or toggle a panel.

Expected behavior

Scrolling up over the render stops live-follow, as it does over chat text. The timeline stays where I scrolled.

Actual behavior

The timeline jumps back to the bottom. Repeated attempts keep snapping back until a wheel, click, or key press lands outside the frame.

Measured with a real mouse wheel (Playwright, chrome-headless-shell 154) against a dev server on main (9fba2093e5). Each run scrolled up two wheel ticks, 300 px total, then resized the window by 20 px:

Pointer position Wheel events the timeline saw Distance from end after scrolling Distance from end after the resize
Over the html_render iframe 0 of 2 300 px 0 px (snapped back)
Over the timeline gutter (control) 2 of 2 300 px 346 px (stayed)

Recording on main, using a neutral fixture thread. The red dot is the real pointer, and the bottom-right counter is the distance from the end. In the first half the pointer is over the HTML render: it scrolls 300 px up, and a 20 px window resize snaps it back to 0. In the second half the pointer is over chat text: same scroll and resize, and it stays at 300 px.

Scroll snaps back after scrolling over an html_render frame

MP4 version

Root cause

Live-follow is turned off only by user gestures on the timeline's scroll node. In ChatView.tsx, the comment at line 6477 says "Live-follow stays active after send/thread-open until an actual list scroll gesture opts out". The listeners are wheel, touchmove, and pointerdown on scrollNode, plus keydown on document (lines 6529–6633). While liveFollowEnabled stays true, MessagesTimeline keeps maintainScrollAtEnd on (line 1418), and LegendList re-pins to the end on the next layout change.

HtmlRenderDocument is a sandboxed, cross-origin iframe (BrowserDocumentFrame.tsx:128, sandbox="allow-scripts allow-forms"). Wheel and touch events over it go to the frame's own document and never reach the parent's listeners. When the page can't scroll itself, the browser passes the scroll up to the timeline. The timeline moves without any event its follow logic can see, so it stays in follow mode while away from the end. McpAppFrame embeds an iframe in the timeline the same way and is probably affected too.

Possible fix directions

  • Treat a scroll away from the end that the timeline didn't start itself as the user leaving follow mode, regardless of which element got the input.
  • Or have the render bootstrap, which already posts ui/notifications/size-changed, also post a message when the reader scrolls inside the frame.

Impact

Minor bug or occasional failure

Version or commit

T3 Code 0.0.46-nightly.20261008.2801. Reproduced on main at 9fba2093e5.

Environment

T3 Code (Nightly) desktop app on macOS 26, connected to t3 serve on Ubuntu 26.04. Reproduced in Chromium against a local dev server.


Claude Opus 5.5 via Claude Code in T3 Code

Activity

  1. juliusmarminge commented on Oct 8, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the precise report and measurements. I checked this against main at 04ad174, which includes 9fba2093e5, and the mechanism you describe holds in the code.

    What I confirmed

    • Live-follow only turns off for input the parent document can see. The opt-out listeners are wheel, touchmove, and pointerdown on the timeline scroll node, plus keydown on document (ChatView.tsx#L6529-L6633). Input inside a sandboxed, opaque-origin frame is delivered to the frame's own document, so none of these fire. The frame is BrowserDocumentFrame.tsx#L123-L128, with sandbox="allow-scripts allow-forms".
    • Scroll position alone does not end follow mode, on purpose. A plain scroll reaches handleScroll → onIsAtEndChange(false) (MessagesTimeline.tsx#L1084-L1111). While following, onIsAtEndChange returns early: it hides the scroll-to-bottom pill and leaves follow on (ChatView.tsx#L6748-L6756). This guards against streaming growth making isAtEnd flicker (see the comment at L6515-L6526 and fix: scrolling up during a running thread no longer snaps back to the bottom #5566). The side effect is that a scroll that chains up out of the frame moves the viewport while timelineLiveFollowEnabled stays true.
    • LegendList then pins back to the end. While liveFollowEnabled is true, maintainScrollAtEnd stays on (MessagesTimeline.tsx#L1418-L1428), so the next layout change snaps the timeline back. The frame itself can trigger that change: when the page reports ui/notifications/size-changed, the row resizes (HtmlRenderFrame.tsx#L47-L50, L102). That fits your "worst right after opening" observation.

    How #16283 is involved. #16283 doesn't forward scroll by postMessage. It sizes the frame to the page's reported height (htmlRender.ts#L118-L132), up to the 2000px cap. The page then has nothing to scroll, and the browser's native scroll chaining moves the timeline. So #16283 makes this path reliable rather than introducing the follow gap. The follow logic never accounted for input inside a frame, and before #16283 the frame likely trapped the scroll instead (inferred, see below). I'd call #16283 the trigger that exposed this, not the root cause.

    McpAppFrame. It embeds the same kind of sandboxed, opaque-origin iframe in a timeline row (McpAppFrame.tsx#L531-L536), and the opt-out listeners can't see its input either. Whether it hits this in practice depends on whether the app's page chains scroll instead of scrolling itself. I haven't reproduced that.

    Fix direction (not yet tried)

    1. Host side, covers every frame: while following, treat a timeline scroll that lowers scrollTop with no programmatic scroll in flight as the user leaving follow mode. Content growth shouldn't lower scrollTop, so this likely wouldn't bring back fix: scrolling up during a running thread no longer snaps back to the bottom #5566. Content shrinking (collapsing a disclosure) can lower it through clamping, though, so it would need a guard or a check of whether the content got shorter. This also covers renders published before fix(web): inline HTML renders no longer trap the thread's scroll #16283, since their bootstrap is frozen at publish time.
    2. Bootstrap bridge: have the injected bootstrap (htmlRender.ts#L346), which already posts size-changed, also post upward wheel/touch intent. The host would call cancelTimelineLiveFollowForUserNavigation. The same bridge could carry the edge-of-frame wheel delta proposed in [Bug]: Wheel scroll stays stuck on a tall inline HTML render instead of chaining to the thread #16977. Limits: it only covers new renders, doesn't cover MCP apps (third-party pages), and any page can send the message. Spoofing it can only turn follow off, which is low risk.

    Option 1 looks like the smaller, more general fix. Option 2 pairs naturally with #16977.

    Related

    Not verified here: I haven't run the reproduction, so the 0-of-2 wheel events / 0 px measurements come from the report. I also haven't confirmed the claim that before #16283 the frame trapped the scroll in this exact scenario; that comes from #16283's description and the code.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 8, 2026
  3. added a commit that references this issue on Oct 8, 2026
    d60783e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions