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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SwiftASB helps Swift apps work with the local Codex app-server without making ap

### Status

SwiftASB has a supported v1 public API for the core local Codex app-server lifecycle. `v1.1.4` is the current released baseline.
SwiftASB has a supported v1 public API for the core local Codex app-server lifecycle. `v1.2.0` is the current released baseline.

### What This Project Is

Expand All @@ -33,7 +33,7 @@ Add SwiftASB from the GitHub package URL:

https://github.com/gaelic-ghost/SwiftASB

Use release `v1.1.4` or newer unless your project intentionally pins an older version.
Use release `v1.2.0` or newer unless your project intentionally pins an older version.

You also need a local Codex CLI installation with app-server support. SwiftASB looks for `codex` in the usual command-line locations, and apps can provide an exact executable path when they need stricter control.

Expand All @@ -45,9 +45,9 @@ For copy-pasteable startup code, open the DocC getting-started guide:

Use SwiftASB when an app needs to show what Codex is doing right now, keep recent command and file activity visible, answer interactive requests, or build SwiftUI state around a running Codex turn.

For app-wide sidebars and launchers, `CodexAppServer.makeLibrary()` provides observable stored-thread lists, cwd or repository grouping, refresh actions, library-local selection state, and app-wide model, MCP, and hook diagnostics snapshots. Thread handles can also name, archive, unarchive, compact, and roll back stored threads through thread-scoped methods.
For app-wide sidebars and launchers, `CodexAppServer.makeLibrary()` provides observable stored-thread lists, cwd or repository grouping, stable worktree groups, repository/worktree thread filters, refresh actions, library-local selection state, app-server-owned worktree snapshots, and app-wide model, MCP, and hook diagnostics snapshots. Thread handles can also name, archive, unarchive, compact, and roll back stored threads through thread-scoped methods.

Use `CodexAppServer.fs` when a sandboxed client needs filesystem metadata, directory listings, file bytes, file discovery, fuzzy file lookup, or file-change watches through the Codex app-server instead of reading local disk directly. File-discovery hits include match kind, matched character ranges, and ranking reasons for picker highlighting and result explanations. `CodexWorkspace` carries app-server-owned workspace permission selections, active permission-profile provenance, and runtime filesystem/network permission facts for started threads and turns. Use `CodexAppServer.config` for effective config reads, and `CodexAppServer.extensions` for app, skill, plugin, and collaboration-mode inventory.
Use `CodexAppServer.fs` when a sandboxed client needs filesystem metadata, directory listings, file bytes, file discovery, fuzzy file lookup, or file-change watches through the Codex app-server instead of reading local disk directly. File-discovery hits include match kind, matched character ranges, and ranking reasons for picker highlighting and result explanations. `CodexWorkspace` carries app-server-owned worktree, Git, workspace permission selection, active permission-profile provenance, and runtime filesystem/network permission facts for started threads and turns. Use `CodexAppServer.config` for effective config reads, and `CodexAppServer.extensions` for app, skill, plugin, and collaboration-mode inventory.

Use `CodexAppServer.ThreadListQD`, `CodexFS.FileDiscoveryQD`, `CodexThread.HistoryWindowQD`, `CodexThread.RecentFilesQD`, and `CodexThread.RecentCommandsQD` when a client needs to preserve repeatable list, file-discovery, history-window, or recent-activity intent without depending on Core Data, SwiftData, direct filesystem reads, or raw app-server paging details.

Expand Down
114 changes: 90 additions & 24 deletions ROADMAP.md

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions Sources/SwiftASB/Public/CodexAppServer+Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,23 @@ public extension CodexAppServer {
public let source: CodexAppServer.ThreadSource
public let status: CodexAppServer.ThreadStatus
public let updatedAt: Int

/// Codex-reported cwd plus optional Git facts for this stored thread.
public var worktree: CodexWorkspace.WorktreeSnapshot {
projectInfo.worktree
}
}

public struct ThreadGroup: Sendable, Equatable, Identifiable {
public let id: String
public let projectInfo: CodexWorkspace.ProjectInfo?
public let title: String
public let threads: [ThreadSnapshot]

/// Codex-reported cwd plus optional Git facts for this group.
public var worktree: CodexWorkspace.WorktreeSnapshot? {
projectInfo?.worktree
}
}

public private(set) var archivedThreads: [ThreadSnapshot]
Expand Down Expand Up @@ -383,6 +393,7 @@ public extension CodexAppServer {
}
}
public private(set) var unarchivedThreads: [ThreadSnapshot]
public private(set) var worktreeGroups: [ThreadGroup]
public private(set) var snapshotCurrentDirectoryPaths: [String]?
public private(set) var snapshotPhase: SnapshotPhase

Expand All @@ -403,6 +414,14 @@ public extension CodexAppServer {
return allThreads.first { $0.id == selectedThreadID }
}

public var selectedWorktree: CodexWorkspace.WorktreeSnapshot? {
selectedThread?.worktree
}

public var selectedRepository: CodexWorkspace.RepositoryInfo? {
selectedThread?.worktree.repository
}

@ObservationIgnored
private let appServer: CodexAppServer

Expand Down Expand Up @@ -470,6 +489,7 @@ public extension CodexAppServer {
self.snapshotPhase = .idle
self.sortedBy = configuration.sortedBy
self.unarchivedThreads = []
self.worktreeGroups = []
applyVisibleState()

if configuration.reconcilesOnCreation {
Expand Down Expand Up @@ -613,6 +633,29 @@ public extension CodexAppServer {
selectThread(nil)
}

public func threads(
in worktree: CodexWorkspace.WorktreeSnapshot,
includeArchived: Bool = false
) -> [ThreadSnapshot] {
threads(inWorktreeID: worktree.id, includeArchived: includeArchived)
}

public func threads(
inWorktreeID worktreeID: String,
includeArchived: Bool = false
) -> [ThreadSnapshot] {
sortedVisibleThreads(includeArchived: includeArchived)
.filter { $0.worktree.id == worktreeID }
}

public func threads(
inRepositoryOriginURL originURL: String,
includeArchived: Bool = false
) -> [ThreadSnapshot] {
sortedVisibleThreads(includeArchived: includeArchived)
.filter { $0.worktree.repository?.originURL == originURL }
}

private func refreshArchiveScope(_ archived: Bool) async {
if isReconciling || isLoadingLocalSnapshot {
return
Expand Down Expand Up @@ -692,6 +735,10 @@ public extension CodexAppServer {
from: unarchivedThreads,
groupedBy: groupedBy
)
worktreeGroups = Self.groups(
from: unarchivedThreads,
groupedBy: .repository
)
}

private func recordSelection(threadID: String) {
Expand Down Expand Up @@ -719,6 +766,15 @@ public extension CodexAppServer {
return uniquePaths.isEmpty ? nil : uniquePaths
}

private func sortedVisibleThreads(includeArchived: Bool) -> [ThreadSnapshot] {
let threads = includeArchived ? allThreads : allThreads.filter { !$0.isArchived }
return Self.sort(
threads,
by: sortedBy,
selectionOrderByThreadID: selectionOrderByThreadID
)
}

private static func sort(
_ threads: [ThreadSnapshot],
by sortedBy: SortedBy,
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ extension CodexAppServer {
public let source: ThreadSource
public let status: ThreadStatus
public let updatedAt: Int

/// Codex-reported cwd plus optional Git facts for this thread.
public var worktree: CodexWorkspace.WorktreeSnapshot {
projectInfo.worktree
}
}

/// Request used to read a stored thread snapshot.
Expand Down
72 changes: 66 additions & 6 deletions Sources/SwiftASB/Public/CodexWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,46 @@ public enum CodexWorkspace {
public let displayName: String
public let currentDirectoryPath: String
public let repository: RepositoryInfo?
public let worktree: WorktreeSnapshot

/// Creates project identity from app-server-owned cwd and optional Git metadata.
public init(
currentDirectoryPath: String,
repository: RepositoryInfo? = nil
) {
let worktree = WorktreeSnapshot(
currentDirectoryPath: currentDirectoryPath,
repository: repository
)
self.id = worktree.id
self.identitySource = worktree.identitySource
self.displayName = worktree.displayName
self.currentDirectoryPath = worktree.currentDirectoryPath
self.repository = worktree.repository
self.worktree = worktree
}
}

/// Codex-reported workspace plus optional Git facts for one thread worktree.
///
/// This value is intentionally a snapshot of app-server payloads. It does
/// not infer a repository root, run Git commands, or inspect local disk.
public struct WorktreeSnapshot: Sendable, Equatable, Identifiable {
public let id: String
public let identitySource: ProjectInfo.IdentitySource
public let displayName: String
public let currentDirectoryPath: String
public let repository: RepositoryInfo?

/// Creates a worktree snapshot from an app-server cwd and optional Git facts.
public init(
currentDirectoryPath: String,
repository: RepositoryInfo? = nil
) {
self.currentDirectoryPath = currentDirectoryPath
self.repository = repository
self.repository = repository?.normalized

if let originURL = repository?.originURL, !originURL.isEmpty {
if let originURL = self.repository?.originURL, !originURL.isEmpty {
self.id = originURL
self.identitySource = .gitOrigin
Comment on lines +185 to 187
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish worktrees by cwd instead of repository origin

WorktreeSnapshot.id is set to originURL whenever Git metadata exists, but Library.threads(inWorktreeID:) and worktreeGroups key off that ID, so two different checkouts/worktrees of the same repository collapse into one logical “worktree.” In that scenario, callers cannot target a single worktree (they always get all threads for that origin), which defeats the new worktree-specific APIs and can mix branch-specific UI/state across separate directories.

Useful? React with 👍 / 👎.

self.displayName = Self.displayName(forGitOriginURL: originURL)
Expand All @@ -163,6 +193,11 @@ public enum CodexWorkspace {
}
}

/// True when the app-server reported any Git metadata for this worktree.
public var hasRepositoryFacts: Bool {
repository?.hasFacts == true
}

private static func displayName(forGitOriginURL originURL: String) -> String {
guard let url = URL(string: originURL),
let host = url.host,
Expand All @@ -189,14 +224,37 @@ public enum CodexWorkspace {
branch: String? = nil,
sha: String? = nil
) {
self.originURL = originURL
self.branch = branch
self.sha = sha
self.originURL = Self.normalizedFact(originURL)
self.branch = Self.normalizedFact(branch)
self.sha = Self.normalizedFact(sha)
}

/// True when Codex reported at least one Git fact for this thread.
public var hasFacts: Bool {
!isEmpty
}

/// Short display form for the reported commit SHA.
public var shortSHA: String? {
guard let sha, !sha.isEmpty else { return nil }
return String(sha.prefix(12))
}

internal var isEmpty: Bool {
originURL == nil && branch == nil && sha == nil
}

internal var normalized: Self? {
isEmpty ? nil : self
}

private static func normalizedFact(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
return nil
}
return trimmed
}
}

/// Thread-session workspace snapshot built from app-server-owned facts.
Expand All @@ -207,6 +265,7 @@ public enum CodexWorkspace {
public let permissionProfile: PermissionProfile?
public let projectInfo: ProjectInfo
public let sandboxPolicy: CodexAppServer.SandboxPolicy
public let worktree: WorktreeSnapshot
}
}

Expand Down Expand Up @@ -388,7 +447,8 @@ extension CodexWorkspace.SessionSnapshot {
instructionSources: session.instructionSources,
permissionProfile: session.permissionProfile,
projectInfo: session.thread.projectInfo,
sandboxPolicy: session.sandboxPolicy
sandboxPolicy: session.sandboxPolicy,
worktree: session.thread.projectInfo.worktree
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ 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, ``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.
`CodexAppServer.Library` is the app-wide companion for launchers, sidebars, and project browsers. It publishes value snapshots for unarchived threads, archived threads, cwd groups, stable worktree groups, ``CodexWorkspace/ProjectInfo`` values for thread and repository-group identity, ``CodexWorkspace/WorktreeSnapshot`` values for Codex-reported cwd plus optional Git facts, 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.

Use ``CodexAppServer/Library/worktreeGroups`` when a sidebar needs repository/workspace sections independent of the current visible grouping mode. Use ``CodexAppServer/Library/threads(inWorktreeID:includeArchived:)`` or ``CodexAppServer/Library/threads(inRepositoryOriginURL:includeArchived:)`` when a project browser needs the sorted threads for one Codex-reported worktree or Git origin without reading local disk.

Use ``CodexAppServer/Library/refreshAppSnapshots()`` when the same app-wide UI needs model capabilities, MCP server status, and hook diagnostics. Library derives hook `cwd` requests from its stored thread snapshots unless configuration provides explicit hook current-directory paths.

Recent companions keep caller-owned UI inputs mutable. For example, views can update selected file or command identifiers and visible item identifiers. SwiftASB uses that information to protect visible or selected payloads while slimming older low-value entries when the resident cache exceeds its budget.
Expand Down
Loading