Agent chat: closing the queue at the latest row keeps the last row on the composer

Merged
TE
12d ago
+483-6

Leo, 2026-10-21: "there's something just a bit awkward about the UI of this queuing and steering and expanding and, like, scrolling. It doesn't scroll to the very bottom. I mean, it's almost like it's allowed to, but then it just shoots back up ... it cuts off at the bottom."

Leo-ask: ask-a3f974b779
Leo-ask: ask-5ea5632ee9 (the new lines in the composer make the scrollbar jump)
Leo-ask: ask-a532e95ee7 (the wheel beside the composer should just scroll)

What was wrong

When you clicked Show less on a long queue at the latest message, the last row stayed a fold row (about 22 px) above the composer for a second. Then the rows dropped down onto it. Scrolling down in that second did nothing, because the view was already at its end.

Root cause: VirtualTranscript.noteQueue (webviews/src/agent-session/acpmux/App.tsx:1824 on feat-cmux-next 1f4437e6f08) collects the prompt ids of the .acpmux-queued cards in the bottom stack. Any id that disappears is treated as a sent prompt on its way into the transcript (heldFor / queueLeft), and the room under the last row is kept for it (floor, FLOOR_HOLD_MS = 1 s). Closing the fold unmounts rows 3 to N (ComposerQueue.tsx, visible = shown.slice(0, steering + FOLD_SHOWS)), so every hidden row looked like a card that had left. The view held a stack's worth of room that no prompt would ever fill, then glided down when the hold ran out.

Fix

  • ComposerQueue puts the ids its fold hides on the list as data-folded-ids.
  • noteQueue counts those prompts as still queued.

Closing the fold now shrinks the room in the same frame, just as opening it grows the room. A card that really leaves (Steer, a sent prompt) still keeps its room as before.

What I checked and left alone

  • Newline in the composer (ask-5ea5632ee9): already fixed on base by #23097, #23186 and #23302. With the latest row in view and scrolled up, Shift-Return leaves scrollTop and the distance from the end unchanged in every painted frame. The new test pins this in place.
  • Wheel beside the composer (ask-a532e95ee7): already fixed on base by #22341 and #23243 (pointer-events: none on the floating stack, and wheelPassThrough.ts). The wheel left and right of the queue, its fold bar and the card scrolls the transcript. The new test pins this for all three bands.
  • Open: #23807 (teamleaderleo, "the view stays pinned to the latest line as it grows") changes the follow and glide code in the same VirtualTranscript, but not noteQueue or the queue. The two changes don't overlap.

Changelog

  • Fixed: Closing a long queue's "Show less" at the latest message keeps the last row on the composer instead of leaving a gap that drops a second later.

Test

New webviews/test/agent-pane-queue-scroll-bottom.test.ts, run in Chromium and WebKit. It has its own stand-in acpmux, one chat of 40 turns plus a running one, and six queued prompts. Every read is of a painted frame (support/paintedFrames.ts).

cd webviews && CMUX_BROWSER_TESTS=1 bun test test/agent-pane-queue-scroll-bottom.test.ts
  • Red on base (feat-cmux-next 87bf8e9656c plus the test commit): 6 pass, 2 fail. "the queue closed at the latest row ..." fails in both engines: the frames after the click hold the last row at gap 46.25 px against its rest of 24.25 px (45.25 against 23.25 in WebKit).
  • Green on head: 8 pass, 0 fail.

The four cases:

  1. Queue opened, then wheel scrolled to the end: the last row is whole above the stack in every frame for 500 ms.
  2. Queue closed at the latest row: no frame in 1.5 s keeps the room.
  3. A new line in the composer: fromEnd and scrollTop stay the same, both at the latest row and scrolled up.
  4. The wheel left and right of the queue, its fold bar and the card: the pointer reaches the transcript, which scrolls both ways.

Observed / Not run

Observed on huge-blue, with the head over feat-cmux-next 87bf8e9656c:

  • bun run check passes. One base file, test/subagent-stopped-after-cancel.test.ts, has a formatting issue on base, and I masked it for the run.
  • With CMUX_BROWSER_TESTS=1, these pass on the head: agent-pane-queue-collapse 13/13, agent-pane-queue-steer 7/7, agent-pane-queue 6/6, agent-pane-bottom-stack 2/2, agent-pane-scrollbar-full-height 10/10, agent-pane-permission-queue 2/2 and the new file 8/8.
  • agent-pane-dock-gutters and agent-pane-wheel-pass-through time out in beforeAll on huge-blue. Their shared papercutPane waits for every finite animation to end, and the running turn's line never settles there. This happens on base too, before any change. The new test gets its gutter coverage through its own fixture.

Not run: the app itself (no tagged build) and an independent review.

No capture: webview-only change. The before and after frames (300 ms after Show less) are kept privately by the lane.

Agent session: cosmic-gray

🤖 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 holding phantom gaps after closing "Show less" on a long queue, and when a queued prompt runs on another client while its card is out of sight.

  • ComposerQueue reports the prompts its fold hides as still queued (data-folded-ids), so closing the fold shrinks the room under the last row in the same frame instead of holding it for about a second.
  • VirtualTranscript re-notes the queued prompts on each queue change, not only on resize, so a prompt run elsewhere while folded or past the list's cap leaves no phantom room when the stack shrinks later.
  • noteQueue now tracks the chat and stack it noted, so switching to a chat with its own queue no longer holds room for the previous chat's prompts.
  • Queue prompt IDs reach VirtualTranscript through a memoized snapshot (queueIds), so the notes re-run only when the queue changes.

Tests

New browser test covers closing the queue at the latest row, opening and scrolling, prompts run elsewhere, switching chats, newlines in the composer, and the wheel beside the composer; it passes in Chromium and WebKit, with the queue-close case failing on base.