CmuxNext: one container level and consistent edge padding (batch 1)
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
| # | Fix | Before | After |
|---|---|---|---|
| 1 | Browser 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 toolbar | toolbar and page inside the pane's one rounded, outlined area |
| 2 | Bottom 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 corner | 10 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 edge | 18 px, like the other pages |
| 4 | Settings 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 row | indented under the row title, no box |
| 5 | Passwords: 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 px | both at 18 px |
| 6 | History, Keybindings, Cloud, CodeRouter and Apps read --page-edge (Apps moves from 16 to 18 px). | mixed 16/18 | 18 |
| 7 | One 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 edges | panes flush, one divider line between them |
| 8 | Sidebar 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 rows | 6 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-pastevs/wt/op-next-theme-edge-containers-1/?entry=pages.settings#/pages.settings/accounts-paste(alsoaccounts-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.



-
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.)


-
Fix 1, browser pane: UNVERIFIED live.
browser.newtimes 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 byBrowserPaneOneContainerTests.
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.paneSeparationresolves to dividers with no column gap. Settinglayout.panePadding,paneCornerRadius,paneBorderColororpaneBorderWidthwithout a separation still means borders (PaneChromeOverrides.describesBorderedPanes), and anappearance.metrics.columnGapthe user set (or a Debug Settings override) applies under every separation (Metrics.stripColumnGap). The schema'slayout.paneSeparationdefault 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, step0c3d28f12303f7d8adfb569d) andgen-strings.mjs --checkpasses. - 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/.
| # | Location | What's nested or doubled | Single-level fix | Status |
|---|---|---|---|---|
| 1 | CmuxNextApp/WindowRootView+MainPane.swift:14-49,127-147; CmuxNextLayout/Views/PaneHostView.swift:95-126; PaneOverlayView.swift:118-153 | The 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) |
| 2 | CmuxNextBrowser/UI/BrowserChromeView+PaneChrome.swift:8-17; CmuxNextApp/PaneContentView.swift:176-196 | Page area rounded and outlined as a second box under the toolbar | Pane content host is the one rounded area; page square | Fixed (1) |
| 3 | CmuxNextApp/WindowRootView+MainPane.swift:136; CmuxNextDesign/MainPane.swift:15 | Main pane r 8 at a 6 pt inset inside ~16 pt window corners | Concentric radius | Fixed (2) |
| 4 | CmuxNextBrowser/UI/BrowserChromeView.swift:239-242 | Toolbar separator stacked on the pane border's top line | Border now at the strip; the separator is the one line between toolbar and page | Fixed by (1) |
| 5 | CmuxNextLayout/Views/PaneOverlayView.swift:136-144 | Inactive dim's square top corners clipped by the card's arc | Goes with #1 | Moot under dividers (pane radius 0); the focus ring's corners at the card corners are square under the card's arc, a follow-up |
| 6 | CmuxNextLayout/Geometry/ColumnGeometry.swift:33; DockStripGeometry.swift:226-227 | Strip gap repeated at the strip's outer edges on top of the gutter | Gap 0 without borders/cards | Fixed (7) |
| 7 | pages/settings/styles.css:124,686; layout.css:54,782 | Two .content padding rules; 32 px page edge + 1 px card + 12 px row | One rule on --page-edge | Fixed (3) |
| 8 | pages/settings/components/AccountsSection.tsx:135,171; styles.css:825-832 | Outlined form card inside the row card (text 96 px in) | Borderless, indented | Fixed (4) |
| 9 | pages/passwords/styles.css:95-137 | Rows 26 px vs titles 18 px | List pads by 8 less | Fixed (5) |
| 10 | History, Keybindings, Cloud, CodeRouter, Apps, Debug settings, Changelog, Markdown, Home channels, New Tab | Page edges of 16, 18, 20, 24 and 32 px | --page-edge | Fixed for the first five (6); Debug settings 24, Changelog 32-40, Markdown 32, Home channels 20, New Tab 16 in batch 2 |
| 11 | CmuxNextLayout/Views/DockBackdropView.swift:40-58; ScreenContentView+Dock.swift:94 | Overlay dock fill rounded with the pane radius on the unpadded cell | paneCornerRadius + panePadding | Batch 2 |
| 12 | agent-session/acpmux/styles.css:454,488 | Question preview (r 10, 4 % fill) inside the question card (r 14, 5 % fill) | Left hairline instead of a fill | Batch 2 (gallery coverage lands with #19007) |
| 13 | agent-session/acpmux/conversation/conversation.css:694-731; styles.css:1,33,55,298,443 | Transcript, composer, thread and notices use 26.5/16/18 px gutters and 720/736/760 px columns | One --cv-gutter/--cv-column pair | Batch 2 |
| 14 | agent-session/acpmux/styles.css:298-303 | Home list is a filled r 18 card around hover rows | Drop the list fill; rows keep hover | Batch 2 (judgment call: it echoes the composer) |
| 15 | pages/apps/styles.css:462-507 | Bordered log box inside a bordered installed-app card | Plain pre, hairline rows | Batch 2 (no gallery variant shows logs) |
| 16 | pages/settings/components/GroupList.tsx:34-41; layout.css:188-200 | Search mode: last row's hairline doubles the card border | Match the wrapped last row | Batch 2 |
| 17 | pages/settings/components/AgentHarnesses.tsx:110,116,261 | Add panel and notices are three more box styles next to the rows card | Rows inside the one card | Batch 2 |
| 18 | pages/home-channels/styles.css:519-536,788-819 | Edit composer card inside a tinted message row | Drop the row tint while editing | Batch 2 |
| 19 | agent-session/acpmux/styles.css:359-380 | Icon tiles inside edited-file and preview cards | Glyph only | Batch 2 |
| 20 | CmuxNextSidebar/Views/Sections/SidebarRegionLayout.swift:116-153 | Carded sections: pills inset 6 px inside a 6 px card (debug look, tiles) | Pills at cardPadding | Batch 2 |
| 21 | CmuxNextHistory/HistoryPageView.swift:68 | Native header 18 px vs List(.plain) default row insets | One inset | Batch 2 |
| 22 | CmuxNextTerminal/GhosttyRuntime+Padding.swift:21-32 | Terminal text 4 px inside pane padding 2 inside gutter 6 | Pane padding now 0 (7); the terminal's own 4 px stays | Partly fixed (7) |
| 23 | CmuxNextSidebar/Views/SidebarNoticeCardView.swift:39,179-181 | Same rect rounded twice (invisible) | One layer | Batch 2 |
| 24 | CmuxNextApp/WindowRootView+SidebarSide.swift; WindowRootView+MainPane.swift | Main pane gutter beside the sidebar on top of the sidebar's row inset | Gutter at window edges only | Fixed (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) andMainPaneWindowCornerTests(radius 8 vs 10) fail. - Green at 652603fb59ee: step
136692af4d683a76c3e67919, all 245 suites pass, both new suites included. - Red at 5310c97079bd (
MainPaneOneInsetTestson top of fixes 1-6), step5c40206b31ba40ab08b956cb: onlyMainPaneOneInsetTestsfails (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 inCmuxNextControlTests.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.swiftdocs: kept the concentric radius and base's shared-surface shade). Merged head a6d1b5107fb8: fleetcheck-release-compile.shBUILD SUCCEEDED, stepc43529e4efc8107ccd781b45. Its suite run (088e5159f7df46b79a9aa919) stopped before the Swift tests on a base red:webviews/src/pages/settings/generated/strings.jsonis stale on f809fe50 (newTabKindkeys from #20127/#20117); this PR changes no strings. - Review fixes (merged feat-cmux-next 59502eb66af4 first). Red at 90b4fa7cd167, step
2ff17a60952ebd6df507641c:MainPaneOneInsetTestsfailsaPaneLookSettingWithoutASeparationKeepsBorders(radius, color and width each resolved to dividers) andaColumnGapIsHonoredUnderDividers(strip gap 0 vs 10). That run also failedSidebarSurfaceTests(the dividers default vs the base test's same-surface assumption; addressed above),KeybindingsFileRelaunchTests(fails on base 59502eb66af4 too, stepd9f33d6a43af308d7f1fcb9b) andBranchStateResourcesTests(passes on base and on the green run). Green at 59cd66683aa5, step6509fcc62aea079a7174c0e8: all 256 suites pass, with no FAIL. Release compile at 59cd66683aa5: BUILD SUCCEEDED, step496f36a43adf7c9d17ae52b9. - 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-nextover schemas, docs/mdm, the settings generated strings and the settings catalog shows only this PR's paneSeparation lines. Final head 66a146863620: suites green (stepf99c98ae686cc00eb1f31b85, 258 suites, no FAIL), Release compile BUILD SUCCEEDED (stepfd30e3dff9caa8f6e567e1ad). - CmuxNextApp size cap (SW14): the sidebar side pins (
SidebarSide.pins, pure constraint construction) moved fromWindowRootView+SidebarSide.swifttoCmuxNextDesign/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 addsbrowser.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 regeneratedschemas/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 exceptSharedIndicatorSurfacesTests, which fails the same way on base tip 6eeccc37 (stepf6f1187393f71f53070d9c4a; 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 oldbordersdefault; they now loadlayout.paneSeparationborders explicitly, and a newbyDefaultFlushPanesKeepAFaintStepBesideTheSidebarpins 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