Skip to content

Close a sidebar selection as one batch: one pass, one question, one undo - #24341

Merged
teamleaderleo merged 10 commits into
feat-cmux-nextfrom
l5/batch-close
Oct 26, 2026
Merged

teamleaderleo merged 10 commits into
feat-cmux-nextfrom
l5/batch-close

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, closing several selected sidebar workspaces (the multi-select close from #23110):

"also idk what's going on with these new workspace deletions but it's really slow and it's missing a LOT... and it's also 2 lines tall for some reason"

and then: "no, it really is deletion not dictation."

What was broken

Cmd-W, the row menu's Close and the sidebar's Cmd-Delete on a selection all send SidebarIntent.close(ids). SidebarBridge+Intents handled that by running Close Workspace once per row (ActionInvocation(closingRow:)), each a full single-workspace WorkspaceCloseFlow:

  • Slow. Every row took its own turn in the window's WorkspaceLanes, and each turn waited for the daemon's reply and then for the store to apply it before the next row's close started. N rows meant N serial round trips, plus a sidebar pass and a selection repair per row.
  • Missing a lot. With a terminal running a program (the usual case), every row asked its own question. They queued one behind another, Return does not confirm them (returnConfirms = false), and any you didn't answer left that row unclosed. Rows were also refused one at a time against a window room that was shrinking.
  • 2 lines tall. The question title was per row: Close “<workspace name>”? in bold, in a dialog fixed at 380 pt (340 pt of text). Agent workspace names are long, and a name over roughly 40 characters wraps that title to two lines. This is an inference from the layout: the base capture uses short names (Close “Deletion 12”?, one line), and I have no capture of Leo's screen. The batch title Close N workspaces? and the toast Closed N workspaces are short, fixed strings, and the after-fix capture shows both on one line.

The fix

A selection of two or more rows now closes as one batch (Handlers/WorkspaceSelectionClose.swift):

  • Every row leaves in one sidebar pass and the selection moves once (InstantClose.begin([ids])).
  • One question for the whole selection, only when a terminal runs a program: Close N workspaces? (the existing WorkspaceBulkClose.prompt, one line). Its process lookups now run concurrently.
  • Every close is sent at once in one lane turn, then the batch waits once per daemon for the store.
  • One Closed N workspaces toast with Undo (WorkspaceGroupClose.offerUndo, the same toast the other bulk closes use) reopens all of them.
  • A paused machine's closes wait for it outside the lane, as single closes do; they join the toast when they finish and never hold it back.
  • A refused row lists again with one refusal toast for the batch.
  • Each row stays reserved until its own close settles, so Close Workspace can't close it twice.

A single row, and Close Workspace with no selection, keep the existing WorkspaceCloseFlow. Close All in Group already applies one model pass and sends every close at once (SidebarBridge .closeGroup), so it is unchanged.

Scope: only the close path. No sidebar rows, groups, pins, top band, footer or animation code changed. This fixes our own regression from #23110.

Risk: a contained close-path behavior fix (the 10-23 rule for small sidebar behavior fixes, so no dogfood hold): a bug here would show as a multi-row close leaving rows open or a missing Undo; a single row keeps the old WorkspaceCloseFlow untouched.

Testing

SidebarSelectionBatchCloseTests (CmuxNextAppTests) runs on a same-tree cmux-tui daemon with the real window:

  • fiftySelectedWorkspacesCloseTogetherAndUndoBringsThemAllBack: selects 50 new workspaces and sends the sidebar's close on the selection. It checks that every row leaves at the press, all 50 close, no refusal notice shows, exactly one Closed 50 workspaces toast shows, and its Undo reopens all 50. It prints the time from the press until all 50 are gone.
  • aSelectionWithRunningProgramsAsksOnceAndClosesEveryRow: 9 selected rows (3 running sleep 600), Close from the row menu (closeWorkspace targeted at a selected row). It checks that exactly one question is asked and all 9 close.

Command: cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=SidebarSelectionBatchCloseTests --env CMUX_CI_XCODE_APP=/Applications/Xcode_26.6.app --ref <sha>

  • Red, test commit 35c72b0: pending (fleet light class, job 9d3d018aeb1b9b54d95ccb1a queued: every Xcode 26.6 suites worker is paused on host_lock or disk; an earlier run at this SHA was cancelled with no worker)
  • Green, head e5b0044: pending (job f20561e500e8fe7765ad51dc queued, same reason)
  • Timing for 50 selected rows, from the press until all are closed: pending the two runs above (the test prints SidebarSelectionBatchClose: 50 rows ... all closed after <duration>).

Fleet app compile (cmux-ci compile-check --app) passed at e5b0044 (job a0309d83b6ef613483ee34c1).

Review (Codex gpt-6.1-sol, xhigh, correctness first) took three rounds of fixes: rows left hidden after closing elsewhere, a double close of the kept row, an offline unhide before the store, offline rows missing from Undo, serialized process lookups, reservations held too long, and a stale undo after Undo. Final verdict: Review (codex, correctness first): SHIP at e5b004496945afce77ffc7047628fd498df0f9d0.

Localization: no new or changed strings. Close %lld workspaces? and Closed %lld workspaces already exist in every locale of Handlers.xcstrings.

Observed / Not run

  • Observed: the base capture (02fa048) shows 11 selected rows running sleep on Cmd-W. The rows left, then one Close “Deletion 12”? question came up, one per row and queued behind each other. After the fix (a04752e), the same steps show one Close 10 workspaces? question and then a one-line Closed 10 workspaces Undo toast. A base capture of 24 idle rows closed them all eventually, but row by row, over several seconds.
  • Observed: the fleet app compile passed at the head, and prepush-gate.py and check-concurrency.sh are clean.
  • Not run: the offline (paused Cloud VM) branch has no test. It was reviewed by reading the code only. Release was not built.

Update after taking over (base merge, 2026-10-26)

Merged feat-cmux-next into the branch (no force-push). On today's base check-app-size.sh refused the 133 new CmuxNextApp source lines (SW14), so the batch flow moved into CmuxNextAppCore as WorkspaceSelectionCloseFlow (closures injected, like WorkspaceCloseFlow), with only its wiring left in the App (WorkspaceSelectionClose). BulkClosePlan, DaemonService.servesBulkClose and the multi-workspace close (now WorkspaceClose.close(_ members:)) moved with it. InstantClose keeps one begin over a list of ids; the toast's Undo hook is an undone: argument of WorkspaceGroupClose.offerUndo; Close Workspace's guard reads WorkspaceSelectionCloseFlow.canStart. No behavior change intended; CmuxNextApp is 107684 source lines vs 107685 on base.

  • Fleet at c045ecc (cmux-ci run, package-test-lane): SidebarSelectionBatchCloseTests, BulkClosePlanTests, BulkCloseReopenTests, SidebarCloseSelectionTests, WorkspaceNothingToDoRefusalTests all passed. SidebarSelectionBatchClose: 50 rows, press held the main actor 0.042 s, all closed after 0.63 s, missed 0, toasts ["Closed 50 workspaces"], notices []; Undo reopened 50 of 50 after 1.19 s; 9 rows (3 running sleep) asked 1 question, missed 0.
  • CI cmux-next swift test red: its failing suites are the known base reds (ActionRunCreated, AgentCursor*, BrowserProfileLinkMenu, BrowserTabMirror) and suites that fail the same way on unrelated PRs' runs; PaneHorizontalScrollClaimTests, SleepySceneCostTests and WindowZoomTests passed on the fleet at this head (CI-runner flakes).
  • Review: Codex gpt-6-astra xhigh, SHIP at c045ecc.

Changelog

Fixed: Closing several selected sidebar workspaces closes them all at once, asks one "Close N workspaces?" question when a program is running, and shows one "Closed N workspaces" toast whose Undo brings them all back.

Proof

5677ebcc8e57a1e5cd4fab5b state=done worker=cmux-linux-pool-1022-065820-cdcca4c0 total_seconds=110.82 queue_and_setup_seconds=0.62 execution_seconds=110.21 free_gib=171.77->171.27
phase=admission seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=input_restore seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=build seconds=95.75 state=done cache_hit=unknown hits=unknown misses=unknown
phase=artifact_upload seconds=14.10 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_checkout seconds=7.63 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_submodules seconds=0.04 state=done cache_hit=unknown hits=unknown misses=unknown
phase=remote_tree seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_web seconds=1.18 state=done cache_hit=unknown hits=unknown misses=unknown
phase=app_exe_prekey_wait seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=swift_key seconds=0.08 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cache_swift_lane seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=switch_answer seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=app_exe_key seconds=0.11 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_app_exe_resolve seconds=3.54 state=done cache_hit=unknown hits=unknown misses=unknown
phase=swift_import_set seconds=0.54 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_app_exe seconds=46.04 state=done cache_hit=unknown hits=unknown misses=unknown
phase=swift_import_set_done seconds=0.18 state=done cache_hit=unknown hits=unknown misses=unknown
phase=app_exe_stage seconds=0.47 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_components seconds=14.08 state=done cache_hit=unknown hits=unknown misses=unknown
phase=component_web seconds=1.00 state=done cache_hit=False hits=unknown misses=unknown
phase=component_fetch_pinned seconds=0.40 state=done cache_hit=False hits=unknown misses=unknown
phase=component_hostd_build seconds=0.00 state=done cache_hit=True hits=unknown misses=unknown
phase=component_chief_build seconds=0.00 state=done cache_hit=True hits=unknown misses=unknown
phase=component_acpmux_build seconds=0.00 state=done cache_hit=True hits=unknown misses=unknown
phase=component_tui_fetch seconds=0.10 state=done cache_hit=True hits=unknown misses=unknown
phase=component_verify_web seconds=0.70 state=done cache_hit=False hits=unknown misses=unknown
phase=component_reject_providers seconds=0.00 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cli_resources seconds=0.30 state=done cache_hit=False hits=unknown misses=unknown
phase=component_diff_sidecar seconds=0.40 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cmux_tui seconds=3.90 state=done cache_hit=False hits=unknown misses=unknown
phase=component_acpmux seconds=0.00 state=done cache_hit=False hits=unknown misses=unknown
phase=component_optchat_chief seconds=0.00 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cnx_hostd seconds=0.00 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cef seconds=2.20 state=done cache_hit=False hits=unknown misses=unknown
phase=component_rb_host seconds=0.40 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cua_helper seconds=0.30 state=done cache_hit=False hits=unknown misses=unknown
phase=component_ghostty_resources seconds=0.30 state=done cache_hit=False hits=unknown misses=unknown
phase=component_server_helper seconds=1.70 state=done cache_hit=False hits=unknown misses=unknown
phase=component_frameworks seconds=0.60 state=done cache_hit=False hits=unknown misses=unknown
phase=component_coderouter seconds=0.10 state=done cache_hit=False hits=unknown misses=unknown
phase=component_cmux_cua seconds=0.90 state=done cache_hit=False hits=unknown misses=unknown
phase=component_ghostty_cli seconds=0.90 state=done cache_hit=False hits=unknown misses=unknown
phase=cmux_resources seconds=1.93 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_ghostty_resources seconds=0.41 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_web_guard seconds=0.80 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_parity seconds=0.11 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_parity_reference seconds=0.29 state=done cache_hit=unknown hits=unknown misses=unknown
phase=parity_reference seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_sign seconds=4.98 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cmux_package seconds=1.48 state=done cache_hit=unknown hits=unknown misses=unknown
phase=switch_answer seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=pool_prewarm seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=linux_pool seconds=0.00 state=done cache_hit=unknown hits=unknown misses=unknown
phase=cache_app seconds=0.38 state=done cache_hit=False hits=unknown misses=1
phase=cache_cmux_tui_source seconds=1.73 state=done cache_hit=True hits=1 misses=unknown
phase=cache_web seconds=1.40 state=done cache_hit=True hits=1 misses=unknown
phase=cache_app_exe seconds=0.06 state=done cache_hit=False hits=unknown misses=1
phase=cache_acpmux seconds=1.41 state=done cache_hit=True hits=1 misses=unknown
phase=cache_optchat_chief seconds=0.83 state=done cache_hit=True hits=1 misses=unknown
phase=cache_cnx_hostd seconds=0.86 state=done cache_hit=True hits=1 misses=unknown
phase=cache_summary seconds=0.00 state=done cache_hit=unknown hits=5 misses=2
remote install: published

Capture steps-4441620e5c at e5b004496945 (fleet build, capture mini, text checked for private data)

Light Dark
light dark

video
Full quality: cap-e5b00449-steps-4441620e5c.mp4

The steps: 12 workspaces running sleep, Shift-Down to select 11 rows (the window keeps one), Cmd-W, then Close in the question. The video shows one Close 10 workspaces? question, then the one-line Closed 10 workspaces Undo toast.

Agent session: cosmic-gray

🤖 Generated with Claude Code

teamleaderleo and others added 5 commits October 21, 2026 23:18
… with one undo

A failing regression for Leo's report on #23110's multi-select close: each
selected row runs its own Close Workspace, one serialized lane turn and one
question at a time, with no "Closed N workspaces" undo toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
Leo: "also idk what's going on with these new workspace deletions but it's
really slow and it's missing a LOT... and it's also 2 lines tall for some
reason" (closing several selected workspaces, #23110).

Each selected row ran its own Close Workspace: its own question titled with
its name (wraps on a long name, queued behind the others, Return does not
confirm it), then its own serialized turn in the window's lane waiting for
the daemon and the store. A selection now hides in one sidebar pass, asks
one "Close N workspaces?" question, sends every close at once in one lane
turn and offers "Closed N workspaces" with Undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
…e closes finish

Review (Codex) findings on the batched selection close:
- a row closed elsewhere while the question was up stayed hidden;
- the row a window keeps could be closed again by Close Workspace;
- an offline machine's close unhid before its store applied it;
- Undo left out offline rows;
- the running-program question asked each workspace's terminals one at a time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
… an offline machine

Review (Codex) at eccb37a: a refused row stayed reserved until every offline
close finished, and one paused machine held back Undo for the rows that had
closed. Each row's reservation now ends when its own close settles; the toast
shows once the live rows settle and is shown again in place as each offline
row closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
…fers the toast

Review (Codex) at 52e9596: Undo pressed before a paused machine's close
finished left that close to show the toast again from undo state already
used. Once Undo ran, a later offline close stays out of the toast; Reopen
Closed Workspace and History still have it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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

Review c045ecc: 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=pr24341 -f ref=l5/batch-close -f platform=macos -f backend_mode=local

web and size checks: green

@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-gzw4j created for this PR by the cmux coordinator.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Independent review by a different model: Codex gpt-6.1-sol at xhigh, correctness first, read-only from the diff and raw files at each head. It ran on cmux16 through acpmux.

The ask, in Leo's words: "also idk what's going on with these new workspace deletions but it's really slow and it's missing a LOT... and it's also 2 lines tall for some reason" / "no, it really is deletion not dictation."

  • a04752e: FIX, 5 findings.
    • A row closed elsewhere during the question stayed hidden.
    • The kept row could be closed twice.
    • An offline close unhid its row before the store applied it.
    • Undo left out offline rows.
    • Process lookups ran one at a time.
    • All five fixed in eccb37a.
  • eccb37a: FIX, 2 findings: reservations were held until every offline close ended, and a paused machine held back Undo. Fixed in 52e9596.
  • 52e9596: FIX, 1 finding: a late offline close reused undo state that was already consumed. Fixed in e5b0044.
  • e5b0044: no remaining findings. It checked that every entrypoint reaches the batch: Cmd-W, the row menu's Close, and the sidebar's Cmd-Delete. Close All in Group keeps its existing concurrent dispatch. The diff stays within the close path.

Fleet app compile at e5b0044: passed (job a0309d83b6ef613483ee34c1). Held for sidebar dogfood per Leo and Lawrence: not to be merged yet.

Review (codex gpt-6.1-sol, correctness first): SHIP at e5b0044

The nested reopened() helper was nonisolated and read main-actor store
properties, so the CmuxNext test target did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray
@cursor

cursor Bot commented Oct 22, 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo teamleaderleo added the hold cmux-next batch queue: keep this PR out of the batch (docs/ci/cmux-next-batch.md) label Oct 22, 2026
@cursor

cursor Bot commented Oct 23, 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.

@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

teamleaderleo and others added 2 commits October 26, 2026 03:06
…xNextApp does not grow

After the merge of the base, check-app-size refused this PR's 133 new
CmuxNextApp source lines (SW14). The batch close flow now lives in
CmuxNextAppCore (WorkspaceSelectionCloseFlow, injected closures as
WorkspaceCloseFlow does), with only its wiring left in the App
(WorkspaceSelectionClose). BulkClosePlan, DaemonService.servesBulkClose
and the multi-workspace close (now WorkspaceClose.close(_ members:)) move
to CmuxNextAppCore with it. InstantClose keeps one begin (a list of ids),
the Undo hook is an `undone` argument of WorkspaceGroupClose.offerUndo,
and Close Workspace's guard reads WorkspaceSelectionCloseFlow.canStart.
No behavior change intended. CmuxNextApp: 107684 source lines vs 107685.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-e6a8cd518b89570f9ff4
@teamleaderleo teamleaderleo removed the hold cmux-next batch queue: keep this PR out of the batch (docs/ci/cmux-next-batch.md) label Oct 26, 2026
@teamleaderleo
teamleaderleo merged commit a439d29 into feat-cmux-next Oct 26, 2026
168 of 179 checks passed
@teamleaderleo
teamleaderleo deleted the l5/batch-close branch October 26, 2026 12:04
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
Re-cut of #21835 on top of #24341's selection batch close. When a
selection holds every row of a group (and the group lists no link, tab or
app), the group closes with them as Close Group does once its rows have
closed: an unpinned group goes, and the same "Closed N workspaces" Undo
reopens the workspaces and then the group; a pinned group stays, folded.
Before, the group stayed behind empty.

SidebarModel.groupsEmptied picks the groups (CmuxNextSidebar); the batch
flow tells the App which live rows closed (liveClosed); an Undo record
for a group alone waits for that group's record (WorkspaceGroupClose).
Four hand-written group walks in CmuxNextApp use SidebarEdits.groups(in:)
so the App target does not grow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-e6a8cd518b89570f9ff4
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
Re-cut of #21835 on top of #24341's selection batch close. When a
selection holds every row of a group (and the group lists no link, tab or
app), the group closes with them as Close Group does once its rows have
closed: an unpinned group goes, and the same "Closed N workspaces" Undo
reopens the workspaces and then the group; a pinned group stays, folded.
Before, the group stayed behind empty.

SidebarModel.groupsEmptied picks the groups (CmuxNextSidebar); the batch
flow tells the App which live rows closed (liveClosed); an Undo record
for a group alone waits for that group's record (WorkspaceGroupClose).
Four hand-written group walks in CmuxNextApp use SidebarEdits.groups(in:)
so the App target does not grow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-e6a8cd518b89570f9ff4
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
Re-cut of #21835 on top of #24341's selection batch close. When a
selection holds every row of a group (and the group lists no link, tab or
app), the group closes with them as Close Group does once its rows have
closed: an unpinned group goes, and the same "Closed N workspaces" Undo
reopens the workspaces and then the group; a pinned group stays, folded.
Before, the group stayed behind empty.

SidebarModel.groupsEmptied picks the groups (CmuxNextSidebar); the batch
flow tells the App which live rows closed (liveClosed); an Undo record
for a group alone waits for that group's record (WorkspaceGroupClose).
Four hand-written group walks in CmuxNextApp use SidebarEdits.groups(in:)
so the App target does not grow.

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

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

lawrencecchen added a commit that referenced this pull request Oct 31, 2026
… become instances (#28266)

* arch: active-look glass scope, batch-close set and kill-switch reader become instances

Lowers the arch-ratchet rows these sites raised with no reviewed exception
(audit-rh, cmux-ad); no behavior change.

- CmuxNextDesign/GlassPanelView.swift: the seven ActiveLookScope statics
  (windows, pending, candidate, materialSite, installed, counts, kinds;
  1335e10, #25704) are one ActiveLookScope instance. The AppKit getter
  hook that its first ActiveLookGlassEffectView installs captures it.
  AppServices makes it once and hands it to the app's ThemeStore
  (ThemeStore.activeLook). The main window gets it through
  WindowRootView(activeLook:), every window-kit window through
  NSWindow.install (scope.store.activeLook) and WindowSurfaceView, and the
  backdrop through WindowMaterialView(activeLook:) and
  GlassPanelView(activeLook:). The hook itself is unchanged.
- CmuxNextAppCore/WorkspaceSelectionCloseFlow.swift: the static running set
  (a439d29, #24341) is WorkspaceSelectionCloseFlow.Running, one per app
  (AppServices.selectionCloses), passed to every batch close and read by
  Close Workspace. It stays app-wide, as the static was.
- CmuxNextUpdater/KillSwitchReader.swift: the static weak app reader and
  start(updater:) (52c918f, #24212) are gone; UpdaterService stops its
  previous reader through its own killSwitchReader property.
- CmuxNextApp/MainMenu+Placements.swift: removes the unused helpGroups,
  groupedItems and otherItems (dead since #24615), which pays for the
  AppServices wiring (app_lines -24).

Tests construct their own ActiveLookScope (InactiveWindowTintTests), and
one more check proves a backdrop with no scope keeps plain glass.
Not compiled or run here (no Swift on the agent VM); fleet compile-check
of the pushed head follows.

* arch: thread the active-look scope to every window owner, no ThemeStore slot; one getter hook per process

Review of 723ce04 (FIX): ThemeStore.activeLook put the one
ActiveLookScope in a public settable slot on ThemeScope.app.store, a static
by another name, read by six window owners through NSWindow.install.

- ThemeStore.activeLook is removed. AppServices holds the scope
  (AppServices.activeLook) and passes it down.
- NSWindow.install(kind:content:scope:activeLook:) takes the scope as a
  parameter with no default, and every caller passes it:
  - the main window (WindowController), from AppServices;
  - browser popups (BrowserPopupWindows, from AppServices) and sign-in
    panels (WebAuthSessionWindows from AppDelegate, then
    WebAuthSessionWindow, then BrowserPopupWindow);
  - the onboarding tool window (OnboardingService, then
    OnboardingWindowPresenter, then OnboardingWindowController);
  - Debug Settings (DebugSettingsService, then
    DebugSettingsWindowController);
  - DevTools and the page info windows, which are child windows: they pass
    their owner window's NSWindow.backdropActiveLook, which reads the
    owner's own WindowMaterialView (as they already take the owner's
    themeScope);
  - BrowserDebugWindow passes nil: it has no entry point.
- ActiveLookScope.install adds a marker method to NSWindow with
  class_addMethod. Only the first install can add it, so the getter hook is
  installed once per process and a later scope (a test's) never stacks a
  second hook over the first. A stacked hook could push the reader past the
  16-frame forcingDepth.
- Tests pass activeLook: nil where they built these windows.

Not compiled here (no Swift on the agent VM); the fleet compile-check of the
pushed head follows.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — c045eccd Deployed Oct 26, 2026 by teamleaderleo via namespace autofix #3287
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.

1 participant