Agent chat: opening or closing Worked for slides the rows below and fades its steps in or out

Merged
TE
13d ago
+519-50

Leo-ask: ask-a5c1976d1c
Leo-ask: ask-d974edac49
Leo-ask: ask-c07aebfca2
Leo-ask: ask-0bfebc52fd

Leo: "and we shuold also have some animation for collapsing and expanding" (the agent chat's transcript folds), and: "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 collpase!", and: "and thne there were a few animations that didnt quite work for collapsing and expanding wrt tool calls and stufff".

What #23093 already covers (merged 2026-10-21): tool groups ("Ran 4 commands ›"), tool, command and MCP rows, settled runs and thoughts open and close with the shared fold (fold.ts). The body's height and opacity animate over --cv-fold-duration (the host's move token, about 180 ms; 0 under Reduce Motion or with cmux animations off), the chevron turns on the same token, and a click on a button.is-toggle holds that line in place.

Scope: motion and anchoring only. This PR doesn't decide what goes inside a "Worked for" fold. Leo's takeaway rule lives in the shared turns.ts function that tp/fold-interim-messages owns (#24300). This PR doesn't touch turns.ts.

Split to stay under 800 lines. This PR is the shared primitive and every in-row collapsible. The "Worked for" and subagent-group row motion (whole virtual rows sliding and fading, turnFold.ts) follows in its own PR once this one lands.

What this PR does

  1. One shared collapsible, useAnchoredCollapse (conversation/useAnchoredCollapse.ts): useAnchoredCollapse({ defaultOpen?, open?, onOpenChange?, animates? }) -> { open, present, toggle, setOpen, headerRef, bodyProps }.
    • The header stays on its pixel and the body grows or shrinks below it, on the shared fold.
    • Inside the transcript, the header sends a bubbling cmux:anchored-collapse event that VirtualTranscript answers with the same hold #23093's click path uses. Keyboard toggles, and headers that are not button.is-toggle, are held too.
    • Outside the transcript, the hook corrects the nearest scroller itself on each frame until the fold ends.
    • Tool groups, tool, command and MCP rows, background task notes, thoughts and the one-file edited-files diff all go through it. agent-message-style adopts it for the agent-message card in its own PR.
  2. Show more / Show less on a long prompt now animate.
    • Show more holds the bubble's top, so the text grows below it, and the button rides the bubble's bottom edge instead of jumping.
    • Show less holds the button where it was clicked, and the text shrinks back above it.
    • A reversal midway starts from the drawn height, and unmounting cancels what plays.
  3. The audit fixes (frame strips below, every collapsible in Chromium and WebKit):
    • The one-file edited-files diff appeared and vanished in one frame. It now goes through the hook.
    • A fold whose body draws its content late (the diff, highlighted code) opened to the few pixels drawn at the click, then snapped to full height. useFold now follows the content as it grows: each larger size takes over from the drawn height for the time left. Its ResizeObserver stops when the fold ends, when another starts, or on unmount.
    • Show more and Show less had no animation at all.
    • Show less on a prompt several screens tall, with motion off, lost its hold. The row shrank under the old offset and the virtualizer unmounted it, taking focus with it. While a hold lasts, the row it holds stays in the mounted ranges, and when the hold moves the offset, the mounted rows take the new offset in the same frame.
    • Tool rows, command groups, MCP calls, thoughts, background task notes and agent-message cards: each header stayed within 0 px in both engines, with no element running two animations at once (table below).

Works with tp/fold-no-blank-space: this PR doesn't touch reportDrawn, placeRows or the row ResizeObserver.

Changelog

  • Changed: Opening or closing a long prompt's Show more and a one-file edited card's diff in agent chat now animates and keeps the clicked line in place.

Frame strips

These are synthetic gallery fixtures, captured with headless Playwright on the fleet at 760x640 in the dark theme. Each fold's token is slowed tenfold to 1800 ms so one fold yields 8 to 12 frames. Each strip shows the open, then the close. Each frame's label gives ms since the click, the clicked header's y offset (dy), and the number of running animations (DUP would mark an element running two at once). Head is bf316ac4d970; the commit after it changes only the motion-off hold.

KindChromium head dy (open / close)WebKit head dyanimations per element
Command group0 / 00 / 01
Tool row0 / 00 / 01
MCP call0 / 00 / 01
Thought0 / 00 / 01
One-file diff0 / 0 (base: no animation)0 / 01
Show morebubble top held; button rides the edge (base: no animation)same1
Agent message card0 / 00 / 01
Background task note0 / 00 / 01

Show more's dy is the button's, which rides the bubble's growing edge by design. The gallery's prompt sits at the transcript's end, where closing lets the browser clamp the offset (the one documented exception, #23093). The browser test below checks Show less mid-transcript.

One-file diff, before (no animation; the diff appears and vanishes in one frame):
before diff chromium
After, Chromium and WebKit:
after diff chromium
after diff webkit

Show more / Show less, before (no animation):
before show more chromium
After, Chromium and WebKit:
after show more chromium
after show more webkit

The rest, after (Chromium; WebKit measured the same, table above):
after command group chromium
after tool row chromium
after mcp call chromium
after thought chromium
after agent message chromium
after background task chromium

Live side by side (tailnet): https://cmux-lawrences-mac-mini.tail137216.ts.net:18796/compare/tp-fold-animation/?entry=agent-pane.transcript

Observed / Not run

  • transcript-fold-anchor-browser.test.ts (Chromium) adds the following:
    • One test per collapsible kind (a thought, a tool call, a command group, Show more). Each opens and closes mid-transcript, and checks on every painted frame that the line stays within 1 px and that no element runs more than one animation.
    • A 400-line prompt's Show less with Reduce Motion keeps its button on its pixel.
  • PR CI's webviews job runs these. The fleet steps sat queued behind the suites label for more than 30 minutes, so I cancelled them.
  • Reviewed by Codex (gpt xhigh, via acpmux) over several rounds. The receipt is in a comment at the head.
  • CI reds that also fail on base:
    • cmux-next checks: "No fleet host names in gallery fixtures", from base files ShowcaseMachineNameTests.swift and ChatInspector.gallery.tsx.
    • cmux-next checks: "Checks job structure", from test_cmux_next_checks_workflow.py web-bundles. This PR touches no workflow.
    • Guard status and release-notary.
  • Not run: an app build (this is a webview-only change).

Agent session: cosmic-gray

🤖 Generated with Claude Code