sidebar: organize and drag workspaces (reorder, groups, glass look)

LU
1mo ago
+39339-23079

the cmd b pane glide is split out into #18840, stacked on this pr.

Summary

The sidebar today is a fixed pane with a blue selection pill, and reordering workspaces drags a ghost image around. This brings in the sidebar redesign from my take-home (supersedes #12029, which was opened from my fork):

  • Floating peek sidebar. When the sidebar is hidden, resting the pointer at the left edge (or on the titlebar sidebar button) slides it in as a floating glass card in its own child window, above the terminal. A dock/float toggle switches between the fixed pane and the floating card.
  • Glass sidebar ground. The sidebar sits on compositor blur with a tint. Default tint is #393939 at 72% (measured from Aside). Selection is always a neutral glass patch with a hairline edge, no blue accent; multi-selection is a weaker glass.
  • Freeform drag reorder. The row (or a whole group block) lifts and follows the pointer; the other rows spring out of the way, and the drop commits once on release. The slot is decided by the lifted block's leading edge, so a tall group can reach the last slots, and the drop planner uses the same probe so the commit matches the preview. Dragged rows stay loaded and drawn during autoscroll. A no-op drop glides home; a real drop settles from where the block was instead of snapping. Dragging a single workspace shows its indent live (full width at top level, indented inside a group).
  • Settings > Sidebar. Liquid Glass toggle, Blur Opacity, Tint Color, Tint Opacity and Match Terminal Background in one card, plus peek reveal speed, disable peek, row density and disable switch on drag. The old Sidebar Accent row is gone (the sidebarSelectionAccent setting is removed).
  • Group Color submenu on the group header's right-click menu (clear, custom color, named palette).
  • File icons in the file explorer from the Symbols icon theme (MIT, listed in THIRD_PARTY_LICENSES.md), and sidebar navigation timing instrumentation.

Defaults change for people who never set these keys: tint color and opacity, window glass, sidebar material and blend mode, compositor glass on. That is what makes the glass look show up out of the box, but it is a visible change for existing users and worth a decision.

Preview (sidebar: glass, drag reorder, group color)

Sidebar toggle and peek motion

Austin said opening and closing the sidebar felt laggy. The old toggle swept the real layout width, so every frame relaid out SwiftUI, the splits and every terminal grid (50-75 ms of main thread per frame in debug). Now:

  • One render-server slide. A single critically damped Core Animation spring moves the window content root and the portal hosts (terminals, browsers) together, so nothing runs on the main thread while it moves.
  • One terminal resize per toggle. A hide drops the docked layout at the keypress, a show commits it when it lands, always at the wide end so no gap opens. Layout and layers land in one transaction, so the terminal never moves a frame late.
  • A press mid-slide reverses from where the sidebar is on screen, carrying its velocity. A programmatic setVisible (narrow window, session restore) drops the slide.
  • Titles drawn from frame 1. The hidden docked list stays live and parks by drawing only, so a show slides in current rows. While hidden its spinners, status pulses and hover pause, and a reveal applies each row at most once.
  • An open right sidebar holds still while the left one slides. Dock mode, Reduce Motion and non-default sidebars still toggle instantly.
  • Peek blur locked to the card. The peek card slides on the render server too, with its own behind-window material in compositor glass mode, so blur, tint and rows move as one layer. The window-level blur and its 320 ms reset timer are gone.
  • Panes meet the sidebars. The 8pt leading inset inside the workspace card is removed, so panes touch the left sidebar edge. The 2pt gap under the band and the trailing inset beside an open right sidebar are gone too, so the split dividers meet the band line and the tab bar hairline lines up with the right sidebar's header divider. The card's bottom-left corner shrinks so its curve ends where the pane starts. The 8pt inset stays where the card meets the window edge.

Collapse, peek and CMD + B

collapsing with cmd b glides the sidebar out and back on the render server, and hovering the titlebar sidebar button while it's collapsed peeks the floating card with its own glass.

Hover and light mode

  • Row hover. A soft wash on the workspace row or group header under the pointer, in the glass selection's shape. Settings > Sidebar > Row Hover turns it off; it's on by default for now and open for feedback. No hover sticks after a drag: it's cleared when the drag starts, off during it, and re-derived from the pointer after the drop and the settle.
  • Group "+" and row X. An Aside-style hover circle (16pt, 15% on hover, 22% pressed, glyph lifted to 65%); the X rests at the same secondary colour as the "+".
  • Light mode, matched to Aside. The stock tint used to be a dark glass in both appearances. In light mode it is now a cool mid-light gray ground (#D1D4D5 at 88%), the selected row and active group header get a glass pill and the hover a glassy white wash (48%, a touch brighter at the top, 0.5pt white rim, layers only); see the two lines below for the final pill and title colours. Dark mode, a chosen tint, a custom selection colour and Match Terminal Background are unchanged.
  • Light active pill is glass now (8b24a2b154b). The flat 90% white pill is replaced by glass that matches the hover's: 74% white with a top-bright gradient, a 0.5pt white rim, an 8% edge and a 5% shadow, on the row and the active group header. Layers only; dark mode, tints and selection colours untouched.
  • Light-mode titles are one colour, black, on every row (03af860df63), active and inactive alike; secondary lines keep their resting colour on the pill too, so the glass alone marks the active row. Dark mode unchanged.

Row hover (setting for now)

rows and group headers get a soft hover fill (glassy white wash in light mode). it sits behind settings > sidebar > row hover, on by default while we get second opinions.

Addition: file type icons in the Files tab

files and folders in the right sidebar's Files tab now get an icon per file type (swift, ts, json, md, folders and so on) instead of one generic glyph.

files tab with the new file icons

Testing

  • Lab: the sidebar was built and iterated in a standalone SwiftPM lab running the same Swift files with fake data, including the Cloud rows with fake machines. That covered drag reorder of rows and groups, settle and glide home, live indent, glass selection and the Settings rows.
  • Tagged builds: earlier tagged cmux DEV builds of this branch, from before the merge with main, ran the floating peek, the dock/float toggle and drag reorder live.
  • Post-merge build: there was no local build after merging main (26857b4ed9b), and none for the follow-up fixes below. Compiling and testing is left to PR CI on 5194180f8a9 (macos / macOS compile admission, macos / swift-package-tests, app-host unit tests). On the first push, swift-package-tests failed in CmuxSettingsUI and CmuxAppKitSupportUI, and compile admission never ran because a guard failed. All three are fixed in 8b472a7b15d and 5d33457b40e. A package conventions lint failure from the follow-ups is fixed in 02816ae413d.
  • Static checks: python3 scripts/verify-local.py --all --swift-changed origin/main passes 17/17 (Swift syntax only, not a compile). These also pass: scripts/localization_catalog.py check, tests/test_cmux_schema_parity.py, tests/test_settings_configuration_review_paths.py, tests/test_cmux_config_schema_embed.py, scripts/sync-test-wiring --check and scripts/wire-app-sources.py --check.
  • Review: a subagent did a correctness review of the full branch diff, and a second subagent reviewed the fixes. The fixes are in the commits after c9b3b667241:
    • the apply's structural/reload branch chain
    • lift cleanup on every drop path, drag-begin reset, a cancellable drop fallback instead of asyncAfter, and zPosition reset after a settle
    • small row edits animate (the old animated branches were unreachable)
    • only user toggles animate the sidebar, and a hide during a show keeps the right width
    • peek panel teardown restores the terminal cursor
    • Clear Color appears only for a group's own color
    • match terminal background has one default everywhere
  • Known risks, fixed after the first review (d1c8c1e396d to 5194180f8a9, each reviewed by a subagent, then the repairs reviewed again):
    • the floating card's window (a second full sidebar list) was mounted for everyone. It now mounts only while a card can show: floating, or hidden docked with peek on. Docked and visible mounts nothing.
    • inline rename and checklist fields in the floating card could not get the keyboard. The card's window now becomes key only while one of its editors has focus and the card is showing, then hands key back to the terminal's window. Hover and row clicks never take key, and a peek stays up while you type.
    • single-row drags showed nothing once taken to another window. They now show a snapshot of the row whenever the pointer is off the list. The in-list lift is unchanged.
    • the mount budget raised to 4 workspaces is back to main's 1. #9244 already fixed warm-switch latency, and every extra mounted workspace keeps its portal rendering and surfaces alive.
    • the card's 320 ms blur removal uses an injected clock with cancellation instead of a plain sleep.
    • creating or closing a workspace during a drag no longer makes the lifted rows bounce. The lift is rebuilt against the new rows in the same update.
  • Floating card stale after a rename (fixed in 23a275dcf7c): with the docked sidebar hidden, a rename committed inline in the peek card was saved but the card kept the old name until the sidebar docked again. The card's snapshot refresh was gated on the docked sidebar's visibility instead of the card's own presentation, so titles, branch and status all froze in the card. Checked on a local tagged build (cmux DEV sidebar-anim) with the docked sidebar hidden: a cmux workspace rename through the debug socket logged workspace.customTitle.write, then the card's sidebar.table.rowsBuild, sidebar.row.titlePaint with the new title and sidebar.table.apply contentChanges=1 within 90 ms. The build before the fix logged only the write. The hidden docked list still does no work: one rows build per rename.
  • File length: every Swift file is within the repo budget (scripts/swift_file_length_budget.py). Cohesive pieces moved to their own files with no behaviour change.
  • Tests: none run locally; CI runs them.
    • cmuxTests/SidebarWorkspaceReorderLiftTests.swift: a group drag keeps the header grab offset (moved from SidebarWorkspaceTableTests.swift), a live-drag lift survives a mid-drag close and every row starts from where it stood, and the drag image only shows off the list.
    • cmuxTests/SidebarPeekPanelWindowTests.swift: the keyboard rules of the card's window.
    • SidebarPresentationModePeekPanelTests (CmuxSidebar package): when the card is mounted.
    • cmuxTests/SidebarPresentedWhileHiddenSnapshotTests.swift: a sidebar presented while the docked one is hidden rebuilds its workspace snapshot on rename. Committed before the fix (7533734b3b6).
  • CI failures on b39bd8872db, fixed in 076ae7fd519, 3c783f96444, 77eab498e04 and 218d22a675f (app-host unit tests; the first run covered only the changed suites, the full suite then ran on 3c783f96444):
    • Sidebar toggle routing (AppDelegateShortcutRoutingTests :1207, :1252, :6975). The toggle went to the right window, but the animated hide keeps isVisible true for its 170 ms sweep, so the sidebar read as still visible right after the toggle. That also meant a second toggle during the sweep was dropped (it asked to hide again). Fixed in the code: SidebarState.requestedVisibility is false as soon as a hide is requested, toggle() and sidebarVisibility(windowId:) read it, and a toggle mid-hide now glides the pane back out from where it is. New test: SidebarStateAnimatedToggleTests.
    • Drop indicator never cleared (hidingClearsReorderIndicator, reorderDragReplansFromStoredWindowPointOnViewportChange). The lift draws the drop preview and never sets an indicator painter, so a drag exit or a hidden sidebar skipped the drop indicator clear and its autoscroll stop. Fixed in the code: retirement also runs for a live accepted drop update.
    • Retired row still interactive (tableRetirementInvalidatesDescriptionLinkAccessibility). With the slide-out on close, didRemove only fires after the 220 ms animation, so the removed row kept its actions, link proxies and any open edit until then. Fixed in the code: rows sliding out are retired when the removal starts. Visible side effect: a link-bearing description goes blank as the row starts to slide out.
    • liveDragLiftIsRebuiltAcrossAStructuralUpdateWithoutBouncing passed on the first run and failed on the second (rows off by 33 pt). The lift rebuild read a presentation copy of each row's transform, and that copy can lag a glide that was just removed. It now reads the model value unless a glide is actually running.
    • Selection colour (SidebarSelectedWorkspaceColorTests). The tests pinned the old blue accent fill. They are replaced by cmuxTests/SidebarGlassSelectionTests.swift, which checks the neutral glass fill and edge, weaker multi-selection, label-coloured text, and that a custom sidebarSelectionColorHex still wins. The multi-selected group header test now expects the glass hairline, and the two link raster tests composite the translucent selection over the dark pane before looking for glyph pixels.
    • Right sidebar backdrop (WindowAppearanceSnapshotTests). The tools sidebar now sits on a solid terminal backdrop on purpose, so the test checks for that. The tint test reads the left sidebar.
    • Browser under-page background (BrowserDeveloperToolsConfigurationTests :2023, :2105). Window glass is now on by default, and a glass window clears a blank page so the glass shows through. Main does the same when glass is on, so this is not a regression. The two refresh tests turn glass off for their run, and BrowserGlassWindowBackgroundTests covers the glass case.
    • WindowOverlayChromeTests (browser content inside the chrome). Compositor glass is on by default and clears the window instead of keeping a native glass root, so the test turns it off to keep checking the native root path.
    • Not from this branch: CLI product tests failed once in tests/test_issue_15066_zsh_watcher_sleep.py (zsh watcher timeout; this branch does not touch shell integration) and passed on the next two runs. testTextBoxSubmitSerializesPasteboardRunsAcrossSurfaces (pasteboard, also failing on other PRs) and CLINotifyProcessIntegrationTests.testAgentHookLaunchEnvironmentDoesNotPersistPathOrShell each failed once on 3c783f96444 and passed on 218d22a675f.
    • On 218d22a675f all seven app-host shards and CLI product tests ran green. Shard 1 still failed afterwards, at its artifact upload step (ETIMEDOUT). The re-run passed, and macOS status is green.
    • Local checks: the tagged build cmux DEV sidebar-anim compiles on 218d22a675f (app target only, the tests compile in CI), with no warnings in the changed files. verify-local.py --all --swift-changed origin/main passes 17/17, the Swift file length budget passes, and sync-test-wiring --check is clean.
  • Toggle and peek motion round (fe96d1b6e65 to d4196a4a1cb, review fixes in 004010ca623 and ee1ca41cd2d, lines in df98f3e7d81): Lucas tested the toggle, reversal and peek by hand on a local tagged build at d4196a4a1cb. A subagent reviewed the four commits correctness-first and a second one reviewed the repair; findings and what was left are in the review comment. The line fixes were checked by pixel on a local tagged build at df98f3e7d81 against the Oct 7 build and a recording of fe96d1b6e65. New tests: SidebarToggleSlideMachineTests (press sequences, reversal from the presented offset with velocity carried, stale and repeated landings, reset) and SidebarStateAnimatedToggleTests.programmaticSetVisibleAlwaysReportsTheCommit. The animator itself has no unit tests (it needs a window and portals). Main merged with scripts/merge-main.sh (150f830df52) since GitHub had the PR as conflicting and no macOS CI had run on d4196a4a1cb.
  • Hidden list, hover and light mode round (5b2c4c90ae0 to dad39a8b65e): SidebarHiddenPresentationTests.visibilityToggleKeepsAppKitTableContainerMounted waits for the toggle to land on isVisible's own commit and checks the new contract (rows and Cloud identity apply while hidden, row animations off, back on at reveal, at most one projection per row, focus handed back when hiding with a sidebar field focused). CI caught two real regressions from the docked list moving into its own hosting view, both fixed: a projected selection binding re-ran the sidebar body on every push (dad39a8b65e), and a sidebar field's window-sized visibleRect broke the focus handoff (b7b23f9a26d). New tests: stockSidebarTintIsAppearanceAware, lightSelectionIsAsidePillOnTheStockTintAndGlassOtherwise. Reviewed by subagents; light values measured against Aside in the Sidebar Lab and a local tagged build, dark mode pixel-identical.
  • Localization: new strings are in Resources/Localizable.xcstrings with all 19 app locales: the Settings rows, the Group Color menu, and older branch strings that had no translations.
  • Licenses: the bundled file explorer icons come from the Symbols icon theme (MIT). They're credited in THIRD_PARTY_LICENSES.md.

Changelog

Added: Floating glass sidebar that peeks in from the left edge, with drag-to-reorder that lifts rows and groups, a Group Color menu, and new sidebar appearance settings

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited, and the result is stated above
  • User-facing docs updated if needed
  • Reviewed with a subagent before merge (cmux-review), and all bot and human review comments resolved

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Reworks the sidebar into a floating glass card that peeks in from the left edge, with freeform drag reordering, Group Color, file icons, and a new Settings > Sidebar customization card.

  • A hidden sidebar hover-reveals via a pure dwell/grace state machine sliding on the render server; a dock/float toggle switches between the fixed pane and the floating card.
  • Glass is compositor blur on a clear window ground with a real blur-radius slider; selection is always a neutral glass patch, and sidebarSelectionAccent is removed.
  • Light mode gets a light glass sidebar while the stock tint is in place: gray ground, glassy pill for the active row, glassy hover.
  • Drags lift rows or whole group blocks with springs, show a snapshot outside the list, and commit once on release.
  • Group headers get a Group Color submenu; the file explorer shows per-type SVG icons (Symbols icon theme, MIT) with Finder/SF fallbacks.
  • Settings > Sidebar adds Liquid Glass, blur/tint sliders, peek reveal speed, row density, and disable-switch-on-drag. The right sidebar stays on its solid terminal-colored ground.

Migration

Users who never set tint, glass, or match-terminal keys now get compositor glass on, match-terminal off, and tint #393939 at 72% — a visible change for existing setups.


[!NOTE]
Medium Risk
Wide visual and default-setting changes across window chrome and sidebar appearance, plus new compositor blur and peek panel lifecycle, though much of the peek logic is covered by unit tests.

Overview
Ships a compositor-glass sidebar ground and related defaults: when compositor glass applies, the window root goes transparent, CGS sets an explicit blur radius (12–60pt), and the left sidebar paints tint-only over that ground. Match terminal background flips to opt-in (false by default); stock tint becomes #393939 at 72% with a light-mode override (#D1D4D5 / 88%) when the stock hex is unchanged. Sidebar material/blend/state and background-glass defaults move to behind-window HUD-style glass; the right tools sidebar always uses a solid terminal-coloured backdrop instead of following workspace glass.

Settings > Sidebar gains a customization card: Liquid Glass, blur/tint sliders (debounced), peek reveal speed, disable peek, row density, disable switch-on-drag, plus search anchors. Catalog/schema defaults align with the new glass look.

Adds a testable peek state machine (SidebarPeekMachine, dwell/grace, holds for menus/focus/drag) and floating-panel UI (glass backdrop, chrome, dock/float toggle, new-workspace footer) with rules for when the peek child window mounts.

Infrastructure: CompositorBlurController.setBackgroundBlur, WindowBackdropPlan.compositorBlurRadius, and backdrop controller wiring; bundled MaterialIcons SVG set + MIT license; minor .gitignore cleanup.

Reviewed by Cursor Bugbot for commit 77b039fd9088dfdad740ae8d6522bc49ad55494e. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Sidebar can now float as an interactive peek panel, with configurable reveal speed, row density, and drag-to-switch behavior.
    • Added sidebar controls for Liquid Glass, blur, tint, and right-sidebar width.
    • Workspace groups now support custom colors, and file lists display matching icons.
    • Sidebar workspace reordering now has smoother drag previews and animations.
  • Updates
    • Sidebar appearance defaults now favor Liquid Glass, with a neutral tint that doesn’t match the terminal background.
    • Sidebar selection styling and workspace row spacing have been refined.