Repository navigation
Finder-style sidebar columns: machines rail, icon-collapsible columns, hover cards - #10505
lawrencecchen wants to merge 63 commits into
Conversation
# 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.
…paces card" This reverts commit dcb0d84.
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.
| 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 | ||
| } | ||
| }) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
💡 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".
| case .addSSHMachine: | ||
| return [.addSSHMachine] |
There was a problem hiding this comment.
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 👍 / 👎.
| private func resolvedSidebarCreationContextID(for workspace: Workspace) -> String { | ||
| if let explicit = workspace.sidebarCreationContextID? | ||
| .trimmingCharacters(in: .whitespacesAndNewlines), | ||
| !explicit.isEmpty, | ||
| isValidSidebarWorkspaceParentContextID(explicit) | ||
| { | ||
| return explicit |
There was a problem hiding this comment.
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 👍 / 👎.
| components = [ | ||
| configuration.transport.rawValue, | ||
| configuration.destination.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), | ||
| configuration.port.map(String.init) ?? "", | ||
| ] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| 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 : [] |
There was a problem hiding this comment.
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 👍 / 👎.
| if tabManager.selectedTabId != workspace.id { | ||
| tabManager.selectWorkspace(workspace) | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| ) | ||
| if moved { | ||
| SidebarDragLifecycleNotification().postClearRequest( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| if let selectedWorkspace, requestedIDs.contains(selectedWorkspace.id) { | ||
| rememberSelectedSidebarWorkspaceFocus() | ||
| } |
There was a problem hiding this comment.
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.
|
All contributors have signed the CLA ✍️ ✅ |
|
Mac fleet instructions for head 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. |
|
Closing; reopen if you still want it. |
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).
automaticids (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.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
sidebar.machine.add_ssh, both columns flipped to icon rails viasidebar.column.set_mode,debug.window.screenshotevidence at each step.Dictionary:
Need help on this PR? Tag
@codesmith-botwith 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
automaticids map to This Mac.Sidebar behavior
Extension migration
CmuxExtensionKitaddscreationContexts, parent-ownedchildColumnroutes, and actions for context and remote-machine management.creationContexts, useselectedCreationContextId, and route the selectedchildColumn, includingcmux.workspaces, to the trailing column.Written for commit ea06d10. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation