cmux NEXT: a pane's tab strip shows with one tab too; its empty space drags the window again

Merged
TE
3d ago
+34-38

Leo (2026-11-03):

"oh shit you guys completely removed the top there is nothinig at the top until you make a tab... which... is interesting... but kinda weird... i also can't drag from this top area at all? idk if this is actually a bug tbh"

"i think you guys are currently hiding it completely when there's only the one tab... but i think that's just too alien of a look :("

Regressed by #33132 (cmux NEXT: a chat alone in its pane hides its lone tab, so the page takes the tab row). That removal was deliberate: Leo asked for it that morning, and design/tabs.md recorded it as a settled design, quoting his "sure you can use it to click and drag the window but like... that's not that big of a deal, is it?". His words above replace that design.

What changes. A pane's tab strip shows at every tab count, one included:

  • PaneContentView.layout no longer hides the strip for a lone chat tab. The strip keeps the same height and frame with zero, one or several tabs, so nothing moves when a tab arrives.
  • The strip's empty space is the window's titlebar again, through the existing TabStripView.titlebarPress / TitlebarDragPolicy path: drag moves the window, and a double-click runs the user's titlebar action. Tabs, chips and + keep their own presses, and tab drag-reorder is unchanged. No new mechanism.
  • Removes PaneTabRowSharing.tabRowTakesLoneTab and TabStripModel.hasLoneTab, which had no other users.
  • The transcript's solo-tab inset (#33142, soloTab: on && !row.tabsShown) only applied while the lone tab was hidden, so it no longer flips when a pane goes between one and two tabs. That flip moved the chat by a row height.

Nothing else is restyled. The chat's header tools still share the row beside the tabs.

Check. TabRowToolsStayClickableTests.theStripShowsAtOneHeightWhateverItsTabCount (replaces aLoneChatTabGivesTheChatTheRow, which pinned the hide). With a chat sharing the row and 0, 1 and 2 tabs, it checks:

  • the strip is shown, with the same frame at every count;
  • the chat is told the pane draws its tabs;
  • a point in the strip's empty space resolves to .empty (the titlebar hit that moves the window), and every tab and + rect resolves to .strip.

Follow-ups. With the hide gone, the hidden-tab paths in AgentPaneTabRow (tabsShown == false: soloTab, the Chromium holeFill, the zero-width tabs hole) and the page's [data-solo-tab] CSS are dead code. They can go in a removal PR.

Spec: leo-workspace design/tabs.md ("A chat alone in its pane draws no tab" is replaced; updated in a notes PR).

Risk: small, one component (pane chrome layout). It removes a hide condition and adds no new state or timers.

Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

Shows a pane's tab strip at every tab count, one included, so a chat alone in its pane no longer leaves a bare, undraggable strip of page at the top. The strip's empty space is the window's titlebar again: it drags the window and double-click runs the user's titlebar action, while tabs, chips, +, and tab drag-reorder keep their own presses. The strip keeps its height and frame at every count, so nothing moves when a tab arrives.

Removes PaneTabRowSharing.tabRowTakesLoneTab and TabStripModel.hasLoneTab, which had no other users. The transcript's solo-tab inset only applied while a lone tab hid the strip, so it no longer flips when a pane goes between one and two tabs. The hidden-tab paths in AgentPaneTabRow and the page's solo-tab CSS are now dead code, left for a removal PR.