From ac129de59a37ddbf4d4ff31d30ea185561ccbe72 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 13:39:50 -0700 Subject: [PATCH 1/4] test: cover Pi JSONL content block titles --- cmuxTests/PiVaultAgentPersistenceTests.swift | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cmuxTests/PiVaultAgentPersistenceTests.swift b/cmuxTests/PiVaultAgentPersistenceTests.swift index 5a3c7b04d46d..ed9335b2ceef 100644 --- a/cmuxTests/PiVaultAgentPersistenceTests.swift +++ b/cmuxTests/PiVaultAgentPersistenceTests.swift @@ -263,6 +263,37 @@ final class PiVaultAgentPersistenceTests: XCTestCase { XCTAssertEqual(entry.gitBranch, "issue-3575-vault-pi-agent-support") } + func testPiJSONLTypedContentBlocksUseFirstUserTextAsTitle() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-pi-vault-title-blocks-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let cwd = "/tmp/pi typed blocks" + let projectDirectory = try XCTUnwrap(PiSessionLocator.projectDirectoryName(for: cwd)) + let sessionDir = tempDir.appendingPathComponent(projectDirectory, isDirectory: true) + try FileManager.default.createDirectory(at: sessionDir, withIntermediateDirectories: true) + let sessionFile = sessionDir.appendingPathComponent("019e1c86-def0-72c9-90d4-8543db20f981.jsonl") + try """ + {"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"assistant preface"}]}} + {"type":"message","message":{"role":"user","content":[{"type":"text","text":"ping"}]}} + """.write(to: sessionFile, atomically: true, encoding: .utf8) + + var registration = CmuxVaultAgentRegistration.builtInPi + registration.sessionDirectory = tempDir.path + let entries = await SessionIndexStore.loadRegisteredAgentEntries( + registration: registration, + needle: "", + cwdFilter: cwd, + offset: 0, + limit: 10 + ) + + let entry = try XCTUnwrap(entries.first) + XCTAssertEqual(entry.title, "ping") + XCTAssertEqual(entry.cwd, cwd) + } + func testPiVaultAgentSnapshotRoundTripBuildsTargetedSessionCommand() throws { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("cmux-pi-vault-agent-\(UUID().uuidString)", isDirectory: true) From eeaf2c1774c12f80ebfe3a9788cb492fcb9bc00b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 13:40:51 -0700 Subject: [PATCH 2/4] fix: parse Pi JSONL text blocks for Vault titles --- Sources/SessionIndexRegisteredAgents.swift | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/Sources/SessionIndexRegisteredAgents.swift b/Sources/SessionIndexRegisteredAgents.swift index 5548a64df404..79fe407e1c16 100644 --- a/Sources/SessionIndexRegisteredAgents.swift +++ b/Sources/SessionIndexRegisteredAgents.swift @@ -151,15 +151,18 @@ extension SessionIndexStore { metadata.branch = firstString(in: object, keys: ["gitBranch", "branch"]) } if metadata.title.isEmpty { - metadata.title = firstString(in: object, keys: ["title", "prompt", "text", "content"]) ?? "" + metadata.title = firstText(in: object, keys: ["title", "prompt", "text", "content"]) ?? "" } if metadata.title.isEmpty, let message = object["message"] as? [String: Any] { - metadata.title = firstString(in: message, keys: ["content", "text"]) ?? "" + let role = firstString(in: message, keys: ["role"]) + if role == nil || isUserRole(role) { + metadata.title = firstText(in: message, keys: ["content", "text"]) ?? "" + } } if metadata.title.isEmpty, let messages = object["messages"] as? [[String: Any]] { metadata.title = messages.compactMap { message in - firstString(in: message, keys: ["role"]) == "user" - ? firstString(in: message, keys: ["content", "text"]) + isUserRole(firstString(in: message, keys: ["role"])) + ? firstText(in: message, keys: ["content", "text"]) : nil }.first ?? "" } @@ -208,6 +211,52 @@ extension SessionIndexStore { return nil } + nonisolated private static func firstText(in object: [String: Any], keys: [String]) -> String? { + for key in keys { + guard let text = firstTextValue(object[key]) else { continue } + return text + } + return nil + } + + nonisolated private static func firstTextValue(_ value: Any?) -> String? { + if let string = value as? String { + return trimmedNonEmpty(string) + } + if let values = value as? [Any] { + for value in values { + if let text = firstTextBlock(value) { + return text + } + } + } + if let block = value as? [String: Any] { + return firstTextBlock(block) + } + return nil + } + + nonisolated private static func firstTextBlock(_ value: Any) -> String? { + if let string = value as? String { + return trimmedNonEmpty(string) + } + guard let block = value as? [String: Any] else { return nil } + if let type = firstString(in: block, keys: ["type"]), + type.caseInsensitiveCompare("text") != .orderedSame { + return nil + } + return firstString(in: block, keys: ["text"]) + } + + nonisolated private static func trimmedNonEmpty(_ value: String) -> String? { + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + nonisolated private static func isUserRole(_ role: String?) -> Bool { + role?.caseInsensitiveCompare("user") == .orderedSame + } + nonisolated private static func piCWDInferred(from url: URL) -> String? { let directoryName = url.deletingLastPathComponent().lastPathComponent guard directoryName.hasPrefix("--"), directoryName.hasSuffix("--"), directoryName.count > 4 else { From d3bf3130e7ef35df9670bd633991419cb7539ffe Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 14:03:56 -0700 Subject: [PATCH 3/4] fix: align Pi JSONL title role handling --- Sources/SessionIndexRegisteredAgents.swift | 20 +++++-- cmuxTests/PiVaultAgentPersistenceTests.swift | 61 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/Sources/SessionIndexRegisteredAgents.swift b/Sources/SessionIndexRegisteredAgents.swift index 79fe407e1c16..14da4de74023 100644 --- a/Sources/SessionIndexRegisteredAgents.swift +++ b/Sources/SessionIndexRegisteredAgents.swift @@ -151,17 +151,16 @@ extension SessionIndexStore { metadata.branch = firstString(in: object, keys: ["gitBranch", "branch"]) } if metadata.title.isEmpty { - metadata.title = firstText(in: object, keys: ["title", "prompt", "text", "content"]) ?? "" + metadata.title = firstTopLevelTitle(in: object) ?? "" } if metadata.title.isEmpty, let message = object["message"] as? [String: Any] { - let role = firstString(in: message, keys: ["role"]) - if role == nil || isUserRole(role) { + if shouldUseMessageAsTitle(message) { metadata.title = firstText(in: message, keys: ["content", "text"]) ?? "" } } if metadata.title.isEmpty, let messages = object["messages"] as? [[String: Any]] { metadata.title = messages.compactMap { message in - isUserRole(firstString(in: message, keys: ["role"])) + shouldUseMessageAsTitle(message) ? firstText(in: message, keys: ["content", "text"]) : nil }.first ?? "" @@ -219,6 +218,14 @@ extension SessionIndexStore { return nil } + nonisolated private static func firstTopLevelTitle(in object: [String: Any]) -> String? { + if let title = firstText(in: object, keys: ["title", "prompt"]) { + return title + } + guard shouldUseMessageAsTitle(object) else { return nil } + return firstText(in: object, keys: ["text", "content"]) + } + nonisolated private static func firstTextValue(_ value: Any?) -> String? { if let string = value as? String { return trimmedNonEmpty(string) @@ -253,6 +260,11 @@ extension SessionIndexStore { return trimmed.isEmpty ? nil : trimmed } + nonisolated private static func shouldUseMessageAsTitle(_ message: [String: Any]) -> Bool { + let role = firstString(in: message, keys: ["role"]) + return role == nil || isUserRole(role) + } + nonisolated private static func isUserRole(_ role: String?) -> Bool { role?.caseInsensitiveCompare("user") == .orderedSame } diff --git a/cmuxTests/PiVaultAgentPersistenceTests.swift b/cmuxTests/PiVaultAgentPersistenceTests.swift index ed9335b2ceef..d7da2d4d7ede 100644 --- a/cmuxTests/PiVaultAgentPersistenceTests.swift +++ b/cmuxTests/PiVaultAgentPersistenceTests.swift @@ -294,6 +294,67 @@ final class PiVaultAgentPersistenceTests: XCTestCase { XCTAssertEqual(entry.cwd, cwd) } + func testPiJSONLTopLevelAssistantTypedContentDoesNotBecomeTitle() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-pi-vault-top-level-role-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let cwd = "/tmp/pi top level role" + let projectDirectory = try XCTUnwrap(PiSessionLocator.projectDirectoryName(for: cwd)) + let sessionDir = tempDir.appendingPathComponent(projectDirectory, isDirectory: true) + try FileManager.default.createDirectory(at: sessionDir, withIntermediateDirectories: true) + let sessionFile = sessionDir.appendingPathComponent("019e1c86-def0-72c9-90d4-8543db20f982.jsonl") + try """ + {"role":"assistant","content":[{"type":"text","text":"assistant preface"}]} + {"role":"user","content":[{"type":"text","text":"implement the vault view"}]} + """.write(to: sessionFile, atomically: true, encoding: .utf8) + + var registration = CmuxVaultAgentRegistration.builtInPi + registration.sessionDirectory = tempDir.path + let entries = await SessionIndexStore.loadRegisteredAgentEntries( + registration: registration, + needle: "", + cwdFilter: cwd, + offset: 0, + limit: 10 + ) + + let entry = try XCTUnwrap(entries.first) + XCTAssertEqual(entry.title, "implement the vault view") + XCTAssertEqual(entry.cwd, cwd) + } + + func testPiJSONLMessagesArrayUsesNilRoleTextAsTitle() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-pi-vault-messages-nil-role-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let cwd = "/tmp/pi nil role" + let projectDirectory = try XCTUnwrap(PiSessionLocator.projectDirectoryName(for: cwd)) + let sessionDir = tempDir.appendingPathComponent(projectDirectory, isDirectory: true) + try FileManager.default.createDirectory(at: sessionDir, withIntermediateDirectories: true) + let sessionFile = sessionDir.appendingPathComponent("019e1c86-def0-72c9-90d4-8543db20f983.jsonl") + try """ + {"messages":[{"content":[{"type":"text","text":"restore without role"}]},{"role":"assistant","content":[{"type":"text","text":"assistant reply"}]}]} + """.write(to: sessionFile, atomically: true, encoding: .utf8) + + var registration = CmuxVaultAgentRegistration.builtInPi + registration.sessionDirectory = tempDir.path + let entries = await SessionIndexStore.loadRegisteredAgentEntries( + registration: registration, + needle: "", + cwdFilter: cwd, + offset: 0, + limit: 10 + ) + + let entry = try XCTUnwrap(entries.first) + XCTAssertEqual(entry.title, "restore without role") + XCTAssertEqual(entry.cwd, cwd) + } + func testPiVaultAgentSnapshotRoundTripBuildsTargetedSessionCommand() throws { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("cmux-pi-vault-agent-\(UUID().uuidString)", isDirectory: true) From 1e1d23391e1266b15b65d6037821dd7f3e1fd68d Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 13 May 2026 14:11:10 -0700 Subject: [PATCH 4/4] fix: require typed text blocks for Pi titles --- Sources/SessionIndexRegisteredAgents.swift | 4 +-- cmuxTests/PiVaultAgentPersistenceTests.swift | 30 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Sources/SessionIndexRegisteredAgents.swift b/Sources/SessionIndexRegisteredAgents.swift index 14da4de74023..08e42c634a23 100644 --- a/Sources/SessionIndexRegisteredAgents.swift +++ b/Sources/SessionIndexRegisteredAgents.swift @@ -248,8 +248,8 @@ extension SessionIndexStore { return trimmedNonEmpty(string) } guard let block = value as? [String: Any] else { return nil } - if let type = firstString(in: block, keys: ["type"]), - type.caseInsensitiveCompare("text") != .orderedSame { + guard let type = firstString(in: block, keys: ["type"]), + type.caseInsensitiveCompare("text") == .orderedSame else { return nil } return firstString(in: block, keys: ["text"]) diff --git a/cmuxTests/PiVaultAgentPersistenceTests.swift b/cmuxTests/PiVaultAgentPersistenceTests.swift index d7da2d4d7ede..988bb92fa5da 100644 --- a/cmuxTests/PiVaultAgentPersistenceTests.swift +++ b/cmuxTests/PiVaultAgentPersistenceTests.swift @@ -355,6 +355,36 @@ final class PiVaultAgentPersistenceTests: XCTestCase { XCTAssertEqual(entry.cwd, cwd) } + func testPiJSONLTypedContentBlocksRequireTextType() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-pi-vault-typed-content-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let cwd = "/tmp/pi typed content" + let projectDirectory = try XCTUnwrap(PiSessionLocator.projectDirectoryName(for: cwd)) + let sessionDir = tempDir.appendingPathComponent(projectDirectory, isDirectory: true) + try FileManager.default.createDirectory(at: sessionDir, withIntermediateDirectories: true) + let sessionFile = sessionDir.appendingPathComponent("019e1c86-def0-72c9-90d4-8543db20f984.jsonl") + try """ + {"message":{"role":"user","content":[{"text":"untyped object"},{"type":"image","text":"image fallback"},{"type":"text","text":"typed text title"}]}} + """.write(to: sessionFile, atomically: true, encoding: .utf8) + + var registration = CmuxVaultAgentRegistration.builtInPi + registration.sessionDirectory = tempDir.path + let entries = await SessionIndexStore.loadRegisteredAgentEntries( + registration: registration, + needle: "", + cwdFilter: cwd, + offset: 0, + limit: 10 + ) + + let entry = try XCTUnwrap(entries.first) + XCTAssertEqual(entry.title, "typed text title") + XCTAssertEqual(entry.cwd, cwd) + } + func testPiVaultAgentSnapshotRoundTripBuildsTargetedSessionCommand() throws { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("cmux-pi-vault-agent-\(UUID().uuidString)", isDirectory: true)