diff --git a/ROADMAP.md b/ROADMAP.md index d9ac41e..09cc5f5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -71,10 +71,10 @@ | Thread-scoped recent-turn observable | `Partially shipped` | `CodexThread.makeRecentTurns(limit:)` now vends a bounded recent-turn observable that prewarms from the local history store, supports explicit older/newer whole-turn window expansion, seeds upstream paging cursors even when the visible initial window came from local history, and falls back to `thread/turns/list` when needed. Live probing showed that upstream turn paging is available only after a non-ephemeral thread has materialized at least one user turn, so recent observable startup now degrades to an empty local-only view for the known ephemeral and pre-materialized live runtime responses instead of surfacing raw protocol text. `RecentTurns` now ships named cache-policy presets for chat UIs, full inspectors, and compact history rails; tracks both resident item counts and weighted resident item cost; slims low-value payloads out of older non-visible completed turns before evicting whole turns; rehydrates slimmed turns when they become visible again; and uses scroll-position, visibility, phase, and velocity signals to drive protected residency plus earlier prefetch. Richer weighting heuristics and deeper policy tuning are still open. | | Thread-scoped recent-file observable | `Partially shipped` | `CodexThread.makeRecentFiles(limit:)` and `makeRecentFiles(_:)` now vend a file-centric recent-files observable that hydrates from persisted file-change items, keeps one resident entry per file-change item, enriches live entries from `item/fileChange/outputDelta` and `item/fileChange/patchUpdated`, can load older file entries from the same turn before stepping farther back through older turns, and supports selection-aware shell-versus-payload slimming with automatic payload rehydration for protected files. `CodexThread.RecentFilesQD` gives callers a repeatable descriptor for the initial resident file window and cache policy. Live probing exercises a real create/edit/delete scenario, and recent-file startup now inherits the same empty local-only degradation as recent-turns for the known live history-unavailable responses. The current weighting now accounts for diff structure and line volume, and shell summaries prefer concise edit summaries over raw terminal status when sealed payload is available. The remaining open work is better payload-cost calibration at the margins and richer structured patch presentation beyond the current text preview. | | Thread-scoped recent-command observable | `Partially shipped` | `CodexThread.makeRecentCommands(limit:)` and `makeRecentCommands(_:)` now vend a command-centric recent-commands observable that hydrates from persisted `commandExecution` items, keeps one resident entry per command item, enriches live entries from `item/commandExecution/outputDelta`, can load older command entries from the same turn before stepping farther back through older turns, and supports selection-aware shell-versus-output slimming with automatic output rehydration for protected commands. `CodexThread.RecentCommandsQD` gives callers a repeatable descriptor for the initial resident command window and cache policy. Recent-command startup now inherits the same empty local-only degradation as recent-turns for the known live history-unavailable responses. Current output weighting accounts for output size and line structure, and shell summaries prefer concise command and output summaries over raw transport detail. The remaining open work is better output-cost calibration and sharper shell-summary heuristics. | -| App-wide observable companion | `In Progress` | `CodexAppServer.makeLibrary()` and `CodexAppServer.Library` now expose Core Data-backed value snapshots for unarchived, archived, cwd-grouped, and repository-grouped threads, current Git branch and origin metadata, bindable sort/grouping policies, thread-list query descriptors, scoped refresh actions, library-local selection, recently selected ordering, local reloads after app-wide thread/turn events, and app-wide model/MCP/hook snapshots for launcher and sidebar UI. `CodexWorkspace` now promotes active permission-profile provenance and runtime filesystem/network permission facts from thread sessions, but the library still needs richer app-wide Git observables and broader app-wide settings/actions. | -| Public query descriptors | `Partially shipped` | `CodexAppServer.ThreadListQD` now provides repeatable thread-list intent for direct app-server `thread/list` reads and app-wide `Library` loading, `CodexFS.FileDiscoveryQD` provides repeatable bounded file-discovery intent over app-server `fs/readDirectory` reads, `CodexThread.HistoryWindowQD` provides repeatable local completed-turn window intent for recent, older, newer, turn-centered, and item-centered reads, and `CodexThread.RecentFilesQD` plus `CodexThread.RecentCommandsQD` describe recent-activity companion startup. Repository grouping now uses app-server Git origin metadata when available and falls back to cwd. Remaining descriptor work includes broader public cursor semantics, selection-centered reads if a concrete caller needs them, and later search-hit hydration. | +| App-wide observable companion | `In Progress` | `CodexAppServer.makeLibrary()` and `CodexAppServer.Library` now expose Core Data-backed value snapshots for unarchived, archived, cwd-grouped, and repository-grouped threads, `CodexWorkspace.ProjectInfo` identity for thread and group displays, `CodexAppServer.ThreadSource` values for source badges, bindable sort/grouping policies, thread-list query descriptors, scoped refresh actions, library-local selection, recently selected ordering, local reloads after app-wide thread/turn events, and app-wide model/MCP/hook snapshots for launcher and sidebar UI. `CodexWorkspace` now promotes active permission-profile provenance, runtime filesystem/network permission facts, and app-server-owned project identity from thread sessions, but the library still needs broader app-wide settings/actions. | +| Public query descriptors | `Partially shipped` | `CodexAppServer.ThreadListQD` now provides repeatable thread-list intent for direct app-server `thread/list` reads and app-wide `Library` loading, `CodexFS.FileDiscoveryQD` provides repeatable bounded file-discovery intent over app-server `fs/readDirectory` reads, `CodexThread.HistoryWindowQD` provides repeatable local completed-turn window intent for recent, older, newer, turn-centered, and item-centered reads, and `CodexThread.RecentFilesQD` plus `CodexThread.RecentCommandsQD` describe recent-activity companion startup. Repository grouping now uses `CodexWorkspace.ProjectInfo`, which identifies a project by Codex-reported Git origin when available and falls back to cwd. Remaining descriptor work includes broader public cursor semantics, selection-centered reads if a concrete caller needs them, and later search-hit hydration. | | Non-UI local history-reading helpers | `Partially shipped` | `CodexThread` now exposes a lightweight `HistoryWindow` page shape for recent local history, older or newer local windows around a known boundary turn id, centered `windowAroundTurn(...)` reads, centered `windowAroundItem(...)` reads, direct `ClosedTurn` reads for one turn, and convenience array helpers over those same windows. This gives non-UI callers an intentional path into the local history store without binding a UI-oriented observable, while still deferring a broader public cursor model, transcript search surface, and richer history-query helpers. | -| Public API curation | `Shipped / ongoing` | The source-organization pass has split app-wide model, MCP, thread-management, history, and observable companion values into focused public files while preserving `CodexAppServer`, `CodexThread`, and `CodexTurnHandle` as the three real owners. The connected public-surface review closed the v1 ownership model; future curation should stay tied to concrete public API additions. | +| Public API curation | `Shipped / ongoing` | The source-organization pass has split app-wide model, MCP, thread-management, history, and observable companion values into focused public files while preserving `CodexAppServer`, `CodexThread`, and `CodexTurnHandle` as the three real owners. The connected public-surface review closed the v1 ownership model; post-v1 curation now includes app-server-owned project identity and thread source facts for launcher UI without exposing generated wire models. Future curation should stay tied to concrete public API additions. | | DocC documentation | `Shipped / ongoing` | `Sources/SwiftASB/SwiftASB.docc/` contains a package landing page, public-handle extension pages, conceptual articles for app-wide capabilities, interactive lifecycle, thread management, history/observable companions, generated-wire boundary notes, and copy-pasteable walkthroughs for startup, progress/approval handling, diagnostics/history, and SwiftUI observable companions. The catalog is validated through Xcode `docbuild`; future work is ordinary stale-link, prose, and symbol-comment refinement as the public API grows. | | Swift Package Index readiness | `Shipped` | `.spi.yml` declares `SwiftASB` as the documentation target, and Swift Package Index lists `gaelic-ghost/SwiftASB` with a documentation link, compatibility/build results, Package ID `9B5839D9-9551-473F-A939-841534A3FC55`, and a 2026-05-06 update timestamp for the latest confirmed indexed release. Recheck SPI after the `v1.1.2` tag is published. | | Contributor documentation split | `Shipped` | `README.md` is now focused on Swift and SwiftUI package users, while `CONTRIBUTING.md` owns contributor setup, validation, DocC, live-test flags, generated-wire refresh, and PR expectations. | diff --git a/Sources/SwiftASB/History/ThreadHistoryStore.swift b/Sources/SwiftASB/History/ThreadHistoryStore.swift index 9e4c51d..a151d2c 100644 --- a/Sources/SwiftASB/History/ThreadHistoryStore.swift +++ b/Sources/SwiftASB/History/ThreadHistoryStore.swift @@ -99,12 +99,14 @@ actor ThreadHistoryStore { let forkedFromTurnID: String? let gitBranch: String? let gitOriginURL: String? + let gitSHA: String? let isArchived: Bool let isClosed: Bool let modelProvider: String let name: String? let preview: String let rollbacks: [RollbackSnapshot] + let source: CodexAppServer.ThreadSource let state: StateSnapshot let statusFlags: [String] let statusType: String @@ -155,12 +157,14 @@ actor ThreadHistoryStore { let forkedFromThreadID: String? let gitBranch: String? let gitOriginURL: String? + let gitSHA: String? let isArchived: Bool let isClosed: Bool let lastCompletedTurnAt: Int? let modelProvider: String let name: String? let preview: String + let source: CodexAppServer.ThreadSource let statusFlags: [String] let statusType: String let updatedAt: Int @@ -644,12 +648,14 @@ actor ThreadHistoryStore { forkedFromTurnID: thread.forkedFromTurnID, gitBranch: thread.gitBranch, gitOriginURL: thread.gitOriginURL, + gitSHA: thread.gitSHA, isArchived: thread.isArchived, isClosed: thread.isClosed, modelProvider: thread.modelProvider, name: thread.name, preview: thread.preview, rollbacks: rollbacks, + source: (try Self.decode(CodexAppServer.ThreadSource.self, from: thread.sourceData)) ?? .unknown, state: .init(completeness: state.completeness), statusFlags: (try Self.decode([String].self, from: thread.statusFlagsData)) ?? [], statusType: thread.statusType, @@ -930,11 +936,13 @@ actor ThreadHistoryStore { thread.currentDirectoryPath = info.currentDirectoryPath thread.ephemeral = info.ephemeral thread.forkedFromThreadID = info.forkedFromThreadID - thread.gitBranch = info.gitInfo?.branch - thread.gitOriginURL = info.gitInfo?.originURL + thread.gitBranch = info.projectInfo.repository?.branch + thread.gitOriginURL = info.projectInfo.repository?.originURL + thread.gitSHA = info.projectInfo.repository?.sha thread.modelProvider = info.modelProvider thread.name = info.name thread.preview = info.preview + thread.sourceData = try? encode(info.source) thread.statusType = info.status.type.rawValue thread.statusFlagsData = try? encode(info.status.activeFlags.map(\.rawValue)) thread.updatedAt = Int64(info.updatedAt) @@ -1099,12 +1107,14 @@ actor ThreadHistoryStore { forkedFromThreadID: thread.forkedFromThreadID, gitBranch: thread.gitBranch, gitOriginURL: thread.gitOriginURL, + gitSHA: thread.gitSHA, isArchived: thread.isArchived, isClosed: thread.isClosed, lastCompletedTurnAt: Self.lastCompletedTurnAt(for: thread), modelProvider: thread.modelProvider, name: thread.name, preview: thread.preview, + source: try decode(CodexAppServer.ThreadSource.self, from: thread.sourceData) ?? .unknown, statusFlags: try decode([String].self, from: thread.statusFlagsData) ?? [], statusType: thread.statusType, updatedAt: Int(thread.updatedAt) @@ -1488,9 +1498,11 @@ actor ThreadHistoryStore { attribute("forkedFromTurnID", .stringAttributeType, isOptional: true), attribute("gitBranch", .stringAttributeType, isOptional: true), attribute("gitOriginURL", .stringAttributeType, isOptional: true), + attribute("gitSHA", .stringAttributeType, isOptional: true), attribute("modelProvider", .stringAttributeType, isOptional: false), attribute("name", .stringAttributeType, isOptional: true), attribute("preview", .stringAttributeType, isOptional: false), + attribute("sourceData", .binaryDataAttributeType, isOptional: true), attribute("statusType", .stringAttributeType, isOptional: false), attribute("statusFlagsData", .binaryDataAttributeType, isOptional: true), attribute("updatedAt", .integer64AttributeType, isOptional: false), @@ -1718,12 +1730,14 @@ final class HistoryThread: NSManagedObject { @NSManaged var forkedFromTurnID: String? @NSManaged var gitBranch: String? @NSManaged var gitOriginURL: String? + @NSManaged var gitSHA: String? @NSManaged var id: String @NSManaged var isArchived: Bool @NSManaged var isClosed: Bool @NSManaged var modelProvider: String @NSManaged var name: String? @NSManaged var preview: String + @NSManaged var sourceData: Data? @NSManaged var state: HistoryThreadState? @NSManaged var statusFlagsData: Data? @NSManaged var statusType: String diff --git a/Sources/SwiftASB/Public/CodexAppServer+Library.swift b/Sources/SwiftASB/Public/CodexAppServer+Library.swift index 5a02a1a..e2749df 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+Library.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+Library.swift @@ -332,20 +332,21 @@ public extension CodexAppServer { public let currentDirectoryPath: String public let ephemeral: Bool public let forkedFromThreadID: String? - public let currentGitBranch: String? - public let currentGitOriginURL: String? public let isArchived: Bool public let isClosed: Bool public let lastCompletedTurnAt: Int? public let modelProvider: String public let name: String? public let preview: String + public let projectInfo: CodexWorkspace.ProjectInfo + public let source: CodexAppServer.ThreadSource public let status: CodexAppServer.ThreadStatus public let updatedAt: Int } public struct ThreadGroup: Sendable, Equatable, Identifiable { public let id: String + public let projectInfo: CodexWorkspace.ProjectInfo? public let title: String public let threads: [ThreadSnapshot] } @@ -776,15 +777,21 @@ public extension CodexAppServer { case .cwd: thread.currentDirectoryPath case .repository: - thread.currentGitOriginURL ?? thread.currentDirectoryPath + thread.projectInfo.id } } return grouped .map { key, threads in - ThreadGroup( + let projectInfo = projectInfo( + forGroupID: key, + threads: threads, + groupedBy: groupedBy + ) + return ThreadGroup( id: key, - title: title(forGroupID: key, groupedBy: groupedBy), + projectInfo: projectInfo, + title: projectInfo?.displayName ?? "Unknown Project", threads: threads ) } @@ -793,28 +800,38 @@ public extension CodexAppServer { } } - private static func title( + private static func projectInfo( forGroupID id: String, + threads: [ThreadSnapshot], groupedBy: GroupedBy - ) -> String { - guard !id.isEmpty else { - return "Unknown Project" + ) -> CodexWorkspace.ProjectInfo? { + guard groupedBy != .none else { + return nil } guard groupedBy == .repository else { - return id + return .init(currentDirectoryPath: id) + } + + guard let representative = threads.first else { + return .init(currentDirectoryPath: id) } - guard let url = URL(string: id), - let host = url.host, - let lastPathComponent = url.pathComponents.last else { - return id + guard representative.projectInfo.identitySource == .gitOrigin else { + return representative.projectInfo } - let repoName = lastPathComponent.hasSuffix(".git") - ? String(lastPathComponent.dropLast(4)) - : lastPathComponent - return repoName.isEmpty ? host : "\(repoName) (\(host))" + let repositories = threads.map(\.projectInfo.repository) + let branch = commonValue(repositories.map { $0?.branch }) + let sha = commonValue(repositories.map { $0?.sha }) + return .init( + currentDirectoryPath: representative.currentDirectoryPath, + repository: .init( + originURL: id, + branch: branch, + sha: sha + ) + ) } private static func newest( @@ -907,14 +924,21 @@ extension CodexAppServer.Library.ThreadSnapshot { currentDirectoryPath: snapshot.currentDirectoryPath, ephemeral: snapshot.ephemeral, forkedFromThreadID: snapshot.forkedFromThreadID, - currentGitBranch: snapshot.gitBranch, - currentGitOriginURL: snapshot.gitOriginURL, isArchived: snapshot.isArchived, isClosed: snapshot.isClosed, lastCompletedTurnAt: snapshot.lastCompletedTurnAt, modelProvider: snapshot.modelProvider, name: snapshot.name, preview: snapshot.preview, + projectInfo: .init( + currentDirectoryPath: snapshot.currentDirectoryPath, + repository: Self.repositoryInfo( + branch: snapshot.gitBranch, + originURL: snapshot.gitOriginURL, + sha: snapshot.gitSHA + ) + ), + source: snapshot.source, status: .init( type: .init(rawValue: snapshot.statusType) ?? .notLoaded, activeFlags: snapshot.statusFlags.compactMap(CodexAppServer.ThreadActiveFlag.init(rawValue:)) @@ -922,4 +946,22 @@ extension CodexAppServer.Library.ThreadSnapshot { updatedAt: snapshot.updatedAt ) } + + private static func repositoryInfo( + branch: String?, + originURL: String?, + sha: String? + ) -> CodexWorkspace.RepositoryInfo? { + let repository = CodexWorkspace.RepositoryInfo( + originURL: originURL, + branch: branch, + sha: sha + ) + return repository.isEmpty ? nil : repository + } +} + +private func commonValue(_ values: [String?]) -> String? { + guard let first = values.first else { return nil } + return values.allSatisfy { $0 == first } ? first : nil } diff --git a/Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift b/Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift index cf87041..89c4f81 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift @@ -210,10 +210,11 @@ extension CodexAppServer { public let currentDirectoryPath: String public let ephemeral: Bool public let forkedFromThreadID: String? - public let gitInfo: GitInfo? public let modelProvider: String public let name: String? public let preview: String + public let projectInfo: CodexWorkspace.ProjectInfo + public let source: ThreadSource public let status: ThreadStatus public let updatedAt: Int } @@ -273,6 +274,84 @@ extension CodexAppServer { case vscode } + /// App-server-reported source for a stored or active thread. + public enum ThreadSource: Sendable, Equatable, Codable { + /// Thread started by the Codex app-server owner. + case appServer + /// Thread started by the Codex CLI. + case cli + /// Thread started by a direct exec integration. + case exec + /// Thread started by a VS Code integration. + case vscode + /// Thread started by a named integration outside the built-in cases. + case custom(String) + /// Thread created by a Codex sub-agent. + case subAgent(SubAgentSource) + /// Source omitted or unknown to the app-server. + case unknown + + /// App-server-reported source for an agent-created child thread. + public struct SubAgentSource: Sendable, Equatable, Codable { + /// Sub-agent source family reported by Codex. + public enum Kind: String, Sendable, Equatable, Codable { + case compact + case memoryConsolidation + case review + case threadSpawn + case other + case unknown + } + + /// Coarse source family for the sub-agent thread. + public let kind: Kind + /// Raw source label when Codex reports a sub-agent source outside the known families. + public let other: String? + /// Thread-spawn details when `kind` is ``Kind/threadSpawn``. + public let threadSpawn: ThreadSpawn? + + /// Creates a sub-agent source value. + public init( + kind: Kind, + other: String? = nil, + threadSpawn: ThreadSpawn? = nil + ) { + self.kind = kind + self.other = other + self.threadSpawn = threadSpawn + } + } + + /// Metadata for a sub-agent thread-spawn source. + public struct ThreadSpawn: Sendable, Equatable, Codable { + /// Human-facing nickname Codex assigned to the spawned agent, when available. + public let agentNickname: String? + /// Agent path reported by Codex, when available. + public let agentPath: String? + /// Role assigned to the spawned agent, when available. + public let agentRole: String? + /// Spawn depth relative to the parent thread. + public let depth: Int + /// Parent thread identifier that spawned this agent thread. + public let parentThreadID: String + + /// Creates a thread-spawn source value. + public init( + agentNickname: String? = nil, + agentPath: String? = nil, + agentRole: String? = nil, + depth: Int, + parentThreadID: String + ) { + self.agentNickname = agentNickname + self.agentPath = agentPath + self.agentRole = agentRole + self.depth = depth + self.parentThreadID = parentThreadID + } + } + } + public struct ThreadListRequest: Sendable, Equatable { public var archived: Bool? public var cursor: String? diff --git a/Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift b/Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift index 03c01ea..603d92d 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift @@ -78,12 +78,6 @@ public extension CodexAppServer { case replace(String) } - /// Stored Git metadata reported for a thread. - struct GitInfo: Sendable, Equatable { - public let branch: String? - public let originURL: String? - public let sha: String? - } } extension CodexAppServer.ThreadMetadataUpdateRequest { @@ -117,13 +111,3 @@ extension CodexProtocolThreadMetadataUpdateParams.FieldUpdate { } } } - -extension CodexAppServer.GitInfo { - init(wireValue: CodexWireGitInfo) { - self.init( - branch: wireValue.branch, - originURL: wireValue.originURL, - sha: wireValue.sha - ) - } -} diff --git a/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift b/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift index 1e8893f..c575bad 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift @@ -554,16 +554,97 @@ extension CodexAppServer.ThreadInfo { currentDirectoryPath: wireValue.cwd, ephemeral: wireValue.ephemeral, forkedFromThreadID: wireValue.forkedFromID, - gitInfo: wireValue.gitInfo.map(CodexAppServer.GitInfo.init), modelProvider: wireValue.modelProvider, name: wireValue.name, preview: wireValue.preview, + projectInfo: .init( + currentDirectoryPath: wireValue.cwd, + repository: wireValue.gitInfo.map(CodexWorkspace.RepositoryInfo.init) + ), + source: .init(wireValue: wireValue.source), status: .init(wireValue: wireValue.status), updatedAt: wireValue.updatedAt ) } } +extension CodexAppServer.ThreadSource { + init(wireValue: CodexWireSessionSourceUnion) { + switch wireValue { + case let .enumeration(source): + self.init(wireValue: source) + case let .codexWireSessionSource(source): + if let custom = source.custom, !custom.isEmpty { + self = .custom(custom) + } else if let subAgent = source.subAgent { + self = .subAgent(.init(wireValue: subAgent)) + } else { + self = .unknown + } + } + } + + private init(wireValue: CodexWireSessionSourceEnum) { + switch wireValue { + case .appServer: + self = .appServer + case .cli: + self = .cli + case .exec: + self = .exec + case .unknown: + self = .unknown + case .vscode: + self = .vscode + } + } +} + +extension CodexAppServer.ThreadSource.SubAgentSource { + init(wireValue: CodexWireSubAgentSourceUnion) { + switch wireValue { + case let .enumeration(source): + self.init(kind: .init(wireValue: source)) + case let .codexWireSubAgentSource(source): + if let threadSpawn = source.threadSpawn { + self.init( + kind: .threadSpawn, + threadSpawn: .init(wireValue: threadSpawn) + ) + } else if let other = source.other, !other.isEmpty { + self.init(kind: .other, other: other) + } else { + self.init(kind: .unknown) + } + } + } +} + +extension CodexAppServer.ThreadSource.SubAgentSource.Kind { + init(wireValue: CodexWireSubAgentSourceEnum) { + switch wireValue { + case .compact: + self = .compact + case .memoryConsolidation: + self = .memoryConsolidation + case .review: + self = .review + } + } +} + +extension CodexAppServer.ThreadSource.ThreadSpawn { + init(wireValue: CodexWireThreadSpawn) { + self.init( + agentNickname: wireValue.agentNickname, + agentPath: wireValue.agentPath, + agentRole: wireValue.agentRole, + depth: wireValue.depth, + parentThreadID: wireValue.parentThreadID + ) + } +} + extension CodexAppServer.CLIExecutableDiagnostics { init(resolution: CodexCLIExecutableResolver.Resolution) { self.init( diff --git a/Sources/SwiftASB/Public/CodexWorkspace.swift b/Sources/SwiftASB/Public/CodexWorkspace.swift index 0c24e2a..0dd5d04 100644 --- a/Sources/SwiftASB/Public/CodexWorkspace.swift +++ b/Sources/SwiftASB/Public/CodexWorkspace.swift @@ -128,13 +128,84 @@ public enum CodexWorkspace { public let enabled: Bool } + /// App-server-owned project identity for a thread or library group. + public struct ProjectInfo: Sendable, Equatable, Identifiable { + /// Fact SwiftASB used to identify the project. + public enum IdentitySource: String, Sendable, Equatable { + /// The project identity comes from Codex-reported Git origin metadata. + case gitOrigin + /// The project identity falls back to the app-server current working directory. + case currentDirectory + } + + public let id: String + public let identitySource: IdentitySource + public let displayName: String + public let currentDirectoryPath: String + public let repository: RepositoryInfo? + + /// Creates project identity from app-server-owned cwd and optional Git metadata. + public init( + currentDirectoryPath: String, + repository: RepositoryInfo? = nil + ) { + self.currentDirectoryPath = currentDirectoryPath + self.repository = repository + + if let originURL = repository?.originURL, !originURL.isEmpty { + self.id = originURL + self.identitySource = .gitOrigin + self.displayName = Self.displayName(forGitOriginURL: originURL) + } else { + self.id = currentDirectoryPath + self.identitySource = .currentDirectory + self.displayName = currentDirectoryPath.isEmpty ? "Unknown Project" : currentDirectoryPath + } + } + + private static func displayName(forGitOriginURL originURL: String) -> String { + guard let url = URL(string: originURL), + let host = url.host, + let lastPathComponent = url.pathComponents.last else { + return originURL + } + + let repositoryName = lastPathComponent.hasSuffix(".git") + ? String(lastPathComponent.dropLast(4)) + : lastPathComponent + return repositoryName.isEmpty ? host : "\(repositoryName) (\(host))" + } + } + + /// Codex-reported Git facts for a project or thread. + public struct RepositoryInfo: Sendable, Equatable { + public let originURL: String? + public let branch: String? + public let sha: String? + + /// Creates repository facts reported by Codex. + public init( + originURL: String? = nil, + branch: String? = nil, + sha: String? = nil + ) { + self.originURL = originURL + self.branch = branch + self.sha = sha + } + + internal var isEmpty: Bool { + originURL == nil && branch == nil && sha == nil + } + } + /// Thread-session workspace snapshot built from app-server-owned facts. public struct SessionSnapshot: Sendable, Equatable { public let activePermissionProfile: ActivePermissionProfile? public let currentDirectoryPath: String - public let gitInfo: CodexAppServer.GitInfo? public let instructionSources: [String] public let permissionProfile: PermissionProfile? + public let projectInfo: ProjectInfo public let sandboxPolicy: CodexAppServer.SandboxPolicy } } @@ -299,14 +370,24 @@ extension CodexWorkspace.NetworkPermissions { } } +extension CodexWorkspace.RepositoryInfo { + init(wireValue: CodexWireGitInfo) { + self.init( + originURL: wireValue.originURL, + branch: wireValue.branch, + sha: wireValue.sha + ) + } +} + extension CodexWorkspace.SessionSnapshot { init(session: CodexAppServer.ThreadSession) { self.init( activePermissionProfile: session.activePermissionProfile, currentDirectoryPath: session.currentDirectoryPath, - gitInfo: session.thread.gitInfo, instructionSources: session.instructionSources, permissionProfile: session.permissionProfile, + projectInfo: session.thread.projectInfo, sandboxPolicy: session.sandboxPolicy ) } diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md index 1bf3e2b..86d7a5b 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md @@ -142,6 +142,7 @@ Set ``ThreadResumeRequest/excludeTurns`` or ``ThreadForkRequest/excludeTurns`` w - ``ThreadMetadataFieldUpdate`` - ``ThreadSession`` - ``ThreadInfo`` +- ``ThreadSource`` - ``ThreadReadRequest`` - ``ThreadReadResult`` - ``ThreadListRequest`` @@ -150,7 +151,6 @@ Set ``ThreadResumeRequest/excludeTurns`` or ``ThreadForkRequest/excludeTurns`` w - ``LoadedThreadListPage`` - ``ThreadTurnsListRequest`` - ``ThreadTurnsPage`` -- ``GitInfo`` - ``CodexWorkspace`` ### Turn Models diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md index 5dc935d..23eec04 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md @@ -61,6 +61,8 @@ Recent observable startup can begin as an empty local-only view when the live ap - ``instructionSources`` - ``model`` - ``modelProvider`` +- ``projectInfo`` +- ``info/source`` - ``activePermissionProfile`` - ``permissionProfile`` - ``reasoningEffort`` diff --git a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md index 15732d7..4e6bac9 100644 --- a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md +++ b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md @@ -87,7 +87,7 @@ final class ThreadInspectorModel { ## Selection And Cache Behavior -`CodexAppServer.Library` is the app-wide companion for launchers, sidebars, and project browsers. It publishes value snapshots for unarchived threads, archived threads, cwd groups, and each thread's current Git branch when app-server provides one; it also reloads from local persistence after app-wide thread and turn events such as archive, unarchive, name changes, status changes, and completed turns. +`CodexAppServer.Library` is the app-wide companion for launchers, sidebars, and project browsers. It publishes value snapshots for unarchived threads, archived threads, cwd groups, ``CodexWorkspace/ProjectInfo`` values for thread and repository-group identity, and ``CodexAppServer/ThreadSource`` values for source badges; it also reloads from local persistence after app-wide thread and turn events such as archive, unarchive, name changes, status changes, and completed turns. Use ``CodexAppServer/Library/selectedThreadID`` and ``CodexAppServer/Library/selectThread(_:)-(String?)`` for library-local selection. The selection timestamp stays inside the library and can drive ``CodexAppServer/Library/SortedBy/selectedNewestFirst`` without writing UI preference state into Codex's stored thread metadata. diff --git a/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md b/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md index fa2a819..9374754 100644 --- a/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md +++ b/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md @@ -16,13 +16,13 @@ There are three public shapes: Use ``CodexAppServer/makeLibrary(configuration:)`` when a client needs stored-thread lists before choosing a thread. The library publishes unarchived threads, archived threads, and grouped unarchived threads. It reads local snapshots first so UI can show a sidebar quickly, then reconciles app-server `thread/list` pages in the background. -`Library.SortedBy` and `Library.GroupedBy` are UI-facing policies. `Library.GroupedBy.cwd` groups by the exact app-server working directory. `Library.GroupedBy.repository` groups by app-server Git origin URL when Codex reports one, then falls back to cwd for threads without Git origin metadata. ``CodexAppServer/ThreadListQD`` is the package-owned query descriptor for repeatable thread-list intent. Use it when the same list intent should drive direct app-server reads through ``CodexAppServer/listThreads(_:cursor:)`` or app-wide observable loading through ``CodexAppServer/Library/Configuration/query``. +`Library.SortedBy` and `Library.GroupedBy` are UI-facing policies. `Library.GroupedBy.cwd` groups by the exact app-server working directory. `Library.GroupedBy.repository` groups by ``CodexWorkspace/ProjectInfo`` identity: Git origin URL when Codex reports one, then cwd for threads without Git origin metadata. ``CodexAppServer/ThreadListQD`` is the package-owned query descriptor for repeatable thread-list intent. Use it when the same list intent should drive direct app-server reads through ``CodexAppServer/listThreads(_:cursor:)`` or app-wide observable loading through ``CodexAppServer/Library/Configuration/query``. Use ``CodexAppServer/Library/refreshAll()``, ``CodexAppServer/Library/refreshUnarchived()``, and ``CodexAppServer/Library/refreshArchived()`` for explicit reconciliation actions. The library also reloads local value snapshots after app-wide thread and turn events, including archive, unarchive, name changes, status changes, and completed turns. Use ``CodexAppServer/Library/selectedThreadID`` and ``CodexAppServer/Library/selectThread(_:)-(String?)`` for caller-owned selection. Selection is library-local state, so apps can keep one library per window or scene without changing the app-server's stored thread metadata. ``CodexAppServer/Library/SortedBy/selectedNewestFirst`` promotes recently selected threads before falling back to newest updated threads. -`cwd` is the session working directory that app-server stores on `ThreadInfo` and matches exactly through `thread/list` cwd filters. Repository grouping is a derived display policy over app-server-owned Git metadata; SwiftASB does not inspect the filesystem to discover repository roots. +`cwd` is the session working directory that app-server stores on `ThreadInfo` and matches exactly through `thread/list` cwd filters. ``CodexWorkspace/ProjectInfo`` is the public project identity value built from app-server-owned cwd and optional Git origin, branch, and SHA facts. ``CodexAppServer/ThreadSource`` is the public thread-origin value for launchers that need to distinguish CLI, app-server, editor, exec, custom, sub-agent, and unknown-source threads. SwiftASB does not inspect the filesystem to discover repository roots. The library can also publish app-wide read snapshots through ``CodexAppServer/Library/refreshAppSnapshots()``. Those snapshots reuse ``CodexAppServer/readModelCapabilities()``, ``CodexAppServer/listMcpServerStatuses(_:)``, and ``CodexAppServer/listHooks(_:)`` so model feature gates, MCP surfaces, and hook diagnostics are observable next to the stored-thread lists without becoming Core Data state. App-list, skill-change, and MCP-server-status notifications trigger this app-snapshot refresh path. diff --git a/Tests/SwiftASBTests/Public/CodexAppServerLibraryTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerLibraryTests.swift index df726df..c86e3f8 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerLibraryTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerLibraryTests.swift @@ -75,9 +75,10 @@ extension CodexAppServerTests { await library.refresh() #expect(library.unarchivedThreads.map(\.id) == ["thread-new", "thread-123"]) - #expect(library.unarchivedThreads.first?.currentGitBranch == "feature/library") + #expect(library.unarchivedThreads.first?.projectInfo.repository?.branch == "feature/library") #expect(library.archivedThreads.map(\.id) == ["thread-archived"]) #expect(library.groups.map(\.id) == ["/tmp/project", "/tmp/project-a"]) + #expect(library.groups.first(where: { $0.id == "/tmp/project-a" })?.projectInfo?.identitySource == .currentDirectory) #expect(library.groups.first(where: { $0.id == "/tmp/project-a" })?.threads.map(\.id) == ["thread-new"]) #expect(library.lastReconciledAt != nil) #expect(library.latestErrorDescription == nil) @@ -107,6 +108,7 @@ extension CodexAppServerTests { storedThread( id: "thread-package-a", cwd: "/tmp/package-a", + gitBranch: "main", gitOriginURL: "https://github.com/gaelic-ghost/SwiftASB.git", name: "Package A", preview: "First repo thread", @@ -116,6 +118,7 @@ extension CodexAppServerTests { storedThread( id: "thread-package-b", cwd: "/tmp/package-b", + gitBranch: "feature/project-info", gitOriginURL: "https://github.com/gaelic-ghost/SwiftASB.git", name: "Package B", preview: "Second repo thread", @@ -161,8 +164,13 @@ extension CodexAppServerTests { library.groups.first { $0.id == "https://github.com/gaelic-ghost/SwiftASB.git" } ) #expect(repositoryGroup.title == "SwiftASB (github.com)") + #expect(repositoryGroup.projectInfo?.identitySource == .gitOrigin) + #expect(repositoryGroup.projectInfo?.repository?.originURL == "https://github.com/gaelic-ghost/SwiftASB.git") + #expect(repositoryGroup.projectInfo?.repository?.branch == nil) #expect(repositoryGroup.threads.map(\.id) == ["thread-package-a", "thread-package-b"]) - #expect(repositoryGroup.threads.first?.currentGitOriginURL == "https://github.com/gaelic-ghost/SwiftASB.git") + #expect(repositoryGroup.threads.first?.projectInfo.repository?.originURL == "https://github.com/gaelic-ghost/SwiftASB.git") + #expect(repositoryGroup.threads.first?.projectInfo.repository?.branch == "main") + #expect(repositoryGroup.threads.first?.source == .cli) await client.stop() await tearDownTemporarySQLiteHistoryStore(historyStore, directory: temporaryDirectory) diff --git a/Tests/SwiftASBTests/Public/CodexAppServerStoredThreadTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerStoredThreadTests.swift index cbc7753..6df213c 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerStoredThreadTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerStoredThreadTests.swift @@ -69,12 +69,14 @@ extension CodexAppServerTests { #expect(archivedPage.threads.count == 1) #expect(archivedPage.threads[0].id == "thread-123") #expect(archivedPage.threads[0].name == "Archived release prep") + #expect(archivedPage.threads[0].source == .cli) #expect(archivedPage.nextCursor == "cursor-next") let archivedSnapshot = try await client.debugThreadHistorySnapshot(threadID: "thread-123") let archivedThread = try #require(archivedSnapshot) #expect(archivedThread.isArchived == true) #expect(archivedThread.name == "Archived release prep") + #expect(archivedThread.source == .cli) #expect(archivedThread.statusType == "notLoaded") #expect(archivedThread.state.completeness == "partial") @@ -106,18 +108,130 @@ extension CodexAppServerTests { #expect(activePage.threads.count == 1) #expect(activePage.threads[0].name == "Active release prep") + #expect(activePage.threads[0].source == .cli) #expect(activePage.nextCursor == nil) let activeSnapshot = try await client.debugThreadHistorySnapshot(threadID: "thread-123") let activeThread = try #require(activeSnapshot) #expect(activeThread.isArchived == false) #expect(activeThread.name == "Active release prep") + #expect(activeThread.source == .cli) #expect(activeThread.statusType == "idle") await client.stop() await tearDownTemporarySQLiteHistoryStore(historyStore, directory: temporaryDirectory) } + @Test("maps custom and sub-agent thread sources from stored thread lists") + func mapsStoredThreadSources() async throws { + let transport = FakeCodexAppServerTransport( + threadListResult: [ + "data": [ + [ + "cliVersion": "0.128.0", + "createdAt": 1713350100, + "cwd": "/tmp/project", + "ephemeral": false, + "id": "thread-custom", + "modelProvider": "openai", + "name": "Zed Thread", + "preview": "Started elsewhere", + "source": [ + "custom": "zed", + ], + "status": ["type": "notLoaded"], + "turns": [], + "updatedAt": 1713350105, + ], + [ + "cliVersion": "0.128.0", + "createdAt": 1713350200, + "cwd": "/tmp/project", + "ephemeral": false, + "id": "thread-subagent", + "modelProvider": "openai", + "name": "Explorer Thread", + "preview": "Spawned exploration", + "source": [ + "subAgent": [ + "thread_spawn": [ + "agent_nickname": "Explorer", + "agent_path": "/tmp/agents/explorer", + "agent_role": "explorer", + "depth": 1, + "parent_thread_id": "thread-parent", + ], + ], + ], + "status": ["type": "notLoaded"], + "turns": [], + "updatedAt": 1713350205, + ], + ], + ] + ) + let (historyStore, temporaryDirectory) = try temporarySQLiteHistoryStore() + let client = CodexAppServer( + transport: transport, + historyStore: historyStore + ) + + try await client.start() + _ = try await client.initialize( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + let page = try await client.listThreads() + + #expect(page.threads.map(\.source) == [ + .custom("zed"), + .subAgent( + .init( + kind: .threadSpawn, + threadSpawn: .init( + agentNickname: "Explorer", + agentPath: "/tmp/agents/explorer", + agentRole: "explorer", + depth: 1, + parentThreadID: "thread-parent" + ) + ) + ), + ]) + + let customSnapshot = try #require( + try await client.debugThreadHistorySnapshot(threadID: "thread-custom") + ) + #expect(customSnapshot.source == .custom("zed")) + + let subAgentSnapshot = try #require( + try await client.debugThreadHistorySnapshot(threadID: "thread-subagent") + ) + #expect( + subAgentSnapshot.source == .subAgent( + .init( + kind: .threadSpawn, + threadSpawn: .init( + agentNickname: "Explorer", + agentPath: "/tmp/agents/explorer", + agentRole: "explorer", + depth: 1, + parentThreadID: "thread-parent" + ) + ) + ) + ) + + await client.stop() + await tearDownTemporarySQLiteHistoryStore(historyStore, directory: temporaryDirectory) + } + @Test("resumes a stored thread and hydrates resumed history into the local history store") func resumesStoredThreadAndHydratesHistory() async throws { let transport = FakeCodexAppServerTransport( @@ -211,11 +325,13 @@ extension CodexAppServerTests { #expect(thread.currentDirectoryPath == "/tmp/project") #expect(thread.info.status.type == .idle) #expect(thread.info.preview == "Hydrated resume preview") + #expect(thread.info.source == .cli) let snapshot = try await client.debugThreadHistorySnapshot(threadID: "thread-123") let threadSnapshot = try #require(snapshot) #expect(threadSnapshot.name == "Resumed Thread") #expect(threadSnapshot.isArchived == false) + #expect(threadSnapshot.source == .cli) #expect(threadSnapshot.statusType == "idle") #expect(threadSnapshot.defaults.approvalPolicy == "onRequest") #expect(threadSnapshot.defaults.currentDirectoryPath == "/tmp/project") @@ -324,6 +440,7 @@ extension CodexAppServerTests { #expect(forkedThread.id == "thread-456") #expect(forkedThread.info.forkedFromThreadID == "thread-123") #expect(forkedThread.info.ephemeral == true) + #expect(forkedThread.info.source == .cli) #expect(forkedThread.info.status.type == .idle) let snapshot = try await client.debugThreadHistorySnapshot(threadID: "thread-456") @@ -331,6 +448,7 @@ extension CodexAppServerTests { #expect(threadSnapshot.forkedFromThreadID == "thread-123") #expect(threadSnapshot.forkedFromTurnID == "turn-shared-1") #expect(threadSnapshot.isArchived == false) + #expect(threadSnapshot.source == .cli) #expect(threadSnapshot.turns.count == 1) #expect(threadSnapshot.turns[0].id == "turn-shared-1") #expect(threadSnapshot.turns[0].items.count == 1) diff --git a/Tests/SwiftASBTests/Public/CodexAppServerThreadManagementTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerThreadManagementTests.swift index 982b450..f36f4ff 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerThreadManagementTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerThreadManagementTests.swift @@ -187,9 +187,9 @@ extension CodexAppServerTests { ) #expect(updatedThread.id == thread.id) - #expect(updatedThread.gitInfo?.branch == "main") - #expect(updatedThread.gitInfo?.originURL == nil) - #expect(updatedThread.gitInfo?.sha == "abc123") + #expect(updatedThread.projectInfo.repository?.branch == "main") + #expect(updatedThread.projectInfo.repository?.originURL == nil) + #expect(updatedThread.projectInfo.repository?.sha == "abc123") let requestPayload = try #require(await transport.recordedRequestPayload(for: "thread/metadata/update")) let request = try #require(try JSONSerialization.jsonObject(with: requestPayload) as? [String: Any]) @@ -237,7 +237,9 @@ extension CodexAppServerTests { #expect(thread.permissionProfile?.fileSystem?.kind == .restricted) #expect(thread.permissionProfile?.fileSystem?.globScanMaxDepth == 4) #expect(thread.workspace.currentDirectoryPath == "/tmp/project") - #expect(thread.workspace.gitInfo == thread.info.gitInfo) + #expect(thread.workspace.projectInfo == thread.info.projectInfo) + #expect(thread.workspace.projectInfo.currentDirectoryPath == "/tmp/project") + #expect(thread.info.source == .cli) let entries = try #require(thread.permissionProfile?.fileSystem?.entries) #expect(entries.first?.access == .write) diff --git a/docs/maintainers/v1-public-api-audit.md b/docs/maintainers/v1-public-api-audit.md index 0422b13..54a5905 100644 --- a/docs/maintainers/v1-public-api-audit.md +++ b/docs/maintainers/v1-public-api-audit.md @@ -449,10 +449,18 @@ Use these decisions for every public symbol: wire models. - [x] Record the repository grouping decision. Decision: `CodexAppServer.Library.GroupedBy.repository` groups thread - snapshots by app-server Git origin URL when available, then falls back to cwd. - SwiftASB stores and exposes `ThreadSnapshot.currentGitOriginURL` as - app-server-owned metadata; it does not inspect local filesystem directories to - derive repository roots. + snapshots by `CodexWorkspace.ProjectInfo` identity. That identity uses + app-server Git origin URL when available, then falls back to cwd. SwiftASB + stores Codex-reported branch, origin URL, and SHA facts under + `CodexWorkspace.RepositoryInfo`; it does not inspect local filesystem + directories to derive repository roots. +- [x] Record the thread source promotion. + Decision: `CodexAppServer.ThreadInfo.source` exposes the app-server-reported + thread origin as `CodexAppServer.ThreadSource`, including built-in sources, + custom source labels, and sub-agent thread-spawn metadata. `Library.ThreadSnapshot` + persists the same value for launcher badges and ranking explanations while + keeping source filters on `ThreadListSourceKind`, because filters still match + the app-server's coarse stored-thread list request shape. - [x] Record the first post-v1 app-server filesystem promotion. Decision: `CodexFS` is the public namespace for read-only filesystem facts routed through the app-server. It currently owns metadata, directory listing, diff --git a/docs/maintainers/v1-public-api-symbol-inventory.md b/docs/maintainers/v1-public-api-symbol-inventory.md index 29fd24d..b317b1b 100644 --- a/docs/maintainers/v1-public-api-symbol-inventory.md +++ b/docs/maintainers/v1-public-api-symbol-inventory.md @@ -1,15 +1,15 @@ # V1 Public API Symbol Inventory -Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot and on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. +Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot, on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices, and on 2026-05-08 for the `CodexWorkspace.ProjectInfo` cleanup and `CodexAppServer.ThreadSource` promotion. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. ## Summary -- Public/open symbols: 1735 -- Public/open types: 271 -- Public/open initializers: 115 -- Public/open methods and type methods: 124 -- Public/open enum cases: 321 -- Public/open properties: 904 +- Public/open symbols: 1867 +- Public/open types: 293 +- Public/open initializers: 130 +- Public/open methods and type methods: 129 +- Public/open enum cases: 363 +- Public/open properties: 951 ## Public Types @@ -21,7 +21,6 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexAppServer.CLIExecutableDiagnostics.Source` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift - `CodexAppServer.ClientInfo` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift - `CodexAppServer.Configuration` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift -- `CodexAppServer.GitInfo` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.GranularApprovalPolicy` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+Compatibility.swift - `CodexAppServer.InitializeCapabilities` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift - `CodexAppServer.InitializeRequest` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift @@ -63,6 +62,10 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexAppServer.ThreadListSortDirection` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSortKey` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSourceKind` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.ThreadSpawn` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadMetadataFieldUpdate` (`enum`) - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataGitInfoUpdate` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataUpdateRequest` (`struct`) - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift @@ -225,6 +228,9 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexWorkspace.PermissionProfile.Kind` (`enum`) - Sources/SwiftASB/Public/CodexWorkspace.swift - `CodexWorkspace.PermissionSelection` (`struct`) - Sources/SwiftASB/Public/CodexWorkspace.swift - `CodexWorkspace.PermissionSelectionModification` (`struct`) - Sources/SwiftASB/Public/CodexWorkspace.swift +- `CodexWorkspace.ProjectInfo` (`struct`) - Sources/SwiftASB/Public/CodexWorkspace.swift +- `CodexWorkspace.ProjectInfo.IdentitySource` (`enum`) - Sources/SwiftASB/Public/CodexWorkspace.swift +- `CodexWorkspace.RepositoryInfo` (`struct`) - Sources/SwiftASB/Public/CodexWorkspace.swift - `CodexWorkspace.SessionSnapshot` (`struct`) - Sources/SwiftASB/Public/CodexWorkspace.swift ## Public Initializers And Methods @@ -291,6 +297,9 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexAppServer.ThreadListSortDirection.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSortKey.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSourceKind.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.init(kind:other:threadSpawn:)` - `init(kind: CodexAppServer.ThreadSource.SubAgentSource.Kind, other: String? = nil, threadSpawn: CodexAppServer.ThreadSource.ThreadSpawn? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.ThreadSpawn.init(agentNickname:agentPath:agentRole:depth:parentThreadID:)` - `init(agentNickname: String? = nil, agentPath: String? = nil, agentRole: String? = nil, depth: Int, parentThreadID: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadMetadataGitInfoUpdate.init(branch:originURL:sha:)` - `init(branch: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged, originURL: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged, sha: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged)` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataUpdateRequest.init(threadID:gitInfo:)` - `init(threadID: String, gitInfo: CodexAppServer.ThreadMetadataGitInfoUpdate? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadReadRequest.init(threadID:includeTurns:)` - `init(threadID: String, includeTurns: Bool = false)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift @@ -425,6 +434,8 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexAppServer.ModelListRequest.init(cursor:limit:includeHidden:)` - `init(cursor: String? = nil, limit: Int? = nil, includeHidden: Bool? = nil)` - Sources/SwiftASB/Public/CodexAppServer+Models.swift - `CodexAppServer.ThreadForkRequest.init(threadID:approvalPolicy:approvalsReviewer:baseInstructions:config:currentDirectoryPath:developerInstructions:ephemeral:excludeTurns:model:modelProvider:personality:sandboxMode:serviceName:serviceTier:)` - `init(threadID: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, baseInstructions: String? = nil, config: [String : CodexAppServer.JSONValue]? = nil, currentDirectoryPath: String? = nil, developerInstructions: String? = nil, ephemeral: Bool? = nil, excludeTurns: Bool? = nil, model: String? = nil, modelProvider: String? = nil, personality: CodexAppServer.Personality? = nil, sandboxMode: CodexAppServer.SandboxMode? = nil, serviceName: String? = nil, serviceTier: CodexAppServer.ServiceTier? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListRequest.init(cursor:limit:sortKey:sortDirection:modelProviders:sourceKinds:archived:currentDirectoryPath:searchTerm:)` - `init(cursor: String? = nil, limit: Int? = nil, sortKey: CodexAppServer.ThreadListSortKey? = nil, sortDirection: CodexAppServer.ThreadListSortDirection? = nil, modelProviders: [String]? = nil, sourceKinds: [CodexAppServer.ThreadListSourceKind]? = nil, archived: Bool? = nil, currentDirectoryPath: String? = nil, searchTerm: String? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.init(kind:other:threadSpawn:)` - `init(kind: CodexAppServer.ThreadSource.SubAgentSource.Kind, other: String? = nil, threadSpawn: CodexAppServer.ThreadSource.ThreadSpawn? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.ThreadSpawn.init(agentNickname:agentPath:agentRole:depth:parentThreadID:)` - `init(agentNickname: String? = nil, agentPath: String? = nil, agentRole: String? = nil, depth: Int, parentThreadID: String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadMetadataGitInfoUpdate.init(branch:originURL:sha:)` - `init(branch: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged, originURL: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged, sha: CodexAppServer.ThreadMetadataFieldUpdate = .unchanged)` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataUpdateRequest.init(threadID:gitInfo:)` - `init(threadID: String, gitInfo: CodexAppServer.ThreadMetadataGitInfoUpdate? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadReadRequest.init(threadID:includeTurns:)` - `init(threadID: String, includeTurns: Bool = false)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift @@ -557,6 +568,19 @@ Generated from `swift package dump-symbol-graph --minimum-access-level public -- - `CodexAppServer.ThreadListSourceKind.exec` - `case exec` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSourceKind.unknown` - `case unknown` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadListSourceKind.vscode` - `case vscode` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.appServer` - `case appServer` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.cli` - `case cli` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.exec` - `case exec` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.vscode` - `case vscode` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.custom(_:)` - `case custom(String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.subAgent(_:)` - `case subAgent(CodexAppServer.ThreadSource.SubAgentSource)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.unknown` - `case unknown` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.compact` - `case compact` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.memoryConsolidation` - `case memoryConsolidation` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.review` - `case review` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.threadSpawn` - `case threadSpawn` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.other` - `case other` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift +- `CodexAppServer.ThreadSource.SubAgentSource.Kind.unknown` - `case unknown` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadMetadataFieldUpdate.clear` - `case clear` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataFieldUpdate.replace(_:)` - `case replace(String)` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift - `CodexAppServer.ThreadMetadataFieldUpdate.unchanged` - `case unchanged` - Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift @@ -735,10 +759,11 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `CodexThread` now exposes thread goals: `Goal`, `Goal.Status`, `GoalSetRequest`, `readGoal()`, `setGoal(_:)`, and `clearGoal()`. - `CodexThreadEvent` now includes `.goalUpdated(_:)` and `.goalCleared(_:)` for app-server goal notifications. - `CodexThread.RecentFilesQD` and `CodexThread.RecentCommandsQD` describe repeatable recent-activity companion startup intent. -- `CodexAppServer.Library.GroupedBy.repository` groups app-wide library snapshots by app-server Git origin metadata with cwd fallback, and `ThreadSnapshot.currentGitOriginURL` exposes the persisted origin value used for that grouping. -- `CodexWorkspace` owns app-server-owned permission selections and runtime workspace permission facts: `PermissionSelection`, `PermissionSelectionModification`, `ActivePermissionProfile`, `ActivePermissionModification`, `PermissionProfile`, `FileSystemPermissions`, `FileSystemSandboxEntry`, `FileSystemAccessMode`, `FileSystemPath`, `FileSystemSpecialPath`, `NetworkPermissions`, and `SessionSnapshot`. +- `CodexAppServer.Library.GroupedBy.repository` groups app-wide library snapshots by `CodexWorkspace.ProjectInfo` identity: app-server Git origin metadata with cwd fallback. +- `CodexWorkspace` owns app-server-owned permission selections, runtime workspace permission facts, and project identity: `PermissionSelection`, `PermissionSelectionModification`, `ActivePermissionProfile`, `ActivePermissionModification`, `PermissionProfile`, `FileSystemPermissions`, `FileSystemSandboxEntry`, `FileSystemAccessMode`, `FileSystemPath`, `FileSystemSpecialPath`, `NetworkPermissions`, `ProjectInfo`, `RepositoryInfo`, and `SessionSnapshot`. - `CodexAppServer.ThreadStartRequest`, `ThreadResumeRequest`, `ThreadForkRequest`, `TurnStartRequest`, `CodexThread.TurnStartRequest`, and `CodexThread.startTextTurn(...)` now accept optional `CodexWorkspace.PermissionSelection` values. -- `CodexAppServer.ThreadSession` and `CodexThread` now expose active permission-profile provenance, runtime permission facts, and a `CodexWorkspace.SessionSnapshot`. +- `CodexAppServer.ThreadSession` and `CodexThread` now expose active permission-profile provenance, runtime permission facts, app-server-owned project identity, and a `CodexWorkspace.SessionSnapshot`. +- `CodexAppServer.ThreadInfo` and `CodexAppServer.Library.ThreadSnapshot` now expose `CodexAppServer.ThreadSource` so launcher UIs can badge CLI, app-server, editor, custom, and sub-agent threads without reading generated wire values. ## Public Property Counts By Source File @@ -746,17 +771,17 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `Sources/SwiftASB/Public/CodexAppServer+CodexExtensions.swift`: 113 public properties - `Sources/SwiftASB/Public/CodexAppServer+Compatibility.swift`: 10 public properties - `Sources/SwiftASB/Public/CodexAppServer+Hooks.swift`: 32 public properties -- `Sources/SwiftASB/Public/CodexAppServer+Library.swift`: 55 public properties +- `Sources/SwiftASB/Public/CodexAppServer+Library.swift`: 56 public properties - `Sources/SwiftASB/Public/CodexAppServer+LoadedThreads.swift`: 4 public properties -- `Sources/SwiftASB/Public/CodexAppServer+MCP.swift`: 34 public properties +- `Sources/SwiftASB/Public/CodexAppServer+MCP.swift`: 43 public properties - `Sources/SwiftASB/Public/CodexAppServer+Models.swift`: 23 public properties -- `Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift`: 96 public properties -- `Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift`: 12 public properties +- `Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift`: 105 public properties +- `Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift`: 10 public properties - `Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift`: 24 public properties - `Sources/SwiftASB/Public/CodexConfig.swift`: 18 public properties -- `Sources/SwiftASB/Public/CodexDiagnostics.swift`: 14 public properties +- `Sources/SwiftASB/Public/CodexDiagnostics.swift`: 29 public properties - `Sources/SwiftASB/Public/CodexErrors.swift`: 1 public properties -- `Sources/SwiftASB/Public/CodexFS.swift`: 36 public properties +- `Sources/SwiftASB/Public/CodexFS.swift`: 44 public properties - `Sources/SwiftASB/Public/CodexInteractiveRequests.swift`: 74 public properties - `Sources/SwiftASB/Public/CodexThread+Dashboard.swift`: 29 public properties - `Sources/SwiftASB/Public/CodexThread+RecentCommands.swift`: 25 public properties @@ -764,4 +789,4 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `Sources/SwiftASB/Public/CodexThread+RecentTurns.swift`: 54 public properties - `Sources/SwiftASB/Public/CodexThread.swift`: 71 public properties - `Sources/SwiftASB/Public/CodexTurnHandle.swift`: 108 public properties -- `Sources/SwiftASB/Public/CodexWorkspace.swift`: 27 public properties +- `Sources/SwiftASB/Public/CodexWorkspace.swift`: 34 public properties