diff --git a/CLI/cmux.swift b/CLI/cmux.swift index b7d6f4e8cc31..0dc9f7ab731c 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -24320,7 +24320,7 @@ struct CMUXCLI { "command": command, "auto_resume": true ] - if let cwd = normalizedHookValue(cwd) ?? normalizedHookValue(launchCommand?.workingDirectory) { + if let cwd = normalizedHookValue(launchCommand?.workingDirectory) ?? normalizedHookValue(cwd) { params["cwd"] = cwd } if let resumeEnvironment, !resumeEnvironment.isEmpty { @@ -24402,7 +24402,7 @@ struct CMUXCLI { guard let argv, !argv.isEmpty else { return nil } return agentSurfaceResumeShellCommand( argv: argv, - workingDirectory: workingDirectory ?? launchCommand?.workingDirectory, + workingDirectory: launchCommand?.workingDirectory ?? workingDirectory, kind: kind, environment: environment ) diff --git a/Sources/RestorableAgentSession.swift b/Sources/RestorableAgentSession.swift index 8152d8c72432..d2221887ea47 100644 --- a/Sources/RestorableAgentSession.swift +++ b/Sources/RestorableAgentSession.swift @@ -362,7 +362,7 @@ enum AgentResumeCommandBuilder { let cwd = !includeWorkingDirectoryPrefix || customRegistration?.cwd == .ignore ? nil - : normalized(workingDirectory ?? launchCommand?.workingDirectory) + : normalized(launchCommand?.workingDirectory ?? workingDirectory) let sanitizedCommandParts = customRegistration == nil ? AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions( from: commandParts, @@ -666,11 +666,14 @@ enum AgentResumeCommandBuilder { let sessionDirectory = normalized(registration.sessionDirectory).map { ($0 as NSString).expandingTildeInPath } + let resolvedCwd = registration.cwd == .ignore + ? nil + : normalized(launchCommand?.workingDirectory ?? workingDirectory) let replacements: [String: String] = [ "sessionId": sessionId, "sessionPath": sessionId, "executable": original.executable, - "cwd": normalized(workingDirectory ?? launchCommand?.workingDirectory) ?? "", + "cwd": resolvedCwd ?? "", "sessionDir": sessionDirectory ?? "", ] var resolved: [String] = [] diff --git a/cmuxTests/RestorableAgentHookProviderResumeTests.swift b/cmuxTests/RestorableAgentHookProviderResumeTests.swift index 1bf75daedca7..426ebe78f7f5 100644 --- a/cmuxTests/RestorableAgentHookProviderResumeTests.swift +++ b/cmuxTests/RestorableAgentHookProviderResumeTests.swift @@ -1,4 +1,5 @@ import CMUXAgentLaunch +import Testing import XCTest #if canImport(cmux_DEV) @@ -641,3 +642,90 @@ extension SocketListenerAcceptPolicyTests { ) } } + +@Suite +struct RestorableAgentResumeWorkingDirectoryTests { + @Test + func claudeResumeCommandChangesToLaunchDirectoryNotDriftedCwd() { + let snapshot = SessionRestorableAgentSnapshot( + kind: .claude, + sessionId: "59729506-e83c-4bfe-a730-1d50d10cc396", + workingDirectory: "/tmp/claude repo/nested", + launchCommand: AgentLaunchCommandSnapshot( + launcher: "claude", + executablePath: "/Users/example/.local/bin/claude", + arguments: ["/Users/example/.local/bin/claude"], + workingDirectory: "/tmp/claude repo", + environment: nil, + capturedAt: 123, + source: "process" + ) + ) + + #expect( + snapshot.resumeCommand + == "{ cd -- '/tmp/claude repo' 2>/dev/null || [ ! -d '/tmp/claude repo' ]; } && '/Users/example/.local/bin/claude' '--resume' '59729506-e83c-4bfe-a730-1d50d10cc396'" + ) + } + + @Test + func customVaultResumeTemplateUsesLaunchDirectoryNotDriftedCwd() throws { + let snapshot = SessionRestorableAgentSnapshot( + kind: .custom("vault-tool"), + sessionId: "session-123", + workingDirectory: "/tmp/vault-drifted", + launchCommand: AgentLaunchCommandSnapshot( + launcher: "vault-tool", + executablePath: "/usr/local/bin/vault-tool", + arguments: ["/usr/local/bin/vault-tool"], + workingDirectory: "/tmp/vault-launch", + environment: nil, + capturedAt: 123, + source: "process" + ), + registration: CmuxVaultAgentRegistration( + id: "vault-tool", + name: "Vault Tool", + detect: CmuxVaultAgentDetectRule(processName: "vault-tool"), + sessionIdSource: .argvOption("--resume"), + resumeCommand: "{{executable}} --resume {{sessionId}} --dir {{cwd}}", + cwd: .preserve + ) + ) + + let command = try #require(snapshot.resumeCommand) + #expect(command.contains("/tmp/vault-launch")) + #expect(!command.contains("/tmp/vault-drifted")) + } + + @Test + func customVaultResumeTemplateSuppressesCwdWhenIgnored() { + let snapshot = SessionRestorableAgentSnapshot( + kind: .custom("vault-tool"), + sessionId: "session-123", + workingDirectory: "/tmp/vault-drifted", + launchCommand: AgentLaunchCommandSnapshot( + launcher: "vault-tool", + executablePath: "/usr/local/bin/vault-tool", + arguments: ["/usr/local/bin/vault-tool"], + workingDirectory: "/tmp/vault-launch", + environment: nil, + capturedAt: 123, + source: "process" + ), + registration: CmuxVaultAgentRegistration( + id: "vault-tool", + name: "Vault Tool", + detect: CmuxVaultAgentDetectRule(processName: "vault-tool"), + sessionIdSource: .argvOption("--resume"), + resumeCommand: "{{executable}} --resume {{sessionId}} --dir {{cwd}}", + cwd: .ignore + ) + ) + + // cwd: .ignore must keep the launch dir out of the resolved command entirely. + let command = snapshot.resumeCommand ?? "" + #expect(!command.contains("/tmp/vault-launch")) + #expect(!command.contains("/tmp/vault-drifted")) + } +}