Agent pane: another agent's message opens below its line, animated, without moving the view

Merged
TE
10d ago
+480-31

Another agent's message in the agent pane (IncomingMessageLine) now opens below its line and closes back into it without moving the view, and both directions animate.

Leo's words: "these situations when collapsibles get expanded, i said earlier last night lawrence was talking about how it shouldnt move your scroll or whatever, just expand the area at the bottom and you're in the same place... also this current one seems to shift the expandable area away. also these all need animations for expand and collapse!"

Leo-ask: ask-a5c1976d1c

Before / after

At the latest row, clicking the message's line (the red dashed guide marks where the line was when clicked):

ClosedJust after the clickSettled open
Before
After

Before, the line jumped 235 px up on the first frame and the whole card appeared at once. After, the line stays at 297.5 px on every frame. The card grows downward from its six-line cut to its full height, and the view stops following the latest row because the card now reaches past the bottom. Recordings (Playwright, Chromium): , . Closing frames: after-3-closing-1, after-4-closed-again.

What changed

This builds on #23932 (the six-line cut, already on feat-cmux-next): the card is always drawn, and opening it shows the rest plus Details.

  • No jump. The card's line and its Details now use useAnchoredCollapse, the transcript's shared anchored collapse from #24050. The line carries headerRef, and opening or closing the card goes through setOpen/toggle, which asks VirtualTranscript to hold the line where it is drawn (ANCHORED_COLLAPSE_EVENT → holdFold/keepFold) while the card changes below it. The transcript is virtualized and the hold works there. It covers the chevron, the plain sender name and a click on the cut card. Reply is the exception: it opens the card without holding the line. You asked to type, so the view follows the editor as it did before. Holding the line there left a long message's focused editor below the view. Before, the line wasn't a disclosure the transcript knew about, so the view kept following the latest row, and the line jumped up by however much the card added.
  • Animation.
    • The cut card slides open to its whole height and back with a new playResize in fold.ts. It animates height and max-height over --cv-fold-duration (the host's move duration, about 190 ms, or 180 ms with no host), using the pane's ease-out, and clips the card while it plays.
    • Details and the envelope under it fold in and out in height and opacity, using the hook's present/bodyProps.
    • Under Reduce Motion, or with the app's animations off, all of this happens at once.
    • When Find in Chat opens a closed message (or its Details) to show a match, nothing animates. Only the reader's own clicks do. If Find lands on a card whose text is still sliding from a click, the slide stops at once. A Details fold already playing from a click finishes, as every fold on base does (useFold has no cancel).
  • Ported onto base (2026-10-26, adopted by the stall reconciler). #24050 landed, so useAnchoredCollapse.ts, ANCHORED_COLLAPSE_EVENT and App.tsx's holdAsked are base's own now and this PR no longer carries them. Base's card gained Minimal Mode (#25002), which already holds the line through the same hook; that path is unchanged. Out of Minimal Mode, the toggle, a click on the cut card and Show more now go through the hook too (before, they only called show), the text plays playResize, and Details' row is the hook's fold.
  • CSS: .cv-received__details > .cv-tool-output becomes a descendant selector, because the envelope now sits inside its fold box. It looks the same.
  • Tests:
    • agent-pane-suggested-tasks now waits for the slide to finish before it reads the opened card's height. Reading it mid-slide was racy.
    • queue-agent-message puts jsdom's CustomEvent on the global, since the hook dispatches one.

Nothing is removed, and no default changes.

Tests

test/received-message-fold-browser.test.ts (new), run on huge-blue in real Chromium with CMUX_BROWSER_TESTS=1:

  • At the latest row: opening the cut card moves the line at most 1 px on every painted frame and after it settles. The motion played, the card's height changed over more than two frames, and the view stopped following.
  • Away from the end: open, Details open, Details close, close. Each one moves the clicked line at most 1 px and plays motion. After closing, the card is cut again and Details is gone.
  • Cut card: a click on the cut card opens it in place.
  • Reduce Motion: open and close happen at once (no motion, at most two card heights), and the line stays put.
  • Against base sources: the first three tests fail. At the latest row the line moved 235 px and no motion played.

agent-pane-suggested-tasks has a new test, "Reply on a long message at the latest row puts its editor in view". It fails with the hold on Reply and passes now. These also passed: agent-pane-suggested-tasks (23, including the Reply flows), transcript-fold-anchor-browser, transcript-fold-anchor, transcript-fold-motion, BackgroundMark, transcript-received-messages, queue-agent-message, transcript-row-message, agent-pane-reveal-message and rowEstimates. vp check --no-fmt passed (lint and types). The full jsdom suite run in a single process fails the same 293 tests on base and head, with no new failures, and so did prepush-gate.py. The formatter flags one file on base (test/subagent-stopped-after-cancel.test.ts), which this PR doesn't touch.

Agent session: agent session

šŸ¤– Generated with Claude Code