From cd1bbe9acf54d9d81108acd086376057e4e2028d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Majcher?= Date: Sun, 22 Mar 2026 14:59:58 +0100 Subject: [PATCH 1/2] CLI: add --no-focus flag to cmux ssh When cmux ssh is used from a script, workspace.select is called immediately after workspace.remote.configure, stealing the user's active workspace focus before SSH is established. Add --no-focus flag (consistent with break-pane/join-pane) to skip the workspace.select call so the caller's workspace retains focus. The caller can then redirect to the new workspace later via cmux select-workspace. Addresses cmux ssh case of #140; complementary to #1418. --- CLI/cmux.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 4e770068868e..d34ed26b3e28 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -3294,6 +3294,7 @@ struct CMUXCLI { let port: Int? let identityFile: String? let workspaceName: String? + let noFocus: Bool let sshOptions: [String] let extraArguments: [String] let localSocketPath: String @@ -3464,7 +3465,9 @@ struct CMUXCLI { if let workspaceWindowId, !workspaceWindowId.isEmpty { selectParams["window_id"] = workspaceWindowId } - _ = try client.sendV2(method: "workspace.select", params: selectParams) + if !sshOptions.noFocus { + _ = try client.sendV2(method: "workspace.select", params: selectParams) + } let remoteState = ((configuredPayload["remote"] as? [String: Any])?["state"] as? String) ?? "unknown" cliDebugLog( "cli.ssh.remote.configure.ok workspace=\(String(workspaceId.prefix(8))) state=\(remoteState)" @@ -3512,6 +3515,7 @@ struct CMUXCLI { var port: Int? var identityFile: String? var workspaceName: String? + var noFocus = false var sshOptions: [String] = [] var extraArguments: [String] = [] @@ -3550,6 +3554,9 @@ struct CMUXCLI { } workspaceName = commandArgs[index + 1] index += 2 + case "--no-focus": + noFocus = true + index += 1 case "--ssh-option": guard index + 1 < commandArgs.count else { throw CLIError(message: "ssh: --ssh-option requires a value") @@ -3585,6 +3592,7 @@ struct CMUXCLI { port: port, identityFile: identityFile, workspaceName: workspaceName, + noFocus: noFocus, sshOptions: sshOptions, extraArguments: extraArguments, localSocketPath: localSocketPath, @@ -6012,6 +6020,7 @@ struct CMUXCLI { --port SSH port --identity SSH identity file path --ssh-option Extra SSH -o option (repeatable) + --no-focus Create workspace without switching to it Example: cmux ssh dev@my-host From 8d3419e25c2955411519ca8160b0ff99ce74a577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Majcher?= Date: Sun, 22 Mar 2026 15:42:31 +0100 Subject: [PATCH 2/2] CLI: add test coverage for --no-focus flag parsing - Fix existing test that constructs SSHCommandOptions directly (add noFocus: false to the initializer call) - Make parseSSHCommandOptions internal so it's accessible from tests - Add testParseSSHCommandOptionsNoFocusFlag covering: - flag sets noFocus=true, destination parses correctly - absent flag defaults noFocus=false - combined with --name and --port --- CLI/cmux.swift | 2 +- cmuxTests/CLIProcessRunnerTests.swift | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index d34ed26b3e28..9f754f92cba7 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -3510,7 +3510,7 @@ struct CMUXCLI { } } - private func parseSSHCommandOptions(_ commandArgs: [String], localSocketPath: String = "", remoteRelayPort: Int = 0) throws -> SSHCommandOptions { + func parseSSHCommandOptions(_ commandArgs: [String], localSocketPath: String = "", remoteRelayPort: Int = 0) throws -> SSHCommandOptions { var destination: String? var port: Int? var identityFile: String? diff --git a/cmuxTests/CLIProcessRunnerTests.swift b/cmuxTests/CLIProcessRunnerTests.swift index 9253e9b72356..949befe0ee4f 100644 --- a/cmuxTests/CLIProcessRunnerTests.swift +++ b/cmuxTests/CLIProcessRunnerTests.swift @@ -340,6 +340,7 @@ final class CLIProcessRunnerTests: XCTestCase { port: nil, identityFile: nil, workspaceName: nil, + noFocus: false, sshOptions: [], extraArguments: [], localSocketPath: "", @@ -411,6 +412,24 @@ print(out.decode("utf-8", "replace"), end="") XCTAssertTrue(remoteCommand.contains("base64"), remoteCommand) } + func testParseSSHCommandOptionsNoFocusFlag() throws { + let cli = CMUXCLI(args: []) + + let withFlag = try cli.parseSSHCommandOptions(["user@host", "--no-focus"]) + XCTAssertTrue(withFlag.noFocus) + XCTAssertEqual(withFlag.destination, "user@host") + + let withoutFlag = try cli.parseSSHCommandOptions(["user@host"]) + XCTAssertFalse(withoutFlag.noFocus) + + let withOtherFlags = try cli.parseSSHCommandOptions([ + "user@host", "--name", "mybox", "--no-focus", "--port", "2222", + ]) + XCTAssertTrue(withOtherFlags.noFocus) + XCTAssertEqual(withOtherFlags.workspaceName, "mybox") + XCTAssertEqual(withOtherFlags.port, 2222) + } + func testEncodedRemoteBootstrapCommandEscapesPercentsForSSHRemoteCommand() throws { let cli = CMUXCLI(args: []) let remoteCommand = cli.sshPercentEscapedRemoteCommand(