Skip to content

feat(cmux-next): Chrome's menu for several selected sidebar workspaces - #23110

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
feat/sidebar-multiselect-menu
Oct 21, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
feat/sidebar-multiselect-menu

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Right-clicking one of several selected sidebar rows (Shift- or Cmd-click) now opens Chrome's menu for several tabs:

  • New Workspace Below and Add Workspaces to New Group
  • Duplicate Workspaces, Move ▸, Pin or Unpin Workspaces, and Mute or Unmute Workspaces
  • More ▸, which holds everything else
  • Close Workspaces, Close Other Workspaces and Close Workspaces Below
  • Reopen Closed Workspace

The color swatches in the header color every selected row.

What the verbs now do on a selection:

  • Close Other, Close Below, Close Above keep the whole selection. Close Other closes every row outside it, Close Below closes the rows under its last row, and Close Above closes the rows over its first row. These closes go through WorkspaceBulkClose, so they ask the same question and show the same "Closed N workspaces" toast with Undo as before.
  • New Workspace Below puts the new row under the last selected row.
  • Mute changes every selected row in one direction. A row's own speaker still changes only that row: it now sets closesRowOnly, and rowTargets respects that flag.
  • Cmd-W closes the selection when the sidebar list has the keyboard and two or more rows are selected. The new key context is sidebar.multiSelection, and it's a scoped default that sends closeWorkspace. Anywhere else, or with one row selected, Cmd-W still closes the shown tab.

A one-row menu keeps its layout, and Reopen Closed Workspace is added to its More ▸ Close section.

A workspace group's menu and its editor now say "Close All in Group" instead of "Close Group". Tab and screen groups keep "Close Group".

#21835 (another lane) makes Close Workspaces on a selection one action, with "Close N Workspaces", one question and one Undo. This PR doesn't touch that path. Both PRs edit WorkspaceBulkClose.plan, but in different hunks.

The menu is a native NSMenu. The webview gallery can't draw it, and a tour's window snapshot doesn't include a menu, so this PR adds no gallery entry; the tests below check the menu's rows instead.

The new strings are translated into all nine languages. localization_catalog.py check reports no errors for the new keys; its other errors are needs_review states that are also present on base.

Tests added (not yet run; they run in CI):

  • ContextMenuEssentialsTests: the selection menu's row order and its 12-row cap, the rows that live in More, the one-row menu's Reopen Closed, and the selection titles.
  • SidebarSelectionVerbsMenuTests: WorkspaceBulkClose.keeping for Other, Below and Above; the sidebar.multiSelection key context; and how Cmd-W resolves with and without a multi-selection.

Changelog

  • Right-click several selected sidebar workspaces for Chrome's tab menu (pin, mute, duplicate, group or close them together, close the others or the rows below, reopen closed); Cmd-W in the sidebar closes the selection; a group's Close reads Close All in Group.

Agent session: cosmic-gray

🤖 Generated with Claude Code

Right-clicking one of several selected rows shows Chrome's menu for several
tabs: New Workspace Below, Add to New Group, Duplicate, Move, Pin, Mute,
Close, Close Other, Close Below, Reopen Closed. Close Other and Close
Below keep the whole selection, New Below goes under the last selected row,
Mute and the color swatches act on every selected row, and Cmd-W in the
sidebar closes the selection. A workspace group's Close reads Close All in
Group.

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 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 23a57b0: 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=pr23110 -f ref=feat/sidebar-multiselect-menu -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 23a57b0b3e; ci-status is green without them.

CI passes on 23a57b0b3e (run 39561080025 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

web and size checks: green

@teamleaderleo

teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Owned by projects-50 on cosmic-gray · session

@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude-review-subagent): SHIP at 23a57b0

No blockers. I read the code and traced it; I didn't build or run tests. cmux-next swift test and the Release compile were still queued when I looked.

What I checked

  • WorkspaceBulkClose.plan, default case: for CLI/MCP (origin != .user), rowTargets returns just the target, so keeping([target]) gives the same rows the old index slicing did. For a person's run, the kept set is the owner window's tree-ordered selection, which always contains the target (the menu targets ids.first). prompt calls the same plan, so the question and the close agree. The keeping cases are covered by the new tests.
  • closesRowOnly is set in only two places: ActionInvocation(closingRow:), which goes to closeWorkspace (that handler has its own check at AppActions+Workspaces.swift:149 and never calls rowTargets), and now the row speaker. So honoring it in rowTargets changes nothing for pin, duplicate, color, new-below or the bulk closes.
  • workspace.newBelow: try covers both sides of the ??, and for a single row the behavior matches the old create path.
  • Selection menu: I traced apply → makeContextMenu with the test registry. Row order is color choices, newBelow, moveToNewGroup, —, duplicate, Move ▸ (first child palette.moveWorkspaceToTop), pin, mute, —, More, —, the three closes, —, reopen. That matches theSelectionMenuFollowsChromesMenuForSeveralTabs, at exactly 12 rows. The one-row menu output is unchanged because an empty tail adds nothing; Reopen lands in More's Close section, which has 6 rows (≤ 8), so it stays inline. generatedMenusShowExactlyThePlacements, everyMenuKeepsAShortTopLevel and moreShowsItsShortFoldersAsSections still hold. The reopenClosedWorkspace handler ignores its target, so the workspace target is harmless.
  • Cmd-W: scoped entries are appended after the ranked default layer, so closeWorkspace wins only when sidebar.multiSelection holds. That key is set only for .sidebar focus with more than one row selected, so focus elsewhere or a single selection still runs closeTab. SidebarListView's keyDown doesn't handle Cmd-W, and KeyRouter runs before the responder chain anyway. The active row stays in the selection (toggleSelection and extendSelection keep it), so a no-target closeWorkspace hits closeTargets and closes the whole selection. The Ghostty reserved-keys list doesn't include Cmd-W. The test APIs exist with the right access: KeyFacts memberwise init, settable FocusState.resolved, KeyContext(_:), KeyBindingTable.resolve, bind(_:) {}.
  • Translations match the catalog's existing wording for Pin/Unpin/Mute/Duplicate. The renamed group label is consistent across ObjectMenuLabels, the editor, both tests and both catalogs.
  • The only red, cmux-next crash-class rules, is not this PR's. It flags CmuxNextBrowser/UI/BrowserToolbarButtonsView.swift:115 super.viewDidHide, a file this PR doesn't touch.

Nits (none block)

  1. Three rows make a new group. The top level now has Add Workspaces to New Group. More still has Move to Group ▸ … New Group (the choiceTrailers row is still drawn by makeChoicesItem) and Group Selected Workspaces. Consider dropping one of them from the selection menu.
  2. The selection menu's close row still reads "Close Workspace" while it closes every selected row. The doc comment at ContextMenuCatalog+Essentials.swift:51 says "Close Workspaces". Adding closeWorkspace to NSMenu.workspaceSelectionTitle would match Chrome's Close Tabs.
  3. Locales: the new keys carry the 9 required locales, but the strings they replace (sidebar.groupEditor.close and the old group label) had 21. So bs, da, it, km, nb, pl, pt-BR, ru, th, tr, uk and vi now show English "Close All in Group" and English selection titles, while almost every other key in both catalogs carries 21.
  4. Stale comments:
    • SidebarContainerView+GroupEditor.swift:26 still says "Close Group".
    • SidebarContextMenus.swift:75 says "the right-clicked row". The menu's target is actually ids.first, the first selected row in row order. The pin handler's comment has the same wording, which predates this PR. Behavior is consistent either way, because the label, the swatch ring and the action all read the first row.
  5. Edge case: scopedEntries drops the Cmd-W entry only when closeWorkspace itself is overridden. If a user unbinds or rebinds closeTab's Cmd-W in cmux.json, the sidebar's Cmd-W still closes workspaces.
  6. Edge case: a no-target Cmd-W first tries closeNewestCreate (AppActions+Workspaces.swift:144). With a create still pending, Cmd-W in a multi-selection closes that pending workspace instead of the selection.
  7. Possible follow-up (this predates the PR): the row's hover pin (SidebarWorkspacePins) still widens to the selection. The speaker now stays per-row through closesRowOnly; the pin could do the same.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Tests at 23a57b0, run on the build fleet with package-test-lane.sh suite because GitHub's cmux-next swift test has been queued for about an hour:

  • SidebarSelectionVerbsMenuTests, ContextMenuEssentialsTests, ContextMenuShapeTests and GroupPaletteDefaultTests passed.
  • cmux-ci compile-check --app passed (job 46a7df971a01be2637a292d9).
  • The only red is cmux-next crash-class rules, which fails on base too, at BrowserToolbarButtonsView.swift:115, a file this PR doesn't touch.

The review receipt is above. Merging.

@teamleaderleo
teamleaderleo merged commit 6b19b5d into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the feat/sidebar-multiselect-menu branch October 21, 2026 08:51
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…guage (#23225)

#23110's new strings (Close All in Group and the selection menu's titles) had
only the 9 required locales, while the strings they replace had 21. Add bs, da,
it, km, nb, pl, pt-BR, ru, th, tr, uk and vi, in the words the catalog already
uses for tabs and workspaces. Also fix two comments #23110 left stale.


Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

teamleaderleo added a commit that referenced this pull request Oct 22, 2026
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
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…ndo (#24341)

* Test: closing 50 selected sidebar workspaces closes them all at once, 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

* Close a sidebar selection as one batch: one pass, one question, one undo

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

* Selection close: end every hidden row, hold reservations until offline 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

* Selection close: release each row as it settles; Undo never waits for 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

* Selection close: an offline close that ends after Undo no longer reoffers 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

* Test: read the reopened workspaces on the main actor

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

* CI: start a cmux-next run (the run at 90ca309 hit startup_failure)

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-next: move the selection batch close into CmuxNextAppCore so CmuxNextApp 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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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