Agent chat: a one-tab pane has no tab-row band; the chat runs up under the tab and its buttons
Summary
Leo's ask (2026-11-03): "well ok this current version the top row area... man what if we could? like just... have even more of that area be not some row blocking it? cuz isn't it kinda arbitrary anyway if you literally only have one tab there? sure you can use it to click and drag the window but like... that's not that big of a deal, is it?"
When a pane holds one tab, its tab row is no longer a band over the chat:
- The transcript runs up to the pane's top under the row. The tab and the header tools (Session Info, terminal, globe, ā®) float over it.
- Text that scrolls under them fades out across the row under a light 6px blur (masked so it fades out at the row's foot), so the tools stay legible.
- The chat's own surface runs through the whole row, so there's no strip of another shade. It covers the row's full width, so it never makes a rectangle behind the tools alone (#32944, #29618).
- At rest the first message sits where it did below the row. The page draws the rows the row's height lower (
VirtualTranscripttopInset) and grows the content by the same amount, so following the latest row, the composer gap and the prompt rail don't change. - When a line sits under the header (a live chat's choice, a peer copy, a watched chat, a handoff error), the transcript stays below it as before, so it never covers that line's buttons.
- The window still drags from the strip's empty space, which stays above the page.
- Two or more tabs: no change.
How it works:
- Native:
TabStripModel.holdsOneTabdecides it.PaneTabRowSharing.offerTabRow(... soloTab:)āsetTabRowSoloāAgentPaneTabRowaddssoloTabto thetabRowevent. The strip lays its pane out again when it gains or loses its second tab (TabStripView.syncFromModel). The strip stays clear, aspaintStripalready keeps it. - Web:
header/tabRow.tsreadssoloTab. The shell and stage getdata-solo-tab.header.csspulls the scroller up under the row, sets the fade over the row's height, adds the blur layer (::before,--acpmux-solo-blur) and offsets the Find bar and the prompt rail below the row. Reduce Transparency drops the blur. - No page title comes back beside the tools (#32007). With one tab, the native tab pill is the title.
Prior art: #32971 (fade, not a band, under the row), #32944 / #29618 (nothing painted behind the tools), #32039 (row surface), #32007 (no title in the row).
References: cmux/live/titlebar-top-row-black-bar-instead-of-blur (Leo wants "the tiny blur at the top", not a bar), cmux/live/tabs-top-right-buttons-background, cmux/live/top-right-toolbar-actions, cmux/live/toolbar-top-right-stray-line.
Spec: design/tabs.md and design/chat-header.md (the rule is added to tabs.md in a follow-up spec PR).
Gallery
agent-pane.header adds 4 variants:
solo-tab-rest: at rest, no band.solo-tab-blur: chosen, scrolled, with a 6px blur.solo-tab-fade-only: no blur. Text under the tools stays sharp and reads as clutter.solo-tab-blur-strong: 14px blur. It frosts the row into a band again, which is what this PR removes.
I picked the 6px blur: a bubble edge passing under the tools softens instead of cutting, and the row never reads as a band of its own.
Testing
CMUX_BROWSER_TESTS=1 bun test test/chat-header-solo-tab.test.ts: 5 tests. Red on base e674d0b4103b: the first 4 written all fail. Green here, 5/5 (the fifth covers a line under the header), in Chromium and WebKit. The real pane checks that the scroller starts at y 0 under a 40px row, the first row is within 1px of where it was, and the row goes back to normal on two tabs. The sheet test checks that the row has the page's colour at rest, faded text under it when scrolled, full ink below it, and a blur on the header.test/chat-header-tab-row-surface.test.ts(2+ tabs, #32971 and #32944) still passes: 34/34.- Swift:
AgentPaneTabRowSoloTests(the event carriessoloTab, and only for an offered row) andTabStripHoldsOneTabTests. These run on the fleet compile below. scripts/web-lint.shpassed.python3 scripts/cmux-next/prepush-gate.pyreported no new violations.
Observed / Not run
- Observed: the browser tests above, plus gallery captures of the variants in local headless Chromium.
- Not run: a signed DEV build on a Mac. Not checked there: the native pill over the blurred row, window drag on the strip's empty space, and a Chromium-hosted chat (its native holes leave the pill uncovered).
Changelog
- Changed: A pane with one tab no longer shows a band across its top: the chat runs up under the tab and its buttons, softly blurred.
Agent session: agent session
š¤ Generated with Claude Code
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith Ā· Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
When a pane holds a single tab, the tab row is no longer a band over the chat: the transcript runs up under the row, and the tab and header tools (Session Info, terminal, globe, ā®) float over the chat.
- Text scrolling under the tools fades out across the row with a light 6px blur, so the tools stay legible; Reduce Transparency drops the blur.
- The chat's own surface runs through the whole row, so no strip of another shade or rectangle behind the tools appears.
- The first message rests where it did below the row; the composer gap and prompt rail are unchanged, and prompt-rail jumps land below the floating row.
- Every line in flow under the header (a live chat's choice, a peer copy, a watched chat, a handoff error, a Create Checkpoint review) keeps the transcript below it ā all are gated by one record the transcript reads, so a new line can't be missed by the solo layout.
- The window still drags from the strip's empty space; two or more tabs keep the row as before.
The native side decides a one-tab row via TabStripModel.holdsOneTab and tells the page through a soloTab field in the tabRow event (it follows the native lone-tab hiding, not a second signal); the strip lays its pane out again when it gains or loses its second tab, including after tabs changed while the strip was hidden. The web side pulls the scroller up under the row and offsets the rows by the row's height. Tests cover the solo row, the fade/blur under the tools, and the line-under-header cases, including the checkpoint review.