Skip to content

Remote tmux (-CC over SSH) mirroring (beta) - #5553

Merged
azooz2003-bit merged 105 commits into
manaflow-ai:mainfrom
robertnisipeanu:remote-tmux-upstream
Jun 15, 2026
Merged

azooz2003-bit merged 105 commits into
manaflow-ai:mainfrom
robertnisipeanu:remote-tmux-upstream

Conversation

@robertnisipeanu

@robertnisipeanu robertnisipeanu commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Note

Dependency resolved — manaflow-ai/bonsplit#143 (the didReorderTabsInPane
delegate this PR's tab/window reorder sync relies on) is merged, and
vendor/bonsplit is pinned to the merged upstream main SHA (5728c21).
This branch builds standalone.

What this adds
An opt-in beta that drives a remote tmux session 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:

tmux cmux
session a workspace in the sidebar
window a tab in that workspace
pane a pane in a native split inside that tab

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 with swap-window.

4-remote-sessions

tmux ls on the left, the cmux sidebar on the right — each session becomes a workspace, each window a tab.

5-windows-and-panes

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/config alias or user@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 runs ssh inline 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.mirror mirrors 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_ed25519
1-connect

Off by default — turn it on in Settings → Beta Features → Remote tmux.
0-beta-feature

How it works
cmux spawns ssh … tmux -CC attach and parses the control-mode stream itself (RemoteTmuxControlStreamParser) rather than relying on a built-in tmux viewer, so the protocol and %begin/%end command 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 with send-keys -H. The remote tmux server owns pane sizing/reflow; cmux stays in lock-step and never reflows locally.

Supported behaviors

  • Sizing — remote client resized to the rendered grid (refresh-client -C), so TUIs aren't stuck at 80×24.
  • Splits / reorder — split-window and swap-window propagation (above).
  • Working directory — remote pane_current_path tracked and shown on the tab.
  • Rename — renaming a tab runs tmux rename-window, so the name syncs to the remote (and other clients).
  • Paste & drop — single-line content via tmux paste-buffer -p, so images arrive as [Image #N] (a real bracketed paste).
  • Mouse — click/scroll/drag reach the remote app; Shift+drag for native cmux copy.
  • Unicode-correct output — multi-byte chars survive being split across %output updates.
  • Live reconnect — a transient SSH/network drop keeps the mirror frozen and re-attaches automatically with capped exponential backoff (the remote tmux server outlives the control client). On reconnect each pane re-seeds — re-applies the client grid (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.
  • Scrollback on attach — an attached tab seeds the pane's history (capture-pane -e -S), so it's scrollable immediately, not only for output printed after attaching. Alt-screen TUIs correctly have no scrollback.
  • Out-of-band reorder — reordering windows on the remote (a second tmux client, or move-window / a mid-list new-window) reorders the cmux tabs to match, preserving the current tab's focus/selection.
  • Disconnect cleanup — when a host's session genuinely ends, its dedicated remote window closes — gated so it never discards local workspaces (or another host's mirror) moved into it, never leaves zero windows, and degrades a sole window to a fresh local workspace.

Surface — Beta Features toggle, the cmux ssh-tmux CLI command, and remote.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 didReorderTabsInPane delegate added in that PR. vendor/bonsplit is pinned to the merged upstream main SHA (5728c21), which also includes the divider-thickness work (manaflow-ai/bonsplit#139) already on bonsplit main.

Known limitations

  • No "reconnecting" indicator — a transient drop is handled silently (the mirror freezes, then resumes). There's no visual badge while offline, and actions taken during the outage (rename/close/split a tab, keystrokes) are dropped and need re-doing after reconnect.
  • Multi-line paste isn't delivered as one bracketed paste — only single-line content is; multi-line text is sent as plain keystrokes.
  • Live cwd updates need control-mode subscriptions (tmux 3.2+); older tmux shows the initial folder only.
  • Scrollback re-wrap on resize is conditional: shell panes re-wrap; panes running a TUI keep their old wrap (the app repaints its visible frame itself on resize).
  • Out-of-band reorder vs. an in-progress local drag — a remote reorder landing mid tab-drag is a narrow, self-healing cosmetic glitch (the final order reconciles on drop).

Related issues


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds an opt‑in Remote tmux beta that mirrors a remote tmux server over SSH via cmux 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.

  • Bug Fixes
    • Control stream/backpressure: bounded parser, safe %begin/%end correlation with first‑attach block drained, FIFO‑safe queued stdin writer, tighter output buffering, and UTF‑8‑safe %output.
    • Attach/reconnect/sizing: seed‑before‑size with a redraw kick, reliable first‑connect sizing (debounced), live auto‑reconnect with backoff and full reseed, conditional shell/TUI reflow via DECAWM no‑reflow, async resize, alt‑screen enter/leave, and mouse forwarding.
    • Routing/API: v1/v2 split/create/new‑pane/new‑surface return “accepted, routed to 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; add remote.tmux.* methods/policy/capabilities.
    • Quit/teardown: close the shared SSH ControlMaster on last‑mirror close and on quit (including connection‑only cases); guard against re‑attach races; avoid sticky empty dedicated windows; don’t drop masters still used by an attach connection.
    • CWD/paste: restore terminal working‑directory inheritance and live updates; upload images to the host over SSH; route single‑line paste/drop via tmux paste-buffer -p for bracketed pastes.
    • UI/perf/docs: avoid main‑thread path stats via a per‑path cache; gate Remote tmux docs/nav to en/ja; add a Settings search anchor; CLI help for ssh-tmux.
    • Close confirmations: prompt before killing remote tabs/panes with an active command; app/window close detaches instead.

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

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "Remote tmux" beta feature for mirroring remote tmux sessions into dedicated local windows or sidebar workspaces via SSH.
    • Added cmux ssh-tmux CLI command with interactive SSH authentication support and customizable port/identity options.
    • Enabled remote session attachment, splitting, renaming, and pasting with automatic working-directory tracking.
  • Documentation

    • Added comprehensive Remote tmux documentation with setup instructions, supported behaviors, and limitations.
    • Added localized UI text in English and Japanese.
  • Chores

    • Updated project dependencies and Xcode configuration.
    • Enhanced .gitignore for local artifacts.

robertnisipeanu and others added 12 commits June 7, 2026 00:40
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>
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

@robertnisipeanu is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Remote 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 cmux ssh-tmux and socket endpoints provide mirroring entry points. Feature is gated behind a beta toggle in settings.

Changes

Remote tmux mirroring feature

Layer / File(s) Summary
Data models, session discovery, and error handling
Sources/RemoteTmuxSession.swift, Sources/RemoteTmuxSessionListParser.swift, Sources/RemoteTmuxCommandResult.swift, Sources/RemoteTmuxLayoutNode.swift, Sources/RemoteTmuxError.swift
RemoteTmuxSession struct and lenient tab-separated parser; RemoteTmuxCommandResult capturing exit code and stdout/stderr; RemoteTmuxLayoutNode tree for pane geometry and split structures; RemoteTmuxError with sanitized user-facing messages.
SSH transport and command execution
Sources/RemoteTmuxHost.swift, Sources/RemoteTmuxSSHTransport.swift
RemoteTmuxHost models SSH endpoint identity with deterministic connectionHash and ControlMaster socket path; builds SSH argv lists for control/interactive auth/tmux modes. RemoteTmuxSSHTransport actor manages session listing and command execution over shared ControlMaster; classifies auth-required vs. no-server failures via stderr heuristics.
Control-mode protocol parsing and message handling
Sources/RemoteTmuxControlMessage.swift, Sources/RemoteTmuxControlStreamParser.swift
RemoteTmuxControlMessage enum enumerates tmux control-mode lifecycle, topology changes, pane output, command results. RemoteTmuxControlStreamParser incrementally parses -CC byte stream with newline buffering, block correlation, ST framing, octal unescaping of pane output.
Control connection lifecycle, reconnect, and command correlation
Sources/RemoteTmuxControlConnection.swift
Manages live ssh tmux -CC subprocess with incremental stdout/stderr draining; tracks connectionState (connecting/connected/reconnecting/ended); maintains mirrored topology and per-pane state. Reconnect with exponential backoff; FIFO-based command correlation; terminal-state restoration with cursor/scroll-region/mouse-tracking seeding.
Manual-I/O terminal surfaces for remote rendering
Sources/RemoteTmuxManualIOWrite.swift, Sources/GhosttyTerminalView.swift
RemoteTmuxManualIOWriteBox retains sendable input handler. TerminalSurface gains MANUAL I/O mode with input handler, grid-resize callback, buffered remote output; configures Ghostty for MANUAL mode; disables/re-enables DECAWM on resize; exposes cellSizePoints() and processRemoteOutput().
Tmux layout tree parsing
Sources/RemoteTmuxLayoutNode.swift, Sources/RemoteTmuxRawLayoutParser.swift
RemoteTmuxLayoutNode with geometry and pane/split content; custom Codable mapping JSON keys. RemoteTmuxRawLayoutParser cursor-based recursive descent; parses WxH,X,Y geometry, pane leaves, horizontal/vertical splits; validates child count and consumes input fully.
Remote tmux controller and orchestration
Sources/RemoteTmuxController.swift
@MainActor coordinator managing per-endpoint SSH transports and control connections; feature-gating via settings catalog; dedicated-window flow with reuse/auth detection; sidebar session mirroring; UI-to-remote command propagation (new-window, rename, reorder, split); lifecycle teardown.
Session-to-workspace mirroring and pane output routing
Sources/RemoteTmuxSessionMirror.swift
Observes control connection state; constructs/updates remote tmux tabs per window; builds multi-pane renderers for windows with >1 pane; routes pane output to renderers or single-pane surfaces; tracks/projects pane cwd; handles active-pane changes and remote session termination.
Per-window multi-pane rendering and UI controls
Sources/RemoteTmuxWindowMirror.swift, Sources/RemoteTmuxWindowMirrorView.swift
RemoteTmuxWindowMirror observable class managing per-window pane panels, synthetic pane IDs, layout/active-pane state; sends client-size updates via refresh-client; routes split/kill/focus to tmux. RemoteTmuxWindowMirrorView renders layout tree as proportional splits with dividers; per-pane header with focus/split/kill actions.
Workspace and tab manager integration with remote mirrors
Sources/Workspace.swift, Sources/TabManager.swift, Sources/WorkspaceContentView.swift
Workspace gains isRemoteTmuxMirror flag and remoteTmuxWindowMirrors map; mirrors suppress restoration and local tab creation/browser creation; implements mirrorTabReorder permutation validation and reorderRemoteTmuxMirrorTabs with activation suppression; tab-bar delegate routes closes/splits/reorders to controller. TabManager forwards rename/close to controller. WorkspaceContentView conditionally renders RemoteTmuxWindowMirrorView when mirror exists.
UI surface creation, clipboard, pasting, and image transfer
Sources/GhosttyTerminalView.swift, Sources/ContentView.swift, Sources/TerminalImageTransfer.swift
Clipboard completion routes text to remoteTmuxController.pasteIntoMirror; ContentView sidebar returns accepted result for remote tmux panel requests and routes double-tap through performNewWorkspaceAction when mirror tabs exist. GhosttyNSView treats mirror panes as splittable and delegates splits/drops to controller. Image transfer checks remoteUploadTarget for mirror surfaces.
AppDelegate lifecycle and window/workspace coordination
Sources/AppDelegate.swift
AppDelegate creates remoteTmuxController; calls detachAll() on termination; excludes dedicated remote windows from session snapshots; routes new-workspace/paste requests through remote handlers; notifies controller on window close with workspace IDs.
Tab manager remote tmux mirror callbacks
Sources/TabManager.swift
TabManager detects remote mirror workspaces and forwards setCustomTitle/closeWorkspace events to remoteTmuxController.
Socket API endpoints for remote tmux operations
Sources/TerminalController+RemoteTmux.swift
Socket handlers for v2RemoteTmuxSessions, v2RemoteTmuxAttach, v2RemoteTmuxMirror, v2RemoteTmuxWindow, v2RemoteTmuxDetach, v2RemoteTmuxState; host/session validation; trust-boundary sanitization (rejects dash-prefix, hidden-Unicode); returns {mirrored: true} or {auth_required: true, ssh_argv}.
TerminalController request routing for remote.tmux. endpoints*
Sources/TerminalController.swift
Routes remote.tmux.{sessions,attach,detach,state,mirror,window} requests to corresponding v2RemoteTmux* handlers.
CLI cmux ssh-tmux command
CLI/cmux.swift
CLI subcommand parsing --port, --identity, --no-focus; calls remote.tmux.window socket endpoint; handles mirrored success and auth_required outcomes; performs interactive SSH authentication with TTY enforcement and foreground process group management to avoid SIGTTIN hangs.
Beta feature settings, UI, and gating
Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift, Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift, Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
Adds remoteTmux boolean DefaultsKey (id: "remoteTmux.beta.enabled"); BetaFeaturesSection UI component with localized on/off subtitles; curated setting entry for sidebar navigation.
Localization and help text
Resources/Localizable.xcstrings
CLI help text (English/Japanese) for cmux ssh-tmux; settings toggle/action labels; beta feature descriptions; all UI strings sourced from localization catalog.
Web documentation and navigation
web/app/[locale]/docs/remote-tmux/page.tsx, web/app/[locale]/components/docs-nav-items.ts, web/messages/en.json, web/messages/ja.json
Generates documentation page with metadata, SEO alternates, mapping table (tmux↔cmux concepts), requirements, feature list, socket-command reference, and limitations; nav item linking; localized EN/JA content.
Behavior tests for protocols, parsing, and logic
cmuxTests/RemoteTmuxAuthTests.swift, cmuxTests/RemoteTmuxControlParserTests.swift, cmuxTests/RemoteTmuxSessionListParserTests.swift
Tests for SSH auth classification, control-argument generation/hashing, interactive auth argv shape; stream parsing/octal-unescaping; session list parsing; pane-state seeding and DECSET/DECSTBM; layout parsing; tab reorder permutations; window-order and session-end decisions.
Xcode project configuration
cmux.xcodeproj/project.pbxproj, .gitignore, vendor/bonsplit
Adds PBXBuildFile and PBXFileReference entries for all RemoteTmux sources and tests; extends PBXSourcesPhase for cmux and cmuxTests; ignores artifacts/ directory; advances bonsplit submodule.

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#2232: Settings UI changes (new "Remote tmux" toggle, curated entries, localized strings, BetaFeaturesSection modifications) directly affect Settings view layout/scrolling and may cause CommandPalette/Settings UI test failures.

Possibly related PRs

  • manaflow-ai/cmux#5483: Extends ControlCommandExecutionPolicy's socket-worker method allowlist to include new remote.tmux.* commands, building on a prior refactor that centralized routing logic.
  • manaflow-ai/cmux#5465: Both PRs modify the same core GhosttyTerminalView.swift terminal-surface machinery—initializer signature and lifecycle wiring—with direct code-level relationships.

Poem

A rabbit's tail twitches with glee,
"Remote tmux mirroring runs wild and free!
Control streams parsed, panes rendered true,
Surfaces manual, sockets anew.
Sessions mirror from lands far and wide—
SSH control and cli-side pride!" 🐰✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

# Conflicts:
#	Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift
#	Sources/GhosttyTerminalView.swift
#	Sources/TerminalController.swift
#	Sources/Workspace.swift
@socket-security

socket-security Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

robertnisipeanu and others added 2 commits June 7, 2026 12:48
…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>
robertnisipeanu and others added 10 commits June 7, 2026 22:57
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>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator

@codex review

This branch was successfully deployed

1 active deployment
Preview – cmux — c690dc74 Deployed Jun 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants