Skip to content

Finder-style sidebar columns: machines rail, icon-collapsible columns, hover cards - #10505

Closed
lawrencecchen wants to merge 63 commits into
mainfrom
feat-sidebar-finder-columns
Closed

lawrencecchen wants to merge 63 commits into
mainfrom
feat-sidebar-finder-columns

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Redo of the #9453 presentation on top of its plumbing (machine registry, workspace routing, per-machine focus memory, persistence, RPC, ExtensionKit contract all kept via merge).

  • The machines column lists places only: This Mac plus remote machines. The Automatic pseudo-row is gone; legacy automatic ids (old sessions, socket callers) resolve to This Mac. Machine selection follows workspace selection like Finder's ancestor cascade, which preserves the old per-machine Cmd+T behavior.
  • Machine rows are compact single lines: hardware icon for This Mac, tinted SF Symbols for SSH/cloud machines, connection dot. Machine selection uses the quiet unemphasized tint; the accent stays on the focused workspaces column.
  • Any column minimizes to a 52pt icon rail when its divider is dragged past the minimum (28pt of deliberate travel, with hysteresis on the way out), with one shared easeInOut(0.18) animation. Double-clicking the machines divider toggles the rail. Widths and modes persist per window.
  • Icon rails get dwell (350ms) hover cards: machines show state and focused workspace, workspaces show title/description/branch/unread, groups show member count. Workspace icon rows render a letter avatar tinted by the workspace color; group headers render their icon symbol; context menus reuse the regular rows' shared builders.
  • The footer spans the whole sidebar region instead of living inside the machines column, so the machines minimum drops from 168 to 132.
  • New debug RPC sidebar.column.set_mode (column: machines|workspaces, mode: regular|icons) drives the same persisted-mode path as the divider.

Mechanism notes: column widths remain regular widths in the layout model; display modes project effective widths so leaving the rail restores the remembered width. A mode flip swaps AppKit cell classes, so the table apply path replaces cells with one atomic reload, and every animated mode change is bracketed with the interactive-resize signals so PTY resizes defer and the table settles authoritatively.

Verification

  • Tagged cloud build green on this head.
  • Local tagged preflight over the debug socket: machines list (no Automatic, This Mac selected), sidebar.machine.add_ssh, both columns flipped to icon rails via sidebar.column.set_mode, debug.window.screenshot evidence at each step.
  • New unit tests: SidebarColumnDisplayPolicyTests (snap band vs clamp band, hysteresis, non-finite fallbacks, shared rail width, machines-only default, legacy automatic-id resolution).
  • Updated: SidebarWidthPolicyTests, TabManagerUnitTests reorder test, SidebarResizeUITests (Automatic absent, region-wide footer, icon-rail drag leg).

Dictionary:

  • Icon rail means a column minimized to a fixed 52pt strip showing only icons.
  • Hysteresis means the snap-in and snap-out thresholds differ so the pointer resting near the boundary cannot flap the column.
  • Tagged build means a debug build with its own bundle id and control socket.

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 from a single workspace list with an Automatic pseudo-row into Finder-style machine-scoped columns: a machines rail and the selected machine’s workspaces. Columns can resize or collapse to 52pt icon rails, with hover cards and switchable navigation and boundary layouts; the Automatic row is gone and legacy automatic ids map to This Mac.

Sidebar behavior

  • Machine selection follows workspace selection and preserves per-machine Cmd+T behavior.
  • Widths, display modes, machine order, focused workspaces, and selection persist per window.
  • Icon rails show workspace or group icons, unread state, shared context menus, and 350ms detail cards.
  • Inner-divider drags keep the terminal edge fixed when possible, and double-clicking toggles the machines rail.
  • Debug controls expose six boundary styles and the Two Columns, Machine Chips, Machine Dropdown, and Bottom Machine Dock layouts.
  • RPCs support context listing, selection, reordering, SSH machine creation, remote cmux TUI attachment, and workspace moves.

Extension migration

  • CmuxExtensionKit adds creationContexts, parent-owned childColumn routes, and actions for context and remote-machine management.
  • Custom sidebars must render creationContexts, use selectedCreationContextId, and route the selected childColumn, including cmux.workspaces, to the trailing column.

Written for commit ea06d10. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a two-column sidebar with independently resizable or icon-only machine and workspace views.
    • Added local and remote machine contexts with workspace reassignment, selection, and reordering.
    • Added SSH machine creation and remote cmux TUI session attachment.
    • Added hover cards, connection indicators, and improved icon-rail presentation.
    • Sidebar layout, selections, widths, and machine ordering now persist across sessions.
    • Added double-click support for restoring sidebar divider behavior.
  • Documentation

    • Expanded extension and custom-sidebar documentation with context data, actions, permissions, and examples.

lawrencecchen and others added 30 commits August 3, 2026 04:21
# Conflicts:
#	scripts/ghosttykit-checksums.txt
Keeps the machines/creation-context plumbing from #9453 (registry, routing,
focus memory, persistence, RPC, ExtensionKit contract) on top of current main.
Presentation gets redone Finder-style in follow-up commits.
… model

- SidebarColumnDisplayMode (regular/icons) with a shared width profile and
  pure snap policy: dragging below min-28 snaps to a 52pt icon rail,
  dragging back past min restores the remembered regular width (hysteresis).
- SidebarLayoutModel stores regular widths plus per-column modes; all
  readers/appliers consume effective widths so the icon rail drives region
  geometry.
- Machines column lists places only: the Automatic pseudo-row is gone,
  default selection is This Mac, legacy automatic snapshots/RPC ids resolve
  to local.
- Machine selection now follows workspace selection (Finder cascade), which
  preserves the old Automatic Cmd+T ergonomics per machine.
- SSH machines use server.rack; column modes persist per window.
- SidebarColumnsContainer replaces the CmuxSidebarColumns host usage: dumb
  geometry shell whose divider reports raw drag widths; snap policy stays in
  ContentView. Double-click the divider toggles the machines icon rail.
- SidebarMachineColumnView replaces SidebarCreationContextColumn: compact
  single-line rows (hardware icon for This Mac, tinted SF Symbols for
  remotes, connection dot), no card chrome, icon-rail presentation with
  dwell hover cards (SidebarHoverCardAnchor + shared card shell).
- Footer spans the whole sidebar region instead of living inside the
  machines column, freeing both columns to shrink.
- Divider drags resolve through SidebarColumnDisplayPolicy for both columns:
  direct-manipulation ticks write unanimated, threshold crossings animate
  onto/off the icon rail; modes persist and restore per window.
- SidebarWorkspaceIconTableCellView renders workspace and group-header rows
  as a single centered glyph (letter avatar tinted by the workspace color,
  group SF Symbol), selection paint, unread dot; context menu reuses the
  regular row's shared builder so both presentations expose identical
  actions.
- Display mode flows through SidebarWorkspaceTableEnvironmentSnapshot, so a
  mode flip reconfigures and re-measures rows through the normal apply pass;
  heightOfRow returns the fixed rail height in icon mode.
- Controller-level dwell hover cards (same 350ms contract as the machines
  column) present workspace/group detail popovers anchored to the hovered
  icon row; torn down on hover change, context menu, or hidden presentation.
- Legacy SwiftUI list keeps the plain width clamp (no icon mode).
- SidebarColumnDisplayPolicyTests: clamp band vs snap band, hysteresis,
  non-finite fallbacks, shared rail width, machines-only default selection,
  legacy automatic-id resolution.
- SidebarResizeUITests: Automatic row asserted absent, footer containment
  widened to the region, new icon-rail drag leg (snap in, restore out).
- Hover-card strings (en/ja); dead Automatic strings removed.
Back to the multiple-of-the-same-sidebar presentation: machine rows use the
identical shared row surface, metrics, palette, and accent selection as
workspace rows (title + subtitle, semibold 12.5/10), with the machine icon
and connection dot on the title line. The icon rail keeps the shared
selection style. The floated glass card is reverted; the flat panel,
hairline divider, and workspaces/terminal border are back.
Comment thread Sources/ContentView.swift
Comment on lines +3752 to +3761
view = AnyView(view.onChange(of: sidebarState.persistedLeadingColumnMode) { newValue in
setSidebarLeadingColumnMode(newValue)
})

view = AnyView(view.onChange(of: sidebarState.persistedPrimaryColumnMode) { newValue in
guard sidebarLayout.primaryColumnMode != newValue else { return }
animateSidebarColumnModeChange {
sidebarLayout.primaryColumnMode = newValue
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Active drag overrides mode RPC

When sidebar.column.set_mode runs during an active divider drag, the observer applies the requested mode but the next drag tick derives another mode from the pointer width and drag completion persists that result, causing the successful RPC's requested mode not to stick.

Knowledge Base Used: macOS App Core (Sources/)

The machines/workspaces line no longer cuts through the titlebar strip or
the footer strip: it starts below the toolbar band (firstRowTopOffset) and
ends above the footer (44pt).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14cd08f906

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +39 to +40
case .addSSHMachine:
return [.addSSHMachine]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require selection permission when addSSHMachine selects

When an extension requests only .addSSHMachine, it can send this action with select: true—the SDK defaults to that value—and addSidebarSSHMachine then calls selectSidebarCreationContext, changing the defaults used by subsequent New Workspace/New Terminal actions. Because .selectCreationContext is a separate consent scope, include it in requiredScopes whenever select is true or make the add action non-selecting.

Useful? React with 👍 / 👎.

Comment on lines +889 to +895
private func resolvedSidebarCreationContextID(for workspace: Workspace) -> String {
if let explicit = workspace.sidebarCreationContextID?
.trimmingCharacters(in: .whitespacesAndNewlines),
!explicit.isEmpty,
isValidSidebarWorkspaceParentContextID(explicit)
{
return explicit

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve remote memberships across window moves

When a local workspace has been explicitly placed under a remote context and is moved to another window, the workspace object retains sidebarCreationContextID, but the destination TabManager does not receive the corresponding registered remote context. This validity check therefore rejects that ID and resolves the workspace as local, so context-menu, CLI, and drag-based cross-window moves silently lose the new navigation membership; transfer the referenced context registration with the workspace before resolving it in the destination.

AGENTS.md reference: AGENTS.md:L111-L115

Useful? React with 👍 / 👎.

Comment on lines +19 to +23
components = [
configuration.transport.rawValue,
configuration.destination.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
configuration.port.map(String.init) ?? "",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve SSH username case in context identities

For SSH destinations such as BuildUser@host and builduser@host, lowercasing the entire destination produces the same context key even though the remote usernames can identify different accounts. The registrations then overwrite each other under one dictionary key, and selecting that row can launch new workspaces with the other account's configuration; normalize only the hostname portion, not the username.

Useful? React with 👍 / 👎.

An inset line dangles and a full-height line cuts through the titlebar and
footer strips, so there is no line at all anymore: the machines column gets
a subtle darker wash (14% black in dark mode, 5% in light), the standard
rail-vs-list separation. Nothing can intersect chrome. The invisible drag
band on the boundary is unchanged.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d16947196

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/ContentView.swift
Comment on lines +11709 to +11716
let scopedTabs = tabManager.sidebarWorkspaces(
forCreationContextID: creationContextID
)
let signpost = SidebarProfilingSignposts.begin("vertical-sidebar-body", "workspaces=\(scopedTabs.count) selected=\(sidebarShortTabId(tabManager.selectedTabId))")
// Retain the native table identity while hidden without continuing the
// O(workspaces) projection pipeline. Reveal rebuilds one authoritative
// snapshot from the current model before the controller applies again.
let tabs = isPresented ? tabManager.tabs : []
let tabs = isPresented ? scopedTabs : []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Observe every workspace exported to installed extensions

When the selected machine contains only a subset of the window's workspaces, this scoped list also becomes renderContext.tabs, which extensionSidebarScrollArea uses to build its observation publishers, while cmuxSidebarSnapshotForCurrentTabs() still exports every workspace. Consequently, metadata changes such as title or process updates in a workspace under another machine never advance snapshotUpdateToken, leaving an installed extension's all-workspace snapshot stale until the user switches machines or another observed event occurs; keep the native renderer scoped, but subscribe the installed-extension snapshot pipeline to all exported workspaces.

Useful? React with 👍 / 👎.

Comment thread Sources/ContentView.swift
Comment on lines +13233 to +13235
if tabManager.selectedTabId != workspace.id {
tabManager.selectWorkspace(workspace)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate remote attaches before changing workspace selection

When an extension supplies a valid target workspace but an invalid context ID or rejected session name, these lines select the target before attachRemoteCmuxTUI returns nil, so the action reports rejection while still moving the user to another workspace. Validate the attach request before selecting, or defer the selection until after the surface is successfully created, so failed extension actions have no navigation side effect.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 999c75b1d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +385 to +387
)
if moved {
SidebarDragLifecycleNotification().postClearRequest(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve whole groups when reparenting header drags

When a group header is dragged onto a machine, SidebarWorkspaceTableController publishes only the group's anchor workspace ID, but this path treats that ID as an ordinary selected workspace rather than applying the whole-group semantics used by the reorder paths. Consequently moveSidebarWorkspaces recognizes a partial-group move, removes the anchor from its group, and reparents only that workspace, unexpectedly dismantling/promoting the group instead of moving it. Resolve the drag through the shared group-aware action path before reparenting.

AGENTS.md reference: AGENTS.md:L113-L113

Useful? React with 👍 / 👎.

The titlebar strip over the sidebar region gets its own tint, terminated by
the (now full-width) titlebar hairline; the footer strip gets the same tint
with its own hairline. The machines/workspaces divider runs exactly between
the two bands, so both endpoints land on a horizontal boundary instead of
cutting through chrome or dangling. The rail wash is gone.
The restore path can flip column modes before the first layout pass,
leaving the table document wider than its clip so rail avatars center on
the stale width and clip at the divider. Settle points (viewport flush,
resize end, mode-flip post-update) now pin the table frame to the clip
width. Chrome band tint raised to 8% so the top bar reads as its own
color.
Six variants selectable live (Debug > Sidebar Boundary, persisted in
UserDefaults, plus debug.sidebar.boundary_style over the socket): full
line, inset line, rail wash, chrome bands + line, chrome bands + wash,
spacing only. Each boundary piece is an observing leaf view so a style
flip invalidates only the boundary chrome. Default stays chrome bands +
line. Temporary dogfood tuning: the winner becomes the only
implementation.
Debug > Debug Windows > Sidebar Boundary… opens a floating utility panel
(same ReleasingWindowController pattern as the other debug windows) with
radio rows and one-line descriptions; clicks apply live while the sidebar
stays visible. The inline Debug-menu picker is gone.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bf4b658d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +515 to +517
if let selectedWorkspace, requestedIDs.contains(selectedWorkspace.id) {
rememberSelectedSidebarWorkspaceFocus()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Follow the selected workspace after reparenting

When the moved set contains the active workspace—through a machine-row drop, socket command, or extension action—this only updates the destination focus cursor and never updates sidebarCreationContextSelection. The next render therefore keeps the old machine selected and filters the active workspace out of the workspaces column; invoke the existing follow-selection reconciliation here, or explicitly select the destination context, after changing membership.

AGENTS.md reference: AGENTS.md:L113-L113

Useful? React with 👍 / 👎.

The debug picker window gains a NAVIGATION LAYOUT section: Two Columns
(current), Machine Chips (one sidebar, machines as chips at the top),
Machine Dropdown (one sidebar, picker menu at the top), Bottom Machine
Dock (one sidebar, machine icon dock above the footer). Single-sidebar
layouts give the workspaces list the whole region, hide the machines
column and its divider machinery, and reuse the same registry/selection
plumbing. Prototype spacing; the winner gets the polish pass and the rest
get deleted. Socket: debug.sidebar.boundary_style accepts layout.
Chip strip and bottom bar fade at both horizontal ends so overflow reads
as scrollable. The bottom dock becomes an Arc-profile-style bar: the
selected machine is a named pill, the rest collapse to icons, selection
auto-scrolls to center.
@github-actions

Copy link
Copy Markdown
Contributor

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Mac fleet instructions for head ea06d101e7dc66dfab0ddb0b0f9128f79b452aea. Planned tag: pr-10505-ea06d101; this is not yet a published build.

JOB_JSON=$(~/.local/bin/cmux-ci submit --kind cmux --command 'CMUX_FLEET_BUILD_TAG=pr-10505-ea06d101 /Users/Shared/cmux-build-fleet/recipes/cmux.sh https://github.com/manaflow-ai/cmux.git ea06d101e7dc66dfab0ddb0b0f9128f79b452aea' --artifact artifacts/cmux.app.zip --workspace https://github.com/manaflow-ai/cmux/pull/10505 --source-digest ea06d101e7dc66dfab0ddb0b0f9128f79b452aea --cache-key cmux:pr-10505 --min-free-bytes 268435456000 --label cmux --label ram48)
JOB_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$JOB_JSON")
~/.local/bin/cmux-ci wait "$JOB_ID" --receipt artifacts/fleet/$JOB_ID.json
~/.local/bin/cmux-ci publish-hq "$JOB_ID"

Use an existing campaign job ID if one is already posted; do not submit a duplicate. A wait timeout leaves the remote job running. Published results will include an exact-head artifact link and timing/disk receipt. This recipe validates the macOS app only, not iOS or tests. Never use maclease or put credentials in a PR comment.

@teamleaderleo teamleaderleo added area: sidebar The workspace sidebar: list, groups, status, reordering closing-soon Conflicting or red with no activity for 7+ days; closes 2026-10-06 unless the label is removed labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Closing; reopen if you still want it.

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

Labels

area: sidebar The workspace sidebar: list, groups, status, reordering closing-soon Conflicting or red with no activity for 7+ days; closes 2026-10-06 unless the label is removed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants