Repository navigation
Remote tmux (-CC over SSH) mirroring (beta) - #5553
Conversation
Mirror a remote host's tmux server in cmux over `ssh -tt … tmux -CC` (iTerm2-style control mode), behind the `remoteTmux` beta flag. The whole control protocol is parsed in Swift (no dependency on ghostty's built-in viewer): sessions become sidebar workspaces, tmux windows become tabs, and a window's panes render as native cmux splits inside the tab. cmux actions propagate to tmux and remote changes flow back via the control stream: attach/create (new-session)/kill/rename/reorder sessions; create (new-window)/kill/rename/reorder windows; render + input + split-window / kill-pane / refresh-client sizing for panes. Closing the dedicated window or quitting cmux only detaches (the remote server stays alive for resume); closing a session/window/pane propagates the matching tmux kill. Entry points (beta-gated): a "Attach Remote tmux…" Command Palette command and File-menu item open a dedicated window mirroring that host. Includes a Settings beta toggle, en+ja localization, and parser unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With `ssh -tt … tmux -CC attach`, ssh's stdio are pipes owned by cmux (no local TTY), so the remote tmux control client defaults to 80x24. The multi-pane mirror already sent `refresh-client -C`, but the single-pane display path (the common case where a claude / claude agents TUI runs) and the openActivePane attach path never reported a size — so a freshly attached session stayed at 80x24 while cmux rendered a larger surface, and TUIs painted into a mismatched grid (doubled borders, overlapping output). - TerminalSurface gains `onManualGridResize`, fired from `updateSize` on a real cell-grid change while the surface is on screen (exact cols/rows from libghostty, deduped). - New `RemoteTmuxControlConnection.setClientSize(columns:rows:)` is the single sizing entrypoint; the single-pane display path, the openActivePane attach path, and the multi-pane window mirror all route through it. - `reconcileWindowMirror` clears the single-pane hook when a window escalates to multi-pane, so the two sizing paths can't both drive one connection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two polish fixes for the remote-tmux mirror. Working directory: a mirrored tab's folder was stuck at "~" because cmux never read the remote pane's cwd. Now each mirrored pane queries `pane_current_path` once (`display-message`) and subscribes for live updates (`refresh-client -B`), parsed via a new `%subscription-changed` message and a new `onPaneCwd` connection observer routed to `workspace.updatePanelDirectory`. Wired at all three pane-creation sites (session-mirror single-pane, window-mirror multi-pane, openActivePane). For a multi-pane window only the active pane's directory is projected onto the tab (per-pane cache + `onActivePaneChanged`), so a background pane can't hijack the folder; the cache is pruned to live panes on each rebuild. Image paste: pasting a screenshot into a remote tmux pane inserted a macOS-local /var/folders path the remote can't read. `resolvedImageTransferTarget()` now routes remote-tmux surfaces through `.detectedSSH(host.detectedSSHSession())`, reusing the existing scp-upload seam over the host's ControlMaster socket, so the image lands on the remote and the remote path is inserted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ively Pasting/dropping an image into a mirrored remote tmux pane inserted the uploaded /tmp path as plain keystrokes (send-keys), so the remote app (claude) showed the path text instead of inlining it as [Image #N]. Native `ssh` + `tmux attach` shows [Image #N] because the live PTY carries the app's bracketed-paste mode (DECSET 2004), so the path arrives as a bracketed paste — which is what claude's image-path detector requires. The -CC mirror is a manual-I/O surface that can't know the pane's 2004 state (capture-pane never replays ESC[?2004h), so ghostty emitted the path unbracketed. Fix: deliver mirror paste/drop through tmux `paste-buffer -p`, which brackets the paste iff the REAL pane has 2004 on (tmux tracks it authoritatively on the real pty). Images now inline as [Image #N]; files paste as a bracketed path; shell panes without 2004 get plain text — matching native behavior. - RemoteTmuxControlConnection.pastePane: set-buffer + paste-buffer -p -d on a per-pane buffer (single-quoted, single-line only). - RemoteTmuxController.pasteIntoMirror/pasteTarget + RemoteTmuxSessionMirror .paneId(forSurfaceId:): resolve the tmux pane behind a cmux surface. - GhosttyTerminalView: route single-line paste (completeClipboardRequest) and drop (insertText) for mirror panes through pasteIntoMirror; multi-line and non-mirror surfaces fall back to the existing path unchanged. - Share displayPanels key parsing via displayPaneTarget (was duplicated in pasteTarget/remoteUploadTarget). Also gitignore stray tmux-*.log debug logs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resizing the cmux window mangled a mirrored remote tmux pane because cmux's
ghostty emulator reflowed the screen independently of tmux, which is the sole
authority on a pane's reflow (control mode streams only the app's incremental
post-SIGWINCH redraw, never a full grid repaint). A native client never reflows
locally; cmux did.
- Match the remote pane's screen: capturePane now queries `#{alternate_on}` and,
for an alt-screen pane, enters the alternate screen on the mirror surface
(ESC[?1049h) before the captured rows. Alt-screen TUIs (vim/htop) then don't
reflow on resize (the alternate screen has no reflow), matching the remote.
- Suppress local reflow on the primary screen: in updateSize, disable DECAWM
(ESC[?7l) across set_size + render_now for manual-I/O mirror surfaces, then
restore it — ghostty reflows only when DECAWM is on at resize time. This
matches iTerm2's "tmux owns the grid; don't reflow locally" approach and fixes
inline TUIs (e.g. claude) rendering misaligned after a resize. render_now also
flushes a GPU frame so the resized grid shows promptly.
Known limitation: a rare residual remains for inline primary-screen TUIs on hard
fast resizes (the deep reflow/scrollback divergence whose complete fix needs a
ghostty no-reflow-surface flag, which requires a GhosttyKit rebuild). Tracked
separately; debug instrumentation lives on the remote-tmux-resize-debug branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse %output payloads from raw bytes so a multi-byte UTF-8 character that tmux splits across two %output notifications survives intact (ghostty's stream parser reassembles split UTF-8 across process_output calls). The previous per-line String(decoding:as: UTF8.self) round-trip replaced each split half with U+FFFD, corrupting box-drawing-heavy TUIs (e.g. claude) at certain sizes — visible as garbled separators that overflow to the next row. Harden the on-capture terminal-state seed in RemoteTmuxControlConnection: - restrict DECSTBM seeding to non-full-window regions (a full-window region is the surface default and would go stale across a resize); - emit the cursor LAST (DECSTBM and DECOM both home the cursor) and make it region-relative when origin mode is on; - clamp untrusted numeric format fields to 0...65535 to avoid an Int overflow trap from a malicious remote; - drop mouse-mode seeding (native cmux selection/scroll is preferred, and the tmux flag->DECSET mapping is ambiguous) and the invalid bracket_paste_flag query (paste fidelity is handled by tmux paste-buffer -p). Add regression tests: a box-drawing char split across two %output notifications survives without U+FFFD, and the pane-state seed places the cursor last / region-relative and suppresses full-window scroll regions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…capture) Restore the remote pane's mouse tracking mode onto the mirror surface so clicks, scroll, and drag reach the remote app (e.g. claude). A TUI sets its mouse mode at startup, before cmux attaches, so it isn't in the live %output — query it from tmux on capture and seed it. tmux's concrete mouse flags map to xterm DECSET levels (verified empirically against tmux 3.6a: set the DECSET in a pane, read the flags back): mouse_standard_flag=1000, mouse_button_flag=1002, mouse_all_flag=1003; the most aggressive that is on wins. mouse_any_flag is tmux's aggregate "any mouse mode on" OR-flag, not a concrete level, so it is not used. Encoding follows mouse_sgr_flag (1006) / mouse_utf8_flag (1005). With mouse tracking on, drag-to-select becomes the remote app's own selection (OSC 52 copy); Shift+drag does a native cmux copy, exactly as a local terminal behaves with a mouse-mode app. Tests cover each concrete tracking level and that the aggregate mouse_any_flag alone enables nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the remote-tmux beta: how tmux maps to cmux (session→workspace, window→tab, and a window's panes→a native split inside one tab), requirements, enabling the Beta Features flag, opening a single pane vs mirroring a whole host, how it works, what it supports (sizing, splits→split-window, reorder→swap-window, cwd, paste/drop, mouse, unicode-correct output), the remote.tmux.* socket commands, and real limitations. Adds the page, the docs nav entry (after SSH), and en/ja message-catalog strings; other locales fall back to en until the translation pipeline fills them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`handleMirrorWindowsReordered` issues `swap-window` against `connection.windowOrder`, but `swap-window` changes window indices without emitting a notification cmux re-reads the order from — so `windowOrder` went stale after the first reorder and the next drag computed swaps against the pre-swap order (no-op or mis-sort). Re-fetch the authoritative order (`requestWindows`) after issuing swaps so reorders keep working across repeated drags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Attach Remote tmux…" (File menu / command palette) takes an SSH destination and opens a new window mirroring the whole host's tmux sessions — it is not a single-pane open. Rewrite the Attaching section to match, and note that the remote.tmux.* socket commands (e.g. remote.tmux.open) offer the finer-grained single-pane variant. en/ja. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the tracked window order locally and synchronously after issuing `swap-window` (`applyWindowReorder`) instead of re-fetching it asynchronously. A `list-windows` re-fetch leaves a gap where a rapid follow-up drag reads the stale order — and an earlier reorder's snapshot can even land after a later one and roll the order back. The swaps achieve exactly the dragged order, so applying it locally matches tmux without a round-trip; out-of-band changes still reconcile on the topology events that already re-fetch. Adds a unit test for the pure reorder helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A leaf-check helper added during development whose call site never materialized (zero references in the codebase). Surfaced by a dead-code audit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@robertnisipeanu is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRemote tmux mirroring enables users to mirror tmux sessions from remote SSH hosts into dedicated cmux windows or sidebar workspaces. The implementation comprises SSH ControlMaster transport, tmux control-mode protocol parsing, per-session control connections, and manual-I/O terminal surface rendering. New CLI command ChangesRemote tmux mirroring feature
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
# Conflicts: # Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift # Sources/GhosttyTerminalView.swift # Sources/TerminalController.swift # Sources/Workspace.swift
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
…ding - rebuild() reorders mirror tabs to match the tmux window order for changes that originate remotely (move-window / mid-list new-window / a second client); focus-preserving and a no-op across split panes - on a remote session end, close the dedicated mirror window — gated so it never discards local work or another host's mirror, never leaves zero windows, and degrades a sole window to a fresh local workspace; distinguish SessionEndReason (.sessionExited vs .transportLost) - seed scrollback history on attach (capture-pane -e -S) so a (re)attached mirror tab is scrollable, not just output printed after attach - clarify the intentional empty list-windows guard - unit tests for the pure helpers (sessionEndAction, mirrorTabReorder) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A network/ssh drop no longer closes the mirror window. The connection keeps the frozen mirror and re-attaches with capped exponential backoff (indefinitely), re-seeding each pane on reconnect (re-apply client size, clear + re-capture contents with scrollback, re-subscribe cwd). It ends only on a genuine tmux %exit, or when a reconnect reaches the host but the session is gone (classified from ssh/tmux stderr) — which auto-closes per the disconnect policy. Reconnect is attach-only, so a session killed during the outage is never silently recreated. - ConnectionState (.connecting/.connected/.reconnecting/.ended); `exited` derived - stderr consumed via AsyncStream, drained before session-gone classification - reconnect re-seed deferred to the first post-reconnect list-windows result so it can't misalign the command-result FIFO - removed the now-unreachable SessionEndReason (transport loss reconnects, so a genuine end always forgets the host) - unit tests for the session-gone stderr classifier Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4fb50b2 to
196caf2
Compare
Add a `cmux ssh-tmux <destination> [--port <n>] [--identity <path>] [--no-focus]` CLI command that opens a dedicated cmux window mirroring a remote host's tmux sessions over tmux control mode (`tmux -CC`) via SSH. Behavior: - Discovery-first auth: try session discovery over the shared SSH ControlMaster in BatchMode (no prompt). Key/agent hosts — and scripts on non-tty stdin — mirror directly with no interactive step. A host that needs interactive auth fails BatchMode discovery; the CLI then runs `ssh` inline in the user's terminal so they can authenticate (password, host-key confirmation, MFA, security-key touch), and retries over the now-open master. - The inline auth child is given the controlling terminal's foreground process group (tcsetpgrp + SIGCONT, restored on exit) so ssh's password/confirmation prompt is not stopped by SIGTTIN. - `ssh -f` backgrounds the master after auth so it does not hold the caller's pty open (no ~60s close hang), and the shared ControlMaster is torn down (`ssh -O exit`) when the mirror window closes, its last session ends, or cmux quits — so the connection never lingers. - Per-endpoint keying by `connectionHash` (destination + port + identity) across transports, connections, mirrors, and window bindings, so the same host on a different port/identity never aliases onto another endpoint's connection. - `~/.ssh/config` aliases and their IdentityFile/ProxyJump/Port are honored (no StrictHostKeyChecking pin). Dash-prefixed destinations and identity files, and hidden control characters, are rejected at the trust boundary as defense against SSH option injection. Adds the `remote.tmux.window` socket method (the CLI entry point), the `RemoteTmuxAttachOutcome` result type, the `cli.help.ssh-tmux` help string (en/ja), docs, and a Swift Testing suite covering the auth classifier, argv policy, and connection keying. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously every host that had been mirrored before quit was reconnected and re-mirrored once per launch, so reopening cmux could spawn several dedicated mirror windows (one per remembered host) unprompted. A mirror needs a live SSH connection; it can't be meaningfully restored from a generic window snapshot, and resurrecting every past host on launch is surprising. Drop the persistence/restore path entirely: remove the `[RemoteTmuxHost]` UserDefaults store (and its legacy destination-only key + migration), the add/forget bookkeeping, and `restoreMirroredHostsOnLaunch()` plus its one-shot launch trigger in `AppDelegate`. Users re-attach explicitly with `cmux ssh-tmux`. Updates the docs limitation note accordingly (en/ja). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With `cmux ssh-tmux` as the attach entry point, the "Attach Remote tmux…" File-menu item and command-palette command (and their shared `promptAttachRemoteTmuxHost` NSAlert flow in AppDelegate+RemoteTmux.swift) are redundant. Remove all three surfaces, unwire the deleted file from the Xcode project, and drop the now-unused menu/palette/alert localization keys (en/ja). Updates the docs attach intro to point at the CLI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mirror `handleSessionWorkspaceClosedByUser` tore down the session's mirror, connection, and dedicated-window binding when the user closed a host's last mirrored workspace, but never closed the shared SSH ControlMaster — unlike the remote-end (`handleSessionEndedRemotely`) and window-close (`handleRemoteWindowClosed`) paths. Because the window binding is cleared first, the window's onClose `handleRemoteWindowClosed` is a no-op, so the master lingered for the full ControlPersist window (~180s) and the stale `transports` entry could surface an opaque failure on a later re-attach. Tear down the transport + master in this path too, sequenced after the `kill-session` command (which still needs the master alive), and drop the transport entry so a re-attach builds a fresh one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`RemoteTmuxSSHTransport.runProcess` installed the `Process` `terminationHandler` after `process.run()`. Foundation does not invoke a `terminationHandler` assigned after the process has already terminated, so an ssh that exits in the window between `run()` and the handler assignment would never resume the continuation and the caller would hang until its timeout. That is most likely on the fast auth-failure exits the `cmux ssh-tmux` discovery probe relies on to classify `auth_required`. Install the handler first and launch inside the continuation, resuming with `launchFailed` if `run()` itself throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…close Two lifecycle gaps found in review: - `mirrorHostInNewWindow` runs under the `remote.tmux.window` 60s `v2VmCall` timeout, which cancels the task on expiry — but the SSH discovery awaits are not cancellation-aware and nothing checked the cancellation flag, so a slow-but-successful probe could land past the timeout and open an orphaned dedicated window after the caller already received a timeout error. Add a `Task.checkCancellation()` before `createMainWindow` so an abandoned attach creates no window. - `handleWindowWorkspacesClosed` (the non-dedicated `remote.tmux.mirror` close path) detached mirrors and stopped connections but never tore down the shared SSH ControlMaster, unlike the dedicated-window, last-session, and remote-end paths. Track the affected hosts and, for any host left with no live mirror or connection, drop the transport and `spawnControlMasterExit` so the master doesn't linger for ControlPersist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ection-only masters on quit Two more lifecycle gaps found in review: - `handleWorkspaceClosed` removes the transport synchronously, then closes the ControlMaster from inside the async kill-session `Task`. A `cmux ssh-tmux` reattach to the same host during that round-trip builds a fresh transport/connection on the same ControlPath, so the stale `ssh -O exit` would drop the new mirror. Before exiting, re-check that no transport, mirror, or connection has reclaimed the endpoint (the Task is @mainactor, so the check + exit is atomic against a reattach). - `detachAll` exited masters only for hosts in `transports`, but the `remote.tmux.attach`/`open` paths open a ControlPersist master through the control connection without ever creating a transport, so those masters survived quit for the ControlPersist window. Collect endpoints from both `connectionsByHostSession` and `transports`, deduped by connectionHash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`runInteractiveAuthSSH` swallowed a `tcsetpgrp` failure with `try?` yet still set `didForegroundChild = true`. If the handoff failed, ssh stayed a background job of the tty, its password/host-key/MFA prompt SIGTTIN-stopped it, and `waitUntilExit()` blocked forever — the exact hang the foreground-process-group dance exists to prevent. Treat the handoff as required: on failure, SIGCONT the child (in case it already stopped), terminate it, and throw an actionable error instead of proceeding into a hang. `didForegroundChild` is now set only after a successful handoff (matching the sibling interactive path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…connection still uses it `handleSessionEndedRemotely` decided to `ssh -O exit` the shared master from `sessionMirrors` alone (`hostHasOtherMirrors`). A `remote.tmux.attach`/`open` control connection for the same endpoint multiplexes over the same ControlPath without a mirror entry, so exiting the master here would drop it. Gate the master teardown additionally on no remaining `connectionsByHostSession` entry for the endpoint (the window-binding teardown stays on `hostHasOtherMirrors`) — matching the connection-aware checks already used in `handleWindowWorkspacesClosed` and `handleWorkspaceClosed`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Codable` existed only to serialize `[RemoteTmuxHost]` into UserDefaults for the mirror persistence layer, which was removed earlier in this branch. Nothing encodes/decodes the type anymore, so drop the conformance; it can be reintroduced deliberately if persistence ever returns. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
|
@codex review |
Note
Dependency resolved — manaflow-ai/bonsplit#143 (the
didReorderTabsInPanedelegate this PR's tab/window reorder sync relies on) is merged, and
vendor/bonsplitis pinned to the merged upstreammainSHA (5728c21).This branch builds standalone.
What this adds
An opt-in beta that drives a remote
tmuxsession from cmux over SSH using tmux control mode (tmux -CC). Instead of a plain SSH terminal, the remote session is reprojected into cmux's native UI — workspaces, tabs, splits, scrollback, copy — and cmux drives the real tmux server behind it. Off by default (Settings → Beta Features → Remote tmux), so local terminals are unchanged.Demo
6-video-demo.mp4
The model mapping (the core of it)
cmux and tmux nest layouts in opposite directions: in cmux a pane holds a row of tabs (each tab = one terminal); in tmux a window holds a split of panes. This bridges them:
The new capability is panes inside a tab: a cmux tab previously held a single terminal, but a multi-pane tmux window is now rendered as a real cmux split layout within one tab (each remote pane is a native cmux terminal pane with the usual chrome). The bridge is two-way — splitting/closing a pane in that tab runs tmux
split-window, and drag-reordering the tabs reorders the tmux windows withswap-window.tmux lson the left, the cmux sidebar on the right — each session becomes a workspace, each window a tab.A tmux window's panes render as a native cmux split inside one tab (left: native tmux; right: cmux).
Using it
Run
cmux ssh-tmux <destination>in a terminal — a~/.ssh/configalias oruser@host(optional--port,--identity,--no-focus). cmux opens a new window mirroring that host's tmux sessions (sessions → workspaces, windows → tabs, multi-pane windows → in-tab splits).Auth is hands-off when it can be: discovery runs over a shared SSH ControlMaster in
BatchMode(no prompt), so key/agent hosts attach with no interaction. If the host needs interactive auth (password, host-key confirmation, MFA, security-key touch), cmux runssshinline in your terminal so you can authenticate, then mirrors the sessions over the now-open connection.~/.ssh/config(IdentityFile/ProxyJump/Port) is honored.For finer control, the
remote.tmux.*socket commands are available (e.g.remote.tmux.mirrormirrors a host's sessions into the current window's sidebar instead of a dedicated window).cmux ssh-tmux dev@example.com cmux ssh-tmux my-ssh-alias --port 2222 --identity ~/.ssh/id_ed25519Off by default — turn it on in Settings → Beta Features → Remote tmux.

How it works
cmux spawns
ssh … tmux -CC attachand parses the control-mode stream itself (RemoteTmuxControlStreamParser) rather than relying on a built-in tmux viewer, so the protocol and%begin/%endcommand correlation are fully owned by cmux. Each remote pane renders into a ghostty manual-I/O surface fed by%output; input (keys + mouse) is forwarded withsend-keys -H. The remote tmux server owns pane sizing/reflow; cmux stays in lock-step and never reflows locally.Supported behaviors
refresh-client -C), so TUIs aren't stuck at 80×24.split-windowandswap-windowpropagation (above).pane_current_pathtracked and shown on the tab.rename-window, so the name syncs to the remote (and other clients).paste-buffer -p, so images arrive as[Image #N](a real bracketed paste).%outputupdates.refresh-client -C), re-captures contents with scrollback, re-subscribes cwd — so the session resumes in place. The mirror ends only on a genuine tmux%exit, or when a reconnect reaches the host but the session is gone (detected from ssh/tmux stderr); reconnect is attach-only, so a session killed during the outage is never silently recreated.capture-pane -e -S), so it's scrollable immediately, not only for output printed after attaching. Alt-screen TUIs correctly have no scrollback.move-window/ a mid-listnew-window) reorders the cmux tabs to match, preserving the current tab's focus/selection.Surface — Beta Features toggle, the
cmux ssh-tmuxCLI command, andremote.tmux.*socket commands. Docs page added at/docs/remote-tmux.Tests — auth (stderr classifier / SSH argv policy / connection-keying), control-stream parser, pane-state seeding, and session/layout parser unit tests (Swift Testing).
Dependency — manaflow-ai/bonsplit#143 (merged). The window/tab reorder sync relies on the
didReorderTabsInPanedelegate added in that PR.vendor/bonsplitis pinned to the merged upstreammainSHA (5728c21), which also includes the divider-thickness work (manaflow-ai/bonsplit#139) already on bonsplitmain.Known limitations
Related issues
-CC) mirroring over SSH ControlMaster (the broader remote-workspace routing for non-tmux operations is out of scope here).tmux -CCcontrol-mode integration (sessions/windows → native cmux tabs/splits, attach); remote-first and behind a beta flag, not the local-sidebar drag-drop UX described in the issue.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds an opt‑in Remote tmux beta that mirrors a remote
tmuxserver over SSH viacmux ssh-tmux, mapping sessions to workspaces, windows to tabs, and panes to native splits. Attach is CLI‑only with interactive SSH auth fallback; mirrors aren’t auto‑restored on launch.%begin/%endcorrelation with first‑attach block drained, FIFO‑safe queued stdin writer, tighter output buffering, and UTF‑8‑safe%output.remote-tmux”; reject unsupported options (cwd/cmd/env/divider/left‑up) before mutation; prevent orphan local splits; propagate remote send failures; veto batch closes on route failure; scope dedicated‑window ownership; addremote.tmux.*methods/policy/capabilities.tmux paste-buffer -pfor bracketed pastes.en/ja; add a Settings search anchor; CLI help forssh-tmux.Written for commit c690dc7. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
cmux ssh-tmuxCLI command with interactive SSH authentication support and customizable port/identity options.Documentation
Chores
.gitignorefor local artifacts.