Skip to content

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

Open
lucasr1b wants to merge 73 commits into
mainfrom
sidebar-animations
Open

lucasr1b wants to merge 73 commits into
mainfrom
sidebar-animations

Conversation

@lucasr1b

@lucasr1b lucasr1b commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

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-glass-drag-reorder-group-color-after.mov

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.

sidebar-toggle-glide.mov

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.

collapse-peek-cmd-b.mov

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 (8b24a2b). 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 (03af860), 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.

light-mode.mov

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.

row-hover.mov

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 (26857b4), and none for the follow-up fixes below. Compiling and testing is left to PR CI on 5194180 (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 8b472a7 and 5d33457. A package conventions lint failure from the follow-ups is fixed in 02816ae.
  • 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 c9b3b66:
    • 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 (d1c8c1e to 5194180, 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. Fix intermittent warm workspace switch latency #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 23a275d): 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 (7533734).
  • CI failures on b39bd88, fixed in 076ae7f, 3c783f9, 77eab49 and 218d22a (app-host unit tests; the first run covered only the changed suites, the full suite then ran on 3c783f9):
    • 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 3c783f9 and passed on 218d22a.
    • On 218d22a 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 218d22a (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 (fe96d1b to d4196a4, review fixes in 004010c and ee1ca41, lines in df98f3e): Lucas tested the toggle, reversal and peek by hand on a local tagged build at d4196a4. 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 df98f3e against the Oct 7 build and a recording of fe96d1b. 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 (150f830) since GitHub had the PR as conflicting and no macOS CI had run on d4196a4.
  • Hidden list, hover and light mode round (5b2c4c9 to dad39a8): 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 (dad39a8), and a sidebar field's window-sized visibleRect broke the focus handoff (b7b23f9). 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.

Written for commit 77b039f. Summary will update on new commits.

View guided diff Turn on auto-fix


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 77b039f. 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.

the command palette's fuzzy matcher was the right scorer for a sidebar
workspace filter, but two domain packages cannot depend on each other, so
it moves down to CmuxFoundation. CommandPaletteFuzzyMatcher stays as a
typealias, so no palette call site changes and its reference-scoring tests
still pin the same numbers.

the filter itself is pure value logic in CmuxSidebar: a prepared index built
once per corpus revision, scored per keystroke. fields are tried in priority
order and the first hit wins, so a row matching on its title never pays to
score its branch, directory and ports (worst-case keystroke 35.9ms -> 10.5ms
at 500 all-matching workspaces; 2.8ms on a realistic 200-workspace corpus).

rows are filtered, never reordered - a sidebar is spatial and re-sorting by
score mid-type moves rows under the pointer. ranking rides along separately
for return-to-jump.
hover-reveal for the collapsed sidebar, as a pure (state, event) -> effects
machine with no timers, views or clock inside it. the timing policy is a
value type so settings configures a policy rather than reaching into the
machine.

the rules that decide whether a hover panel is bearable are all here and all
tested: dwell before revealing so crossing the edge en route to a terminal
never flashes the sidebar; grace before dismissing so the pointer can travel
the diagonal from edge strip into panel; a drag skips dwell because it has
already declared intent; and a hold set (pointer inside, context menu open,
keyboard focus, drag in flight) refuses dismissal, which is what stops a
context menu from dismissing the panel it was opened from.

21 tests, including stale timers firing in the wrong phase and a hold taken
between the grace timer being scheduled and firing.
the filter chrome is split from its text input so the same frame hosts a
live TextField in the app and a plain Text anywhere the field is drawn
without being editable. match emphasis is a tinted pill behind the run
rather than coloured text: recolouring would put a second blue beside the
selection fill, which is already the accent, and the two would compete.
the tint is scheme-aware because the accent needs more alpha on near-black
than on near-white to read the same.

a miss is reported by the match count reaching 0 and by an empty state that
names the query, with no colour change anywhere. the earlier orange ring
introduced a hue the sidebar palette does not have.

CmuxSidebarGallery renders these to PNG offscreen via ImageRenderer, no
window and no screen-recording permission, in seconds rather than the ~15
minutes a full app build costs. it is deliberately not wired into
cmux.xcodeproj. it immediately paid for itself by catching a literal
\(queryText) in the empty state and a .link button that renders AppKit's
link blue next to the accent.
the float/dock switch is one control in a fixed corner, and both modes host
the identical rail at the identical width: switching moves the frame, never
reflows the rows, so the row being read keeps its shape across the change.

the gloss is the rim, not the fill. a uniform hairline reads as a sticker;
catching more light along the top edge and falling off downward is what makes
the card read as glass lying over the terminal rather than a rectangle drawn
on it. the border is drawn from the panel's own side because the backdrop is
whatever the terminal happens to be showing.

new-workspace is a full-width footer row rather than a + glyph, so the
gesture that picks a workspace is the gesture that makes one.

note: ImageRenderer cannot rasterise backdrop blur, so the gallery shows the
material as a flat fill. layout, rim and shadow are reviewable offscreen; the
blur itself has to be judged in the running app.
rides on the existing retained-AppKit-sidebar path: cmux already keeps the
workspace table mounted behind a zero-width shell while the sidebar is
hidden, so revealing it is a frame and opacity change with no table to
build. that is what makes peek instant rather than merely fast, and it is
why the reveal translates a laid-out subtree instead of animating its width,
which would re-lay-out every row on every frame.

the edge strip is an NSTrackingArea, not a hit-test. hitTest on the terminal
host is on the typing-latency path per CLAUDE.md and runs for every
keystroke; a tracking area is serviced by AppKit's mouse-moved plumbing and
removes itself entirely while the sidebar is docked. it also returns nil
from hitTest so it can never eat a click meant for the leading column of the
terminal.

presentationMode joins VerticalTabsSidebar's == because the toggle's glyph
flips on it and would otherwise be skipped by the equality gate; the toggle
closure stays out, like every other action bundle.
…lease

drag visuals run on a 120hz pointer poll that no appkit drag callback can
touch. destination callbacks (planner, overlay, exited) only track the drop
plan; the commit stays on the stock release path. frozen frame capture at
drag start makes displacement boundaries immovable mid-drag, spring glides
use explicit CASpringAnimation because view-backing layers ignore implicit
transaction animations, and dragged rows keep full opacity since the row
itself is the drag visual (ghost suppressed for single-row drags).
closing a workspace slides its row out and creating one inserts in place
instead of reloading every visible cell, which was both the create lag and
the visual blink. selection coalescing window drops to 40ms now that
switches are cheap.
the peek card lives in its own child panel above the portal-hosted terminal
(the only way to draw over it), with real behind-window blur, its own hover
tracking, and instant reveal from the titlebar toggle. terminal cursor rects
yield to the card while it accepts the pointer so titles stop flashing the
ibeam. docking over a revealed card swaps in place with no ghost slide.
the sidebar toggle sweeps the real layout width through the same live
resize path the manual divider uses, so pane and terminal move together
with no choreography to drift out of sync. window ground is the sidebar
material window-wide, the workspace band and terminal draw as one card
rounded where it meets the glass, and the last four workspaces stay
mounted so recent tab switches are a visibility flip instead of a cold
mount.
- settings: sidebar glass tint (one wash shared by docked and floating),
  peek reveal speed, disable peek, row density, disable switch on drag
- window glass on by default: transparent window so the sidebar samples
  real behind-window vibrancy instead of flat frost
- vscode material icon pack in the file explorer, finder/sf fallback
- env-gated nav timing lines around switch, create, close, toggle, peek
- compact floating panel top inset, dropped the new workspace button
…cker fix

sidebar ground is now clear window + compositor blur radius instead of an
appkit material, so blur is a real slider (1x-5x). tint slider drives a slate
grey wash (default 44%). floating peek card gets the same blur only while it
is showing so it never blurs the docked rows under it. files/find/vault
sidebar stays on a solid terminal-coloured ground. new sidebar accent
setting: blue or flat grey glass pill. toggle-button hover no longer pops the
card during a hide (300ms window). close button gets a centred hover circle.
sidebar defaults baked (hudWindow fallback, behind-window, always-active) and
the upstream defaults-stamping migration is defanged.
dragging a workspace group moved the header row alone while its members
stayed put. the freeform lift now carries the header plus its visible
member rows as one block, other rows and groups part around the block's
real height, and a group can never land inside another group mid-drag.
single row drags are unchanged; the layout math is now exact for mixed
row heights. commit path is upstream's existing whole-group move.
i removed a filter engine i added as it was mostly for testing, and removed
a preview tool that rendered sidebar states to png images without launching
the app, which i used as a dev tool for visual iteration. the fuzzy matcher
goes back to the command palette where it lived.
…snapping

- grab offset captured at native drag start so a group header doesn't jump
- slot decided by the lifted block's leading edge, planner fed the same probe,
  so a tall group can reach the last slots
- dragged rows pinned via prepareContent and drawn as a static snapshot so
  autoscroll can't unload or blank them
- no-op drop glides home, real drops settle from where the block was with an
  ease-out, a re-grab clears any in-flight settle
- single workspace drag indents live (top level vs inside a group)
- optional active look on a dragged group header (sidebarDragHeaderActiveLook)
neutral translucent fill with a hairline edge, no blue accent; multi-selection
is a weaker glass. fill and edge strength tunable via
sidebarGlassSelectionFillOpacity / sidebarGlassSelectionEdgeOpacity. tint
measured from aside. drops the unused glass pill color.
…pacity rows

match terminal background moves into the same card, the sidebar accent row
goes (selection is always glass now), search entries follow.
clear, custom color prompt, then the named palette, same as the workspace
row's color menu. new defaulted onSetColor action routes to
TabManager.setWorkspaceGroupColor.
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at 26857b4.

Resolved conflicts:
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Merge-main-previous-head: 027e247
Merge-main-base: 26857b4
…catalog, 19 locales)

settings search titles use the same keys as their rows. the blur and tint
subtitles drop the 0%/100% wording, a bare % reads as a format specifier.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds floating and edge-triggered sidebar presentation, compositor-glass appearance settings, workspace reorder interactions, material file icons, sidebar customization controls, and related tests and resources.

Changes

Sidebar Peek and Floating Presentation

Layer / File(s) Summary
Peek state model and transitions
Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Peek/*, Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/*
Adds typed peek policies, events, phases, holds, state transitions, and timer effects. Tests cover reveal, dismissal, holds, stale timers, and panel requirements.
Peek panel, controls, and lifecycle
Sources/Sidebar/Peek/*, Sources/ContentView+SidebarPeek.swift, Sources/ContentView.swift
Connects peek events to edge tracking and a child panel. Adds floating-card presentation, pointer and keyboard handling, presentation controls, and per-window lifecycle wiring.
Peek settings and sidebar visibility
Packages/macOS/CmuxSettings/..., Packages/macOS/CmuxSettingsUI/..., Sources/Sidebar/SidebarState.swift, Sources/Sidebar/SidebarToggleAnimator.swift
Adds peek, row-density, and drag-switch settings. Sidebar visibility now tracks requested state and supports animated docked transitions and floating-panel insets.

Sidebar Glass Appearance

Layer / File(s) Summary
Appearance and compositor blur
Packages/macOS/CmuxAppKitSupportUI/..., Packages/macOS/CmuxWorkspaces/..., Sources/Windowing/*, Sources/ContentView.swift
Adds compositor-glass eligibility, configurable blur radius, transparent-root backdrop plans, and separate left and right sidebar policies.
Appearance controls and selection styling
Packages/macOS/CmuxSettings/..., Packages/macOS/CmuxSettingsUI/..., Sources/Sidebar/SidebarGlassSelection.swift, Sources/Sidebar/SidebarAppearanceSupport.swift
Adds glass, tint, blur, matching, and density controls. Selection rendering uses translucent fills and edges. Defaults, tests, schemas, and documentation are updated.

Workspace Reorder Interaction

Layer / File(s) Summary
Lifted-row previews and drag ghost
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController+Reorder*, Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView*
Adds lifted-row snapshots, pointer-offset tracking, row-shift previews, drag ghosts, and lift carry-over across structural updates.
Drop planning and table updates
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableController+ReorderDrop.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableViewImpl.swift
Adds drop-plan handling, indicators, commit and transform settling, pinned row preparation, and animation for eligible small row changes.

Sidebar and Resource Updates

Layer / File(s) Summary
Material file icons
Sources/MaterialFileIconResolver.swift, Sources/FileExplorerCellView.swift, Resources/material-icons.json, Resources/MaterialIcons/LICENSE, THIRD_PARTY_LICENSES.md
Adds bundled icon mappings and file/folder icon resolution. File explorer cells display resolved material icons without tint.
Sidebar actions and navigation instrumentation
Sources/Sidebar/AppKitList/Cells/*, Sources/Sidebar/SidebarNavigationTimings.swift, Sources/AppDelegate.swift
Adds group color actions, changes selection coalescing to 40 ms, and records workspace navigation timings.
Build and supporting changes
cmux.xcodeproj/project.pbxproj, Sources/TerminalBrowserHostNormalizer.swift, Sources/WorkspaceGroupNewWorkspaceTarget.swift, .gitignore, documentation and schema files
Registers new sources, resources, and tests. Updates browser, workspace-group, configuration, localization, licensing, and repository files.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SidebarPeekEdgeTrackingView
  participant SidebarPeekController
  participant SidebarPeekMachine
  participant SidebarPeekPanelHost
  SidebarPeekEdgeTrackingView->>SidebarPeekController: forward pointer edge event
  SidebarPeekController->>SidebarPeekMachine: apply event
  SidebarPeekMachine->>SidebarPeekController: return phase and timer effects
  SidebarPeekController->>SidebarPeekPanelHost: update panel presentation
Loading

Suggested reviewers: austinywang


Merge Risk: 🟡 Moderate · up to d4196

After switching between floating and docked mode, the sidebar can show an empty or stale workspace list. Background resizes or a fast double toggle can also undo a sidebar show or hide. These issues affect a common interaction and should be fixed before merge.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (9 errors, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime Error The diff adds blocking or timing-based synchronization in shipped Swift code. SidebarSection+Customization.swift:238 uses Task.sleep(for: .milliseconds(45)) to debounce settings writes. `SidebarPe… Replace the settings debounce and peek dwell/grace Task.sleep calls with a cancellation-aware scheduler or timer abstraction that emits an explicit callback or state transition. Replace the 120 Hz mouse poll with an event-driven drag/poin…
Cmux Swift Concurrency Error The diff adds a new Combine-backed app-state model in Sources/Sidebar/Peek/SidebarPeekController.swift: it imports Combine, conforms to ObservableObject, and exposes @Published presentsPanel (li… Migrate the new sidebar state to Swift Observation. Convert SidebarPeekController to @Observable, mark timer and machine implementation fields with @ObservationIgnored as needed, replace @StateObject with @State, and update any af…
Cmux Swift Package Boundaries Error The diff adds independently testable sidebar motion logic to the app target. Sources/Sidebar/SidebarToggleSlideMachine.swift is a 170-line Foundation-only spring and state machine with value types, … Move SidebarToggleSlideMachine.swift and its unit tests into the existing CmuxSidebar target and CmuxSidebarTests target. Expose SidebarToggleSlideMachine as the first public API, with the required public Slide, Effect, initiali…
Cmux Swift Logging Error The PR adds production app/runtime file logging in Sources/Sidebar/SidebarNavigationTimings.swift. It reads CMUX_NAV_TIMINGS_LOG, opens the caller-supplied path with FileHandle, and writes raw d… Remove the ad hoc FileHandle path and appendLine writes. Route the diagnostic messages through a unified Logger with an appropriate subsystem/category, or use the existing cmuxDebugLog only for a DEBUG-only probe. Preserve `OSSignpo…
Cmux Full Internationalization Error The PR adds 31 production strings to Resources/Localizable.xcstrings (for example, the new sidebar settings in SidebarSection+Customization.swift and the new peek labels). Every new entry has tran… Add a real Khmer (km) translation to every new catalog key added by this PR. Replace the copied-English Reset values in settings.sidebar.glassBlur.reset, settings.sidebar.glassTint.reset, and settings.sidebar.tintColor.reset for `…
Cmux Swiftui State Layout Error The diff introduces new Combine-based SwiftUI state. SidebarPeekController is a new ObservableObject with @Published presentsPanel (Sources/Sidebar/Peek/SidebarPeekController.swift:12-15), and `… Replace the new SwiftUI-observed controllers and added published state with Observation-based models (@Observable held by @State) or immutable snapshots with action closures. Remove the ObservableObject/@Published conformances and t…
Cmux Architecture Rethink Error The PR introduces a 120 Hz polling side channel for reorder rendering. SidebarWorkspaceTableController+ReorderLift.swift:13-24 starts a repeating Timer that reads NSEvent.mouseLocation and drive… Make one reorder coordinator or state machine the source of truth for the drag session, preview, drop plan, transform handoff, and cleanup. Feed it authoritative drag begin/move/drop/end and table-apply events, with an explicit generation a…
Cmux Swift Auxiliary Window Close Shortcuts Error The new user-visible SidebarPeekPanelWindow is an NSPanel child window. SidebarPeekPanelHost.swift:217-237 creates and attaches it, and SidebarPeekPanelWindow.swift:46,69-81 allows it to becom… Assign a stable identifier such as cmux.sidebarPeekPanel to SidebarPeekPanelWindow when it is created, and add that identifier to cmuxAuxiliaryWindowIdentifiers in Sources/cmuxApp.swift. This routes Cmd+W through the shared `cmuxWin…
Cmux No Test Or Debug Seam In Production Source Error The PR adds debug-only observability seams to production source. Sources/Sidebar/Peek/SidebarPeekController.swift adds #if DEBUG debugLastHoverUptime and a Darwin debug trigger (`lines 37-79, 91… Move the debug trigger, debugLastHoverUptime, debugWindow, and debugSidebarState integration out of the main production types and into a dedicated debug file or folder. If the probe is not required, remove the seams. Do not add test-o…
Docstring Coverage Inconclusive Docstring coverage is 38.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 198 functions across 58 files. (2 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Cmux Cloud Persistent Session And Early Input Passed PASS: The pull-request diff does not change Cloud terminal creation or transport code. No changed path is under daemon/remote, cmux-tui, or Cloud VM transport services. The only Cloud-related text…
Cmux Swift Actor Isolation Passed No changed production code matches the actor-isolation failure conditions. The new CmuxSidebar value models are Sendable and live in Swift 6 package targets without a MainActor default-isolation…
Cmux Browser Automation Off-Main Passed No browser socket automation routing changed in this PR. The authoritative diff leaves Sources/TerminalController.swift and `Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCo…
Cmux Expensive Synchronous Load Passed The pull request does not add or move an expensive agent-history load. The diff adds no RestorableAgentSessionIndex.load(), SharedLiveAgentIndex, hook-store, transcript, trajectory, or workstream-…
Cmux Cache Substitution Correctness Passed No qualifying cache substitution is introduced. The workspace snapshot path still rebuilds from the current tabManager.tabs at Sources/ContentView.swift:13233-13264; the diff only changes the refr…
Cmux No Hacky Sleeps Passed PASS. The pull request introduces no changed TypeScript, JavaScript, shell, or non-Swift runtime code. The only changed Python file is the test-only schema-parity fixture, and its diff only adds `NOT_…
Cmux Algorithmic Complexity Passed PASS. The changed reorder path uses linear scans over workspace rows. In Sources/Sidebar/AppKitList/SidebarWorkspaceTableController+ReorderLift.swift, reorderUnits advances through each row or gro…
Cmux Swift @Concurrent Passed The authoritative Swift diff adds no @concurrent annotation and no nonisolated async function. The added asynchronous work only sleeps, yields, schedules main-actor UI updates, or supports tests. …
Cmux Swiftpm Lockfiles Passed Pass. The PR changes the root .gitignore and cmux.xcodeproj/project.pbxproj, but it does not add a Package.resolved ignore rule. No Package.swift or Package.resolved file changed. The Xcode …
Cmux User-Facing Error Privacy Passed PASS — The diff adds sidebar settings, accessibility/help text, group-color actions, and navigation behavior. It does not add or change a user-facing error, alert, command output, API error body, or r…
Cmux Source Artifacts Passed No changed path violates the source-control artifact rule. The diff contains source, tests, configuration, documentation, localization/schema data, and project wiring. The 355-file `Resources/Material…
Title check Passed The title clearly identifies the sidebar redesign, workspace reordering, groups, and glass appearance. It is concise and related to the main changes.
Description check Passed The description is detailed and includes Summary, Testing, Changelog, and Checklist sections. It documents behavior, tests, CI results, risks, localization, licensing, and screenshots or videos. The d…

Full details: Docstring Coverage

Explanation

Docstring coverage is 38.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 198 functions across 58 files. (2 skipped: 1 unsupported, 1 too large.)


Full details: Cmux Swift Blocking Runtime

Explanation

The diff adds blocking or timing-based synchronization in shipped Swift code. SidebarSection+Customization.swift:238 uses Task.sleep(for: .milliseconds(45)) to debounce settings writes. SidebarPeekController.swift:172 uses Task.sleep for dwell and grace state transitions. SidebarWorkspaceTableController+ReorderLift.swift:13-24 adds a repeating 120 Hz Timer that polls the mouse during production drag handling. SidebarToggleAnimator.swift:178 pumps RunLoop.current until a time deadline, and lines 220-222 use DispatchQueue.main.asyncAfter as a fallback to land an animation when a delegate callback is missed. These are new production paths, not test scaffolding. The rule allows short visual animation delays only when they are visual timing and do not use Task.sleep; these waits and polls coordinate settings, state transitions, drag behavior, layout, and completion.

Resolution

Replace the settings debounce and peek dwell/grace Task.sleep calls with a cancellation-aware scheduler or timer abstraction that emits an explicit callback or state transition. Replace the 120 Hz mouse poll with an event-driven drag/pointer signal or a dedicated frame callback owned by the drag session. Replace the run-loop deadline pump with an AppKit layout/completion signal. Replace the delayed animation fallback with an explicit Core Animation or transaction completion path and an idempotent state transition. Do not use raw sleeps, delayed dispatch, or repeating polling in production synchronization.


Full details: Cmux Swift Concurrency

Explanation

The diff adds a new Combine-backed app-state model in Sources/Sidebar/Peek/SidebarPeekController.swift: it imports Combine, conforms to ObservableObject, and exposes @Published presentsPanel (lines 1–15). ContentView stores it with @StateObject at line 946, and the sidebar lifecycle reads presentsPanel to drive the floating card. This is not a required AppKit callback boundary. The repository already uses Observation and @Observable for comparable app models, while the controller already uses Swift concurrency for its stored, cancellable timer tasks. The diff also adds @Published sidebar state in SidebarLayoutModel and SidebarState, which expands the same legacy pattern.

Resolution

Migrate the new sidebar state to Swift Observation. Convert SidebarPeekController to @Observable, mark timer and machine implementation fields with @ObservationIgnored as needed, replace @StateObject with @State, and update any affected bindings. Avoid adding new @Published properties to SidebarLayoutModel and SidebarState; migrate those newly observed fields and their SwiftUI consumers to Observation as part of the same change. Keep the existing stored and cancelled Task timer/debounce operations.


Full details: Cmux Swift Package Boundaries

Explanation

The diff adds independently testable sidebar motion logic to the app target. Sources/Sidebar/SidebarToggleSlideMachine.swift is a 170-line Foundation-only spring and state machine with value types, pure position calculations, and effect-based transitions. The app target registers it in cmux.xcodeproj/project.pbxproj, and cmuxTests/SidebarFocusBoundaryLifecycleTests.swift tests it directly with a fake clock. This matches the rule for core logic that can compile and test without AppKit, SwiftUI view state, or app lifecycle code. The PR already adds the suitable CmuxSidebar SwiftPM target and places the analogous pure SidebarPeekMachine behind that boundary.

Resolution

Move SidebarToggleSlideMachine.swift and its unit tests into the existing CmuxSidebar target and CmuxSidebarTests target. Expose SidebarToggleSlideMachine as the first public API, with the required public Slide, Effect, initializer, and transition methods; expose SidebarSlideSpring if it remains in the public signature or test API. Import CmuxSidebar from SidebarToggleAnimator and keep only the AppKit/Core Animation orchestration in Sources/Sidebar/SidebarToggleAnimator.swift. Remove the machine from the app target’s Xcode source phase.


Full details: Cmux Swift Logging

Explanation

The PR adds production app/runtime file logging in Sources/Sidebar/SidebarNavigationTimings.swift. It reads CMUX_NAV_TIMINGS_LOG, opens the caller-supplied path with FileHandle, and writes raw diagnostic lines with logHandle.write(data) at lines 26-36 and 114-116. New runtime call sites emit navigation, reorder, toggle, and frame diagnostics through this path. The code is intentionally compiled in Release, so the debug-only exception does not apply. The timing values do not expose personal data, but the file logger bypasses unified logging.

Resolution

Remove the ad hoc FileHandle path and appendLine writes. Route the diagnostic messages through a unified Logger with an appropriate subsystem/category, or use the existing cmuxDebugLog only for a DEBUG-only probe. Preserve OSSignposter intervals for Instruments if needed, and keep any high-frequency diagnostics sampled and non-sensitive.


Full details: Cmux Full Internationalization

Explanation

The PR adds 31 production strings to Resources/Localizable.xcstrings (for example, the new sidebar settings in SidebarSection+Customization.swift and the new peek labels). Every new entry has translations for 19 locales, but all 31 omit km, which is an existing locale in the touched catalog. Several new Reset entries also use copied English for bs, da, it, nb, pl, pt-BR, ru, th, tr, and uk. The Swift call sites use the required localized API, but the catalog coverage is incomplete.

Resolution

Add a real Khmer (km) translation to every new catalog key added by this PR. Replace the copied-English Reset values in settings.sidebar.glassBlur.reset, settings.sidebar.glassTint.reset, and settings.sidebar.tintColor.reset for bs, da, it, nb, pl, pt-BR, ru, th, tr, and uk. Preserve the existing Liquid Glass brand omission only if that invariant branding is intentional and accepted by the localization policy.


Full details: Cmux Swiftui State Layout

Explanation

The diff introduces new Combine-based SwiftUI state. SidebarPeekController is a new ObservableObject with @Published presentsPanel (Sources/Sidebar/Peek/SidebarPeekController.swift:12-15), and ContentView owns it with @StateObject at Sources/ContentView.swift:946. SidebarToggleAnimator is also a new ObservableObject owned with @StateObject at Sources/ContentView.swift:950. These are cmux-owned controllers, not AppKit bridge views where SwiftUI observation is incidental. The change also materially adds @Published state to the legacy SidebarState and SidebarLayoutModel types. The repository already uses @Observable models, so the allowed legacy-state exception does not cover these new state paths.

Resolution

Replace the new SwiftUI-observed controllers and added published state with Observation-based models (@Observable held by @State) or immutable snapshots with action closures. Remove the ObservableObject/@Published conformances and the @StateObject ownership from ContentView; keep SidebarToggleAnimator as a non-observed AppKit coordinator if SwiftUI does not read its state. Migrate the new SidebarState.presentationMode and SidebarLayoutModel.docksSidebar observation to the same modern shape, and preserve narrow view observation through value inputs and explicit actions.


Full details: Cmux Architecture Rethink

Explanation

The PR introduces a 120 Hz polling side channel for reorder rendering. SidebarWorkspaceTableController+ReorderLift.swift:13-24 starts a repeating Timer that reads NSEvent.mouseLocation and drives the lift outside AppKit drag callbacks. The controller documents that this bypasses destination callbacks because they churn (SidebarWorkspaceTableController.swift:1914-1926). The same behavior is split across the poll, overlay callbacks (SidebarWorkspaceTableController.swift:2882-2910), drag-end cleanup (:1564-1578), table-apply handoff (:831-986), and a one-second Task.sleep fallback (SidebarWorkspaceTableController+ReorderDrop.swift:58-70). This violates the rule's polling and timing-repair conditions. It leaves stale lift transforms, drop plans, and drag sessions representable when callback, table-update, and drop timing changes. The PR introduces this behavior; the base controller has no reorder polling.

Resolution

Make one reorder coordinator or state machine the source of truth for the drag session, preview, drop plan, transform handoff, and cleanup. Feed it authoritative drag begin/move/drop/end and table-apply events, with an explicit generation and terminal transition. Remove the controller-owned 120 Hz timer, the duplicated poll/overlay update paths, and the one-second fallback. If AppKit requires a platform bridge for continuous pointer tracking, isolate that bridge behind the coordinator and emit value snapshots and lifecycle events instead of mutating controller state. Add tests for every terminal path, including drop-before-apply, drag-end-before-drop, table rebuild during a drag, and teardown.


Full details: Cmux Swift Auxiliary Window Close Shortcuts

Explanation

The new user-visible SidebarPeekPanelWindow is an NSPanel child window. SidebarPeekPanelHost.swift:217-237 creates and attaches it, and SidebarPeekPanelWindow.swift:46,69-81 allows it to become key while an editor is active. The panel has no NSUserInterfaceItemIdentifier assignment, and no cmux.* identifier appears in the new peek sources. Therefore cmuxWindowShouldOwnCloseShortcut cannot recognize it. The deterministic lint passes because it only checks literal identifier assignments; this is the broader review-only pattern.

Resolution

Assign a stable identifier such as cmux.sidebarPeekPanel to SidebarPeekPanelWindow when it is created, and add that identifier to cmuxAuxiliaryWindowIdentifiers in Sources/cmuxApp.swift. This routes Cmd+W through the shared cmuxWindowShouldOwnCloseShortcut path. The existing scripts/lint_auxiliary_window_close_shortcuts.py does not detect this omission until an identifier assignment exists.


Full details: Cmux No Test Or Debug Seam In Production Source

Explanation

The PR adds debug-only observability seams to production source. Sources/Sidebar/Peek/SidebarPeekController.swift adds #if DEBUG debugLastHoverUptime and a Darwin debug trigger (lines 37-79, 91-93); the value is read only by the debug probe in SidebarPeekPanelHost.swift. Sources/Sidebar/SidebarToggleAnimator.swift adds #if DEBUG debugWindow and debugSidebarState accessors (lines 326-329), which are read only by SidebarToggleSlideProbe.swift. These members expose private state for a debugger/probe, have no normal production caller, and are not isolated in a dedicated debug file. The changed code therefore matches the rule's prohibited debug seam conditions.

Resolution

Move the debug trigger, debugLastHoverUptime, debugWindow, and debugSidebarState integration out of the main production types and into a dedicated debug file or folder. If the probe is not required, remove the seams. Do not add test-only accessors to shipping files; for test observation, widen only the needed state from private to internal and read it from the test target with @testable import. Use the canonical fix in #6452 as the reference.


✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch sidebar-animations

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR


  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@blacksmith-sh

This comment has been minimized.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Probably yours: TerminalAndGhosttyTests.swift:3049, GhosttyResponderResolutionTests.swift, NewMachineSheetLayoutTests.swift:233 and 2 more fails here but not on main's latest full suite. 1 more also fail on main (#18514). Seen on other PRs too (likely flaky): CommandPaletteFocusStealingTests.swift:55, crash Fatal error: interpreter worker test crash sentinel at RenderInterpreterRunner.swift:29, crash Fatal error: render fixture crash sentinel at main.swift:76 and 1 more.

CI failed on 77b039fd90 (run 37927500598 attempt 1): 5 code, 2 unknown.

Job Verdict Why
macos / swift-package-tests code a test failed
macos / app-host unit tests (4/7) code a test failed
macos / app-host unit tests (2/7) unknown no known signature; failed step: Enumerate built app-host tests
macos / app-host unit tests (5/7) unknown no known signature; failed step: Enumerate built app-host tests
macos / app-host unit tests (7/7) code a test failed
macos / app-host unit tests (6/7) code a test failed
macos / app-host unit tests (3/7) code a test failed

Failures:

  • seen on other PRs CommandPaletteFocusStealingTests.swift:55 doesNotReadTextViewDelegateForClassification(): Expectation failed: (textView.delegateReadCount → 4) == 0 (job)
  • seen on other PRs crash Fatal error: interpreter worker test crash sentinel at RenderInterpreterRunner.swift:29 (job)
  • seen on other PRs crash Fatal error: render fixture crash sentinel at main.swift:76 (job)
  • new in this PR TerminalAndGhosttyTests.swift:3049 GhosttyResponderResolutionTests.testDoesNotReadTextViewDelegateForGhosttyResponderResolution: XCTAssertEqual failed: ("4") is not equal to ("0") - Ghostty responder resolution must avoid NSTextView.delegate because... (job)
  • new in this PR GhosttyResponderResolutionTests.swift GhosttyResponderResolutionTests.testDoesNotReadTextViewDelegateForGhosttyResponderResolution(): failed (job)
  • seen on other PRs CloudTreeHeaderActionsTests.swift:168 Hovered header actions stay in the accessibility tree with their labels: Expectation failed: (Self.accessibilityAttribute(.description, getter: "accessibilityLabel", of: menuElement) as? String... (job)
  • also red on main CmuxTopProcessSnapshotCaptureCoordinatorTests.swift:141 A slow fresh quit census returns at its deadline: Time limit was exceeded: 300.000 seconds (job)
  • new in this PR NewMachineSheetLayoutTests.swift:233 long base machine names keep every control inside the sheet: Expectation failed: (basePopUp → nil) != nil (job)
  • new in this PR NewMachineSheetLayoutTests.swift:155 the Base, Size, and Network pop-ups share a leading edge at their own widths: Expectation failed: popUps.first { $0.itemTitles.contains { $0.hasPrefix("cmux-devbox-") } } → nil (job)
  • new in this PR BrowserDesignModeComposerHostingViewTests.swift:73 presentedComposerRoutesEventsOnlyWithinTheCardFrame(): Expectation failed: (hit(topLeftPoint: NSPoint(x: 150, y: 320)) → nil) != nil (job)
Matched log lines
macos / swift-package-tests: ✘ Test doesNotReadTextViewDelegateForClassification() recorded an issue at CommandPaletteFocusStealingTests.swift:55:9: Expectation failed: (textView.delegateReadCount → 4) == 0
macos / app-host unit tests (4/7): /tmp/cmux-ci-2/src/cmuxTests/TerminalAndGhosttyTests.swift:3049: error: -[cmuxTests.GhosttyResponderResolutionTests testDoesNotReadTextViewDelegateForGhosttyResponderResolution] : XCTAssertEqual failed: ("4") is not equal to ("0") - Ghostty responder resolution must avoid NSTextView.delegate because
macos / app-host unit tests (7/7): ✘ Test "Hovered header actions stay in the accessibility tree with their labels" recorded an issue at CloudTreeHeaderActionsTests.swift:168:9: Expectation failed: (Self.accessibilityAttribute(.description, getter: "accessibilityLabel", of: menuElement) as? String → nil) == "Manage My Devices"
macos / app-host unit tests (6/7): ✘ Test "long base machine names keep every control inside the sheet" recorded an issue with 1 argument layout → .grid at NewMachineSheetLayoutTests.swift:233:9: Expectation failed: (basePopUp → nil) != nil
macos / app-host unit tests (3/7): ✘ Test presentedComposerRoutesEventsOnlyWithinTheCardFrame() recorded an issue at BrowserDesignModeComposerHostingViewTests.swift:73:9: Expectation failed: (hit(topLeftPoint: NSPoint(x: 150, y: 320)) → nil) != nil

Not re-run automatically: macos / swift-package-tests, macos / app-host unit tests (4/7), macos / app-host unit tests (2/7), macos / app-host unit tests (5/7), macos / app-host unit tests (7/7) are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood tours of 77b039fd

sidebar-and-chrome-tour at 77b039fd: failure (run)

the run left no frames (see the run log)

Tours are picked by the paths globs in dogfood/scenarios/*.json; a Dogfood-tours: a, b line in the description picks them instead (none turns this off). Look at every frame before merging: a green tour only means no step failed.

…, not cmux.json paths

fixes test_every_advertised_path_is_supported
…p fork wording

ContentView rendered with matchTerminalBackground off while the catalog,
Settings and the other readers said on. the catalog, schema and docs now
say off too, which is what the glass sidebar needs. the dead legacy preset
migration goes and the diagnostics report the real bgGlassEnabled default.
… of the controller

review fixes:
- the apply's forced-reload / reconfigure tail hung off the settle check and
  ran after every structural apply; it is back on the structural chain and
  the settle runs after it
- every structural apply ends the lift (snapshots, hidden cells, transforms,
  pinned rows), not just the reload and pure-reorder branches, so a drop into
  a collapsed group or one that changes heights no longer leaves blank cells
- a new drag cancels a pending drop hand-off and ends any leftover lift; the
  grab offset is captured after the duplicate-begin check
- the drop commit fallback uses the clock + cancellable task pattern instead
  of asyncAfter; dismantle clears it and the lift
- settle raises rows to zPosition 100 and now drops them back after the glide
- the animated insert/remove branches were unreachable behind the pure-edit
  path; small pure edits now animate there (3 rows or fewer, no height change)
- multi-selection glass keeps an Increase Contrast boost

file length: the reorder lift and drop code move into
SidebarWorkspaceTableController+ReorderLift/+ReorderDrop, the glyph button
into its own file, the glass selection colors into SidebarGlassSelection, the
row cell's lift helpers into an extension.
…group's own color

- only user toggles animate; programmatic setVisible (narrow-window
  auto-collapse, session restore) applies at once so callers can read
  isVisible right after. a second hide during the sweep is ignored instead of
  capturing the half-collapsed width
- the peek panel bridge tears its panel down on dismantle and gives the
  terminal its cursor rects back
- Clear Color shows only when the group has its own color (a config color
  cannot be cleared from the menu)
- peek lifecycle modifiers, the toggle hover broadcast and the terminal's
  cursor yield move into ContentView+SidebarPeek helpers (file length)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@lucasr1b lucasr1b changed the title sidebar: floating peek, glass ground, freeform drag reorder, group color sidebar: floating peek, glass ground, freeform drag reorder, group color + file icons Oct 8, 2026
…a reveal applies once

- the parked docked list keeps applying content (titles, order, cloud
  identity), so a show slides in current rows. spinners, status pulses and
  hover pause while it's off screen (#14832's concern) through a table-level
  rows-on-screen gate, and resume on reveal
- VerticalTabsSidebar no longer subscribes to SidebarState it never reads. in
  its own hosting view every visibility publish cost a full body pass, which
  is where the triple row projection on reveal came from
- isRevealed on the sidebar also unmounts the titlebar double-click/drag strip
  and minimal mode controls over a hidden sidebar
- the reveal's push waits a main-queue turn instead of running in the landing
  frame
- SidebarHiddenPresentationTests waits for the toggle to land on isVisible's
  own commit (the slide's completion), asserts rows and cloud identity apply
  while hidden with row animations off, resume on reveal, and at most one
  projection per row. the provider-scope test moved to its own file unchanged
…t of the landing

review follow-ups on 5b2c4c9:
- the animator turns the docked table's rows on screen when a show starts and
  off when a hide lands, so spinners and status pulses slide in moving, as
  before, instead of waiting for SwiftUI after the landing
- the deferred reveal push runs in the default run loop mode, so the landing's
  event tracking spin can't pick it up, and the rest of the landing update
  rides the pending push
- the hidden presentation test has a one minute limit and checks that rows
  were added while hidden rather than an exact count
…, same tint

- SidebarHeaderGlyphButton gets an opt-in hover fill: a circle on its 16pt
  bounds in the surface's foreground, 15% on hover, 22% pressed, glyph lifted
  to 65%. the group header plus and the row close button use it, in both
  active and inactive rows
- the row close button rests at the same secondary label colour as the plus
  (it was 70% of the selected foreground on active rows)
- hover settles from the live pointer on every reveal and conceal, so a click
  that reflows rows, a drag, or a row scrolling under a still pointer can't
  strand it
…n place

the stock #393939 at 72% applied in both appearances, so in light mode the
sidebar stayed a dark glass under dark text. with no light override and the
stock hex, light mode now uses #F2F2F2 (the stock opacity maps to 72% too, a
chosen opacity is kept). main's light #000000 at 18% assumed a light material
under it; the compositor ground is the raw desktop blur. dark is unchanged and
a chosen colour wins in both modes. test: stockSidebarTintIsAppearanceAware
… hover around drags

- an edgeless soft wash on the row under the pointer (the glass selection's
  shape, 5% dark / 3.5% light, tunable via sidebarRowHoverFillOpacity[Light]);
  selection, multi-selection and custom fills win, the header plus draws on top
- gated by the sidebarRowHover default (on unless turned off), read where the
  wash is painted and followed live through KVO: the loaded-cell sweep
  repaints every row. hover-revealed chrome (close X, header plus) is
  unaffected
- drags: hover is cleared at drag start and before the lift snapshot,
  suppressed while the drag runs, and re-derived from the live pointer after
  the drop and after the settle; the sweep covers every loaded row view
- the hover code lives in SidebarRowHover.swift and
  SidebarWorkspaceTableController+Hover.swift, so the cell files and the
  controller stay inside the length budget
writes sidebarRowHover (UI-only, on by default) from a private row in the
section file, so the package interface is unchanged. search entry next to the
other sidebar appearance rows. strings in the app catalog in all 19 locales

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

only in light mode, only while the stock tint is in place (a chosen tint,
a light-mode tint, a custom selection colour or matching the terminal opt
out); dark is unchanged
- ground: #D1D4D5 at 88%, a cool mid-light gray (~#CACDCE over the desktop)
- selected row and active group header: a solid white pill with a black 25%
  hairline and a soft 1 pt drop shadow; text on it goes near-black through the
  existing contrast path
- hover: an edgeless white wash at 62% between the ground and the pill,
  driven by sidebarRowHoverFillOpacityLight (a black wash, capped at 20%,
  outside this look)
- unselected row titles: black at 69% (~#414141 on the ground)
- the glass selection's opacity tuning keys only shape the translucent patch,
  which this look does not use
…, not solid

lands around 250 on the gray ground; the hairline, the soft shadow and the
0.62 hover wash below it are unchanged. the active group header uses the same
fill
#expect decomposes allSatisfy(\.isPresentationActive) and types the key
path as a throwing function, which failed the test target's compile
black at 7% (was 14%), a barely-there lift; tunable through
sidebarSelectionShadowOpacityLight. the hairline is unchanged
… pill

showOptimisticAnchorActive (press feedback and the active look while dragging
a group) painted label colour at 8% directly, a gray patch in the light look;
it now uses the header's own active fill
layers only, no blur: a white fill a touch brighter at the top (48% mid,
10% spread) and a 0.5 pt white inner rim at 55%, like the selection's glass
edge; no shadow. tunable through sidebarRowHoverFillOpacityLight,
sidebarRowHoverGradientLight and sidebarRowHoverRimLight. the hover layers are
gradient layers; dark keeps its flat wash

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

since the docked list moved into its own unclipped hosting view, a sidebar
field's visibleRect came back window-sized, its midpoint fell outside the
sidebar, and hiding the sidebar while renaming no longer handed focus back to
the terminal. clamp the visible rect to the responder's bounds
dark keeps the translucent white patch; light on the stock tint is white at
90% with a 25% hairline, and a chosen tint keeps the translucent black patch
…port file

the reveal projection check reports which known frames ran each pass; the
run loop and toggle helpers move to SidebarHiddenPresentationTests+Support
to keep the file inside its length budget
…cted binding

the docked list lives in its own hosting view, and each ContentView update
pushed a fresh `$sidebarSelectionState.selection` binding, which SwiftUI took
as a change and re-ran VerticalTabsSidebar's body despite its equatable gate:
three full row projections per reveal (CI named the passes: hosting view
layout, `_selection changed`). the sidebar now reads SidebarSelectionState from
the environment (both hosted environments inject it) through a computed
`selection`
@lucasr1b

lucasr1b commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

follow-up round (5b2c4c9..dad39a8)

hidden sidebar (ci fix, lucas's option 1)

  • the hidden docked list stays live for content (titles, order, cloud identity), so a show slides in current rows
  • spinners, status pulses and hover pause while it's off screen (Pace sidebar spinner animations and stop them while hidden #14832's concern) and start on the first frame of a show
  • a reveal applies each row at most once: VerticalTabsSidebar subscribed to SidebarState without reading it, and in its own hosting view every visibility publish cost a full extra body pass
  • the titlebar double-click/drag strip and minimal mode controls no longer stay mounted over a hidden sidebar
  • SidebarHiddenPresentationTests waits for the toggle to land on isVisible's own commit (no sleeps, one minute limit) and checks the new contract
  • reviewed by a subagent, then its findings fixed (spinners slid in static, the deferred push could run inside the landing, test robustness)

hover

  • group "+" and row X: aside-style circle (16pt, 15% hover, 22% pressed, glyph to 65%); the X rests at the same secondary colour as the "+"
  • row hover on workspace rows and group headers, behind Settings > Sidebar > Row Hover (on by default for now, open for feedback). it follows the setting live
  • stuck hover after a drag is fixed: cleared at drag start and before the lift snapshot, off during the drag, re-derived from the pointer after the drop and the settle

light mode, matched to aside (stock tint only; dark, a chosen tint, a custom selection colour and match terminal background are unchanged)

  • ground #D1D4D5 at 88%, a cool mid-light gray
  • selected row and active group header: white at 90% with a 25% hairline and a 7% 1pt shadow; pressed or dragged headers take it too
  • glassy white hover: 48% white, a touch brighter at the top, 0.5pt white rim, no blur
  • unselected titles at ~#414141

fixed overnight from ci

  • a sidebar field's visibleRect came back window-sized in the docked list's own (unclipped) hosting view, so hiding the sidebar while renaming no longer handed focus back to the terminal. the focus boundary now measures the responder within its bounds (b7b23f9)
  • the reveal still projected every row three times: ContentView passed $sidebarSelectionState.selection, and each push of a fresh projected binding re-ran the sidebar body past its equatable gate. the sidebar now reads SidebarSelectionState from the environment (dad39a8). the test names what drives each pass if this regresses
  • SidebarGlassSelectionTests updated for the approved light pill; a test compile error (a key path in #expect) and the row hover search anchor fixed

ci on dad39a8: everything this branch touches is green, including SidebarHiddenPresentationTests. one red left, not ours: BrowserReplRenderHostTests.hiddenDrivenTabRendersOffEveryScreenAndReturnsToItsPane also fails on main's full suite (#18514); I'll merge main once it's fixed there. flakes that passed on rerun: runner pasteboard (swift-package-tests), simulator worker, WorkspaceTerminalFocusRecoveryTests, and app-host shards refused by a screen-locked mini or a launch receipt

checked on a local tagged build; light values measured against aside screenshots in the sidebar lab and the app. dark mode pixel-identical before and after

lucasr1b and others added 2 commits October 9, 2026 08:15
the stock light pill was a flat 90% white sticker with a dark 25% hairline.
it is now a brighter piece of the hover's glass: 74% white with a top-bright
gradient, a 0.5 pt white inner rim, an 8% outer edge and a 5% shadow, all
layers on the existing wash layer. the active group header matches, the
reorder lift carries it, and dark mode, custom tints and selection colours
are untouched. each value is tunable through a sidebarSelection*Light key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
with the stock light look, row titles now take the pill's near-black on
every row instead of near-black on the active row and a 69% gray elsewhere,
and secondary lines keep their resting colour on the glass pill, so the pill
alone marks the active row. dark mode, custom tints and selection colours
keep their own contrast rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at ef397b8.

Resolved conflicts:
- Resources/Localizable.xcstrings: xcstrings key-level union
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Merge-main-previous-head: 03af860
Merge-main-base: ef397b8
@lucasr1b lucasr1b changed the title sidebar: floating peek, glass ground, freeform drag reorder, group color + file icons sidebar: organize and drag workspaces (reorder, groups, glass look) Oct 9, 2026
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at cd85f00.

Resolved conflicts:
- Resources/Localizable.xcstrings: xcstrings key-level union
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Merge-main-previous-head: 53d4ead
Merge-main-base: cd85f00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 77b039f. Configure here.

if tintHex.caseInsensitiveCompare(stock.hex) == .orderedSame {
resolvedHex = WindowChromeSidebarTintDefaults.light.hex
if tintOpacity == stock.opacity { opacity = WindowChromeSidebarTintDefaults.light.opacity }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light stock tint ignores empty override

Medium Severity

The new light-mode stock swap only runs when tintHexLight is nil. An empty string, the catalog's stored "no override" value for sidebarAppearance.lightModeTintColor, is treated as an override, so the dark #393939 glass stays in place. Light-mode row titles are black, so the sidebar can become hard to read.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77b039f. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants