cmux NEXT: move composer menus onto shared UI primitives

TE
2d ago
+507-533

Batch 1 moves composer attach/plus, slash, mention, and permission picker menus onto the shared ui/Menu and ui/Listbox primitives. The shared popup owns focus, keyboard navigation, glass, row metrics, dismissal, and popup direction; editor-owned slash/@ focus remains virtual and native popup transport stays in ui/nativePopup.ts.

Leo’s direction: “there's just tons of design systems related stuff for spacing gutters icons etc whatever like we don't yet feel "designed" yet” and “i generally also have disliked how we have like no sense of system at ALL for ANYTHING like we're not even looking at some shadcn type stuff”.

Leo's composer/picker direction (2026-11-04): “Oh our composer hat but with nicer looking stuff”.

This supersedes the remaining composer-menu integration work in #27616 while preserving its settled grid metrics. #27616 remains the source for the shared menu-grid CSS and is held by its owner; this PR consumes those metrics through ui/Menu.

References used: claude/live/composer-add-menu, claude/live/composer-slash-commands, claude/live/composer-mode-menu-claude-code-reference, codex/live/composer-plus-menu-add-actions-and-plugins.

Gallery coverage remains in agent-pane.composer-plus, agent-pane.composer-plus-menu, agent-pane.slash-menu, agent-pane.file-picker-loading, agent-pane.effort-picker, and agent-pane.composer-pickers.

Verification:

  • bun run typecheck (4279 files, pass)
  • bun test src/agent-session/acpmux/MentionMenu.test.tsx src/agent-session/acpmux/PaletteMenu.ime.test.tsx (21 pass)
  • bun test test/composer-menu-dismiss.test.ts test/composer-field-menu.test.ts test/composer-menu-width.test.ts test/access-chip-refused-mode.test.tsx test/home-composer-menu.test.tsx test/composer-searchable-pickers.test.tsx (40 pass, 3 browser tests skipped outside CI)
  • bun scripts/check-ui-review.ts --shrink-allowlist && bun scripts/check-design-tokens.ts --update-baseline
  • bun test src/agent-session/acpmux/slashViewport.test.ts src/agent-session/acpmux/composerFocus.test.ts (18 pass, including Interface Size scaled-row geometry)

🤖 Generated with Codex

Agent session: agent session


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Moves the composer's attach/plus, slash, mention, and permission picker menus onto the shared ui/Menu and ui/Listbox primitives, replacing the hand-rolled popup, focus, and keyboard-navigation code in ComposerPickers and the slash/mention lists. Behavior is preserved, including the editor-held virtual focus in slash/mention suggestions, the current choice's checkmark, and focus returning to the opener after closing (covering cancelled native picker captures). Tests query the portaled rows from the popup host and follow the picker's role change from listbox/option to menu/menuitem, including the latency flow selectors.

Shared components

  • ComposerPickers renders through Menu/MenuPopup/MenuItem, whose shared positioner owns placement, dismissal, glass, and row metrics; popups portal to the shared popup host, and the backdropLayer/useUiAnchor plumbing is removed.
  • SlashMenu and MentionMenu use Listbox with a controlled active index so the prompt keeps focus; controlled lists stay out of the tab order, and the mention loading row renders as the empty slot so it cannot duplicate.
  • Listbox gains activeIndex, onActiveIndexChange, listRef, empty, footer, getItemId, aria-busy, id, window, and onScroll props, plus scroll-into-view for the active row; the slash menu's windowed scrolling and offscreen spacers now live in the shared component.
  • Slash rows move to the shared 28px row grid (previously 31px), and the virtual list measures the rendered row so its windowed scroll stays aligned under interface scaling.
  • MenuItem gains data-value and pointer/mouse hover handlers, and migrated rows force the shared hover background, since WebKit's pointer navigation does not always set data-highlighted on portaled rows.

Styling and hit-testing

  • Portaled picker menus keep the + menu's width, glass tint, popup cap, and reduced-transparency behavior through .acpmux-composer-plus and .acpmux-menu selectors, and hide the picker scrollbar so scrolling does not shift the width.
  • The mask-based edge fade becomes a sticky pointer-transparent overlay sized by the shared scroll-fade tokens, because WebKit treats masked pixels as outside hit-testing and would make row edges unpressable; the fade's space is reserved in the popup padding.
    • menu rows are inset from the popup edge and get a dedicated .acpmux-menu-hit-area cover on the raised layer (row content stays on the base layer), so the whole row surface stays pressable where the fade overlay overlaps it.
  • Migrated menus leave the UI review allowlist, and tests query portaled rows from the popup host instead of the composer wrapper.