cmux NEXT: customize every right-click menu as a tree (Settings > Menus, Customize Menu…)
Summary
Every right-click menu in cmux-next can be arranged by the person, from Settings > Menus or from Customize Menu… at the end of each menu (also Customize Right-Click Menus… in the palette, the app menu and cmux settings customize-menus).
The menu is edited as a tree, so any row goes anywhere at any depth:
- move any row into any submenu (Move ▸, More ▸, More › Options ▸ …) or back to the top level, by dragging its grip (before/after any row, between a submenu's rows, onto a closed submenu's name; at a submenu's end, sideways travel picks inside or after) or with Move to… (a native menu of every destination) or the row's right-click menu;
- move a submenu into another, with its rows; reorder rows inside a submenu;
- make your own submenus (New Submenu, rename, delete — its rows stay where it was);
- hide rows in place (dimmed; Show puts them back exactly there); add/remove separators; Reset to Default per menu.
Layouts persist per menu in cmux.json contextMenus.layouts.<menu> as {items: [row keys, "-", {key, title?, items}], hidden: [...]} (the earlier {top, more} shape still reads). A row the app adds later shows in the submenu the app makes it in, wherever the person moved that submenu, with its section's dividers; rows the app retires are dropped. Saving the app's own arrangement removes the key, so a reset menu keeps following app changes. contextMenus.showCustomizeItem turns the Customize Menu… row off (a schema row: palette, cmux settings, MCP; agents may set it).
How it works
ContextMenuLayout(CmuxNextActions): the tree,normalized(against:)/applied(to:);ContextMenuEntry.groupfor the person's submenus.ActionRegistry.contextMenuEntries(for:removing:)is the arranged lookup every whole-menu call site uses; call-site filters apply after it at any depth;makeContextMenu(customize:)ends whole menus with Customize Menu….- Settings > Menus (
webviews/src/pages/settings:ContextMenusCard,menuLayouts.ts,menuDrag.ts) writes throughContextMenusPageProvider(CmuxNextPages), which admits only the person's gesture on the Settings page. - Gesture fixes found while verifying: a Chromium page now counts a mouse-up as the person's input (a drag's release saves); a native-menu choice backs the page call (upstream landed the same fix during this PR; this PR uses theirs). Settings serves the
symbol/prefix so rows show their icons. - DEBUG-only:
debug.pagedrag/press(trusted DevTools pointer input on a Chromium app page, for no-focus verification).
Not arranged: the menu opened on several selected workspaces (Chrome's multi-select menu) keeps its own rows.
Changelog
- Added: Customize any right-click menu in Settings > Menus or with Customize Menu… at its end: move any row into any submenu, reorder, make your own submenus, hide, add separators, reset.
Testing
Fleet runs (no local builds):
- Swift (
package-test-lane.sh suite, Xcode 26.6): 23 of 25 suites pass atac1caa4, includingContextMenuLayoutTests,ContextMenusSettingTests,ContextMenusPageProviderTests,ContextMenuEssentialsTests,ContextMenuShapeTests,ActionSurfaceParityTests,FileExplorerTests,MachineIconTests,CEFPageGestureTests,PaletteSettingWriteTests. Base reds (also red at the merge baseb48588c):ContextMenuStandardTests(duplicate icons on upstream rows),TabSelectionActionsTests(upstream stash rows). CEFPageGestureTestsred at the test commit, green at the fix (release counted as the person's gesture).- Webviews (
run-webviews-tests.sh, focused):contextMenus,menuLayouts,render,categories,claimPaint,search,check-type-scalepass.rowSummaryfails on an upstream key (settings.layout.pagesHandleCommandScroll.summaryover 60 chars in bs/de/es…), base red. - Exports regenerated on the fleet (
regenerate-settings-exports.sh,regenerate-action-exports.sh). scripts/cmux-next/prepush-gate.pyclean.
Observed / Not run
- Observed in a tagged build (
menucustqa, LG HDR 4K, no focus taken): the workspace right-click menu ends with Customize Menu…, which opens Settings > Menus on that menu; a pointer drag reordered the Terminal menu and saved (no "only you can change your menus"); Reset worked; the drag recording shows the gap opening, no blinking line. - Not run in the real app on the final head: the tree moves (row into Move ▸, Move ▸ into More, own submenu) and the between-children drag; covered by the web tests above.
Reviews: two independent correctness reviews; all FIX FIRST findings fixed (dividers for unnamed submenus/sections, dividers never inside a reordered section, fast release and mid-drag refresh, rename by key, engine menu at any depth, setting on every surface, joinedMachine from #27245).
🤖 Generated with Claude Code