Add cmux ssh --pane: pane-scoped SSH connections

Closed
LA
25d ago
+606-87

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) on WorkspaceRemoteConfiguration, persisted in SessionRemoteWorkspaceSnapshot (omitted for workspace scope so older builds are unaffected).
  • WorkspaceRemoteInheritance (.fromSourcePane/.always/.never) decides remote membership for new surfaces. Session restore passes explicit .always/.never from the snapshot instead of guessing from a mid-restore source pane; browser panels record isRemoteScoped in their session snapshot so pane-scoped remote browsers survive app restart.
  • workspace.remote.configure validates scope transitions instead of clobbering a live connection: pane-scoped configure on an already-remote workspace errors; a second ssh --pane to 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 + disconnectRemote context action (en/ja localized), https://github.com/manaflow-ai/bonsplit/pull/146.

Testing

  • ./scripts/reload.sh --tag ssh-pane builds clean after merging latest main (rebased onto the ControlCommandCoordinator extraction; configure + tab-action changes re-applied in TerminalController+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 snapshot isRemoteScoped roundtrip + legacy decode.
  • Bonsplit unit tests for the indicator and context action availability live in the bonsplit PR.

Issues


View with Codesmith Autofix with Codesmith
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: --pane flag; 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) and WorkspaceRemoteInheritance govern 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.
  • Dependencies

    • Updated bonsplit for the per-tab remote indicator and the “Disconnect SSH” tab action.

Summary by CodeRabbit

  • New Features

    • Added --pane SSH 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
  • Bug Fixes

    • Validation and cleanup improvements for pane-scoped connection lifecycle
  • Tests

    • New tests covering pane-scoped behaviors and snapshot roundtrips