Repository navigation
cmux NEXT: an indicator in a hidden pane stops its loop; debug.loops lists every loop - #34074
Conversation
…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
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
|
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). Follow-ups:
|
|
Found 2 test failures on Blacksmith runners: Failures
|
|
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
Follow-ups: see findings 1–2. |
…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
Bugbot needs on-demand usage enabledBugbot 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. |
|
Review (subagent claude-opus-5-5 medium): SHIP at 6bbb7a6 Visual verdict: unchanged by this diff Follow-ups:
|
|
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 - |
|
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
Static concurrency and selector checks pass. AppKit tests could not run on Linux. Follow-ups:
|
…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.
|
Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order). This PR raised
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. |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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.StatusIndicatorViewstopped its loop only for window occlusion. The sidebar list and the tab strip pushisWindowVisibleto 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. ExistingStatusIndicatorActivationCostTestsstill 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.shok,check-objc-selectors.py(same 5 failures as base;NSView viewDidHideis in the probe table).Measuring it
Before/after from
scripts/cmux-next/bench-idle.shwith parked busy workspaces, plusdebug.loopshidden_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
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Stops a spinner's Core Animation loop when its pane is parked, and adds
debug.loopsto list every repeating animation and whether anyone can see it.StatusIndicatorViewnow 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 ownisHiddenis not counted, and the existing hidden/occlusion andowesResyncbehavior is unchanged.debug.loopslives inCmuxNextDesign, sincecheck-app-sizeforbidsCmuxNextAppfrom growing, and walks each window's layer tree once per call, reportingloopsandhidden_loopsper window and in total, withlimitcapping the item list.debug.wakeupscan't see these loops because they run in the render server.Written for commit 6bbb7a6. Summary will update on new commits.