cmux NEXT: expanding a tool call or thought animates and never moves your view

Merged
TE
15d ago
+1035-84

Opening or closing anything in the agent chat's transcript no longer moves your view. The line you clicked (a tool group, a tool, command or MCP call, a settled run, a folded thought, a turn's "Worked for") stays on the pixel you clicked it at while what it opens lays out. Before, a chat following its reply jumped to the new latest row as soon as something opened, so the line you clicked shot up by everything it revealed.

Every one of those collapsibles now opens and closes in one motion: the body's height and opacity, easing out, with the chevron turning on the same duration. Under Reduce Motion, or with the app's animations off, it opens and closes at once.

How it holds still. A capture-phase click on a disclosure line (foldAnchor.ts) records where the line is drawn. Until its fold has played (plus 150 ms for the last measure and the rows "Worked for" inserts), each placement pass in VirtualTranscript puts that line back by moving the offset. It does that instead of following the latest row or keeping the top row. When you're following the reply and what opened pushes the latest row out of view, the view stops following (Jump to latest shows), so the reply streaming on doesn't pull you away. There's one case where the line can still move: closing something at the very end of the transcript. There's nothing below to fill the view, so the browser clamps the offset and the rows above come down by what closed, as in any scroller.

The motion. useFold (fold.ts) keeps a closing body drawn until it has slid away. If you reverse a fold midway, it starts from where it is drawn. It reads --cv-fold-duration, which conversation.css sets from the host's move duration (--agent-motion-move, AgentPaneTheme.swift: about 190 ms, and 0 under Reduce Motion or with animation off), with 180 ms when there's no host. The chevron (.cv-rotor) turns on the same token. The newtab-resize-anim lane owns shared duration tokens but hasn't published any yet. I messaged it, and this is the single alias for it to repoint. ThoughtRow drops its own 200 ms height-only animation for the shared fold. Only the reader's toggle plays it; the automatic fold still snaps.

The rows "Worked for" opens are whole virtual rows. They get the transcript's existing row entrance, not a height fold.

Changelog

  • Changed: Opening or closing a tool call, a thought or "Worked for" in agent chat animates and keeps the clicked line where it was, instead of jumping the view.

Observed / Not run

Ran locally (bun 1.4.2, webviews/):

  • bun test test/transcript-fold-anchor.test.tsx: red on base 7a4b464b375 (the "Worked for" line moved from 453.5 px to 235.5 px down the view), green on this head (2 pass). The test is committed first (e9418c66cf5).
  • bun test test/transcript-fold-motion.test.tsx: 13 pass. Each collapsible plays height and opacity over the token, keeps a closing body until it finishes, and doesn't animate under Reduce Motion, with data-agent-motion="off", or with a 0 ms token. A reversed fold starts from its drawn height, and the CSS ties the token to --agent-motion-move and the chevron.
  • ThoughtRow.test.tsx (20 pass), 21 other transcript, find and tool-row jsdom suites (109 pass, 0 fail) and gallery-coverage.test.ts pass.
  • bun run check: formatting, layers and type check pass. It exits 1 on one lint warning, jsx-a11y(prefer-tag-over-role) in src/pages/history/HistoryPage.tsx:177. This PR doesn't touch that file, and the warning is the same on base 7f3a43acecf.

Not run locally (browser tests run only in CI here):

  • test/transcript-fold-anchor-browser.test.ts (Chromium). It checks that the clicked line moves at most 1 px on every frame of the fold, while following a streaming reply and for "Worked for", and that no fold plays under Reduce Motion.
  • The new gallery play agent-pane.transcript / fold-holds-clicked-line. At the latest row it opens Worked for, then a command, then closes it, and fails if any line moved.
  • No app build: the change is webview-only.

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

Opening or closing a tool call, thought, "Worked for", or any other collapsible in agent chat now animates and keeps the clicked line exactly where it was, instead of jumping the view.

  • Every collapsible opens and closes with one shared fold: height and opacity over --cv-fold-duration (the host's move duration, ~190 ms), with the chevron turning on the same token.
  • Under Reduce Motion or with the pane's animations off, everything opens and closes instantly.
  • When the opened content pushes the streaming reply out of view, the view stops following (Jump to latest appears); the line still holds until the fold settles.
  • A closing fold stays pinned shut until its body is removed, so the rows below don't jump the frame before React commits.
  • One exception: closing something at the very end of the transcript can still shift the view, as the browser clamps the scroll offset with nothing below to fill it.