Repository navigation
Close a sidebar selection as one batch: one pass, one question, one undo - #24341
Conversation
… 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
|
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)
web and size checks: green |
This comment has been minimized.
This comment has been minimized.
|
Bead |
|
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."
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
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. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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. |
This comment has been minimized.
This comment has been minimized.
|
Owned by an agent session · session |
…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
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
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
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
|
Shipped in:
|
… 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.
Leo, closing several selected sidebar workspaces (the multi-select close from #23110):
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+Intentshandled that by running Close Workspace once per row (ActionInvocation(closingRow:)), each a full single-workspaceWorkspaceCloseFlow: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.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.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 titleClose N workspaces?and the toastClosed N workspacesare 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):InstantClose.begin([ids])).Close N workspaces?(the existingWorkspaceBulkClose.prompt, one line). Its process lookups now run concurrently.Closed N workspacestoast with Undo (WorkspaceGroupClose.offerUndo, the same toast the other bulk closes use) reopens all of them.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
WorkspaceCloseFlowuntouched.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 oneClosed 50 workspacestoast shows, and its Undo reopens all 50. It prints the time from the press until all 50 are gone.aSelectionWithRunningProgramsAsksOnceAndClosesEveryRow: 9 selected rows (3 runningsleep 600), Close from the row menu (closeWorkspacetargeted 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>lightclass, 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)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?andClosed %lld workspacesalready exist in every locale ofHandlers.xcstrings.Observed / Not run
sleepon Cmd-W. The rows left, then oneClose “Deletion 12”?question came up, one per row and queued behind each other. After the fix (a04752e), the same steps show oneClose 10 workspaces?question and then a one-lineClosed 10 workspaces Undotoast. A base capture of 24 idle rows closed them all eventually, but row by row, over several seconds.prepush-gate.pyandcheck-concurrency.share clean.Update after taking over (base merge, 2026-10-26)
Merged
feat-cmux-nextinto the branch (no force-push). On today's basecheck-app-size.shrefused the 133 new CmuxNextApp source lines (SW14), so the batch flow moved intoCmuxNextAppCoreasWorkspaceSelectionCloseFlow(closures injected, likeWorkspaceCloseFlow), with only its wiring left in the App (WorkspaceSelectionClose).BulkClosePlan,DaemonService.servesBulkCloseand the multi-workspace close (nowWorkspaceClose.close(_ members:)) moved with it.InstantClosekeeps onebeginover a list of ids; the toast's Undo hook is anundone:argument ofWorkspaceGroupClose.offerUndo; Close Workspace's guard readsWorkspaceSelectionCloseFlow.canStart. No behavior change intended; CmuxNextApp is 107684 source lines vs 107685 on base.cmux-ci run, package-test-lane):SidebarSelectionBatchCloseTests,BulkClosePlanTests,BulkCloseReopenTests,SidebarCloseSelectionTests,WorkspaceNothingToDoRefusalTestsall 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.cmux-next swift testred: 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).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-4441620e5cate5b004496945(fleet build, capture mini, text checked for private data)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 oneClose 10 workspaces?question, then the one-lineClosed 10 workspaces Undotoast.Agent session: cosmic-gray
🤖 Generated with Claude Code