Skip to content

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

Merged
teamleaderleo merged 4 commits into
feat-cmux-nextfrom
lane/tt-critic-hidden-loops
Nov 3, 2026
Merged

teamleaderleo merged 4 commits into
feat-cmux-nextfrom
lane/tt-critic-hidden-loops

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Carries #24806 (its lane is gone; this VM publishes only lane/ branches), merged up to current feat-cmux-next. 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: agent session

🤖 Generated with Claude Code


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


Summary by cubic

Stops a spinner's Core Animation loop when its pane is parked, and adds debug.loops to list every repeating animation and whether anyone can see it.

  • An indicator inside a pane (the agent pane's loading mark) kept looping on a hidden layer; StatusIndicatorView now leaves the loop off while an ancestor is hidden and starts it again when the pane shows. Parking a workspace costs the loops of its spinning indicators alone. The view's own isHidden is not counted, and the existing hidden/occlusion and owesResync behavior is unchanged.
  • debug.loops lives in CmuxNextDesign, since check-app-size forbids CmuxNextApp from growing, and walks each window's layer tree once per call, reporting 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.

Written for commit 6bbb7a6. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 2 commits October 24, 2026 05:59
…lists every loop

A parked pane or workspace is hidden in place, and an indicator inside it
(the agent pane's loading mark) kept its Core Animation loop on a hidden
layer: its window was still visible, and only the sidebar list and tab
strip tell their indicators when they hide. StatusIndicatorView now leaves
the loop off while an ancestor is hidden and starts it again when shown.
Only a view with a running loop does work on hide, so parking a workspace
costs its spinning indicators alone.

debug.loops walks each window's layer tree once per call and lists every
repeating animation with whether anyone can see it, so hidden loops can be
counted: debug.wakeups cannot see render-server loops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c41-4fe5-72b2-80ac-30312856d2cd&host=huge-blue
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 6bbb7a6: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34074 -f ref=lane/tt-critic-hidden-loops -f platform=macos -f backend_mode=local

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent claude-opus-5-5 medium): SHIP at dab33f2

Visual verdict: unchanged by this diff (no rendered UI change; loop is only paused while an ancestor is hidden).
Checked: compiles against current APIs (JSONValue.intValue, runningAnimation, Motion.reduceMotionOverride, debug.loops inside the DEBUG/bench block); no refresh re-entrancy (viewDidHide guards on a running loop); restart goes through SpinnerClock so spinners stay in phase; resync/occlusion paths unchanged; tests cover both park orders.

Follow-ups:

  • [follow-up] Ancestors hidden only at the layer level (layer.isHidden / alpha 0) still loop; debug.loops reports them.
  • [follow-up] DebugLoops.report filters hidden loops twice per window.
  • [follow-up] DebugLoops.swift compiles into Release; could sit under #if DEBUG || CMUX_BENCH_HOOKS like its call site.

@blacksmith-sh

blacksmith-sh Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
nightly still handles the legacy tunnel extension/
nightly still handles the legacy tunnel extension
View Logs
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)/
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)
View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at dab33f2

Reviewer: codex gpt-6-astra high, fast tier.

Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at dab33f2

Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] DebugLoops.collect only traverses sublayers, so repeating animations attached to CALayer.mask are omitted. This undercounts loops and can falsely report hidden_loops: 0; the braille indicator’s repeating mask animation is one example. DebugLoops.swift:57

  2. [P2] [follow-up] The new tests create windows but the suite lacks .closesItsWindows and never closes them. Those windows remain in the shared test process and can accumulate across the design test target. StatusIndicatorHiddenAncestorTests.swift:7

Follow-ups: see findings 1–2.

@cmux-bot cmux-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmux-bot approves dab33f2: an independent review passed this exact head (router-review receipt by teamleaderleo). A push needs a new review; a later BLOCK at this head dismisses this approval.

…t grow

check-app-size refuses any CmuxNextApp source growth; the layer walk moves to
CmuxNextDesign/Diagnostics and returns a JSONSerialization graph, and the
debug.page_host_pool registration folds onto one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c855d74d054a598b8a20
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent claude-opus-5-5 medium): SHIP at 6bbb7a6

Visual verdict: unchanged by this diff
Checked: DebugLoops now in CmuxNextDesign (public/MainActor fit the target's settings; Int/Bool/NSNull and nested arrays bridge through JSONValue(foundation:)); no stale App copy; StatusIndicatorView hide/unhide re-entry guarded, resync/occlusion unchanged, restarts rejoin the SpinnerClock phase; tests sound.

Follow-ups:

  • [follow-up] DebugLoops counts a loop as visible from opacity/hidden flags only; a clipped or offscreen loop still counts as visible, so hidden_loops can undercount.
  • [follow-up] Ancestors hidden only at the layer level (layer.isHidden / alpha 0) still loop (from the earlier review).

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, sweep, via the review router; full diff from merge base 786f51b to this head): SHIP at b57d4d4

[P2] [follow-up] Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Diagnostics/DebugLoops.swift:61 - debug.loops omits animations with finite repeatCount values, despite claiming to report every repeating animation, so loops and hidden_loops can be understated - treat any repeatCount > 0 as repeating and recurse through groups.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 6bbb7a6

Reviewer: codex gpt-6.1-sol xhigh, fast tier.

Follow-ups: 2. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at 6bbb7a6
Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] DebugLoops.swift:57: Traverse layer.mask too. Braille spinners run their infinite frames animation on a mask, so they are omitted from the items and totals, potentially reporting zero hidden loops incorrectly.

Static concurrency and selector checks pass. AppKit tests could not run on Linux.

Follow-ups:

  • Include mask animations in debug.loops and cover a braille spinner.
  • [P2] [follow-up] StatusIndicatorView.swift:122: Supply native hide/show captures; no images were available.

@cmux-bot cmux-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmux-bot approves 6bbb7a6: an independent review passed this exact head (router-review receipt by teamleaderleo). A push needs a new review; a later BLOCK at this head dismisses this approval.

@teamleaderleo
teamleaderleo merged commit c35c2e2 into feat-cmux-next Nov 3, 2026
154 of 167 checks passed
@teamleaderleo
teamleaderleo deleted the lane/tt-critic-hidden-loops branch November 3, 2026 19:56
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…prit (#34629)

* base red: the Chief teleport chip's tooltip is the themed one (NativeTooltipGuardTests, from 894d34f)

894d34f (Chief teleport: location chip, Teleport menu) set the chip's
tooltip with toolTip =, which draws macOS's stock bubble; NativeTooltipGuardTests
refuses it (Leo 2026-10-12/20: every hover is themed or gone). It is
themedToolTip now (TooltipPresenter draws it). Same text.

* base red: the All Themes Playground's sample hover is the themed one (NativeTooltipGuardTests, from c0230ef)

c0230ef (Status dots keep their hue at 3:1 in every theme; All Themes
Playground) put .help( on each sample, which draws the stock bubble;
NativeTooltipGuardTests refuses it. It is .themedHelp( now. Same text.

* base red: BrowserChromeIconsTests follows 9d31a6e's one app-wide ⋮ (test only)

9d31a6e (one pane ⋮ size and column, Lawrence msg 19627: "the three
vertical dots, there are 2 of them, and they are different sizes") made the
browser toolbar's More draw the app's action.more.vertical on purpose, with a
chrome-icons allow comment. The test still asked for Chrome's icon there. More
joins the buttons that keep cmux's icon, and a new case pins its ⋮.

* base red: PaneChromeEvenInsetTests follows 9d31a6e's trailing control column (test only)

9d31a6e put the tab bar's ⋮ center on Metrics.paneTrailingControlCenter,
the browser toolbar's ⋮ column (Lawrence msg 19627), instead of as far from the
trailing edge as from the top. The test still asked for the old equal gaps (off
by 2 pt in every density). It now checks the ⋮ center sits on that column.

* base red: the status and unread dot tests read the dot's color in the scope that draws it (test only, after c0230ef)

c0230ef (Status dots keep their hue at 3:1 in every theme; user 2026-11-03:
"we need to make sure every single theme, all the UI elements are colored
properly") made Palette.statusUnread/Attention/Error and notificationBlue
theme colors: the system hue with its lightness moved per theme to 3:1 on the
rows. The views draw them in performWithTheme; SidebarStatusColorTests,
HomeSidebarUnreadDotTests and HomeUnreadBlueDotTests still read them outside
any theme scope (the bare system color), so every comparison was off. They now
read the expected color with the drawing view's performWithTheme.

* base red: SidebarLookContractTests reads the hover close on the trailing column, after cad2620's row gap (test only)

cad2620 (#32647, Leo 2026-11-02) moved each row's fill in from the
sidebar's edge (6 pt compact, 8 pt comfortable) and kept every trailing mark
on the grid's 24 pt line on purpose: "Trailing marks stay on the grid's 24 pt
column". The close did not move, but the fill's end moved toward it, so at
comfortable density the close ends 7 pt inside the fill, not the 8 pt row
inset that theHoverCloseIsTheRowsTrailingX asked for (237 > 236.5).

The test now checks that the close centers on the row's trailing column and
stays inside its row. Other grid tests pin the column itself.

* base red: AgentPaneNativeSessionInfoTests closes its windows and carries .closesItsWindows (from #32646)

WindowCleanupCoverageTests asks every suite that opens a window to carry
.closesItsWindows (#27236). 9c772fa (#32646) and 419a87a (#32704)
opened a window in two tests and never closed it. Each now closes its window
in its own teardown, and the suite carries the trait.

* base red: AgentPaneChromiumMoveTests carries .closesItsWindows (from #32777)

872ef32 (#32777) added a suite that opens a window per test without the
trait WindowCleanupCoverageTests asks for (#27236). Each test already closes
its window; the suite now carries .closesItsWindows.

* base red: AgentShareTests closes the Invite card's window and carries .closesItsWindows (from #33967)

26fd471 (#33967) opened a window for the Invite People card test and never
closed it, in a suite without the trait WindowCleanupCoverageTests asks for
(#27236). The test now closes its window, and the suite carries the trait.

* base red: ColumnStripPageClipTests carries .closesItsWindows (from #32515)

9593a3a (#32515) added a test that opens a window, in a suite without the
trait WindowCleanupCoverageTests asks for (#27236). The test already closes its
window; the suite now carries .closesItsWindows.

* base red: CookieImportConsentPanelTests closes its windows and carries .closesItsWindows (from #33066)

58ec39f (#33066) added tests that open a parent window and a consent panel
and never close them, in a suite without the trait WindowCleanupCoverageTests
asks for (#27236). The panel only orders out when it is answered. Each test
now closes the panel and its parent in its own teardown, and the suite carries
the trait. No source change.

* base red: DebugNativeRowToolsFixtureTests carries .closesItsWindows (from #28618)

201e1de (#28618) added tests that open windows, in a suite without the
trait WindowCleanupCoverageTests asks for (#27236). Each test already closes
its window; the suite now carries .closesItsWindows.

* base red: InspectElementNativeCostTests carries .closesItsWindows (from 7f2173f)

7f2173f added a test that opens a window, in a suite without the trait
WindowCleanupCoverageTests asks for (#27236). The test already closes its
window; the suite now carries .closesItsWindows.

* base red: the two window-free suites in KeyboardResizeArrowDirectionTests carry .closesItsWindows (from #34123)

4f13a23 (#34123) put KeyboardResizeShownStepTests and
KeyboardResizeTradeSettleTests in a file whose other suites open windows.
WindowCleanupCoverageTests (#27236) reads the file, so every top-level suite
there carries the trait. These two open no window; the trait only checks
that.

* base red: SidebarPaneGapTests carries .closesItsWindows (from #28052)

5b91230 (#28052) added tests that open windows, in a suite without the
trait WindowCleanupCoverageTests asks for (#27236). Each test already closes
its window; the suite now carries .closesItsWindows.

* base red: the Link Hint suites close their shell windows and carry .closesItsWindows (from #32537)

67a63bd (#32537) gave the Link Hint test rigs a shell window (and one test
a page window) that no test closed, in suites without the trait
WindowCleanupCoverageTests asks for (#27236). Each test now closes its rig's
windows in its own teardown, and both suites carry the trait.

* base red: StatusIndicatorHiddenAncestorTests closes its windows and carries .closesItsWindows (from #34074)

c35c2e2 (#34074) added a fixture that opens a window no test closed, in a
suite without the trait WindowCleanupCoverageTests asks for (#27236). Each test
now closes its fixture's window, and the suite carries the trait.

* base red: ScreenshotsRememberedAccessTests carries .closesItsWindows (from efacde9)

efacde9 added tests that open windows, in a suite without the trait
WindowCleanupCoverageTests asks for (#27236). Each test already closes its
window; the suite now carries .closesItsWindows.

* base red: TabSwitchWorkBudgetTests sizes its strip for 30 tabs at 9d31a6e's minimum width (test only)

9d31a6e made the narrowest tab keep about four title characters: the
minimum inactive width went from 48 to 72 pt compact (Lawrence msg 19596:
"all here is ... which is not very helpful"). 30 tabs at 72 pt need 2160 pt, so
the test's 1800 pt strip scrolled, and the switch scrolled it (occlusion passes
4 -> 75). The test reads the switch budget of a row where every tab shows, so
its strip is now as wide as 30 tabs at the strip's own minimum need.

* base red: the omnibar's own field editor pads its line by 2 pt from the start (OmnibarFocusLayoutShiftTests, from 15972f3)

15972f3 ("Palette: the empty field's caret sits at the placeholder's left
edge") removed EditorAlignedTextFieldCell's fixed 3 pt shift and draws the
resting line at the title rect, with the line padding of the field's editor.
The palette's editor is AppKit's shared one, already at the field editor's
2 pt padding. The omnibar hands out its own OmnibarFieldEditor, a new text view
at TextKit's 5 pt padding until it first edits. So at rest the address drew
3 pt right of where focus puts it (OmnibarFocusLayoutShiftTests: editing x 3,
rest x 6), a layout shift on the first focus (user, 2026-10-27: "after focus on
omnibar there is layout shift").

The omnibar's editor now starts at the 2 pt padding a field editor has.
@lawrencecchen

Copy link
Copy Markdown
Contributor

Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order).

This PR raised swift_logic_lines by +56 at its merge commit c35c2e2: scripts/cmux-next/swift-logic-lines.py before and after the merge, with the same table (the tip's, c6be943). The largest declarations of the rise:

  • enum DebugLoops in Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Diagnostics/DebugLoops.swift: +56 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)

plans/cmux-next/layer-ownership.md says Rust owns every state, list and rule and Swift is the platform shell. Please move the new logic of this PR in a follow-up: to Rust (the crate that owns the area; the owner lists in STATUS 12147-12165 name the target per area), or at least out of the app into its package. If a declaration above is platform code (AppKit, Core Animation, OS calls, strings, tokens, DEBUG tooling), reply here and the table gets a PLATFORM row with your reason instead.

The swift-logic row is a report, not a gate: nothing blocks on this comment.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 6bbb7a65 Deployed Nov 3, 2026 by teamleaderleo via wait for same-key tree publisher #107397
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants