Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1373c60
docs: design for remote tmux mirror as sidebar group
0xJord4n Jul 3, 2026
448bbce
docs: verify spec assumptions against code, settle registry + focus s…
0xJord4n Jul 3, 2026
0be01ff
docs: redesign ssh-tmux to mirror into current window (drop group)
0xJord4n Jul 3, 2026
c9df04d
docs: implementation plan for ssh-tmux current-window mirror
0xJord4n Jul 3, 2026
f74f809
docs: drop trivial reuse helper+tests from plan (parsimony), inline t…
0xJord4n Jul 3, 2026
6a35d81
feat(remote-tmux): mirror ssh-tmux into current window (harden mirror…
0xJord4n Jul 3, 2026
e63cf9f
fix(remote-tmux): raise window on reuse + fail loudly when no session…
0xJord4n Jul 3, 2026
61dd044
feat(remote-tmux): add remote.tmux.attach_here socket command
0xJord4n Jul 3, 2026
96e11fe
feat(cli): ssh-tmux mirrors into current window by default, add --new…
0xJord4n Jul 3, 2026
63523f8
docs(remote-tmux): document current-window mirror default and --new-w…
0xJord4n Jul 3, 2026
0506370
test(remote-tmux): update capabilities assertion for attach_here meth…
0xJord4n Jul 3, 2026
27c98e6
docs(remote-tmux): update stale comments for attach_here method rename
0xJord4n Jul 3, 2026
da694fd
test(remote-tmux): guard attach_here socket dispatch + param validation
0xJord4n Jul 3, 2026
df56250
fix(remote-tmux): address PR review — throw on unresolved window_id, …
0xJord4n Jul 3, 2026
955a8ff
Fix remote tmux attach budget guard
austinywang Jul 3, 2026
adcba4a
Fix remote tmux mirror review findings
austinywang Jul 3, 2026
16c082f
Preserve legacy remote tmux mirror semantics
austinywang Jul 3, 2026
6ea724f
Fix current-window mirror discovery closure
austinywang Jul 3, 2026
e47c8e2
Consolidate partial remote tmux mirrors
austinywang Jul 3, 2026
4d819b7
Handle existing remote tmux mirror reuse
austinywang Jul 3, 2026
15eef68
Clean up failed current-window tmux mirrors
austinywang Jul 3, 2026
0e5abdb
Avoid blocking worker for tmux caller lookup
austinywang Jul 3, 2026
8cb1c93
test(remote-tmux): failing test - mirror close must not mark session …
0xJord4n Jul 4, 2026
54417d7
fix(remote-tmux): detach on mirror close, never kill the remote session
0xJord4n Jul 4, 2026
f261e2b
docs(remote-tmux): fix stale kill-on-commit comment on batch close path
0xJord4n Jul 4, 2026
e989430
Merge origin/main into feat/sub-workspace
austinywang Jul 13, 2026
00e297c
test(remote-tmux): cover socket detach and mirror consolidation
austinywang Jul 13, 2026
e3857d5
ci(remote-tmux): gate detach and placement regressions
austinywang Jul 13, 2026
5ee7b3b
ci(remote-tmux): run focused gate before flaky suites
austinywang Jul 13, 2026
2596034
test(remote-tmux): import socket routing types
austinywang Jul 13, 2026
750104f
fix(remote-tmux): detach socket closes and consolidate mirrors
austinywang Jul 13, 2026
af48632
test(remote-tmux): require explicit socket focus intent
austinywang Jul 13, 2026
8fb59c0
fix(remote-tmux): keep window socket focus-neutral by default
austinywang Jul 13, 2026
f6b650e
fix(remote-tmux): cover tab detach and report local window errors
austinywang Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,26 @@ jobs:
echo "::warning::Passwordless sudo unavailable; XCTest will use its default automation-mode setup"
fi

- name: Run remote tmux mirror detach and placement regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# Closing a mirrored workspace must never kill the remote tmux session,
# and --new-window must consolidate mirrors moved across source windows.
# Keep these destructive/topology regressions outside the tolerant full
# suite so an ordinary assertion failure cannot be accepted as expected.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/RemoteTmuxMirrorCloseDetachTests \
test

- name: Run browser system proxy mirror regression
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
Expand Down
52 changes: 26 additions & 26 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8683,16 +8683,8 @@ struct CMUXCLI {
)
}

/// `cmux ssh-tmux <destination>` — mirror a remote host's tmux sessions as
/// workspaces in the current window over `tmux -CC` (the remote-tmux beta).
///
/// Unlike `cmux ssh`, this carries no cmuxd-remote/relay bootstrap: it only
/// drives the SSH ControlMaster the mirror multiplexes over. The app's mirror
/// control client uses plain pipes and cannot service interactive auth, so if
/// the host needs a password / host-key confirmation / MFA / FIDO touch, the
/// app returns the `ssh` argv and this CLI runs it **inline in the user's
/// terminal** (which supplies the tty), then retries the mirror over the
/// now-authenticated master.
/// Mirrors a remote host's tmux sessions; interactive SSH authentication runs
/// inline in the caller's terminal before retrying over the shared master.
private func runRemoteTmux(
commandArgs: [String],
client: SocketClient,
Expand All @@ -8702,11 +8694,10 @@ struct CMUXCLI {
var port: Int?
var identityFile: String?
var noFocus = false
var newWindow = false

// Intentional subset of parseSSHCommandOptions: the mirror verb has a
// different pipeline (no relay/cmuxd bootstrap, no `--` passthrough, no
// --ssh-option/--name/--window), so it parses only the flags it supports
// rather than reusing the heavier SSH-workspace parser.
// Intentional subset of parseSSHCommandOptions: ssh-tmux has no relay,
// passthrough, --ssh-option, --name, or --window support.
var index = 0
while index < commandArgs.count {
let arg = commandArgs[index]
Expand All @@ -8729,6 +8720,9 @@ struct CMUXCLI {
case "--no-focus":
noFocus = true
index += 1
case "--new-window":
newWindow = true
index += 1
default:
if arg.hasPrefix("-") {
throw CLIError(
Expand All @@ -8752,21 +8746,20 @@ struct CMUXCLI {
if let port { params["port"] = port }
if let identityFile, !identityFile.isEmpty { params["identity_file"] = identityFile }
params["activate"] = !noFocus
try applyWindowOrCallerContext(to: &params, client: client, windowRaw: nil)

// The first call runs a non-interactive (BatchMode) discovery in the app,
// which can take a couple of seconds; show progress so it doesn't look idle.
if !newWindow {
try applyWindowOrCallerContext(to: &params, client: client, windowRaw: nil)
}
// BatchMode discovery can take a couple of seconds; show progress.
if !jsonOutput {
print("Connecting to \(destination)…")
}

// The app reports `auth_required` at most once per attempt; run the
// returned interactive ssh, then retry exactly once. `didAuthenticate`
// bounds the loop so a host that keeps reporting auth-required can't spin.
// Retry interactive authentication once; never spin on auth-required.
let method = newWindow ? "remote.tmux.window" : "remote.tmux.mirror"
var didAuthenticate = false
while true {
let result = try client.sendV2(
method: "remote.tmux.mirror",
method: method,
params: params,
responseTimeout: 75 // > the app-side 60s timeout, so the app's result/error always arrives first
)
Expand All @@ -8793,8 +8786,7 @@ struct CMUXCLI {
}
try runInteractiveAuthSSH(sshArgv: sshArgv, destination: destination)
didAuthenticate = true
// Retry immediately so the just-opened ControlMaster (ControlPersist)
// is still warm; tell the user we're proceeding.
// Retry while the just-opened ControlMaster is warm.
if !jsonOutput {
print("Authenticated; opening remote tmux mirror for \(destination)…")
}
Expand Down Expand Up @@ -15862,7 +15854,7 @@ struct CMUXCLI {
cmux ssh dev@my-host --ssh-option UserKnownHostsFile=/dev/null --ssh-option StrictHostKeyChecking=no
""")
case "ssh-tmux":
return String(localized: "cli.help.ssh-tmux", defaultValue: """
let help = String(localized: "cli.help.ssh-tmux", defaultValue: """
Usage: cmux ssh-tmux <destination> [--port <n>] [--identity <path>] [--no-focus]

Mirror a remote host's tmux sessions into the current window's sidebar over
Expand All @@ -15885,6 +15877,14 @@ struct CMUXCLI {
cmux ssh-tmux dev@my-host
cmux ssh-tmux dev@my-host --port 2222 --identity ~/.ssh/id_ed25519
""")
let newWindowHelp = String(
localized: "cli.help.ssh-tmux.newWindow",
defaultValue: """
Additional flag:
--new-window Open the mirror in a dedicated new window
"""
)
return "\(help)\n\n\(newWindowHelp)"
case "ssh-session-list":
return """
Usage: cmux ssh-session-list [--workspace <id|ref|index> | --all-workspaces]
Expand Down Expand Up @@ -35202,7 +35202,7 @@ export default CMUXSessionRestore;
list-workspaces [--window <id|ref|index>]
new-workspace [--name <title>] [--description <text>] [--cwd <path>] [--command <text>] [--layout <json>] [--window <id|ref|index>] [--focus <true|false>] [--group <id|ref>] [--group-placement afterCurrent|top|end] [--group-reference <workspace>]
ssh <destination> [--name <title>] [--port <n>] [--identity <path>] [-A|--forward-agent] [-a|--no-forward-agent] [--ssh-option <opt>] [--window <id|ref|index>] [--no-focus] [-- <remote-command-args>]
ssh-tmux <destination> [--port <n>] [--identity <path>] [--no-focus]
ssh-tmux <destination> [--port <n>] [--identity <path>] [--no-focus] [--new-window]
ssh-session-list [--workspace <id|ref|index> | --all-workspaces]
ssh-session-attach --session-id <id> [--workspace <id|ref|index>] [--pane <id|ref|index> | --split <left|right|up|down>]
ssh-session-cleanup [--workspace <id|ref|index> | --all-workspaces] (--session-id <id> | --all)
Expand Down
Loading