cmux NEXT: working spinners turn in phase, as stepping spokes instead of a 3/4 arc

Merged
TE
7d ago
+268-45

Leo, on a screenshot of two 3/4-arc working spinners side by side, each turning out of phase with the other: "WHY THE FING DID YOU DESYNC THESE", then "WHO THE F EVEN DECIDED TO PUT THAT 3/4 CIRCLE ANYWAYS", "WHEN".

What was wrong

Both clocks already existed: ui/spinnerClock.ts on the web and SpinnerClock natively. Each one had a hole.

  • Web: a hidden page pauses every loop (:root[data-cmux-page-hidden] … animation-play-state: paused, desktop.css). When the page shows again those loops resume where they stopped. No animationstart fires for a resumed loop, so the clock never realigned them. A spinner mounted after the page shows starts on the clock, so it sits out of step with the ones already on the page. Also, the animationstart handler gave a start time to loops that start while the page is hidden, which unpaused them so they ran unseen.
  • Native: every spinner read Unix time afresh to pick its phase. The media clock stops while the Mac sleeps and Unix time does not, so a spinner started after a sleep was out of step with one turning since before it.

Change

  • Web: a MutationObserver on the page-hidden attribute calls realignSpinners when the page shows. It puts every running shared loop back on the shared phase; cmux-wave joins the shared set. Loops that start while the page is hidden are left paused until it shows.
  • Native: SpinnerClock counts from the media clock on one Unix offset per process (processEpoch). Every spinner in the app keeps one phase across sleeps.
  • The glyph: the agent working-state mark, natively (StatusIndicatorPlan.working, the Codex-like default set and the Debug sets) and in the transcript (Spinner with data-working), is now the macOS activity indicator's eight spokes. They step round one spoke at a time on the shared clock. That is the native look ChatGPT's and Claude's Mac apps use for their progress spinners. Loading spinners (page loads, harness checks, <Spinner loading>) keep their arc, and no other indicator changes. The Debug working looks (breathing or still dot) work as before.

Where the 3/4 arc came from

  • Native arc: 6388291c7c3 (2026-10-02, "cmux-next: one shared status indicator (arc, native spinner, dot, ring) with stacking and settings"), with arcLength defaulting to 0.72: "Lawrence wants the thin loading arc built in and customizable, including the native macOS spinner."
  • Arc as the agent working mark everywhere: #20410 (2026-10-14, "CmuxNext: Codex-like status icons by default, plus static status glyphs") made the Codex-like set the default: "Codex's sidebar: a grey spinner (a still ring under Reduce Motion), …". That set draws work as the turning arc.
  • Web transcript arc: d="M8 2.75a5.25 5.25 0 1 1-4.55 2.63" came in with #16602 (2026-10-01, "cmux-next agent pane: Codex Markdown, code blocks and prompt bubbles").

Tests

  • webviews/test/spinner-phase.test.ts (new, Chromium, real stylesheets and the page's clock):
    • two spinners mounted 337 ms apart share a phase;
    • a spinner a hidden page paused for 411 ms comes back in step with one mounted after the page shows, and both match the clock.
    • The second test fails on base. The test is committed before the fix (7b76fc850d9).
  • SpinnerClockTests.twoWorkingSpinnersStartedAcrossASleepShareOnePhase (new): two working spinners started 0.37 s apart on the media clock share a phase. It fails on base, where the default clock read the wall clock afresh.
  • Native plan tests updated to the working spinner: CodexStatusIconSetTests, StatusWorkingLookTests, NoBreathingDotTests, SidebarRowNoBlinkTests.

Observed / Not run

  • Ran locally: bun test test/spinner-clock.test.ts passes. The browser file skips off CI. bun run check:fixed-waits and check:browser-lanes pass. prepush-gate.py passes.
  • Fleet runs are listed in comments below.
  • Not run: a DEV build capture of the spokes. Leo tries the build.

Changelog

  • Changed: Agent working spinners are the macOS activity-indicator spokes and always turn in step, including after a pane was hidden or the Mac slept.

risk:normal

Agent session: agent session

šŸ¤– Generated with Claude Code