native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
7.1 kB · 192 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193import Foundationimport ProwlCLIShared
struct AgentsRuntimeSnapshot { let repositoriesState: RepositoriesFeature.State let listSnapshot: ListRuntimeSnapshot /// Live detector results keyed by pane. Reducer entries intentionally skip /// raw-state-only changes to avoid invalidating the sidebar, so CLI snapshots /// must source state and reason from terminal state instead. let screenDetectionsBySurfaceID: [UUID: AgentScreenDetection] let decisionsBySurfaceID: [UUID: AgentStateDecision] let signalsBySurfaceID: [UUID: AgentSignalsPayload]
init( repositoriesState: RepositoriesFeature.State, listSnapshot: ListRuntimeSnapshot, screenDetectionsBySurfaceID: [UUID: AgentScreenDetection], signalsBySurfaceID: [UUID: AgentSignalsPayload] = [:], decisionsBySurfaceID: [UUID: AgentStateDecision] = [:] ) { self.repositoriesState = repositoriesState self.listSnapshot = listSnapshot self.screenDetectionsBySurfaceID = screenDetectionsBySurfaceID self.signalsBySurfaceID = signalsBySurfaceID self.decisionsBySurfaceID = decisionsBySurfaceID }}
final class AgentsCommandHandler: CommandHandler { typealias SnapshotProvider = @MainActor () throws -> AgentsRuntimeSnapshot
private struct TerminalAgentContext { let worktree: ListRuntimeSnapshot.Worktree let tab: ListRuntimeSnapshot.Tab let pane: ListRuntimeSnapshot.Pane let focused: Bool }
private let snapshotProvider: SnapshotProvider private let dateFormatter: ISO8601DateFormatter
init(snapshotProvider: @escaping SnapshotProvider) { self.snapshotProvider = snapshotProvider self.dateFormatter = ISO8601DateFormatter() self.dateFormatter.formatOptions = [.withInternetDateTime] self.dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) }
// swiftlint:disable:next async_without_await func handle(envelope: CommandEnvelope) async -> CommandResponse { do { let snapshot = try snapshotProvider() let payload = makePayload(from: snapshot, includeHandles: envelope.output == .text) return try CommandResponse( ok: true, command: "agents", schemaVersion: "prowl.cli.agents.v1", data: RawJSON(encoding: payload) ) } catch { return CommandResponse( ok: false, command: "agents", schemaVersion: "prowl.cli.agents.v1", error: CommandError( code: CLIErrorCode.agentsFailed, message: "Failed to list agents." ) ) } }
private func makePayload( from snapshot: AgentsRuntimeSnapshot, includeHandles: Bool ) -> AgentsCommandPayload { let repositoriesState = snapshot.repositoriesState let metadata = SidebarListView.activeAgentWorktreeMetadata( repositories: repositoriesState.repositories, customTitles: repositoriesState.repositoryCustomTitles ) let terminalContexts = makeTerminalContexts(from: snapshot.listSnapshot) let worktreeContexts = Dictionary( ListRuntimeSnapshotBuilder .orderedWorktreeContexts(from: repositoriesState) .map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first } )
let agents = repositoriesState.activeAgents.entries.compactMap { entry -> AgentsCommandAgent? in guard let terminalContext = terminalContexts[entry.surfaceID] else { return nil } let display = SidebarListView.activeAgentRowDisplay( for: entry, repositories: repositoriesState.repositories, metadata: metadata ) let projectPath = projectPath( for: entry, repositoriesState: repositoriesState, worktreeContexts: worktreeContexts)
let screenDetection = snapshot.screenDetectionsBySurfaceID[entry.surfaceID] let decision = snapshot.decisionsBySurfaceID[entry.surfaceID] ?? entry.stateDecision return AgentsCommandAgent( id: entry.surfaceID.uuidString, type: entry.agent.rawValue, name: entry.displayName, status: AgentsCommandStatus(rawValue: entry.displayState.rawValue) ?? .idle, rawState: (screenDetection?.state ?? entry.rawState).rawValue, detectionReason: decision?.reason.identifier ?? screenDetection?.reason.identifier, screenReason: screenDetection?.reason.identifier ?? decision?.screenReason?.identifier, lastChangedAt: dateFormatter.string(from: entry.lastChangedAt), project: AgentsCommandProject( name: display.repositoryName, branch: display.branchName, path: projectPath ), worktree: AgentsCommandWorktree( id: terminalContext.worktree.id, name: terminalContext.worktree.name, path: terminalContext.worktree.path, rootPath: terminalContext.worktree.rootPath, kind: terminalContext.worktree.kind.rawValue ), tab: AgentsCommandTab( id: terminalContext.tab.id.uuidString, title: terminalContext.tab.title, selected: terminalContext.tab.selected ), pane: AgentsCommandPane( id: terminalContext.pane.id.uuidString, handle: includeHandles ? terminalContext.pane.handle : nil, index: entry.paneIndex, title: terminalContext.pane.title, cwd: terminalContext.pane.cwd, focused: terminalContext.focused ), session: entry.session.map { AgentsCommandSession( id: $0.id, path: $0.transcriptPath?.path, confidence: $0.confidence.rawValue, source: $0.source.rawValue ) }, signals: snapshot.signalsBySurfaceID[entry.surfaceID] ?? .empty ) }
return AgentsCommandPayload(count: agents.count, agents: agents) }
private func makeTerminalContexts(from snapshot: ListRuntimeSnapshot) -> [UUID: TerminalAgentContext] { var contexts: [UUID: TerminalAgentContext] = [:] for worktree in snapshot.worktrees { for tab in worktree.tabs { for pane in tab.panes { contexts[pane.id] = TerminalAgentContext( worktree: worktree, tab: tab, pane: pane, focused: worktree.id == snapshot.focusedWorktreeID && tab.selected && tab.focusedPaneID == pane.id ) } } } return contexts }
private func projectPath( for entry: ActiveAgentEntry, repositoriesState: RepositoriesFeature.State, worktreeContexts: [Worktree.ID: ListRuntimeSnapshotBuilder.WorktreeContext] ) -> String { if let workingDirectory = entry.workingDirectory, let key = SidebarListView.resolveWorktreeID( forWorkingDirectory: workingDirectory, in: repositoriesState.repositories ), let context = worktreeContexts[key] { return context.path } if let workingDirectory = entry.workingDirectory { return workingDirectory.path(percentEncoded: false) } return worktreeContexts[entry.worktreeID]?.path ?? entry.worktreeName }}