Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 65 additions & 4 deletions Sources/SessionIndexRegisteredAgents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,17 @@ 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 = firstTopLevelTitle(in: object) ?? ""
}
if metadata.title.isEmpty, let message = object["message"] as? [String: Any] {
metadata.title = firstString(in: message, keys: ["content", "text"]) ?? ""
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
firstString(in: message, keys: ["role"]) == "user"
? firstString(in: message, keys: ["content", "text"])
shouldUseMessageAsTitle(message)
? firstText(in: message, keys: ["content", "text"])
: nil
}.first ?? ""
}
Expand Down Expand Up @@ -208,6 +210,65 @@ 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 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)
}
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 }
guard let type = firstString(in: block, keys: ["type"]),
type.caseInsensitiveCompare("text") == .orderedSame else {
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 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
}

nonisolated private static func piCWDInferred(from url: URL) -> String? {
let directoryName = url.deletingLastPathComponent().lastPathComponent
guard directoryName.hasPrefix("--"), directoryName.hasSuffix("--"), directoryName.count > 4 else {
Expand Down
122 changes: 122 additions & 0 deletions cmuxTests/PiVaultAgentPersistenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,128 @@ 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 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 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)
Expand Down
Loading