cmux NEXT agent pane: chat switch motion experiment (rows, title and Session Info as one movement)

Merged
TE
5d ago
+522-1

Summary

Leo, 2026-11-01: "honestly i just want to be able to be able to eventually play full-on animations like this", and "we're not actually pushing things to their limit yet. ofc we've had to do A F***ING LOT of parity work and not much experimentation yet. which we should do. a lot of."

A choreographed chat switch, as an experiment (chat-switch-motion). When the agent pane opens another chat, the transcript's visible rows, the chat title and the Session Info body move as one performance instead of the transcript fading alone (a cold open) or nothing moving (a cached one).

ArmWhat moves
off (default)Today: the cold open's 120 ms fade, and a cached chat drawn in one frame.
fadeTranscript, title and Session Info fade in together over 120 ms, with no travel.
staggerThe title, then the visible rows from the bottom up (the reading position settles first), each rising 6 px on one spring, 16 ms apart. Session Info's body follows from the right. Everything settles within 240 ms.
depthThe transcript comes forward from 98.5% as one sheet while Session Info slides 8 px in from its edge, on the same spring, in 180 ms.
  • Rules kept (design/motion-and-polish.md):
    • Transform and opacity only.
    • Every part settles within the 240 ms ceiling.
    • The switch itself is never delayed: the new chat is in the DOM on the frame it lands, and the motion plays over it.
    • Interruptible: a switch during a switch starts each element from the value it is drawn at.
    • Motion off, or a zero --agent-motion-in, draws nothing. Reduce Motion gets a fade.
  • Stop and cleanup. No timer, observer or frame loop. The motion is Web Animations with finite durations that end on their own. The next switch cancels them (after reading where they are), and so does the transcript unmounting (chatSwitch.ts stop(), the layout effect cleanup in App.tsx).
  • Why it stays off by default. Rule 14 asks for a cached view to be "instant if cached", and Leo has not ruled on whether a switch performance outranks that. The arms ship as an experiment to try: in the pane's web inspector, set localStorage["cmux.experiments"] = '{"chat-switch-motion":"stagger"}' and reload. Compare arms in the gallery entry agent-pane.chat-switch. Its script opens a second chat, then goes back mid-switch to show the interruption.
  • Harness. chat.switch.<arm> (test/latency/actions.ts, flows.harness.ts switch=) measures every frame from the switch until no part is still moving, with Session Info open.

My pick: stagger. It reads as one movement, keeps the bottom rows (where the reader is) still first, and costs no more frames than depth (table below).

Reviews

  • Codex (gpt-6-astra, high) gave SHIP at the previous head 81bd242fa552. It found two follow-ups, both fixed in this head with a red/green test: finished animations were counted as moving, so a second switch showed no motion, and the gallery's interrupt step never interrupted.
  • An Opus subagent delta review at this head gave SHIP (receipt below).

Performance

Performance

Shared latency harness (webviews/test/latency, cases chat.switch.<arm>, opt-in with CMUX_LATENCY_EXPERIMENTS=1): open one chat, switch to another, and record frames until the new title shows and no finite animation is running. Headless Chromium on the Linux pool, 10 runs, at head 91c9f4e0fcb3f3d6cb7fbc1a01218cd370e8272d. before is the off arm, which is today's default and is unchanged.

metricbefore (off)fadestaggerdepthharness
frame p5016.9 ms16.7 ms16.7 ms16.7 msbun scripts/latency/run.ts --engine chromium --runs 10 --base HEAD --page "chat switch"
frame p9536.7 ms30.9 ms30.1 ms31.1 mssame
frame max39.5 ms40.5 ms42.4 ms40.6 mssame
frames / dropped21 / 10114 / 10150 / 11131 / 10same
long tasks0000same

Read: each arm adds about 100 animated frames and no dropped frames beyond the switch's own render. The roughly 10 dropped frames appear with motion off too, so they come from rendering the new chat, not from the motion. The off arm's p95 is higher only because it has 21 samples. The motion is compositor-only (transform and opacity through WAAPI) and adds no long task.

Testing

  • CMUX_LOCAL_HEAVY_CHECKS=0 bun run check in webviews/: the only reds (vp fmt --check on three test files, plus vp lint and vp check floating-promise warnings at App.tsx lines 11541 to 12578) fail the same way on base.
  • Latency harness, through cmux-ci test (numbers above).

Observed / Not run

  • Not run: a DEV build. The experiment defaults to off, so the shipped switch does not change.

Changelog

none

Agent session: agent session

šŸ¤– Generated with Claude Code