Repository navigation
cmux NEXT agent pane: chat switch motion experiment (rows, title and Session Info as one movement) - #30602
Conversation
…e and Session Info as one movement) Experiment chat-switch-motion (off by default): fade, stagger (title, then the visible rows from the bottom up on one spring, Session Info after) and depth (the transcript forward from 98.5%, Session Info from its edge). Transform and opacity only, settled within 240 ms, interruptible from the drawn value, Web Animations that end on their own and are cancelled on the next switch and on unmount; motion off draws nothing, Reduce Motion fades. Gallery entry agent-pane.chat-switch and latency cases chat.switch.<arm>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
… first chat keeps its fade, an empty chat's first send is no switch; experiment cases stay out of the CI scoreboard Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
… opacity only, rows on screen bottom first, nothing with motion off); entranceAllowed reads the pane's own document Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
There was a problem hiding this comment.
3 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/src/agent-session/acpmux/chatSwitch.test.ts">
<violation number="1" location="webviews/src/agent-session/acpmux/chatSwitch.test.ts:66">
P2: This assertion uses a dataset key the host does not set for motion-off, so it can pass while the real zero-motion path remains untested. Use the host's `agentMotion` key here, and align the implementation if `cmuxMotion` is intended as a separate supported signal.</violation>
</file>
<file name="webviews/src/agent-session/acpmux/chatSwitch.gallery.ts">
<violation number="1" location="webviews/src/agent-session/acpmux/chatSwitch.gallery.ts:68">
P2: This script never exercises an interrupted chat switch: the second step re-delivers the already-active second snapshot, waits after the prior animation has settled, and only then switches back to the first chat. Combine the forward switch, two-frame wait, and return switch in one play step (or otherwise avoid the runner's per-step animation wait) so the gallery actually validates interruptibility.</violation>
</file>
<file name="webviews/src/agent-session/acpmux/App.tsx">
<violation number="1" location="webviews/src/agent-session/acpmux/App.tsx:5192">
P2: Switching to a chat with no rows leaves the previous switch animation running because this effect returns before stopping it. Cancel `switchPlay.current` whenever the new switch has no rows, so the old title, Session Info, and transcript animation cannot continue over the new chat.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| throw new Error("animated"); | ||
| }; | ||
| expect(playChatSwitch(scroller, "off")).toBeUndefined(); | ||
| doc.documentElement.dataset.cmuxMotion = "off"; |
There was a problem hiding this comment.
P2: This assertion uses a dataset key the host does not set for motion-off, so it can pass while the real zero-motion path remains untested. Use the host's agentMotion key here, and align the implementation if cmuxMotion is intended as a separate supported signal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/chatSwitch.test.ts, line 66:
<comment>This assertion uses a dataset key the host does not set for motion-off, so it can pass while the real zero-motion path remains untested. Use the host's `agentMotion` key here, and align the implementation if `cmuxMotion` is intended as a separate supported signal.</comment>
<file context>
@@ -0,0 +1,68 @@
+ throw new Error("animated");
+ };
+ expect(playChatSwitch(scroller, "off")).toBeUndefined();
+ doc.documentElement.dataset.cmuxMotion = "off";
+ expect(playChatSwitch(scroller, "stagger")).toBeUndefined();
+});
</file context>
| }; | ||
|
|
||
| const toSecond: Play = async (ctx) => { | ||
| deliver(ctx, secondChat()); |
There was a problem hiding this comment.
P2: This script never exercises an interrupted chat switch: the second step re-delivers the already-active second snapshot, waits after the prior animation has settled, and only then switches back to the first chat. Combine the forward switch, two-frame wait, and return switch in one play step (or otherwise avoid the runner's per-step animation wait) so the gallery actually validates interruptibility.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/chatSwitch.gallery.ts, line 68:
<comment>This script never exercises an interrupted chat switch: the second step re-delivers the already-active second snapshot, waits after the prior animation has settled, and only then switches back to the first chat. Combine the forward switch, two-frame wait, and return switch in one play step (or otherwise avoid the runner's per-step animation wait) so the gallery actually validates interruptibility.</comment>
<file context>
@@ -0,0 +1,108 @@
+};
+
+const toSecond: Play = async (ctx) => {
+ deliver(ctx, secondChat());
+ await ctx.waitFor(() => ctx.document.querySelector(".acpmux-thread-title")?.textContent?.startsWith("Profile"));
+};
</file context>
| // CHAT SWITCH PLAY: the last layout effect, so the new chat's rows are placed and scrolled to where | ||
| // they will be drawn when the play picks the visible ones (CHAT SWITCH MOTION above). | ||
| useLayoutEffect(() => { | ||
| if (!switchPending.current || !hasRows) return; |
There was a problem hiding this comment.
P2: Switching to a chat with no rows leaves the previous switch animation running because this effect returns before stopping it. Cancel switchPlay.current whenever the new switch has no rows, so the old title, Session Info, and transcript animation cannot continue over the new chat.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/App.tsx, line 5192:
<comment>Switching to a chat with no rows leaves the previous switch animation running because this effect returns before stopping it. Cancel `switchPlay.current` whenever the new switch has no rows, so the old title, Session Info, and transcript animation cannot continue over the new chat.</comment>
<file context>
@@ -5155,6 +5186,13 @@ export function VirtualTranscript({
+ // CHAT SWITCH PLAY: the last layout effect, so the new chat's rows are placed and scrolled to where
+ // they will be drawn when the play picks the visible ones (CHAT SWITCH MOTION above).
+ useLayoutEffect(() => {
+ if (!switchPending.current || !hasRows) return;
+ switchPending.current = false;
+ switchPlay.current = playChatSwitch(ref.current, switchArm, switchPlay.current);
</file context>
| if (!switchPending.current || !hasRows) return; | |
| if (!switchPending.current || !hasRows) { | |
| switchPlay.current?.stop(); | |
| switchPlay.current = undefined; | |
| return; | |
| } |
… parts still in flight start from their drawn value); the gallery's interrupt step really interrupts; tests for two switches in a row Red/green: bun test src/agent-session/acpmux/chatSwitch.test.ts fails 'a second switch after the first finished moves again from the start' without the chatSwitch.ts change and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
teamleaderleo
left a comment
There was a problem hiding this comment.
Review (subagent claude-opus-5-5): SHIP at 91c9f4e
…-chat-switch # Conflicts: # webviews/src/experiments/registry.ts
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Stale: these images are of Gallery before/after for
States
Pick other states with a Passes: CI passes on CI passes on Written by web and size checks: green Shipped in:
|
teamleaderleo
left a comment
There was a problem hiding this comment.
Review (subagent claude-opus-5-5): SHIP at 2bec404
|
Found 6 test failures on Blacksmith runners: Failures
|
teamleaderleo
left a comment
There was a problem hiding this comment.
W2-21 independent source review at 2bec404567bb6fa229de72407a68826008cd0900 (design/ motion-and-polish + foundations):
Missing lifecycle case: a chat-switch animation already in flight when the pane loses visibility or the motion preference changes. chatSwitch.ts:31-41 samples entranceAllowed, data-cmux-motion and matchMedia before starting WAAPI. chatSwitch.ts:156-171 then owns live Animation instances, but App.tsx:3331-3336 only stops them on transcript unmount (and App.tsx:5196 on the next played switch). The host's installRevealSettle guards new entrances after a reattach; it does not cancel an existing switch animation.
Please exercise start stagger/depth -> hide the window mid-flight -> show it again, and start stagger -> toggle Reduce Motion or the host's motion-off setting mid-flight. An interrupted animation should settle/cancel in the hidden or reduced frame and return with the current chat fully drawn, without resuming old title/transcript/Session Info motion. A small test could expose stop() to a visibility/motion-policy subscription in VirtualTranscript, dispatch visibilitychange / matchMedia.change, and assert every in-flight fake Animation.cancel() was called; keep the regular mid-chat interruption behavior. The test suite today covers preference only before play and a second chat switch, so this case remains untested.
This is a source-backed test gap / predicted failure mode, not an observed signed-app reproduction. The experiment remains off by default; the point is to prove lifecycle behavior before promotion. The broader W2-21 app-level critique will be posted on HQ #5944.
… as a Web Animations surface popover-instant-open.test.ts lists every script that drives the Web Animations API; the chat switch's one animate() call (a transcript, title and Session Info entrance, not a popup) is now listed. Kept both sides of the latency harness and App.tsx conflicts with the merged glass work. Red/green: bun test test/popover-instant-open.test.ts failed 'every Web Animations call is a listed non-popup surface' on 2bec404 (CI shard 6) and passes here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
teamleaderleo
left a comment
There was a problem hiding this comment.
Review (subagent claude-opus-5-5): SHIP at 639b374
|
Review (next-review patch-id; carried): SHIP at 4a05114 The independent SHIP at 639b374 carries to this head: the PR's own change ( |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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).off(default)fadestaggerdepthdesign/motion-and-polish.md):--agent-motion-in, draws nothing. Reduce Motion gets a fade.chatSwitch.tsstop(), the layout effect cleanup inApp.tsx).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.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
Performance
Performance
Shared latency harness (
webviews/test/latency, caseschat.switch.<arm>, opt-in withCMUX_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 91c9f4e.beforeis the off arm, which is today's default and is unchanged.bun scripts/latency/run.ts --engine chromium --runs 10 --base HEAD --page "chat switch"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.cmux-ci test(numbers above).Observed / Not run
off, so the shipped switch does not change.Changelog
none
Agent session: agent session
🤖 Generated with Claude Code