cmux NEXT agent pane: chat switch motion experiment (rows, title and Session Info as one movement)
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).
| Arm | What moves |
|---|---|
off (default) | Today: the cold open's 120 ms fade, and a cached chat drawn in one frame. |
fade | Transcript, title and Session Info fade in together over 120 ms, with no travel. |
stagger | The 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. |
depth | The 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.tsstop(), the layout effect cleanup inApp.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 entryagent-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.tsswitch=) 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.
| metric | before (off) | fade | stagger | depth | harness |
|---|---|---|---|---|---|
| frame p50 | 16.9 ms | 16.7 ms | 16.7 ms | 16.7 ms | bun scripts/latency/run.ts --engine chromium --runs 10 --base HEAD --page "chat switch" |
| frame p95 | 36.7 ms | 30.9 ms | 30.1 ms | 31.1 ms | same |
| frame max | 39.5 ms | 40.5 ms | 42.4 ms | 40.6 ms | same |
| frames / dropped | 21 / 10 | 114 / 10 | 150 / 11 | 131 / 10 | same |
| long tasks | 0 | 0 | 0 | 0 | same |
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 checkinwebviews/: the only reds (vp fmt --checkon three test files, plusvp lintandvp checkfloating-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