Simplify codebase per audit of 48e3457

This commit is contained in:
Ian Gulliver
2026-10-23 17:10:23 -07:00
parent c651fe472d
commit f1e13bd9a8
18 changed files with 279 additions and 418 deletions
+7 -2
View File
@@ -148,7 +148,9 @@ paths:
/api/me/lead/notes:
get:
operationId: listLeadNotes
description: What the calling user's lead agent has noted about how they work, oldest first. The lead keeps them with its remember and forget tools.
description: >
What the calling user's lead agent has noted about how they work, oldest first. The lead keeps them with its remember and forget tools.
lead.note_saved (payload: the LeadNote) and lead.note_deleted (payload: its id) events tell the user's clients of changes.
responses:
"200":
description: The notes.
@@ -173,7 +175,7 @@ paths:
operationId: listSavedCommands
description: >
The calling user's saved commands on this server, by name. Each starts a terminal in a task that runs it.
command.saved and command.deleted events tell the user's clients of changes.
command.saved (payload: the SavedCommand) and command.deleted (payload: its id) events tell the user's clients of changes.
responses:
"200":
description: The saved commands.
@@ -1426,6 +1428,9 @@ paths:
/api/workflows:
get:
operationId: listWorkflows
description: >
Every saved workflow. workflow.updated (payload: the WorkflowSummary) and workflow.deleted (payload: its name) events
tell clients of changes; a rename sends workflow.deleted for the old name, then workflow.updated for the new one.
responses:
"200":
description: Every workflow's name and description, by name.
+14 -13
View File
@@ -22,6 +22,7 @@ import (
const (
defaultReadLimit = 2000
clientByre = "/Applications/Byre.app/Contents/Helpers/byre"
clientProperty = `"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."}`
)
func (m *Manager) clientShellTools() []mcp.Tool {
@@ -46,7 +47,7 @@ func (m *Manager) clientShellTools() []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"command": {"type": "string"},
"timeout_ms": {"type": "integer", "description": "Kill the command after this long (at most 600000)."}
},
@@ -66,7 +67,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file or directory, relative to your worktree, absolute, or starting with ~/."},
"dest": {"type": "string", "description": "Its new path on the client, absolute or starting with ~/, e.g. ~/Applications/Byre.app."}
},
@@ -84,7 +85,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file or directory, relative to your worktree, absolute, or starting with ~/."}
},
"required": ["client", "path"]
@@ -103,7 +104,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file, relative to your worktree, absolute, or starting with ~/."},
"url": {"type": "string", "description": "Instead of path: an http or https address to open in a new tab."}
},
@@ -125,7 +126,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"op": {"type": "string", "enum": ["tabs", "navigate", "js", "capture"]},
"tab": {"type": "integer", "description": "The tab's number from tabs (default: the tab the Page view shows)."},
"url": {"type": "string", "description": "For navigate."},
@@ -145,7 +146,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file or directory, relative to your worktree, absolute, or starting with ~/."}
},
"required": ["client", "path"]
@@ -161,7 +162,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file or directory on the client, absolute or starting with ~/."},
"dest": {"type": "string", "description": "Its new path, relative to your worktree, absolute, or starting with ~/; parent directories are created."}
},
@@ -178,7 +179,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"offset": {"type": "integer", "description": "The line number to start from (default 1)."},
"limit": {"type": "integer", "description": "The most lines to return (default 2000)."}
@@ -193,7 +194,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"content": {"type": "string"}
},
@@ -208,7 +209,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"old_string": {"type": "string"},
"new_string": {"type": "string"}
@@ -223,7 +224,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"pattern": {"type": "string"},
"path": {"type": "string", "description": "The file or directory to search, absolute or starting with ~/."},
"glob": {"type": "string", "description": "Search only files whose names match this glob, e.g. *.go."},
@@ -240,7 +241,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"path": {"type": "string", "description": "The directory, absolute or starting with ~/."}
},
"required": ["client", "path"]
@@ -257,7 +258,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
` + clientProperty + `,
"tool": {"type": "string", "enum": ["screen_list", "capture", "ax_tree", "act", "browser", "chrome"]},
"arguments": {"type": "object", "description": "The tool's arguments."}
},
+1 -1
View File
@@ -71,7 +71,7 @@ func (m *Manager) takeBounce(ctx context.Context, l *live, final string) (bool,
if !asksQuestion(final) || m.isPaused(l.id) {
return false, nil
}
first, err := m.store.FirstPrompt(ctx, l.taskID)
first, err := m.store.FirstTaskMessage(ctx, l.taskID)
if err != nil {
return false, err
}
+111 -109
View File
@@ -5139,115 +5139,117 @@ var swaggerSpec = []string{
"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",
"8QGDu+K+fmvKyY8f//8BAA==",
"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",
"atmPH///AQA=",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+2 -14
View File
@@ -104,11 +104,7 @@ func (s *Store) StartDiscardTurn(ctx context.Context, taskID, sessionID, actor s
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(api.AgentSessionStateRunning), sessionID)
if err != nil {
return err
}
return emit(sessionEvent("session.state", projectID, taskID, "", sessionID, actor, map[string]any{"state": string(api.AgentSessionStateRunning)}))
return setSessionStateTx(ctx, tx, emit, sessionID, api.AgentSessionStateRunning, actor, nil, projectID, taskID, "")
})
}
@@ -152,15 +148,7 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
}
handsOff := continues && role == string(api.Implementer) && state == DiscardNone
if !handsOff {
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(api.AgentSessionStateIdle), sessionID)
if err != nil {
return err
}
if payload == nil {
payload = map[string]any{}
}
payload["state"] = string(api.AgentSessionStateIdle)
err = emit(sessionEvent("session.state", projectID, taskID, owner, sessionID, actor, payload))
err = setSessionStateTx(ctx, tx, emit, sessionID, api.AgentSessionStateIdle, actor, payload, projectID, taskID, owner)
if err != nil {
return err
}
+4 -11
View File
@@ -995,15 +995,6 @@ func (s *Store) TranscriptEvents(ctx context.Context, taskID string, after int64
return s.queryEvents(ctx, query+" ORDER BY seq LIMIT ?", append(args, limit)...)
}
func (s *Store) FirstPrompt(ctx context.Context, taskID string) (string, error) {
text := ""
err := s.db.QueryRowContext(ctx, `
SELECT coalesce(json_extract(payload, '$.text'), '') FROM events
WHERE task_id = ? AND kind = 'user.message' ORDER BY seq LIMIT 1
`, taskID).Scan(&text)
return text, err
}
func (s *Store) LastMessage(ctx context.Context, sessionID string) (api.Event, bool, error) {
evs, err := s.queryEvents(ctx, `
SELECT `+eventColumns+`
@@ -1324,12 +1315,14 @@ func transitionSessionTx(ctx context.Context, tx *sql.Tx, emit emitFunc, id stri
if state == api.AgentSessionStateRunning && role == string(api.Implementer) && landing != "" && landing != id {
return fmt.Errorf("%w: held while task %s merges and continues", ErrStateChanged, taskID)
}
return setSessionStateTx(ctx, tx, emit, id, state, actor, payload, projectID, taskID, owner)
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(state), id)
func setSessionStateTx(ctx context.Context, tx *sql.Tx, emit emitFunc, id string, state api.AgentSessionState, actor string, payload map[string]any, projectID, taskID, owner string) error {
_, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(state), id)
if err != nil {
return err
}
if payload == nil {
payload = map[string]any{}
}
+1 -1
View File
@@ -88,7 +88,7 @@ func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.Work
if err != nil {
return err
}
return emit(api.Event{Kind: "workflow.updated", Actor: actor, Payload: map[string]any{"name": name}})
return emit(api.Event{Kind: "workflow.updated", Actor: actor, Payload: ToPayload(api.WorkflowSummary{Name: name, Description: def.Description})})
})
}
+1 -12
View File
@@ -506,18 +506,7 @@ final class ChatModel {
store.focusTask(task.id)
return
}
if team {
if images != nil {
throw ApiError(status: 0, message: "The board takes text only; drop the images to post.")
}
_ = try await api.postToBoard(path: .init(taskId: id ?? ""), body: .json(.init(text: text))).noContent
return
}
if let sessionId {
_ = try await api.sendMessage(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: model, effort: effort, sendId: sendID))).accepted
return
}
_ = try await api.createSession(path: .init(taskId: id ?? ""), body: .json(.init(prompt: text, model: firstModel, effort: firstEffort, images: images, sendId: sendID))).created
try await store.messageTask(id ?? "", text, images: images, model: model, effort: effort, firstModel: firstModel, firstEffort: firstEffort, sendID: sendID)
}
}
+22 -58
View File
@@ -28,79 +28,43 @@ struct NotesView: View {
private struct LeadNotes: View {
@Environment(Store.self) private var store
@ViewState private var list: [LeadNote]?
@ViewState private var failure: String?
@ViewState private var loads = 0
@ViewState private var project = notesProject
@ViewState private var off: (() -> Void)?
var body: some View {
let list = store.leadNotes
VStack(alignment: .leading, spacing: 0) {
if let list {
if list.isEmpty {
hint("Your lead hasn't noted anything about how you work yet.")
} else {
let groups = [("", "Every project")] + store.projects.map { ($0.id, $0.name) }
let current = groups.contains { $0.0 == project } ? project : ""
let shown = list.filter { ($0.projectId ?? "") == current }
let label = { (g: (String, String)) in "\(g.1) (\(list.filter { ($0.projectId ?? "") == g.0 }.count))" }
DropdownMenu(label: label(groups.first { $0.0 == current } ?? groups[0]), boxed: true) {
ForEach(groups, id: \.0) { g in
Button(label(g)) {
project = g.0
notesProject = g.0
}
}
}
.help("Which notes to show")
.padding(.bottom, 8)
if shown.isEmpty {
hint("No notes here yet.")
} else {
ForEach(shown, id: \.id) { note in
NoteRow(note: note)
if list.isEmpty {
hint("Your lead hasn't noted anything about how you work yet.")
} else {
let groups = [("", "Every project")] + store.projects.map { ($0.id, $0.name) }
let current = groups.contains { $0.0 == project } ? project : ""
let shown = list.filter { ($0.projectId ?? "") == current }
let label = { (g: (String, String)) in "\(g.1) (\(list.filter { ($0.projectId ?? "") == g.0 }.count))" }
DropdownMenu(label: label(groups.first { $0.0 == current } ?? groups[0]), boxed: true) {
ForEach(groups, id: \.0) { g in
Button(label(g)) {
project = g.0
notesProject = g.0
}
}
}
} else if let failure {
hint(failure)
.help("Which notes to show")
.padding(.bottom, 8)
if shown.isEmpty {
hint("No notes here yet.")
} else {
ForEach(shown, id: \.id) { note in
NoteRow(note: note)
}
}
}
}
.padding(.top, 8)
.onAppear {
off = store.events.add { ev in
if ev.kind == "lead.note_saved" || ev.kind == "lead.note_deleted" {
load()
}
}
load()
}
.onDisappear {
off?()
off = nil
loads += 1
}
}
private func hint(_ text: String) -> some View {
Text(text).foregroundStyle(Palette.muted)
}
private func load() {
loads += 1
let n = loads
Task {
do {
let loaded = try await store.api.client.listLeadNotes().ok.body.json
if n == loads {
list = loaded
}
} catch {
failure = errorText(error)
list = nil
}
}
}
}
private struct NoteRow: View {
+2 -10
View File
@@ -108,24 +108,16 @@ private struct ProjectWorkflowMenu: View {
let audit: Bool
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var workflows: [String] = []
var body: some View {
let current = audit ? project.auditWorkflow : project.reviewWorkflow
DropdownMenu(label: current.isEmpty ? "Off" : current, size: 11, color: Palette.muted) {
Button("Off") { set("") }
ForEach(workflows, id: \.self) { name in
Button(name) { set(name) }
ForEach(store.workflows, id: \.name) { w in
Button(w.name) { set(w.name) }
}
}
.help(audit ? "The workflow byre runs in the background on \(project.defaultBranch)'s newest commit to check the whole codebase" : "The review workflow whose reviewers review every snapshot of this project's tasks")
.task {
do {
workflows = try await store.api.client.listWorkflows().ok.body.json.map(\.name)
} catch {
workflows = []
}
}
}
private func set(_ name: String) {
+1 -25
View File
@@ -4,8 +4,6 @@ import SwiftUI
struct QuickRunSetting: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var workflows: [WorkflowSummary] = []
@ViewState private var off: (() -> Void)?
var body: some View {
let current = store.me?.quickRun ?? .init(projectId: "", workflow: "")
@@ -14,7 +12,7 @@ struct QuickRunSetting: View {
Button("None") {
save(.init(projectId: "", workflow: ""))
}
ForEach(workflows, id: \.name) { w in
ForEach(store.workflows, id: \.name) { w in
Button(w.name) {
save(.init(projectId: current.projectId.isEmpty ? store.projects.first?.id ?? "" : current.projectId, workflow: w.name))
}
@@ -35,28 +33,6 @@ struct QuickRunSetting: View {
}
}
.padding(.top, 8)
.onAppear {
off = store.events.add { ev in
if ev.kind == "workflow.updated" || ev.kind == "workflow.deleted" {
load()
}
}
load()
}
.onDisappear {
off?()
off = nil
}
}
private func load() {
Task {
do {
workflows = try await store.api.client.listWorkflows().ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
private func save(_ q: Components.Schemas.QuickRun) {
+6 -11
View File
@@ -103,17 +103,16 @@ private final class ScheduleDraft {
var dayOfMonth: Int
var workflow: String
var prompt: String
var workflows: [String] = []
var failure = ""
init(_ sc: Schedule?) {
init(_ sc: Schedule?, workflows: [WorkflowSummary]) {
title = sc?.title ?? ""
every = sc?.every ?? .week
weekdays = Set(sc?.weekdays ?? [.mon])
hour = sc?.hour ?? 9
minute = sc?.minute ?? 0
dayOfMonth = sc.map { $0.dayOfMonth == 0 ? 1 : $0.dayOfMonth } ?? 1
workflow = sc?.workflow ?? ""
workflow = sc?.workflow ?? workflows.first?.name ?? ""
prompt = sc?.prompt ?? ""
}
@@ -137,13 +136,7 @@ private final class ScheduleDraft {
@MainActor
func editSchedule(_ project: Project, _ existing: Schedule?, _ store: Store, _ overlays: Overlays) async {
let draft = ScheduleDraft(existing)
Task {
draft.workflows = (try? await store.api.client.listWorkflows().ok.body.json.map(\.name)) ?? []
if draft.workflow.isEmpty, let first = draft.workflows.first {
draft.workflow = first
}
}
let draft = ScheduleDraft(existing, workflows: store.workflows)
while true {
let buttons = existing == nil
? [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Create", value: "save", primary: true)]
@@ -180,6 +173,7 @@ func editSchedule(_ project: Project, _ existing: Schedule?, _ store: Store, _ o
private struct ScheduleEditor: View {
@Bindable var draft: ScheduleDraft
let project: Project
@Environment(Store.self) private var store
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -214,7 +208,8 @@ private struct ScheduleEditor: View {
}
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text("Run workflow").foregroundStyle(Palette.muted)
let names = draft.workflow.isEmpty || draft.workflows.contains(draft.workflow) ? draft.workflows : [draft.workflow] + draft.workflows
let saved = store.workflows.map(\.name)
let names = draft.workflow.isEmpty || saved.contains(draft.workflow) ? saved : [draft.workflow] + saved
ChoiceMenu(value: $draft.workflow, options: names.map { ($0, $0) })
}
Text("The workflow's brief, given to every stage (optional).")
+1 -25
View File
@@ -289,15 +289,13 @@ private struct PrefsBox: View {
private struct SavedCommands: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var list: [SavedCommand] = []
@ViewState private var editing: String?
@ViewState private var name = ""
@ViewState private var command = ""
@ViewState private var off: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(list, id: \.id) { c in
ForEach(store.savedCommands, id: \.id) { c in
if editing == c.id {
form(c.id)
} else {
@@ -333,18 +331,6 @@ private struct SavedCommands: View {
}
}
.padding(.top, 8)
.onAppear {
off = store.events.add { ev in
if ev.kind == "command.saved" || ev.kind == "command.deleted" {
load()
}
}
load()
}
.onDisappear {
off?()
off = nil
}
}
private func form(_ id: String?) -> some View {
@@ -379,16 +365,6 @@ private struct SavedCommands: View {
.padding(.vertical, 6)
}
private func load() {
Task {
do {
list = try await store.api.client.listSavedCommands().ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
private func save(_ id: String?, _ valid: Bool) {
if !valid {
return
+4 -1
View File
@@ -248,7 +248,10 @@ final class Connection {
async let lead: Void = store.loadLeadSessions()
async let reads: Void = store.loadSummaryReads()
async let terminals: Void = store.loadTerminals()
_ = try await (me, projects, inbox, lead, reads, terminals)
async let workflows: Void = store.loadWorkflows()
async let notes: Void = store.loadLeadNotes()
async let commands: Void = store.loadSavedCommands()
_ = try await (me, projects, inbox, lead, reads, terminals, workflows, notes, commands)
try await store.loadFocus()
guard !stopped else {
await store.stop()
+95 -45
View File
@@ -66,6 +66,9 @@ final class Store {
var projects: [Project] = []
var tasks: [String: [ByreTask]] = [:]
var schedules: [String: [Schedule]] = [:]
var workflows: [WorkflowSummary] = []
var leadNotes: [LeadNote] = []
var savedCommands: [SavedCommand] = []
var sessions: [String: [AgentSession]] = [:]
var inbox: [InboxItem] = []
private(set) var terminals: [String: [TaskTerminal]] = [:]
@@ -105,7 +108,7 @@ final class Store {
@ObservationIgnored private var sessionReloads: Set<String> = []
@ObservationIgnored private var sessionRefetches: Set<String> = []
@ObservationIgnored private var taskRefetches: [String: Set<String>] = [:]
@ObservationIgnored private var terminalChanges = 0
@ObservationIgnored private var changeCounts: [String: Int] = [:]
@ObservationIgnored private var streamOpens = 0
@ObservationIgnored private var globalStream: EventStream?
@ObservationIgnored private var leadStream: EventStream?
@@ -341,27 +344,58 @@ final class Store {
}
func loadSchedules(_ projectId: String) async throws {
let latest = claimRequest("schedules:\(projectId)")
let list = try await api.client.listSchedules(path: .init(projectId: projectId)).ok.body.json
if !latest() {
try await fetch("schedules:\(projectId)", { try await api.client.listSchedules(path: .init(projectId: projectId)).ok.body.json }) { schedules[projectId] = $0 }
}
func loadWorkflows() async throws {
try await fetch("workflows", { try await api.client.listWorkflows().ok.body.json }) { workflows = $0 }
}
func loadLeadNotes() async throws {
try await fetch("leadNotes", { try await api.client.listLeadNotes().ok.body.json }) { leadNotes = $0 }
}
func loadSavedCommands() async throws {
try await fetch("savedCommands", { try await api.client.listSavedCommands().ok.body.json }) { savedCommands = $0 }
}
// A reload lands only when no newer request for the same key has started since, and reloads instead when a stream event changed the key's data while it ran.
private func fetch<T>(_ key: String, _ get: () async throws -> T, _ set: (T) -> Void) async throws {
let latest = claimRequest(key)
let seen = changeCounts[key, default: 0]
let value = try await get()
guard latest() else {
return
}
schedules[projectId] = list
if changeCounts[key, default: 0] != seen {
try await fetch(key, get, set)
return
}
set(value)
}
private func changed(_ key: String) {
changeCounts[key, default: 0] += 1
}
private func applySchedule(_ ev: ByreEvent) {
guard let projectId = ev.projectId else {
return
}
changed("schedules:\(projectId)")
var list = schedules[projectId] ?? []
if ev.kind == "schedule.saved" {
let sc = try! decodePayload(ev.payload, as: Schedule.self)
list.removeAll { $0.id == sc.id }
list.append(sc)
upsert(&list, try! decodePayload(ev.payload, as: Schedule.self), by: \.id) { ($0.nextRun, $0.id) < ($1.nextRun, $1.id) }
} else if let id = str(ev.payload.value, "id") {
list.removeAll { $0.id == id }
}
schedules[projectId] = list.sorted { ($0.nextRun, $0.id) < ($1.nextRun, $1.id) }
schedules[projectId] = list
}
private func upsert<T, K: Equatable>(_ list: inout [T], _ item: T, by key: (T) -> K, order: (T, T) -> Bool) {
list.removeAll { key($0) == key(item) }
list.append(item)
list.sort(by: order)
}
private func claimRequest(_ key: String) -> () -> Bool {
@@ -462,45 +496,33 @@ final class Store {
}
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
try await fetch("sessions:\(taskId)", { try await api.client.listSessions(path: .init(taskId: taskId)).ok.body.json }) { sessions[taskId] = $0 }
}
func messageTask(_ taskId: String, _ text: String, images ids: [String]? = nil) async throws {
let sessions = try await api.client.listSessions(path: .init(taskId: taskId)).ok.body.json
if let live = sessions.last(where: { $0.state != .completed }) {
_ = try await api.client.sendMessage(path: .init(sessionId: live.id), body: .json(.init(text: text, images: ids))).accepted
} else {
_ = try await api.client.createSession(path: .init(taskId: taskId), body: .json(.init(prompt: text, images: ids))).created
func messageTask(_ taskId: String, _ text: String, images: [String]? = nil, model: String? = nil, effort: String? = nil, firstModel: String? = nil, firstEffort: String? = nil, sendID: String? = nil) async throws {
if !team(taskId).isEmpty {
if images != nil {
throw ApiError(status: 0, message: "The board takes text only; drop the images to post.")
}
_ = try await api.client.postToBoard(path: .init(taskId: taskId), body: .json(.init(text: text))).noContent
return
}
if let live = currentSession(taskId), live.state != .completed {
_ = try await api.client.sendMessage(path: .init(sessionId: live.id), body: .json(.init(text: text, images: images, model: model, effort: effort, sendId: sendID))).accepted
return
}
_ = try await api.client.createSession(path: .init(taskId: taskId), body: .json(.init(prompt: text, model: firstModel, effort: firstEffort, images: images, sendId: sendID))).created
}
func loadInbox() async throws {
let latest = claimRequest("inbox")
let items = try await api.client.listInbox().ok.body.json
if !latest() {
return
try await fetch("inbox", { try await api.client.listInbox().ok.body.json }) {
inbox = $0
drafts?.prune(self)
}
inbox = items
drafts?.prune(self)
}
func loadTerminals() async throws {
let latest = claimRequest("terminals")
let changes = terminalChanges
let list = try await api.client.listTerminals().ok.body.json
if !latest() {
return
}
if changes != terminalChanges {
try await loadTerminals()
return
}
terminals = Dictionary(grouping: list.terminals, by: \.taskId)
try await fetch("terminals", { try await api.client.listTerminals().ok.body.json }) { terminals = Dictionary(grouping: $0.terminals, by: \.taskId) }
}
func loadLeadSessions() async throws {
@@ -508,12 +530,7 @@ final class Store {
}
func loadFocus() async throws {
let latest = claimRequest("focus")
let f = try await api.client.getFocus().ok.body.json
if !latest() {
return
}
applyFocus(f)
try await fetch("focus", { try await api.client.getFocus().ok.body.json }) { applyFocus($0) }
}
func applyFocus(_ f: Focus) {
@@ -802,6 +819,15 @@ final class Store {
spawn("load summary reads") {
try await self.loadSummaryReads()
}
spawn("load workflows") {
try await self.loadWorkflows()
}
spawn("load lead notes") {
try await self.loadLeadNotes()
}
spawn("load saved commands") {
try await self.loadSavedCommands()
}
for (_, task) in activeTasks() {
spawn("load sessions") {
try await self.loadSessions(task.id)
@@ -829,7 +855,7 @@ final class Store {
confirmSend(taskId, ev)
}
if ev.kind == "terminals.changed", let taskId = ev.taskId {
terminalChanges += 1
changed("terminals")
terminals[taskId] = try! decodePayload(ev.payload, as: TerminalList.self).terminals
}
if ev.seq == 0 {
@@ -850,6 +876,30 @@ final class Store {
}
case "schedule.saved", "schedule.deleted":
applySchedule(ev)
case "workflow.updated":
changed("workflows")
upsert(&workflows, try! decodePayload(ev.payload, as: WorkflowSummary.self), by: \.name) { $0.name < $1.name }
case "workflow.deleted":
changed("workflows")
if let name = str(ev.payload.value, "name") {
workflows.removeAll { $0.name == name }
}
case "lead.note_saved":
changed("leadNotes")
upsert(&leadNotes, try! decodePayload(ev.payload, as: LeadNote.self), by: \.id) { ($0.createdAt, $0.id) < ($1.createdAt, $1.id) }
case "lead.note_deleted":
changed("leadNotes")
if let id = str(ev.payload.value, "id") {
leadNotes.removeAll { $0.id == id }
}
case "command.saved":
changed("savedCommands")
upsert(&savedCommands, try! decodePayload(ev.payload, as: SavedCommand.self), by: \.id) { ($0.name, $0.id) < ($1.name, $1.id) }
case "command.deleted":
changed("savedCommands")
if let id = str(ev.payload.value, "id") {
savedCommands.removeAll { $0.id == id }
}
case "task.created", "task.title", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_canceled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created":
if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && (try? decodePayload(ev.payload, as: Snapshot.self))?.isCutPoint != true) {
clearTask(self, taskId)
+1 -9
View File
@@ -178,23 +178,15 @@ final class SwitcherModel {
#if os(macOS)
@ObservationIgnored var pending: NSEvent?
#endif
var workflows: [WorkflowSummary] = []
init(store: Store, overlays: Overlays, anchor: CGRect) {
self.store = store
self.overlays = overlays
self.anchor = anchor
Task {
do {
workflows = try await store.api.client.listWorkflows().ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
private func workflowMatch(_ title: String) -> (WorkflowSummary, String)? {
for w in workflows.sorted(by: { $0.name.count > $1.name.count }) {
for w in store.workflows.sorted(by: { $0.name.count > $1.name.count }) {
guard title.lowercased().hasPrefix(w.name.lowercased()) else {
continue
}
+1 -25
View File
@@ -19,12 +19,10 @@ func terminalFocused() -> Bool {
struct TerminalTab: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var commands: [SavedCommand] = []
@ViewState private var selected: String?
@ViewState private var status = ""
@ViewState private var failure: String?
@ViewState private var starting = false
@ViewState private var off: (() -> Void)?
var body: some View {
let taskId = store.selectedTaskId
@@ -55,7 +53,7 @@ struct TerminalTab: View {
.buttonStyle(SmallLinkButtonStyle())
.help("Open another shell in the task's worktree")
.padding(.leading, 8)
ForEach(commands, id: \.id) { c in
ForEach(store.savedCommands, id: \.id) { c in
Button("▸ \(c.name)") {
start(c.id)
}
@@ -105,28 +103,6 @@ struct TerminalTab: View {
start(nil)
}
}
.onAppear {
off = store.events.add { ev in
if ev.kind == "command.saved" || ev.kind == "command.deleted" {
loadCommands()
}
}
loadCommands()
}
.onDisappear {
off?()
off = nil
}
}
private func loadCommands() {
Task {
do {
commands = try await store.api.client.listSavedCommands().ok.body.json
} catch {
overlays.showAlert(errorText(error))
}
}
}
private func start(_ commandId: String?) {
+5 -46
View File
@@ -50,59 +50,18 @@ struct WorkflowsView: View {
private struct WorkflowList: View {
let edit: (String) -> Void
@Environment(Store.self) private var store
@ViewState private var list: [WorkflowSummary]?
@ViewState private var failure: String?
@ViewState private var loads = 0
@ViewState private var off: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let list {
if list.isEmpty {
hint("No saved workflows. Click New to write one.")
} else {
ForEach(list.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }, id: \.name) { w in
WorkflowRow(workflow: w) { edit(w.name) }
}
if store.workflows.isEmpty {
Text("No saved workflows. Click New to write one.").foregroundStyle(Palette.muted)
} else {
ForEach(store.workflows.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }, id: \.name) { w in
WorkflowRow(workflow: w) { edit(w.name) }
}
} else if let failure {
hint(failure)
}
}
.padding(.top, 8)
.onAppear {
off = store.events.add { ev in
if ev.kind == "workflow.updated" || ev.kind == "workflow.deleted" {
load()
}
}
load()
}
.onDisappear {
off?()
off = nil
loads += 1
}
}
private func hint(_ text: String) -> some View {
Text(text).foregroundStyle(Palette.muted)
}
private func load() {
loads += 1
let n = loads
Task {
do {
let loaded = try await store.api.client.listWorkflows().ok.body.json
if n == loads {
list = loaded
}
} catch {
failure = errorText(error)
list = nil
}
}
}
}