CmuxNext: one container level and consistent edge padding (batch 1)

Merged
TE
23d ago
+478-188

A browser pane drew two boxes: the tab strip and toolbar sat above a second rounded, outlined box that held only the page, so the page's top corners rounded under the toolbar. The curved main pane's corners used a fixed 8 pt radius 6 pt in from the window's ~16 pt corners, so the gutter pinched at the window's bottom corners. Inside the curved main pane, every pane was its own hairline-outlined, rounded box 2 pt in (4 pt at column edges), and beside the sidebar the main pane's 6 pt gutter stacked on the sidebar's own 6 pt row inset. First-party pages each picked their own edge padding, and Settings nested an outlined form card inside its row card.

This batch fixes those. Every pane now has one rounded content area below its tab strip; a browser's toolbar sits inside it and the page is square under it. The main pane's radius is the window's corner radius less the gutter. The main pane's gutter is now the only inset: panes inside it are flush and split by one divider line, and the main pane sits flush against the sidebar, so the sidebar's 6 pt row inset is its inner gutter on both sides. Web pages share one --page-edge token (18 px), and the Settings forms no longer draw a card inside a card.

Per fix

#FixBeforeAfter
1Browser pane: one rounded area. The pane's content host (PaneContentView) rounds every kind of content; BrowserChromeView reports no header and keeps its page square. A Chromium page is cut only at the pane's bottom corners.page box rounded and outlined under the toolbartoolbar and page inside the pane's one rounded, outlined area
2Bottom edge: Metrics.mainPaneCornerRadius(windowCornerRadius:) makes the main pane concentric with the window corners (NSWindow.frameCornerRadius, moved out of CEFClipShape). The Debug Settings radius still wins; fullscreen keeps 8.8 pt corner inside a 16 pt window corner10 pt on a 16 pt window (one gutter width all round)
3--page-edge (18 px) in pages/shared/desktop.css; Settings .content uses it (was 32 px, plus a dead 28 px rule in styles.css).Settings content 32 px from the pane edge18 px, like the other pages
4Settings Accounts confirm/paste forms are part of their row, not an outlined card in the row's card.1 px border, 8 px radius, 10 px padding inside the rowindented under the row title, no box
5Passwords: row text started 8 px right of section titles (list 18 + row pill 8). The list pads by 8 less, as History does.rows at 26 px, titles at 18 pxboth at 18 px
6History, Keybindings, Cloud, CodeRouter and Apps read --page-edge (Apps moves from 16 to 18 px).mixed 16/1818
7One inset inside the main pane: with the curved main pane on, an unset layout.paneSeparation resolves to dividers (Metrics.defaultPaneSeparation), so pane padding, per-pane border and pane radius are 0; PaneChromeGeometry.stripGap is 0 unless panes are bordered or carded. Explicit borders/cards settings are unchanged.outlined r 6 box per pane, 2 pt in, 4 pt at column edgespanes flush, one divider line between them
8Sidebar inner gutter: the main pane's leading (or trailing, sidebar on the right) gutter applies only at the window edge; beside the sidebar it is 0, so the sidebar's 6 pt row inset is the gap on both sides of its rows.6 pt row inset + 6 pt gutter = 12 pt right of the rows6 pt, matching the left

Web before/after: the gallery-pr workflow posts the base vs head render of the touched entries on this PR. Paths (gallery host, base is /latest/):

  • Settings Accounts: /latest/?entry=pages.settings#/pages.settings/accounts-paste vs /wt/op-next-theme-edge-containers-1/?entry=pages.settings#/pages.settings/accounts-paste (also accounts-confirmation, general)
  • Passwords: ?entry=pages.passwords#/pages.passwords/lists
  • History: ?entry=pages.history#/pages.history/empty; Apps: ?entry=pages.apps#/pages.apps/list

Native captures: head builds on a capture slot (fleet job 8e397ad44329354edea495e7 at 652603fb for fixes 1-2 and as the before of 7-8; job 71440fbdeb995eceacfd30d8 at 354313e48328 as the after of 7-8), one window by id, showcase seed, host names masked.

  • Fixes 7 and 8, insets and sidebar gutter. Before: base pane chrome (bordered pane boxes inside the card, gutter beside the sidebar). After: head. The left pane's content differs because the showcase seed selected a different tab; the edges are what to compare.

    insets, dark

    insets, light

    sidebar gutter, zoomed

  • Fix 2, bottom edge: before is the same build with the Debug Settings radius forced to the old 8 pt; after is the default. The card corner now runs one gutter width inside the window corner. (The focus ring inside it is remainder #1: the pane's own box.)

    main pane corner, 4x

    main pane, whole window

  • Fix 1, browser pane: UNVERIFIED live. browser.new times out on capture slots (a known slot limit), so no after still exists. Before, from the archived base capture of 2026-10-10 (cmux-next-showcase/2026-10-10-cx-0rnn-incognito-badge): the page is a second outlined, rounded box under the bookmarks bar. The geometry is covered by BrowserPaneOneContainerTests.

    browser pane on base

Behavior changes to know about:

  • A tab dragged over a browser's toolbar now lands on the pane's split zones, not the tab strip, because the header (the band tab drops treat as the strip) is the tab strip alone.
  • With the curved main pane on and no pane-chrome key set, layout.paneSeparation resolves to dividers with no column gap. Setting layout.panePadding, paneCornerRadius, paneBorderColor or paneBorderWidth without a separation still means borders (PaneChromeOverrides.describesBorderedPanes), and an appearance.metrics.columnGap the user set (or a Debug Settings override) applies under every separation (Metrics.stripColumnGap). The schema's layout.paneSeparation default is now unset, labeled "Default" (so Settings no longer shows Borders selected), and its help states the conditional default in all 21 locales; settings exports were regenerated on the fleet (export-settings-artifacts.sh, step 0c3d28f12303f7d8adfb569d) and gen-strings.mjs --check passes.
  • The flush panes also move the pane tab pills up by the old 2 pt padding (the coordinator has been told; tab strip sizing is untouched).

Audit (ranked)

Batch 1 status in the last column. Native paths are under Packages/macOS/CmuxNext/Sources/, web paths under webviews/src/.

#LocationWhat's nested or doubledSingle-level fixStatus
1CmuxNextApp/WindowRootView+MainPane.swift:14-49,127-147; CmuxNextLayout/Views/PaneHostView.swift:95-126; PaneOverlayView.swift:118-153The curved main pane (r 8, gutter 6) holds a hairline-outlined, r 6 box per pane 2 pt inside it (columns: +2 pt strip gap, so 4 pt at the sides vs 2 pt top and bottom). Two boxes at every outer edge.Inside the card, separate panes with dividers (layout.paneSeparation default), give pane corners at the card's corners the card radius (ring, dim, attention), drop the strip's outer gap.Fixed (7)
2CmuxNextBrowser/UI/BrowserChromeView+PaneChrome.swift:8-17; CmuxNextApp/PaneContentView.swift:176-196Page area rounded and outlined as a second box under the toolbarPane content host is the one rounded area; page squareFixed (1)
3CmuxNextApp/WindowRootView+MainPane.swift:136; CmuxNextDesign/MainPane.swift:15Main pane r 8 at a 6 pt inset inside ~16 pt window cornersConcentric radiusFixed (2)
4CmuxNextBrowser/UI/BrowserChromeView.swift:239-242Toolbar separator stacked on the pane border's top lineBorder now at the strip; the separator is the one line between toolbar and pageFixed by (1)
5CmuxNextLayout/Views/PaneOverlayView.swift:136-144Inactive dim's square top corners clipped by the card's arcGoes with #1Moot under dividers (pane radius 0); the focus ring's corners at the card corners are square under the card's arc, a follow-up
6CmuxNextLayout/Geometry/ColumnGeometry.swift:33; DockStripGeometry.swift:226-227Strip gap repeated at the strip's outer edges on top of the gutterGap 0 without borders/cardsFixed (7)
7pages/settings/styles.css:124,686; layout.css:54,782Two .content padding rules; 32 px page edge + 1 px card + 12 px rowOne rule on --page-edgeFixed (3)
8pages/settings/components/AccountsSection.tsx:135,171; styles.css:825-832Outlined form card inside the row card (text 96 px in)Borderless, indentedFixed (4)
9pages/passwords/styles.css:95-137Rows 26 px vs titles 18 pxList pads by 8 lessFixed (5)
10History, Keybindings, Cloud, CodeRouter, Apps, Debug settings, Changelog, Markdown, Home channels, New TabPage edges of 16, 18, 20, 24 and 32 px--page-edgeFixed for the first five (6); Debug settings 24, Changelog 32-40, Markdown 32, Home channels 20, New Tab 16 in batch 2
11CmuxNextLayout/Views/DockBackdropView.swift:40-58; ScreenContentView+Dock.swift:94Overlay dock fill rounded with the pane radius on the unpadded cellpaneCornerRadius + panePaddingBatch 2
12agent-session/acpmux/styles.css:454,488Question preview (r 10, 4 % fill) inside the question card (r 14, 5 % fill)Left hairline instead of a fillBatch 2 (gallery coverage lands with #19007)
13agent-session/acpmux/conversation/conversation.css:694-731; styles.css:1,33,55,298,443Transcript, composer, thread and notices use 26.5/16/18 px gutters and 720/736/760 px columnsOne --cv-gutter/--cv-column pairBatch 2
14agent-session/acpmux/styles.css:298-303Home list is a filled r 18 card around hover rowsDrop the list fill; rows keep hoverBatch 2 (judgment call: it echoes the composer)
15pages/apps/styles.css:462-507Bordered log box inside a bordered installed-app cardPlain pre, hairline rowsBatch 2 (no gallery variant shows logs)
16pages/settings/components/GroupList.tsx:34-41; layout.css:188-200Search mode: last row's hairline doubles the card borderMatch the wrapped last rowBatch 2
17pages/settings/components/AgentHarnesses.tsx:110,116,261Add panel and notices are three more box styles next to the rows cardRows inside the one cardBatch 2
18pages/home-channels/styles.css:519-536,788-819Edit composer card inside a tinted message rowDrop the row tint while editingBatch 2
19agent-session/acpmux/styles.css:359-380Icon tiles inside edited-file and preview cardsGlyph onlyBatch 2
20CmuxNextSidebar/Views/Sections/SidebarRegionLayout.swift:116-153Carded sections: pills inset 6 px inside a 6 px card (debug look, tiles)Pills at cardPaddingBatch 2
21CmuxNextHistory/HistoryPageView.swift:68Native header 18 px vs List(.plain) default row insetsOne insetBatch 2
22CmuxNextTerminal/GhosttyRuntime+Padding.swift:21-32Terminal text 4 px inside pane padding 2 inside gutter 6Pane padding now 0 (7); the terminal's own 4 px staysPartly fixed (7)
23CmuxNextSidebar/Views/SidebarNoticeCardView.swift:39,179-181Same rect rounded twice (invisible)One layerBatch 2
24CmuxNextApp/WindowRootView+SidebarSide.swift; WindowRootView+MainPane.swiftMain pane gutter beside the sidebar on top of the sidebar's row insetGutter at window edges onlyFixed (8)

Review finding 5 (doubled hairline at the main pane's trailing edge), checked on the 2-column after still (build 71440fbd): the trailing edge shows only the focus ring (2 px), then the gutter, with no second hairline. At the boundary between the columns, the 1 px divider sits right beside the focused pane's 2 px focus ring, so the two read as one thicker line there. This is a batch-2 follow-up and not fixed here.

op-next-toolbar area (PaneToolbar*.swift, PaneContentView+Toolbar.swift, TitlebarToolbarBand.swift, ChatHeaderTools.tsx): no nesting or doubled padding found; this PR does not touch those files. No first-party page wrapper carries a radius flush against the pane edge. Code wells (git.css:35, Inspector.css:161) are left as intended.

Reference: Codex desktop app (cmux-app-screenshots/captures/codex/app-20261003/01-chat-turn-footer-side-panel.png): content is flush to the window, panes are split by one line, and only the window rounds.

Testing

Red then green on the fleet, scripts/ci/run-swift-testing-suites.sh Packages/macOS/CmuxNext:

  • Red at 0107a2452c56 (the two test commits on base), step c9ea1654b92d7754750441bf: 243 suites pass, BrowserPaneOneContainerTests (6 issues: header 56 vs strip 28, page radius 6, host radius 0, clip without the toolbar, top corners cut) and MainPaneWindowCornerTests (radius 8 vs 10) fail.
  • Green at 652603fb59ee: step 136692af4d683a76c3e67919, all 245 suites pass, both new suites included.
  • Red at 5310c97079bd (MainPaneOneInsetTests on top of fixes 1-6), step 5c40206b31ba40ab08b956cb: only MainPaneOneInsetTests fails (padding 2, border shown, radius 6, strip gap 2, content rect inset, main pane minX 214 vs sidebar maxX 208).
  • Green at 354313e48328: step cca0e7deec7020c7e32bbe4e, all suites pass, including the three new ones. The first run at this head (0e77e459e788edf856638230) had one timeout in CmuxNextControlTests.ActionRunCreatedTests ("action.run did not finish within 1999 ms"). That suite is untouched by this PR and passed in the red run, and the rerun at the same SHA passed.
  • Merged with feat-cmux-next f809fe50da9b (one conflict in MainPane.swift docs: kept the concentric radius and base's shared-surface shade). Merged head a6d1b5107fb8: fleet check-release-compile.sh BUILD SUCCEEDED, step c43529e4efc8107ccd781b45. Its suite run (088e5159f7df46b79a9aa919) stopped before the Swift tests on a base red: webviews/src/pages/settings/generated/strings.json is stale on f809fe50 (newTabKind keys from #20127/#20117); this PR changes no strings.
  • Review fixes (merged feat-cmux-next 59502eb66af4 first). Red at 90b4fa7cd167, step 2ff17a60952ebd6df507641c: MainPaneOneInsetTests fails aPaneLookSettingWithoutASeparationKeepsBorders (radius, color and width each resolved to dividers) and aColumnGapIsHonoredUnderDividers (strip gap 0 vs 10). That run also failed SidebarSurfaceTests (the dividers default vs the base test's same-surface assumption; addressed above), KeybindingsFileRelaunchTests (fails on base 59502eb66af4 too, step d9f33d6a43af308d7f1fcb9b) and BranchStateResourcesTests (passes on base and on the green run). Green at 59cd66683aa5, step 6509fcc62aea079a7174c0e8: all 256 suites pass, with no FAIL. Release compile at 59cd66683aa5: BUILD SUCCEEDED, step 496f36a43adf7c9d17ae52b9.
  • Merged feat-cmux-next e8498587b926 (no conflicts; base had changed settings), then regenerated the settings exports on the fleet at that merge (step d732863588c9ad3191d9a178) and reran gen-strings; git diff --numstat origin/feat-cmux-next over schemas, docs/mdm, the settings generated strings and the settings catalog shows only this PR's paneSeparation lines. Final head 66a146863620: suites green (step f99c98ae686cc00eb1f31b85, 258 suites, no FAIL), Release compile BUILD SUCCEEDED (step fd30e3dff9caa8f6e567e1ad).
  • CmuxNextApp size cap (SW14): the sidebar side pins (SidebarSide.pins, pure constraint construction) moved from WindowRootView+SidebarSide.swift to CmuxNextDesign/SidebarSide+Pins.swift. CmuxNextApp is now 13 lines smaller than on base (check-app-size.sh --base: 113788, cap 113801).
  • Merged feat-cmux-next c38cc9e23f32 (has the NewTabScreen lint fix 9b151f346), then regenerated the settings exports at that merge (step 5468feadc062f3c20f25da0d) and reran gen-strings. The textual merge had reverted base's Show Tips export changes; the regen restores them. It also adds browser.toolbarButtons, whose code landed on base (cx-d0d.81) without its exports, so the numstat against base shows those lines along with this PR's paneSeparation lines. The autofix bot regenerated schemas/defaults/cmux-next.json (paneSeparation null, the toolbarButtons default); final head 774c6ed8 is that autofix on a151e885, with no Swift change.
  • Final head a151e885: suites (step 73ea010c137c918a72663c11) pass except SharedIndicatorSurfacesTests, which fails the same way on base tip 6eeccc37 (step f6f1187393f71f53070d9c4a; busy-indicator default from cx-ag5.6.5).
  • #20117's shared sidebar surface takes no shade only while a line parts the sidebar from the main pane. Under the default dividers there is no pane outline, so base's own rule gives the faint step beside the sidebar (as it already did for cards). SidebarSurfaceTests' four default-case checks assumed the old borders default; they now load layout.paneSeparation borders explicitly, and a new byDefaultFlushPanesKeepAFaintStepBesideTheSidebar pins the default. The after captures predate this.

The browser test's toolbar precondition compared minY in an unflipped view (it failed on base for the wrong reason); the fix commit compares heights instead.

Also run on the head: check-concurrency.sh ok, check-theme-scope.sh 0, check-system-blue.py ok, check-motion.sh ok, check-no-godfiles.sh --base origin/feat-cmux-next flags no touched file (its reds are base files: CmuxConfigSnapshot, AppServices, AgentHandlers, AgentTabs, AgentTabStore, AgentPaneModel, cmux-tui), crash_ratchet.py adds nothing in touched modules (its three reds, in ClosedHistoryRetention.swift, CmuxNextDaemon and CmuxNextTerminal, are on base), lint-ios-package-conventions.sh adds nothing (three base namespace-type errors in untouched files). Webviews: bun run check passes every ratchet; vp check reports formatting in three pages/changelog files this PR does not touch (base). bun test for the settings, passwords, history and apps pages passes.

Not verified live: the browser toolbar inside the pane (capture slots cannot open a browser tab); the main pane corner, insets and sidebar gutter were checked live on head builds.

Changelog

  • Changed: cmux NEXT panes have one rounded container: a browser's toolbar and page sit inside the pane's rounded area, the main pane's corners follow the window's, panes inside it are flush with one divider line, the sidebar's gutter matches on both sides, and first-party pages share one edge padding without cards inside cards.

🤖 Generated with Claude Code