cmux next: the model menu keeps its size as you switch provider tabs

LU
9d ago
+0-0

Summary

the model button's compact menu opens above its chip, so its bottom edge is fixed and its top edge moves with its height. each provider tab on the rail lists a different number of rows: the chat's own tab adds Effort, Account and Fast mode, another harness has more or fewer top models, a signed-out one has a note and Log In. so a click on a provider tab grew or shrank the menu from the top, and the rail tabs moved out from under the pointer. the width changed the same way: the menu is as wide as the shown tab's longest row.

fix: the list keeps the tallest tab's height and the widest tab's width for the whole opening.

  • tabExtent counts one tab's unfiltered rows and separators, the same way the list draws them.
  • QuickMenuSpacer draws the tallest tab's rows and separators unseen (visibility: hidden, aria-hidden, inert) in the same grid cell as the real list, with the same row and separator classes. the cell is as tall as the tallest tab.
  • for the width, the spacer also holds every tab's rows at no height (model names with their number key or the chosen model's check, Effort, More models, Account, Fast mode, the update row). the cell is as wide as the widest tab, still inside the menu's 220 to 360 px.
  • the spacer puts a separator after each of its first rows, so separator margins do not collapse.
  • a typed filter (up to nine rows across tabs) is counted too, so typing keeps the same height.
  • a tab click, arrow key or typed letter scrolls the list to its top: the held height keeps a tall tab's scroll range.
  • a menu taller than its room still scrolls inside it, as before.
  • no measuring, no effects: the size comes from the families the menu already has.
  • a typed filter can still widen the menu: a filtered row adds its harness's name. holding room for that would make every menu wider than its tabs need.
  • the menu still opens above its chip, clear of the new tab's suggestions.

the full picker (Cmd-Ctrl-M) is not changed. it already never shrinks within one opening (d9ce210e).

Preview (model menu: switching provider tabs)

Testing

new test in webviews/test/model-menu-compact.test.tsx, committed before the fix:

  • every rail tab fits in the same list height and width, the tallest and widest tab's, for the whole opening: opens the real composer's compact menu, clicks every rail tab, counts the drawn rows and separators, and expects the held space to stay the same, fit every tab and equal the tallest. then types a letter and expects the same space.
  • it also checks that no two spacer separators touch, that typing resets the scroll to the top, and that the width probe holds every tab's model rows, the same on every tab.
  • red on the base (no held space), green on the head.

Observed / Not run

  • observed: bun test test/model-menu-compact.test.tsx: 22 pass. the related menu tests (12 files): 31 pass, 7 skip, 0 fail.
  • observed: scripts/cmux-next/prepush-gate.py and scripts/web-lint.sh pass on the head.
  • observed: one read-only review of the diff. no P0 or P1. three P2s, all fixed in 9ff81270b5b: spacer separator margins collapsed, a short tab could keep a tall tab's scroll position, a filter could be taller than every tab.
  • observed: one read-only review of the width change. no P0 or P1. one P2, fixed in 724ea2ddd59: the chosen model's row has a check with its own padding, wider than a number key.
  • observed: a tagged local build of this branch (before 724ea2ddd59). the menu on a new tab measured 309 x 321 pt on the OpenCode tab; the build before the width change measured 245 pt wide on Claude Code and about 307 pt on OpenCode.
  • observed: the width probe's CSS alone in WebKit (playwright): 258 px without the probe, 309 px with a longer row in it.
  • not covered by tests: real pixel sizes (jsdom has no layout). the spacer uses the real rows' classes, so its rows get their heights and widths.

Changelog

Fixed: the model menu keeps its size as you switch provider tabs, so the tabs stay under the pointer.