Agent pane: another agent's message opens below its line, animated, without moving the view
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):
| Closed | Just after the click | Settled 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 carriesheaderRef, and opening or closing the card goes throughsetOpen/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
playResizeinfold.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 (
useFoldhas no cancel).
- The cut card slides open to its whole height and back with a new
- Ported onto base (2026-10-26, adopted by the stall reconciler). #24050 landed, so
useAnchoredCollapse.ts,ANCHORED_COLLAPSE_EVENTand App.tsx'sholdAskedare 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 calledshow), the text playsplayResize, and Details' row is the hook's fold. - CSS:
.cv-received__details > .cv-tool-outputbecomes a descendant selector, because the envelope now sits inside its fold box. It looks the same. - Tests:
agent-pane-suggested-tasksnow waits for the slide to finish before it reads the opened card's height. Reading it mid-slide was racy.queue-agent-messageputs jsdom'sCustomEventon 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





