cmux NEXT: Claude Code style model menu with an Account switcher

Closed
TE
17d ago
+1922-99

Summary

Leo asked for the composer's model and effort pickers to look like Claude Code's, and for an Account switcher under Effort.

A click on the model chip now opens a short rounded menu headed "Model · this session":

  • the harness's top models (the same list the full picker shows first, with the running model among them). Keys 1–9 pick a model in place. The running model's check sits at the trailing edge, where the other rows show their number.
  • Effort › shows the current level and opens a submenu of the model's levels, plus Ultracode where the model offers it, then Fast Mode (or Codex's Service Tier). Before the session reports, it uses the held levels the old held chip used, and the chip names a held pick.
  • Account › lists the accounts for this chat and checks the active one. A pick switches the chat.
  • More models › opens the existing full picker (harness rail, search, Add harness…). Typing a letter in the short menu opens the full picker searching for it. Cmd-Ctrl-M still opens the full picker directly.

The chip itself is a quiet label, "Opus 5.5 Medium" ("Opus 5.5 Med" in a narrow row), with no chevron, and the separate effort chip goes away. The bolt beside the mic and the permission chip are unchanged. Fast mode is also in Effort, because the bolt drops out of narrow rows.

Where accounts come from

acpmux routes (_acpmux/route/list): a direct subscription, an API key, subrouter, CodeRouter, the cmux router and so on. Switching calls _acpmux/chat/route.set with when: "after-turn". acpmux binds the route to this chat and restarts the harness on it, resuming the same agent session. An idle chat switches at once; a running turn finishes first. "CLI login" unbinds the chat, so it goes back to its workspace or family route, or to the harness's own login. A chat with no routes yet shows "CLI login" and "Add accounts with cmux route add". If acpmux refuses a switch (the route is down, or it serves another family), the Account row says "Couldn’t switch account". The menu reads the list each time it opens and never polls. Only a chat on this Mac's acpmux gets the row: acpmux refuses routes to Web and peer connections.

Known gap: a chat on a profile's own route (the cmux harness's built-in cmux route) with no chat binding shows "CLI login", because route/list's active doesn't read the profile route. I'll fix that in acpmux separately.

#21852 adds serves per route to route/list, so this menu lists only the routes that can run the chat. Against an acpmux without it, every route is listed and acpmux still refuses a pick that can't serve the chat.

Not covered: several OAuth logins of the same CLI (two Claude Max accounts). A route sets an endpoint and key, not CLAUDE_CONFIG_DIR or CODEX_HOME, and per-session env refuses those keys. Adding a config dir to a route isn't enough on its own: Claude and Codex keep their transcripts under that directory, so the resume after a switch would not find the session. That needs acpmux to carry the transcript across, which is a separate change.

Debug Settings

agent.modelMenu: Compact (default) or Full (the chip opens the rail picker, as before). The value reaches the page in the composer event. A page with no host, such as a gallery or a test, keeps the full picker, so the existing picker tests are unchanged.

Stacks on feat-cmux-next. It doesn't overlap the lines changed by #21701, #21702, #21704 or #21744. In ui/Menu.tsx it only adds a buttonRef to MenuButton, which #21705 doesn't touch.

Testing

  • webviews/test/model-menu-compact.test.tsx (new, JSDOM, real composer and catalog): checks the chip text and that there's no effort chip, the menu rows, number keys picking a model, the Effort submenu sending the level, Account listing only the serving routes with the active one checked and a pick calling set, the empty-routes note, and More models / typing opening the full picker with the query.
    • Red on the test-only commit afec0982 (module missing).
    • Green on f8641f7e: 6 pass.
    • Review fixes (24433c4a) add 3 more: fast mode under Effort, a refused switch named on the Account row, and Cmd-Ctrl-M over the compact menu leaving the full picker open. The last one fails with the fix reverted. 9 pass.
  • Existing picker tests (model-picker-*, EffortPicker.test.tsx and every test importing ComposerPickers, composerSettings or ui/Menu): 89 pass.
  • bun run check: passes (format, lint, types, layers, type scale, protocol).
  • scripts/localization_catalog.py check: no errors on the six new keys (picker.modelThisSession, picker.account, picker.harnessLogin, picker.accountHint, picker.accountFailed, picker.accountSwitchFailed). Each is translated in all 21 pane locales.
  • Swift: added AgentPaneModelMenuStyleTests and a page-value test. Not run locally (no local Swift builds on this machine); CI runs them.
  • Not checked live in a tagged build yet. A JSDOM probe showed that Base UI submenus close their parent when the root menu has no trigger button, so the chip is a real MenuButton.

Changelog

Changed: The model chip opens a short Claude Code style menu with numbered models, Effort, an Account switcher and More models

Proof

No screenshots yet (no local app build); dogfood with the dev-build label.

🤖 Generated with Claude Code