cmux-next sidebar: group header and line follow Chrome's tab groups, with an option to join them
Leo wants sidebar groups to look like Chrome's tab groups (he sent a Chrome screenshot as the reference on 10-19). Lawrence's 0eef92ecac6 already put the solid bar back in Minimal Mode. Before this PR, the members' line was a second color (the palette swatch), 3 pt wide, and started a 2 pt step below the bar by accident. Bar and line read as two separate things.
History (git blame)
This puts back a Chrome-style look that base had and then lost:
- 3ca7be38252e (Lawrence Chen, 2026-10-08, cx-rcby), "workspace groups look and work like Chrome tab groups": a solid header bar, with members carrying a bar in the group's color.
- bd17fe1738e7 (#18991, 2026-10-10, cx-q5jw): the members' line runs unbroken from the header. In
SidebarListView+GroupLines.swiftit started athead.midY, inside the bar. - 718b6988635e (#20892, 2026-10-15, cx-w4q7j, a cg-design-judge verdict) changed that to
top = head.midY + bar / 2 + Metrics.space1. The line has started a step below the bar ever since, leaving the gap under every header. This PR replaces that with Leo's 10-19 Chrome reference.
@lawrencecchen FYI, no action needed: Leo decided on 10-19 that the members' line uses the header bar's exact color. That overrides the line color GroupPaletteDefaultTests pinned for your palette (the swatch color), and the test now pins the bar color instead. The bar's fill colors are unchanged.
Defaults
The members' line itself changes from base for everyone, on purpose: it follows Leo's 10-19 Chrome reference (the bar's color, a cut below the bar, a thinner line with trimmed ends). Only the join is optional: sidebar.groupLineJoinsHeader is a new setting and defaults to false. It is the only default this PR adds. The defaults snapshot also lists agents.subsessions.answerQuestions (false), which base #22740 added without updating schemas/defaults/cmux-next.json; the regenerated export picks it up.
What changes
Default ("Chrome"), as close to Leo's reference as the existing tokens allow:
- The header bar fills its row and rounds its corners with the row radius (
SidebarStyle.groupHeaderCornerRadius=rowCornerRadius), as Lawrence set on the base (bce81b40f35, cb00fe652ad). - An 8 pt cut (
space4,SidebarStyle.groupLineCut) separates the bar from the line. - The line is 2 pt (
space1), in the bar's exact rest color (GroupTint.lineFill, the bar'sheaderChipfill). It has Chrome's trimmed ends: the left corners are square and the right corners are cut by a quarter circle as wide as the line. Its right edge sits where the bar's corner arc ends (groupBarX). It runs to the bottom of the last member row. - Member content is indented
space2(it wasspace1), so it sits about 11 pt right of ungrouped rows, as in Chrome.
Option sidebar.groupLineJoinsHeader ("Join Group Lines to Headers", off by default; Settings > Appearance > Sidebar), Leo's "all lined up on the left":
- The line is flush with the bar's left edge and touches it.
- The bar's bottom-left corner is square there, so bar and line form one L shape. The line's top end hides under that corner.
- The join applies to the band header style only (the default; the Debug "Group header" style tunable from #22864 has line, tinted and wash looks, which keep separate lines).
- Changing the setting redraws an open sidebar. The setting is localized in all 21 languages, and agents may set it, as they can the other sidebar look settings.
Both:
- The line views sit under the decorations view, so every header row draws over them.
- Drag lift cards use the same line view, so their lines repaint in the window's theme scope.
- An unnamed group still shows the full solid bar. VoiceOver reads it as "Unnamed group, N workspaces" instead of ", N workspaces".
- The settings exports (
schemas/settings/settings-schema.json,docs/mdm/*,schemas/defaults/cmux-next.json) are regenerated on the fleet (run-swift-testing-suites.shin export-settings mode) at the PR head and committed as generated files only.
Tests
Red first: the test commits 8f315e7175 and 60fc546fe1 come before their changes.
SidebarGroupSolidHeaderTestscovers Minimal Mode on and off, with named and unnamed groups:theMembersLineStartsACutBelowTheBarchecks the default: the cut isgroupLineCut, the line isspace1wide andgroupBarXinside the bar, the line ends at the last member row's bottom, and the bar rounds all corners withgroupHeaderCornerRadius.theJoinSettingRunsTheLineIntoTheBarchecks the option: the line is flush left and touching, and only the bar's bottom-left corner is square.turningTheJoinOnAndOffMovesTheLinechanges the setting live, under Reduce Motion.anUnnamedGroupHeaderReadsAsAGroupcovers the VoiceOver label.aHeaderAddedAfterItsLineStillDrawsOverItis kept from before.
GroupPaletteDefaultTestsrequires line color == the bar's color in the dark, light and wallpaper themes.SidebarGroupChromeLookTestschecks the cut and the trimmed ends.SidebarGroupLineJoinsHeaderSettingTestscovers the parse, the default, a bad-value diagnostic and the Settings descriptor.- Green: all 5 suites pass on the fleet (cmux-ci step 8baa9da661139f0a249d6466, aws-m4pro-2, Xcode 26.6). That run was on ceb50183, which is 84d8320b plus a scratch render test.
- A shrinking line moves with its rows again, joined or not (re-review of 2fc60a4b, fixed here at Leo's request rather than as a follow-up). Off, the line draws its own stretchable image (fixed ends, stretching middle), so it follows its presented size in every frame of the rows' spring; base's mask was laid out at the end size and cut the line at once. On, the joined outline springs to its new length on the rows'
.movespring (GroupHeaderJoinedBar) and keeps the joined shape until it is back in the bar.- Red first:
SidebarGroupLineShrinkMotionTests(aShrinkingLinesEndsFollowItsPresentedSize,aJoinedLineShrinksWithItsRows) at 6c22354495eb, before the fix dd6139459cc7. - Command:
cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --arg Packages/macOS/CmuxNext --shards 3 --ref <sha>(fleet; the suite isSidebarGroupLineShrinkMotionTests). - Red at 6c22354495eb:
aShrinkingLinesEndsFollowItsPresentedSizefails (abs(mask.frame.maxY - layer.bounds.maxY) โ 40.0, the line cut 40 pt short mid-shrink);aJoinedLineShrinksWithItsRowsskipped there (no display on that runner). - Green at dd6139459cc7 and at the head: both pass;
aJoinedLineShrinksWithItsRowspasses on display runners (aws-m4pro-8).
- Red first:
- A second review found the joined outline mask and the bar's corner radius had no null actions, so Core Animation could add its own 0.25 s animation outside the motion gate. 739af088 gives those keys null actions (only
Motion.sidebar.set's springs move them) and drops a recycled header's pending spring.aJoinedOutlineChangesAtOnceWithoutAnimatingandaLayoutMidSpringKeepsTheJoinedLinesSpringguard it, but they are regression guards, not a reproduction: on the pre-fix source (scratch 4ef6b906, pinned to the display runner aws-m4pro-8) they pass too, so I could not show the implicit animation happening in practice. - The joined bar's springs moved into
GroupHeaderJoinedBar.swift(debe91c5, no behavior change) to keepGroupHeaderRowView.swiftunder the god-file ceiling. - Locally (no build): the god-file check on the changed files passes.
check-l10n,check-motionandcheck-concurrencyflag only base files. - Review: a correctness-first subagent reviewed the change. Its findings and their resolutions are in the PR comment.
Renders (real SidebarView layer renders from fleet test runs, dark and light theme rooms) were sent to Leo: before on the base, the default cut, and the join option.
Reconciled with the base (claude-sr2/sidebar-cleanup adopted this orphan)
Base 01797cf2aaf already had #22864 (header style and palette tunables) and Lawrence's bar changes, so one base merge (7fbba6bfe88) reconciled them: Lawrence's bar height and radius win; Leo's line, cut, join and line color stay. The fix commit 453db57a6f6 then answers an independent review:
- A joined header stays realized while any of its line is on screen (
reach(of:)), so a tall group keeps its line after its header scrolls off. - The line's end trim never animates on its own (
CATransaction.setDisableActions). SidebarListView+Drag.swiftdoesn't grow past the god-file ceiling.- The string catalogs add only the new keys;
docs/mdmis left to the autofix.
Follow-ups, joined mode only (off by default): changing the Debug header style leaves stale lines until the next reload; an inline rename hides the joined line; turning the setting on, or growing a group, while its header is above the realized range leaves the line missing until the next scroll (reach(of:) reads the previous layout's lengths).
Observed / Not run
- Observed: full fleet suite runs at the base, 6c22354495eb, dd6139459cc7, 739af088, a06be31d and f42d4aea. Every suite this PR adds or changes passes at the head commits. The PR's other reds are the base's: pinned to the same display runner (aws-m4pro-8), base 70a56a0b fails 27 suites and head f42d4aea 25, none only at the head. The sidebar frame-budget suites (
SidebarGroupToggleScalingTests,SidebarScrollCostTests) fail on display runners at the base too (a group fold 32 ms on base vs 13.5 ms at the head there). - Observed:
cmux-ci compile-check --appgreen at f42d4aea (job 35c0c549df667001b8353a6b); prepush gate,check-motion, the god-file check (a ratchet-debt warning only) anddefaults_guard.py check-freshpass. Settings exports regenerated on the fleet after each base merge (generated files only). - Observed: independent review subagents: SHIP at 739af088 (full), SHIP on the delta to 02456db1 (tests, merges, exports).
- Not run: Swift suites on this Mac (fleet and CI only); no new renders since the first round.
Changelog
- Changed: In cmux NEXT's sidebar, a group's line matches its header's color and follows Chrome's tab groups: a small gap below the header and a thin line with trimmed ends, which moves with its rows as they open and close. A new Sidebar setting, Join Group Lines to Headers, lines the line up with the header's left edge as one shape.
Agent session: cosmic-gray
๐ค Generated with Claude Code