cmux NEXT: an indicator in a hidden pane stops its loop; debug.loops lists every loop
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.viewDidHiderefreshes only a view whose loop is running, andviewDidUnhiderefreshes 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 ownisHidden(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 reportsloopsandhidden_loopsper window and in total, withlimitcapping the item list.debug.wakeupscan'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](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
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
parkingThePaneStopsTheLoopAndShowingItStartsItAgainand 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.