Add cmux ssh --pane: pane-scoped SSH connections
Summary
cmux ssh dev@host --pane connects the calling pane to a remote host instead of creating a new remote workspace. The pane execs the same ssh startup wrapper a remote-workspace pane runs, so session start/end report through the existing surface tracking. New splits/tabs created from a remote-attached pane inherit the connection (terminal startup command, browser proxy + remote data store); surfaces created from local panes stay local. Member tabs show a per-tab remote indicator and a "Disconnect SSH" context action, which closes the remote PTY and respawns the pane as a local shell behind an explanatory banner.
Mechanics:
WorkspaceRemoteScope(.workspace/.pane) onWorkspaceRemoteConfiguration, persisted inSessionRemoteWorkspaceSnapshot(omitted for workspace scope so older builds are unaffected).WorkspaceRemoteInheritance(.fromSourcePane/.always/.never) decides remote membership for new surfaces. Session restore passes explicit.always/.neverfrom the snapshot instead of guessing from a mid-restore source pane; browser panels recordisRemoteScopedin their session snapshot so pane-scoped remote browsers survive app restart.workspace.remote.configurevalidates scope transitions instead of clobbering a live connection: pane-scoped configure on an already-remote workspace errors; a secondssh --paneto the same target joins the existing connection (the app returns its startup command for the CLI to exec); a different target errors; workspace-scoped configure over a pane-scoped connection errors symmetrically.- Pane-scope demotion counts only remote-scoped browser panels, so a local browser no longer keeps a dead pane connection alive. Browsers moved into a pane-scoped workspace stay local.
- Bonsplit: per-tab remote indicator +
disconnectRemotecontext action (en/ja localized), https://github.com/manaflow-ai/bonsplit/pull/146.
Testing
./scripts/reload.sh --tag ssh-panebuilds clean after merging latest main (rebased onto the ControlCommandCoordinator extraction; configure + tab-action changes re-applied inTerminalController+ControlWorkspaceContext.swift/TerminalController+ControlSystemContext2.swift).- New unit tests in
cmuxTests/WorkspaceRemoteConnectionTests.swift: scope snapshot roundtrip, legacy snapshot without scope restores workspace scope, pane-scope join target matching (per-invocation relay fields ignored; host/port/identity respected), browser snapshotisRemoteScopedroundtrip + legacy decode. - Bonsplit unit tests for the indicator and context action availability live in the bonsplit PR.
Issues
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.
[!NOTE]
High Risk
Touches SSH/remote connection lifecycle, control-socket configure semantics, and session restore; mis-scoping could strand panes or clobber live connections.
Overview
Adds cmux ssh --pane, which wires the current terminal pane to a remote host instead of spawning a new remote workspace. The CLI reads CMUX_WORKSPACE_ID / CMUX_SURFACE_ID, calls workspace.remote.configure with scope: pane and seed_surface_id, then execs the SSH startup wrapper (reusing shared sshRemoteConfigureParams). A matching pane-scoped connection in the same workspace joins via joined_existing + startup_command rather than starting a second session.
Remote scope model: WorkspaceRemoteScope (.workspace / .pane) on WorkspaceRemoteConfiguration, persisted in session snapshots (pane only). WorkspaceRemoteInheritance controls whether new terminals/browsers get remote startup, proxy, and data store—by default from the source pane in pane scope; restore uses snapshot flags (isRemoteScoped on browsers, explicit .always/.never on terminals).
App behavior: workspace.remote.configure rejects scope clashes (whole-workspace vs pane-scoped) and duplicate targets; pane configure seeds only the named surface. Per-tab remote indicators, Disconnect SSH (disconnect_remote tab action + context menu), and disconnectRemoteSurface (close PTY, respawn local shell with banner). Browser proxy/status apply only to remoteScopedBrowserPanelIds; demotion after last SSH pane ignores local browsers in pane scope.
Reviewed by Cursor Bugbot for commit 6000fb34e24b109c175d65b02aed4dadc0e97efa. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds pane-scoped SSH: cmux ssh dev@host --pane connects only the current pane; new splits/tabs from that pane inherit the remote connection while other panes stay local. Member tabs show a remote indicator and offer “Disconnect SSH”.
-
New Features
- CLI:
--paneflag; blocks--name,--window, remote command args, and--json. - Joins an existing pane-scoped connection in the same workspace when host/port/identity match; otherwise returns a clear error. Prevents pane↔workspace scope changes on a live connection.
- The CLI execs the same SSH startup wrapper used by remote-workspace panes, so session lifecycle is tracked.
- Remote scope plumbing:
WorkspaceRemoteScope(workspace/pane) andWorkspaceRemoteInheritancegovern which new surfaces are remote. - Session restore: browser panels persist
isRemoteScoped; remote scope is saved in the session snapshot (omitted for workspace scope for backward compatibility). Only remote-scoped browsers keep a pane-scoped connection alive; moved-in local browsers stay local.
- CLI:
-
Dependencies
- Updated
bonsplitfor the per-tab remote indicator and the “Disconnect SSH” tab action.
- Updated
Summary by CodeRabbit
-
New Features
- Added
--paneSSH mode to connect the current pane to a remote host instead of creating a new workspace - Pane-scoped remote connections: join/leave behavior, pane-only indicators, and targeted pane disconnect
- Session restore and inheritance controls for pane vs workspace remote membership
- Added
-
Bug Fixes
- Validation and cleanup improvements for pane-scoped connection lifecycle
-
Tests
- New tests covering pane-scoped behaviors and snapshot roundtrips