fix: composer chips collapse in narrow panes instead of overlapping Send

Closed
TE
18d ago
+5930-642

In a narrow pane the agent composer fell apart. At 280 px, and at 360 px with interface scale 1.25, the Mode chip drew over the mic and Send. On the New Tab page the model chip's icon was squeezed to a 4–13 px sliver.

Base's compact composer row (#20454, hq#1957 item 15) now sets the collapse order: the permission label goes first (composerRowFit.ts), then Effort shortens, and the model shrinks last. This PR keeps that order and adds three things it lacked:

  • The composer decides, not the window. The composer and the New Tab card are CSS size containers (container: composer). Their 480 and 220 px tiers were @media rules, which a split pane never triggered; they now follow the composer's own width. UI scale and the New Tab page take the same steps.
  • One more step for the chat bar, where its row cannot fit. After the permission label, Effort and the model have given way, Mode still runs into the mic below a 298 px composer: a pane under about 334 px, since the composer pads itself 18 px on each side. A 334 px pane leaves the row's 12 px group gap. In this step the model goes to its icon (its name stays in aria-label/title), the permission label leaves the row the way data-fit="icons" takes it out, the chevrons go, the group gap narrows to 8 px, and the card reaches 10 px from the pane's edges (not in quick chat). The 220 px step keeps base's row variables (#20785).
  • New Tab pickers. They now use the row's chip rules, so icons keep their size and the model and Effort shorten in the same order. The narrow step above doesn't apply to them: the New Tab card often shows the model alone, and an in-app capture at 280 pt showed it cut to its icon with half the row empty.

The turn's own controls (+, mic, Stop, Send) never shrink.

Testing

A new gallery entry, agent-pane.narrow, covers 9 states: controls, working, pull request bars, New Tab, code block, a chat-wide permission grant, and the model menu, access menu and summary card each opened by a play. Its presets are 280/360/480 px, so the gallery PR diff renders those widths. Each play checks the rendered geometry:

  • nothing scrolls the page sideways;
  • no control overlaps another or leaves its row;
  • no icon is drawn narrower than its aspect;
  • Send, Stop and the mic keep their full size;
  • clipped text ends in an ellipsis.

The menu plays also fail on a clipped or covered popup. Effort must stay visible in every state, the model must show its name at ≥ 340 px, and at ≥ 700 px the whole composer must show.

webviews/test/narrow-layouts.test.ts drives those plays in Chromium at 280/1, 360/1, 480/1, 360/1.25, 480/0.8, 760/1 and 1200/1 (scale is emulated as WKWebView pageZoom).

  • On the merged base row with only the container sizing applied, 3 of 7 sizes fail: Mode overlaps the mic and Send at 280 and at 360×1.25, and the New Tab model icon is squeezed.
  • An in-app capture at a 360 pt split showed the model cut to its icon with about 90 px of the row free, because the first threshold (330 px) measured the composer's content box. The model-name check fails on 94712979857f at 360 px in 8 states (6 pass, 1 fail).
  • With the step at 298 px (d83448da52e2), 7 of 7 pass: CMUX_BROWSER_TESTS=1 bun test test/narrow-layouts.test.ts.
  • Base's own row tests pass on this head: composer-row-layout 27/27, composer-row-fit 3/3, composer-menu-width 10/10. composer-menu-width's new "complete control rail" case (#20785) put .acpmux-composer and .acpmux-composer-bar on one element, so no container query reached the bar, and CI failed it at 180 px (scrollWidth 181). Its fixture now nests the bar inside the composer, as Composer.tsx renders it.
  • bun run check passes.

Localization: no user-facing strings change.

Changelog

Fixed: In a narrow pane or at a larger UI scale, the composer's controls no longer overlap the mic and Send

Proof

The Showcase stills below are real cmux NEXT captures from DEV builds of d83448da52e2 (head) and its base d28e48caa888. Each shows a chat split to 280, 360 and 480 pt, dark and light, base on the left and head on the right; the location strip is blurred to hide the capture mini's host name.

  • At 280 pt, base draws the Mode lock on top of the mic. Head keeps every control separate: +, the model's icon, "M…", the lock, the mic and Send.
  • At 360 and 480 pt the row is unchanged: the model shows its name and Effort its value.

Review

A correctness review subagent found four issues, all fixed in 8b338161e8de:

  • New Tab wrap: in a narrow pane the New Tab card's mic and Send wrapped to a second line. A new same-line check fails at 280, 360 and 360×1.25 on 83b8921906906.
  • New Tab location: the 480 px location tier no longer reached New Tab's location row. The container is now .nt-composer.
  • "Add agent": in the narrow step it became a blank button.
  • Comment: it overstated which names stay in aria-label.

Left as is:

  • Just above 298 px the model name and Effort value end in a near-ellipsis. That is base's shrink order (#20454).
  • The card's 8 px outset in the narrow step is not applied to the location strip.

Not in this PR

  • The split-pane minimum width (native, layout.minimumPaneWidth, default 200) comes in a separate PR.
  • The tab strip at narrow widths also comes in a separate PR.

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited, and the result is stated above
  • Reviewed with a subagent before merge, and all bot and human review comments resolved

Showcase

narrow composer dark

narrow composer light

Open the live gallery

🤖 Generated with Claude Code