cmux NEXT: customize every right-click menu as a tree (Settings > Menus, Customize Menu…)

AZ
6d ago
+12084-69

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.group for 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 through ContextMenusPageProvider (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.page drag / 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 at ac1caa4, including ContextMenuLayoutTests, ContextMenusSettingTests, ContextMenusPageProviderTests, ContextMenuEssentialsTests, ContextMenuShapeTests, ActionSurfaceParityTests, FileExplorerTests, MachineIconTests, CEFPageGestureTests, PaletteSettingWriteTests. Base reds (also red at the merge base b48588c): ContextMenuStandardTests (duplicate icons on upstream rows), TabSelectionActionsTests (upstream stash rows).
  • CEFPageGestureTests red 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-scale pass. rowSummary fails on an upstream key (settings.layout.pagesHandleCommandScroll.summary over 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.py clean.

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