cmux NEXT Inspector: running subagents shimmer in one wave instead of pulsing out of step

Merged
TE
15d ago
+396-33

Summary

Leo, on the Inspector's Subagents avatars: "these glimmer at different intervals ... it would look cool if they went in a wave, like how thinking glimmers in ChatGPT."

Before this, each running avatar ran its own acpmux-chat-inspector-breathe loop (2.4 s). Each loop started whenever its avatar mounted, so the avatars pulsed out of step. Now there is one shared shimmer that sweeps across the running avatars from left to right:

  • Shared primitive webviews/src/ui/wave.ts + ui/wave.css, loaded through the desktop layer that every page imports:
    • waveIndices(items, live) gives running items consecutive indices in display order. Finished and failed items get none.
    • waveProps(index) returns the acpmux-wave-item class and --wave-index.
    • installWave(doc) starts every cmux-wave loop on one epoch: Unix time mod the period, the same base as the spinner clock's spinnerStartTime. Items that mount at different moments still line up, so only their index offsets them. It also sets <html data-cmux-page-hidden> while visibilityState is hidden.
  • Look: a 1.6 s loop, 120 ms step per index, opacity only. Each item rests at 0.55 and brightens to 1 as the crest passes. The delay is pulled back whole periods so it is never positive, which means no item sits waiting at full strength.
  • Gates: the wave runs only under prefers-reduced-motion: no-preference. It stops when data-cmux-motion is off or reduced (the WebTheme motion gate), when data-status-indicators="static" is set, and when the page is hidden. A stopped avatar shows at full strength.
  • Applied to: the Inspector's avatar row (SubagentsSection.tsx, through Avatar's new wave prop and the started-chat avatars). The old per-avatar breathe is gone. The summary's subagent dots stay still as before, so this adds no new pulsing dot (see #23870).
  • Other rows can join: any running row joins the wave by setting acpmux-wave-item and --wave-index on its indicator. #23916 already does this on its AgentLine icons.
  • Overlap with #23916: #23916 replaces this avatar row with named lines, so the two PRs conflict in SubagentsSection.tsx and inspector.css. Whichever lands second takes #23916's side of those two files and keeps ui/wave.*. #23916's running lines then join the wave as they are.
  • Overlap with #23870: none. This diff stays away from desktop.css, spinnerClock.ts and static-status-loops.test.ts.

No capture: webview-only (Linux gallery / Playwright video below).

Video: Playwright recordings (Chromium, 9.6 s, dark) of the Inspector gallery entry agent-pane.chat-inspector / full, with 5 running and 3 ended avatars, served by bun run gallery:dev on huge-blue. The files are in Leo's lane folder:

  • after (head): leo-workspace/briefs/2026-10-20-gui-push/lanes/session-panel/wave/after-dark.webm
  • before (base): .../wave/before-dark.webm
  • still: .../wave/after-still-dark.png

Measured in the page at the head:

  • All 5 running avatars share one startTime.
  • Their animation-delays are -32000, -31880, -31760, -31640 and -31520 ms, so one crest crosses left to right at 120 ms per avatar.
  • The 3 ended avatars run no animation and stay at opacity 1.

Testing

bun test test/running-wave.test.tsx checks:

  • running items get consecutive indices, and done or failed items get none;
  • alignWave gives loops that start at different moments one shared start;
  • the rendered SubagentsSection markup;
  • the CSS gates: Reduce Motion, data-cmux-motion off or reduced, static indicators, hidden page;
  • the keyframes animate opacity only.

CMUX_BROWSER_TESTS=1 bun test test/running-wave-loops.test.ts runs in real Chromium and checks:

  • items 2 and 3 indices apart get delays 240 and 360 ms apart, all negative;
  • every start sits on the Unix epoch ((start + timeOrigin) % 1600 ā‰ˆ 0), including the start of an item that mounts 337 ms later. With installWave disabled, this test fails;
  • the WebTheme bootstrap's motion: off, motion: reduced and statusIndicators: static each stop the wave, and so does Reduce Motion;
  • a visibilitychange to hidden stops it, and becoming visible again restarts it on the epoch.

Also:

  • test/static-status-loops.test.ts, test/spinner-clock.test.ts and test/chat-inspector.test.tsx pass unchanged.
  • scripts/web-lint.sh and python3 scripts/cmux-next/prepush-gate.py pass.
  • cmux-ci compile-check --app passes.

Observed / Not run

  • Observed: the commands above, run on huge-blue (Linux, Chromium).
  • Not run: I did not view the change in the native app. It is webview-only, and the video is from Chromium.

Changelog

Changed: running subagents' avatars in the Inspector shimmer together in one wave instead of pulsing out of step.

šŸ¤– Generated with Claude Code

Agent session: cosmic-gray