import Foundation import Observation import SwiftData let taskTabs: [TaskTab] = [.transcript, .changes, .files, .terminal, .inbox, .reviews, .workflow, .project] private let historyPage = 1000 private let leadPage = 50 private let routeHash = Re(#"^#/(task|project)/(\w+)(?:/(\w+))?$"#) struct TaskState { enum Kind { case answer, merging, held, error, working, idle } let kind: Kind let label: String let title: String } private let workingTitles: [ByreTask.StatePayload: String] = [ .running: "The agent is mid-turn or its subagents are running", .reviewPending: "A review of the turn's work is about to start", .reviewing: "A review is running", .discarding: "Discarding", ] @MainActor @Observable final class Store { let api: API var me: User? var projects: [Project] = [] var tasks: [String: [ByreTask]] = [:] var sessions: [String: [AgentSession]] = [:] var inbox: [InboxItem] = [] private(set) var selectedTaskId: String? var pinned: Int? var reviewed: Int? var connected = false private(set) var focus = Focus(tabs: [.transcript], tab: .transcript) var leadSessions: [AgentSession] = [] let leadFeed: Feed var jump: (taskId: String, seq: Int64)? var composerFocus: String? var summaryReads: [String: Int64] = [:] private(set) var leadMore = false var todoBusy: [String: String] = [:] private(set) var drafts: Drafts? let images = ImageDrafts() let events = Listeners() let paneFocus: PaneFocus private var taskFeeds: [String: Feed] = [:] @ObservationIgnored private var historyLoads: Set = [] @ObservationIgnored private var requestSeqs: [String: Int] = [:] @ObservationIgnored private var globalStream: EventStream? @ObservationIgnored private var leadStream: EventStream? @ObservationIgnored private var stopped = false init(api: API) { self.api = api leadFeed = Feed(shown: { true }) paneFocus = PaneFocus(api: api) } func stop() async { stopped = true globalStream?.stop() leadStream?.stop() drafts?.flush() await paneFocus.leave() } func loadDrafts() { drafts = Drafts(origin: api.origin, login: me!.login) drafts!.prune(self) } var todoStarting: Set { Set(todoBusy.keys) } func startTodo(_ projectId: String, _ title: String, discuss: Bool, _ overlays: Overlays) { let key = "\(projectId)\n\(title)" if todoBusy[key] != nil { return } todoBusy[key] = discuss ? "discuss" : "start" Task { do { _ = try await api.client.startTodoItem(path: .init(projectId: projectId), body: .json(.init(title: title, model: models[0], discuss: discuss))).created } catch { overlays.showAlert(errorText(error)) } todoBusy[key] = nil } } func startSuggestion(_ projectId: String, _ title: String, brief: String, _ overlays: Overlays) { let key = "\(projectId)\n\(title)" if todoBusy[key] != nil { return } todoBusy[key] = "start" Task { do { _ = try await api.client.createTask(path: .init(projectId: projectId), body: .json(.init(title: title, prompt: brief, model: models[0]))).created } catch { overlays.showAlert(errorText(error)) } todoBusy[key] = nil } } func discussSuggestion(_ projectId: String, _ title: String, brief: String, _ overlays: Overlays) { let key = "\(projectId)\n\(title)" if todoBusy[key] != nil { return } todoBusy[key] = "discuss" let lead = currentLead() let live = lead != nil && lead!.state != .completed Task { do { _ = try await api.client.messageLead(body: .json(.init(text: "Let's discuss your suggestion \"\(title)\" before starting it:\n\n\(brief)", model: live ? nil : models[0]))).accepted } catch { overlays.showAlert(errorText(error)) } todoBusy[key] = nil } } var transcript: [ByreEvent] { selectedFeed?.events ?? [] } var selectedFeed: Feed? { selectedTaskId.flatMap { taskFeeds[$0] } } func activeTasks() -> [(project: Project, task: ByreTask)] { projects.flatMap { project in (tasks[project.id] ?? []).filter { $0.status == .active }.map { (project, $0) } } } func findTask(_ id: String) -> ByreTask? { for list in tasks.values { if let task = list.first(where: { $0.id == id }) { return task } } return nil } func currentSession(_ taskId: String) -> AgentSession? { sessions[taskId]?.last(where: { $0.state != .completed }) ?? sessions[taskId]?.last } func taskState(_ task: ByreTask) -> TaskState { let open = inbox.filter { $0.taskId == task.id } if open.contains(where: { $0.kind == .question }) { return TaskState(kind: .answer, label: "Needs your answer", title: "The agent asked a question in the inbox") } if open.contains(where: { $0.kind == .clientRun }) { return TaskState(kind: .answer, label: "Ready to run", title: "The agent asked to run something on your Mac; see the inbox") } if task.merging { return TaskState(kind: .merging, label: "Merging", title: "The merge turn is running") } if task.mergingIntoParent { return TaskState(kind: .merging, label: "Merging", title: "Waiting for the parent task's agent to merge it") } if task.mergeQueuedAt != nil { return TaskState(kind: .held, label: "Merge held", title: "Waiting for the merge in progress in this project to finish") } if task.mergeAfterReview { return TaskState(kind: .held, label: "Merge after review", title: "Merges when the running review finishes with no findings") } if !open.isEmpty { return TaskState(kind: .error, label: "Error", title: "The agent's session failed or hit a rate limit; see the inbox") } if let working = workingTitles[task.state] { return TaskState(kind: .working, label: "Working", title: working) } return TaskState(kind: .idle, label: "Idle", title: "Waiting for a message") } func currentLead() -> AgentSession? { leadSessions.last(where: { $0.state != .completed }) ?? leadSessions.last } func snapshots() -> [Snapshot] { transcript.filter { $0.kind == "snapshot.created" }.map { try! decodePayload($0.payload, as: Snapshot.self) } } func pinSnapshot(_ n: Int?) { pinned = n } func pickReview(_ n: Int) { reviewed = n } func showReview(_ n: Int) { pickReview(n) focusTab(.reviews) } func loadMe() async throws { me = try await api.client.getMe().ok.body.json } func loadProjects() async throws { projects = try await api.client.listProjects().ok.body.json try await each(projects.map(\.id)) { id in try await self.loadTasks(id) } } private func claimRequest(_ key: String) -> () -> Bool { let seq = (requestSeqs[key] ?? 0) + 1 requestSeqs[key] = seq return { [weak self] in self?.requestSeqs[key] == seq } } func loadTasks(_ projectId: String) async throws { let latest = claimRequest("tasks:\(projectId)") let list = try await api.client.listTasks(path: .init(projectId: projectId)).ok.body.json if !latest() { return } tasks[projectId] = list for t in list where t.status == .active { _ = taskFeed(t.id) } dropClosedFeeds() drafts?.prune(self) try await each(list.filter { $0.status == .active }.map(\.id)) { id in try await self.loadSessions(id) } } func loadSessions(_ taskId: String) async throws { let latest = claimRequest("sessions:\(taskId)") let list = try await api.client.listSessions(path: .init(taskId: taskId)).ok.body.json if !latest() { return } sessions[taskId] = list } func loadInbox() async throws { let latest = claimRequest("inbox") let items = try await api.client.listInbox().ok.body.json if !latest() { return } inbox = items drafts?.prune(self) } func loadLeadSessions() async throws { leadSessions = try await api.client.listLeadSessions().ok.body.json } func loadFocus() async throws { let latest = claimRequest("focus") let f = try await api.client.getFocus().ok.body.json if !latest() { return } applyFocus(f) } func applyFocus(_ f: Focus) { focus = f if f.taskId != selectedTaskId { selectTask(f.taskId) } } func focusedProject() -> Project? { projects.first { $0.id == focus.projectId } } func setFocus(taskId: String?? = .none, projectId: String?? = .none, tab: TaskTab? = nil) async throws { let task = taskId ?? focus.taskId let project = task != nil ? findTask(task!)?.projectId : (projectId ?? focus.projectId) var next = Focus(tabs: focus.tabs, tab: tab ?? focus.tab) if let task, !task.isEmpty { next.taskId = task } if let project, !project.isEmpty { next.projectId = project } let latest = claimRequest("focus") applyFocus(next) let f = try await api.client.setFocus(body: .json(next)).ok.body.json if !latest() { return } applyFocus(f) } func focusTask(_ taskId: String, _ tab: TaskTab = .transcript) { spawn("set focus") { try await self.setFocus(taskId: taskId, tab: tab) } } func focusProject(_ projectId: String) { spawn("set focus") { try await self.setFocus(taskId: .some(nil), projectId: projectId, tab: .project) } } func focusTab(_ tab: TaskTab) { spawn("set focus") { try await self.setFocus(tab: tab) } } func route(_ hash: String) { let m = routeHash.exec(hash) let kind = m?[1] ?? nil let id = m?[2] ?? nil let name = m?[3] ?? nil let tab = taskTabs.first { $0.rawValue == name } if kind == "task", let id { if id != focus.taskId || (tab != nil && tab != focus.tab) { spawn("set focus") { try await self.setFocus(taskId: id, tab: tab) } } return } let projectId = kind == "project" ? id : nil if focus.taskId != nil || projectId != focus.projectId { spawn("set focus") { try await self.setFocus(taskId: .some(nil), projectId: .some(projectId)) } } } private func loadHistory(_ taskId: String, _ feed: Feed) async throws { var got: [ByreEvent] = [] while true { let page = try await api.client.listTaskEvents(path: .init(taskId: taskId), query: .init(after: got.last?.seq ?? 0, limit: historyPage)).ok.body.json got += page if page.count < historyPage { break } } feed.load(got) } private func taskFeed(_ taskId: String) -> Feed { let feed = taskFeeds[taskId] ?? Feed(shown: { [weak self] in self?.selectedTaskId == taskId }) taskFeeds[taskId] = feed let key = ObjectIdentifier(feed) if feed.ready || historyLoads.contains(key) { return feed } historyLoads.insert(key) Task { do { try await loadHistory(taskId, feed) } catch { log.error("load task history \(taskId, privacy: .public): \(errorText(error), privacy: .public)") } historyLoads.remove(key) } return feed } private func dropClosedFeeds() { for taskId in taskFeeds.keys { if taskId != selectedTaskId, let task = findTask(taskId), task.status != .active { taskFeeds[taskId] = nil } } } func transcriptLoading() -> Bool { selectedTaskId != nil && selectedFeed?.ready != true } func leadLoading() -> Bool { !leadFeed.ready } func lastVisits() -> [String: Double] { fetchRecords(FetchDescriptor()).reduce(into: [:]) { visits, v in visits[v.taskId] = max(visits[v.taskId] ?? 0, v.at) } } private func markVisited(_ taskId: String?) { guard let taskId else { return } let at = Date().timeIntervalSince1970 * 1000 let records = fetchRecords(FetchDescriptor(predicate: #Predicate { $0.taskId == taskId })) if records.isEmpty { persistence.insert(VisitRecord(taskId: taskId, at: at)) } else { records[0].at = at for record in records.dropFirst() { persistence.delete(record) } } saveRecords() } func loadSummaryReads() async throws { summaryReads = try await api.client.getSummaryReads().ok.body.json.additionalProperties } func unreadSummary(_ task: ByreTask) -> Bool { task.workflow == nil && task.summarySeq > (summaryReads[task.id] ?? 0) } private func noteSummaryRead(_ taskId: String, _ seq: Int64) -> Bool { if (summaryReads[taskId] ?? 0) >= seq { return false } summaryReads[taskId] = seq return true } func markSummaryRead(_ taskId: String, _ seq: Int64) { if !noteSummaryRead(taskId, seq) { return } spawn("mark summary read") { _ = try await self.api.client.markSummaryRead(path: .init(taskId: taskId), body: .json(.init(seq: seq))).noContent } } func openSummary(_ task: ByreTask) { jump = (task.id, task.summarySeq) focusTask(task.id, .transcript) } func selectTask(_ taskId: String?) { markVisited(selectedTaskId) markVisited(taskId) selectedTaskId = taskId if let taskId { _ = taskFeed(taskId) } pinned = nil reviewed = nil dropClosedFeeds() } private func fetchLeadTail() async -> EventPage? { while !stopped { do { return try await api.client.listLeadEvents(query: .init(turns: leadPage)).ok.body.json } catch { log.error("load lead history: \(errorText(error), privacy: .public)") try? await Task.sleep(for: .seconds(2)) } } return nil } func startLeadStream() async { guard let page = await fetchLeadTail(), !stopped else { return } leadMore = page.more leadStream = EventStream(api: api, after: page.events.last?.seq ?? 0, lead: true, onEvent: { [weak self] ev in self?.leadFeed.push(ev) }, onState: { _ in }, onReady: { [weak self] in self?.leadFeed.load(page.events) }) } func startGlobalStream(after: Int64) { globalStream = EventStream(api: api, after: after, lead: false, onEvent: { [weak self] ev in self?.handleEvent(ev) }, onState: { [weak self] up in self?.connected = up if up { self?.paneFocus.reconnected() } }, onReady: {}) } func leadHasOlder() -> Bool { leadFeed.ready && leadMore } func loadOlderLead() async throws -> [ByreEvent] { guard let first = leadFeed.events.first else { return [] } let page = try await api.client.listLeadEvents(query: .init(before: first.seq, turns: leadPage)).ok.body.json leadMore = page.more leadFeed.prepend(page.events) return page.events } func handleEvent(_ ev: ByreEvent) { if let taskId = ev.taskId { taskFeeds[taskId]?.push(ev) } if ev.seq == 0 { events.send(ev) return } switch ev.kind { case "user.updated": if ev.payload.value["login"] as? String == me?.login { spawn("load me") { try await self.loadMe() } } case "project.created": spawn("load projects") { try await self.loadProjects() } case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state": if let projectId = ev.projectId { spawn("load tasks") { try await self.loadTasks(projectId) } } case "session.created", "session.state", "session.subagents", "session.model": if ev.owner != nil { spawn("load lead sessions") { try await self.loadLeadSessions() } } else if let taskId = ev.taskId { spawn("load sessions") { try await self.loadSessions(taskId) } } case "inbox.created": notifyInbox(self, try! decodePayload(ev.payload, as: InboxItem.self)) spawn("load inbox") { try await self.loadInbox() } case "inbox.updated": spawn("load inbox") { try await self.loadInbox() } case "turn.ended": notifyTurn(self, ev) if ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId { updateTask(taskId) { task in if ev.seq > task.summarySeq { task.summarySeq = ev.seq } } } case "summary.read": if let taskId = ev.payload.value["task_id"] as? String, let seq = ev.payload.value["seq"] as? Int { _ = noteSummaryRead(taskId, Int64(seq)) } case "focus.updated": if ev.actor != me?.login { _ = claimRequest("focus") applyFocus(try! decodePayload(ev.payload, as: Focus.self)) } default: break } events.send(ev) } private func updateTask(_ id: String, _ change: (inout ByreTask) -> Void) { for (projectId, list) in tasks { if let i = list.firstIndex(where: { $0.id == id }) { change(&tasks[projectId]![i]) } } } func spawn(_ what: String, _ op: @escaping @MainActor () async throws -> Void) { Task { do { try await op() } catch { log.error("\(what, privacy: .public): \(errorText(error), privacy: .public)") } } } private func each(_ ids: [String], _ op: @escaping @Sendable @MainActor (String) async throws -> Void) async throws { try await withThrowingTaskGroup(of: Void.self) { group in for id in ids { group.addTask { try await op(id) } } try await group.waitForAll() } } }