cmux-next sidebar: group header and line follow Chrome's tab groups, with an option to join them

Closed
TE
14d ago
+27352-26203

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.swift it started at head.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's headerChip fill). 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 was space1), 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.sh in 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.

  • SidebarGroupSolidHeaderTests covers Minimal Mode on and off, with named and unnamed groups:
    • theMembersLineStartsACutBelowTheBar checks the default: the cut is groupLineCut, the line is space1 wide and groupBarX inside the bar, the line ends at the last member row's bottom, and the bar rounds all corners with groupHeaderCornerRadius.
    • theJoinSettingRunsTheLineIntoTheBar checks the option: the line is flush left and touching, and only the bar's bottom-left corner is square.
    • turningTheJoinOnAndOffMovesTheLine changes the setting live, under Reduce Motion.
    • anUnnamedGroupHeaderReadsAsAGroup covers the VoiceOver label.
    • aHeaderAddedAfterItsLineStillDrawsOverIt is kept from before.
  • GroupPaletteDefaultTests requires line color == the bar's color in the dark, light and wallpaper themes.
  • SidebarGroupChromeLookTests checks the cut and the trimmed ends.
  • SidebarGroupLineJoinsHeaderSettingTests covers 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' .move spring (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 is SidebarGroupLineShrinkMotionTests).
    • Red at 6c22354495eb: aShrinkingLinesEndsFollowItsPresentedSize fails (abs(mask.frame.maxY - layer.bounds.maxY) โ†’ 40.0, the line cut 40 pt short mid-shrink); aJoinedLineShrinksWithItsRows skipped there (no display on that runner).
    • Green at dd6139459cc7 and at the head: both pass; aJoinedLineShrinksWithItsRows passes on display runners (aws-m4pro-8).
  • 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. aJoinedOutlineChangesAtOnceWithoutAnimating and aLayoutMidSpringKeepsTheJoinedLinesSpring guard 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 keep GroupHeaderRowView.swift under the god-file ceiling.
  • Locally (no build): the god-file check on the changed files passes. check-l10n, check-motion and check-concurrency flag 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.swift doesn't grow past the god-file ceiling.
  • The string catalogs add only the new keys; docs/mdm is 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 --app green at f42d4aea (job 35c0c549df667001b8353a6b); prepush gate, check-motion, the god-file check (a ratchet-debt warning only) and defaults_guard.py check-fresh pass. 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