cmux NEXT: an indicator in a hidden pane stops its loop; debug.loops lists every loop

Draft
TE
2d ago
+147-2

Draft from think tank, not for merge yet. Discussion: manaflow-ai/cmuxterm-hq#3716 (and #3658, whose narrowed slice this is).

What a user sees

Nothing changes on screen. A spinner inside a pane that is parked (another tab or workspace selected) no longer keeps its Core Animation loop running on a hidden layer. Showing the pane starts it again, in phase with every other spinner.

Why

A parked pane or workspace is hidden in place (PaneContentView+Parking.retire(), WindowRootView.withdraw()), and its window stays visible. StatusIndicatorView stopped its loop only for window occlusion. The sidebar list and the tab strip push isWindowVisible to their indicators when they hide (SidebarListView, TabStripView.syncShown), but an indicator inside a pane (the agent pane's loading mark, AgentPaneView+Loading.swift) has no such host.

Change

  • StatusIndicatorView: while an ancestor is hidden, the plan carries no animation. viewDidHide refreshes only a view whose loop is running, and viewDidUnhide refreshes only a view whose loop was left off because of a hidden ancestor. Parking a workspace therefore costs its spinning indicators, not every indicator. The view's own isHidden (which follows its glyph) is not counted, so a refresh never re-enters itself. Existing behaviour is unchanged: the deferred resync on show (owesResync), occlusion tracking and the activation cost.
  • debug.loops (new, DebugLoops.swift): walks each window's layer tree once per call and lists every repeating animation (repeatCount = .infinity, repeatDuration > 0, or a group containing one), with its key, layer, owning view and whether it can be seen (window visible and occlusion-visible, no hidden or transparent layer or view above it). It reports loops and hidden_loops per window and in total, with limit capping the item list. debug.wakeups can't see these loops because they run in the render server. No timer, no observer.

Overlap: #24240, #23074 and #24151 also add lines to AppControl.swift's command list. They register different commands, so there's no shared fix.

Tests

StatusIndicatorHiddenAncestorTests: parking the pane stops a working indicator's loop and showing it starts it again; an indicator configured while parked shows its mark and loops once shown. Existing StatusIndicatorActivationCostTests still bound activation resyncs (the hide/unhide path does not touch the resync counter). Compile and tests run in PR CI. Not built locally. Local: check-no-godfiles.sh --base (no growth), check-concurrency.sh ok, check-objc-selectors.py (same 5 failures as base; NSView viewDidHide is in the probe table).

Measuring it

Before/after from scripts/cmux-next/bench-idle.sh with parked busy workspaces, plus debug.loops hidden_loops (expected 0 after). Whether the render server spends anything on a hidden layer's animation is the open question; this PR doesn't claim a CPU win.

Risk

Low. One flag and two overrides in StatusIndicatorView; a debug-only command.

Agent session: huge-blue

🤖 Generated with Claude Code


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


Summary by cubic

An indicator inside a parked pane (the agent pane's loading mark) kept its Core Animation loop running on a hidden layer, because only the sidebar list and tab strip told their indicators when they hid. StatusIndicatorView now leaves the loop off while an ancestor is hidden and starts it again when shown; existing behavior (deferred resync on show, occlusion tracking, activation cost) is unchanged. The view's own isHidden is not counted, so a refresh never re-enters itself.

Adds debug.loops, which walks each window's layer tree once per call and lists every repeating animation with its key, layer, owning view, and whether it is visible, plus per-window and total loops and hidden_loops counts. debug.wakeups cannot see these loops because they run in the render server; {"limit": N} caps the item list (default 50).

  • Now parkingThePaneStopsTheLoopAndShowingItStartsItAgain and an indicator configured while parked loops once shown.

  • Draft from think tank; discussion in manaflow-ai/cmuxterm-hq#3716, narrowed slice of #3658. Overlaps with #24240, #23074, #24151 only in that they also add lines to AppControl.swift's command list; they register different commands.