Skip to content

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

Merged
teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/ux-exp-chat-switch
Nov 1, 2026
Merged

teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/ux-exp-chat-switch

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

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.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 81bd242. 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 91c9f4e. 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 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

teamleaderleo and others added 3 commits November 1, 2026 08:46
…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
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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
@cursor

cursor Bot commented Nov 1, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot 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 teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (subagent claude-opus-5-5): SHIP at 91c9f4e

…-chat-switch

# Conflicts:
#	webviews/src/experiments/registry.ts
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 2bec404: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30602 -f ref=lane/ux-exp-chat-switch -f platform=macos -f backend_mode=local

Stale: these images are of 639b374; the head is now 4a05114 and a new capture is running.

Gallery before/after for 639b374: 16 states rendered, 4 images differ, 1177 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41305316238 -R manaflow-ai/cmux -n gallery-advisory-30602-639b374437eea32415ea026b986957d6b0421599 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-switch/first-chat Apple System Colors Light new 0% own chatSwitch.gallery.ts
agent-pane.chat-switch/first-chat Apple System Colors new 0% own chatSwitch.gallery.ts
agent-pane.chat-switch/second-chat Apple System Colors Light new 0% own chatSwitch.gallery.ts
agent-pane.chat-switch/second-chat Apple System Colors new 0% own chatSwitch.gallery.ts
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Passes: CI passes on 4a0511465a.

CI passes on 4a0511465a (run 41309839141 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

web and size checks: green

Shipped in:

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (subagent claude-opus-5-5): SHIP at 2bec404

@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp5lsiot8j/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmp6znooyoe/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (subagent claude-opus-5-5): SHIP at 639b374

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 1, 2026 13:08 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 1, 2026 13:09 Inactive
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (next-review patch-id; carried): SHIP at 4a05114

The independent SHIP at 639b374 carries to this head: the PR's own change (git diff $(git merge-base feat-cmux-next HEAD) HEAD) has the same git patch-id --verbatim (1e95bed2042be7c3) at both heads, and no merge between them was resolved by hand. Nothing new to review. A newer BLOCK, or any change to the PR's own diff, ends the carry.

@teamleaderleo
teamleaderleo merged commit c7caf1e into feat-cmux-next Nov 1, 2026
175 of 177 checks passed
@teamleaderleo
teamleaderleo deleted the lane/ux-exp-chat-switch branch November 1, 2026 13:50

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 4a051146 Deployed Nov 1, 2026 by cmux-bot[bot] via wait for same-key tree publisher #96256
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant