From 2ed6ac65f9ca071103cdbba6f134adf8179d6a91 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Thu, 28 May 2026 23:33:43 -0700 Subject: [PATCH 1/2] test: cover Claude restore cwd after subagent --- cmuxTests/SessionIndexViewTests.swift | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/cmuxTests/SessionIndexViewTests.swift b/cmuxTests/SessionIndexViewTests.swift index 8f131cb6a3..4e36d0c624 100644 --- a/cmuxTests/SessionIndexViewTests.swift +++ b/cmuxTests/SessionIndexViewTests.swift @@ -137,6 +137,51 @@ final class SessionIndexViewTests: XCTestCase { ) } + func testClaudeDirectorySnapshotKeepsProjectCwdWhenSubagentCwdAppears() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-claude-subagent-cwd-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: root) } + + let configDir = root.appendingPathComponent("claude-config", isDirectory: true) + let parentCwd = root.appendingPathComponent("parent repo", isDirectory: true) + let subagentCwd = root.appendingPathComponent("subagent scratch", isDirectory: true) + try FileManager.default.createDirectory(at: parentCwd, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: subagentCwd, withIntermediateDirectories: true) + + let transcriptURL = configDir + .appendingPathComponent("projects", isDirectory: true) + .appendingPathComponent(RestorableAgentSessionIndex.encodeClaudeProjectDir(parentCwd.path), isDirectory: true) + .appendingPathComponent("claude-parent-session.jsonl", isDirectory: false) + try FileManager.default.createDirectory( + at: transcriptURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + try [ + #"{"type":"user","cwd":"\#(parentCwd.path)","message":{"role":"user","content":"restore parent repo"}}"#, + #"{"type":"assistant","message":{"model":"claude-sonnet-4-5"}}"#, + #"{"type":"user","isMeta":true,"cwd":"\#(subagentCwd.path)","message":{"role":"user","content":"subagent bookkeeping"}}"#, + ].joined(separator: "\n").write(to: transcriptURL, atomically: true, encoding: .utf8) + + let previousClaudeConfigDir = getenv("CLAUDE_CONFIG_DIR").map { String(cString: $0) } + setenv("CLAUDE_CONFIG_DIR", configDir.path, 1) + defer { + if let previousClaudeConfigDir { + setenv("CLAUDE_CONFIG_DIR", previousClaudeConfigDir, 1) + } else { + unsetenv("CLAUDE_CONFIG_DIR") + } + } + + let snapshot = await SessionIndexStore().loadDirectorySnapshot(cwd: parentCwd.path) + let entry = try XCTUnwrap(snapshot.entries.first { $0.sessionId == "claude-parent-session" }) + + XCTAssertEqual(entry.cwd, parentCwd.path) + XCTAssertEqual( + entry.resumeCommand, + "cd '\(parentCwd.path)' && claude --resume claude-parent-session" + ) + } + func testCurrentDirectorySetterDoesNotPublishEqualValue() { let store = SessionIndexStore() var emittedValues: [String?] = [] From 4b235c2690e20ad30befd886d130407162d80267 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Thu, 28 May 2026 23:39:34 -0700 Subject: [PATCH 2/2] fix: preserve Claude restore cwd after subagent --- Sources/SessionIndexStore.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SessionIndexStore.swift b/Sources/SessionIndexStore.swift index 08fb9c53a0..19f6982dc7 100644 --- a/Sources/SessionIndexStore.swift +++ b/Sources/SessionIndexStore.swift @@ -752,13 +752,15 @@ final class SessionIndexStore: ObservableObject { nonisolated private static func extractClaudeMetadata(head: String, tail: String, projectDir: String) -> ClaudeParsed { var out = ClaudeParsed() out.cwd = decodeClaudeProjectDir(projectDir) + var didReadTranscriptCwd = false for line in head.split(separator: "\n", omittingEmptySubsequences: true) { guard let data = line.data(using: .utf8), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue } let isMeta = (obj["isMeta"] as? Bool) ?? false - if let cwdField = obj["cwd"] as? String, !cwdField.isEmpty { + if !didReadTranscriptCwd, let cwdField = obj["cwd"] as? String, !cwdField.isEmpty { out.cwd = cwdField + didReadTranscriptCwd = true } if let branchField = obj["gitBranch"] as? String, !branchField.isEmpty { out.branch = branchField