Agent chat: the transcript stays pinned at the bottom while a reply streams

Merged
TE
15d ago
+454-15

Summary

At the bottom of an agent chat, the transcript jumped up and down while a reply streamed (Leo, 2026-10-20: "we need that padding fix in"). I sampled every painted frame while a reply streamed a few words at a time and found three causes:

  • A tool call or the working line made the reply drop and slide back (20โ€“200px). The follow glide slid the whole thread by the scroll offset's step. That step also counted rows that grew above the reply (the working line opening over a turn's first tool call) and layouts of the same frame that never painted (a new turn's rows placed by estimate, then by drawn height). Now each glide holds the latest row where it was last painted and slides only by what that row moved on screen.
  • A queued prompt's card leaving the bottom stack moved the view down a card's height (about 54px), then back up. When the card went, the bottom-stack room shrank, the offset clamped down, and the prompt arrived as the latest row a moment later. Now a followed view keeps that room under the last row until the prompt arrives. If the queued prompt was deleted instead and never comes, the room goes after 1 s with a glide. This works both for a stack that floats over the transcript (today) and for one in the flow under it (#22819).
  • In WebKit, a wheel scroll up during streaming was sometimes yanked back to the bottom (3 runs in 6 on base). WebKit moves the offset a frame before it fires the scroll event, so a snapshot landing in between still read as "following". Now an upward move past the deadband right after the reader's own input, or past the stray-scroll limit otherwise, counts as the reader's and unpins at once.

The CSS stays as it is. overflow-anchor: none on .acpmux-scroll is still right, because VirtualTranscript owns following. The bottom-stack room is already measured before paint (#22272).

Testing

New webviews/test/agent-pane-stream-jitter.test.ts (browser lane, Chromium and WebKit, the pane's dev entry with a stand-in acpmux daemon). It streams 160 small chunks with a tool call, the working line, and a queued card that comes and goes. It also sends a queued prompt as the next turn, and wheels up mid-stream. On every frame the reply's text must only move up, the view must rest at the end, and the wheel must never be pulled back.

Command: cd webviews && CMUX_BROWSER_TESTS=1 bun test test/agent-pane-stream-jitter.test.ts

base 96cb2bed124 (test commit 6379fa509f6)head
tool call / working line / queue cardfail in both engines (336.6 -> 366.8, -96.3 -> -42.9)pass
queued prompt sent as the next turnfail in both engines (230.5 -> 272.9, 43.2 -> 64.6)pass
wheel up while streaming (WebKit)3 of 6 runs pulled back to the bottom6 of 6 pass

Observed / Not run

  • Ran on cmux16 (macOS, Playwright Chromium and WebKit): the new file, agent-pane-chat-column, agent-pane-follow-live, agent-pane-queue-steer, agent-pane-bottom-stack, agent-pane-scrollbar-full-height, agent-pane-wheel-pass-through and all 25 transcript-* files: 141 pass, 0 fail. scripts/web-lint.sh and prepush-gate.py pass.
  • With #22819's source applied on top (stack in the flow), the queue and wheel cases pass. One Chromium frame still moves down 1.7px when the queue card leaves; WebKit passes. #22819 can tighten that once it lands.
  • Not run: the native app (no tagged build). WKWebView in the app was only exercised through Playwright's WebKit. No before/after video yet: frame samples are in the test output above.

Changelog

Fixed: The agent chat stays pinned at the bottom while a reply streams, a tool call starts, or a queued prompt is sent, instead of jumping up and down

Agent session: cmux16

๐Ÿค– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes the agent chat transcript jumping up and down while a reply streams, so the view stays pinned at the bottom through tool calls, the working line, and queued prompts. New browser tests sample every painted frame in Chromium and WebKit, covering a streaming reply with a tool call, a queued card that comes and goes, a queued prompt sent as the next turn, and a wheel up mid-stream.

Bug Fixes

  • Glides now hold the latest row where it was last painted and slide only by that row's movement, not by the scroll offset's step (which also counted rows grown above it and layouts that never painted).
  • A queued prompt's card leaving the bottom stack no longer drops the view by a card's height; the view keeps that room under the last row until the prompt arrives, or for 1s if the prompt never comes. This holds whether the stack floats over the transcript or sits under it in flow.
  • In WebKit, a wheel scroll up during streaming is no longer yanked back to the bottom; a move up past the deadband counts as the reader's input and unpins immediately.
  • A clamped end is compared with a pixel of tolerance, since the browser rounds the content's fractional height; a clamp that rounded a 1000.4px height up to 1001 otherwise read as the reader scrolling up and unpinned.