cmux NEXT: working spinners turn in phase, as stepping spokes instead of a 3/4 arc
Merged
TE
7d ago+268+268+012345678901234567890123456789-45-45-01234567890123456789
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. Noanimationstartfires 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, theanimationstarthandler 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
MutationObserveron the page-hidden attribute callsrealignSpinnerswhen the page shows. It puts every running shared loop back on the shared phase;cmux-wavejoins the shared set. Loops that start while the page is hidden are left paused until it shows. - Native:
SpinnerClockcounts 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 (Spinnerwithdata-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"), witharcLengthdefaulting 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.tspasses. The browser file skips off CI.bun run check:fixed-waitsandcheck:browser-lanespass.prepush-gate.pypasses. - 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