From e7724977c8c19a4f20b3e35fb4143918de5c654b Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 20 Oct 2026 08:51:32 -0700 Subject: [PATCH] Fold task state into one server field; add ready cancel and dismiss --- api/openapi.yaml | 25 +- docs/operations.md | 8 +- internal/agents/lead.go | 10 +- internal/agents/manager.go | 2 +- internal/agents/ready.go | 41 +- internal/api/api.gen.go | 651 +++++++++--------- internal/server/server.go | 14 + ...-asking-held-and-ready-into-task-state.sql | 22 + internal/store/store.go | 38 +- mac/Sources/Byre/Chat.swift | 18 +- mac/Sources/Byre/Store.swift | 60 +- mac/Sources/Byre/TranscriptBuilder.swift | 5 +- 12 files changed, 506 insertions(+), 388 deletions(-) create mode 100644 internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql diff --git a/api/openapi.yaml b/api/openapi.yaml index d528330..03956d4 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -483,6 +483,20 @@ paths: schema: { $ref: "#/components/schemas/Task" } "404": { $ref: "#/components/responses/Error" } + /api/tasks/{taskId}/ready: + parameters: + - { name: taskId, in: path, required: true, schema: { type: string } } + delete: + operationId: dismissReady + description: Dismisses the agent's standing ready_for_merge or suggest_discard, so the task is no longer ready_to_merge or discard_suggested; a task.ready_cleared event records it. + responses: + "200": + description: Dismissed. + content: + application/json: + schema: { $ref: "#/components/schemas/Task" } + "404": { $ref: "#/components/responses/Error" } + /api/tasks/{taskId}/merge-in: parameters: - { name: taskId, in: path, required: true, schema: { type: string } } @@ -1307,20 +1321,15 @@ components: workflow_stage: { type: string, description: "With workflow: the name of the stage this task runs." } state: type: string - description: "What the task is doing, kept by the store in the same transaction as every change to it (a task.state event carries each change): merged or discarded once closed, else discarding, merging, the state of its agent's latest live session when that isn't idle, reviewing while a review runs, review_pending between a turn's end and its snapshot's review starting or being skipped, idle, or no_agent." - enum: [running, idle, review_pending, reviewing, merging, discarding, merged, discarded, no_agent] - x-enum-varnames: [TaskStateRunning, TaskStateIdle, TaskStateReviewPending, TaskStateReviewing, TaskStateMerging, TaskStateDiscarding, TaskStateMerged, TaskStateDiscarded, TaskStateNoAgent] + description: "The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; idle; no_agent when it has no live agent." + enum: [running, idle, review_pending, reviewing, merging, discarding, merged, discarded, no_agent, asking, merge_held, merge_after_review, ready_to_merge, discard_suggested] + x-enum-varnames: [TaskStateRunning, TaskStateIdle, TaskStateReviewPending, TaskStateReviewing, TaskStateMerging, TaskStateDiscarding, TaskStateMerged, TaskStateDiscarded, TaskStateNoAgent, TaskStateAsking, TaskStateMergeHeld, TaskStateMergeAfterReview, TaskStateReadyToMerge, TaskStateDiscardSuggested] merging: { type: boolean, description: "Whether a Merge was started and hasn't finished: its turn is running, or the turn ended and byre is squashing, pushing and closing the task." } discarding: { type: boolean, description: "Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed." } parent_task_id: { type: string, description: "The task whose agent created this one as its subtask; absent for other tasks." } merging_into_parent: { type: boolean, description: "Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet." } merge_after_review: { type: boolean, description: "Whether a Merge with after_review is waiting for the running review of the latest snapshot to finish." } summary_seq: { type: integer, format: int64, description: "The seq of the task's latest turn.ended event with summary: true; 0 when it has none." } - ready: - type: string - description: "What the agent last told the human to do with the task: merge (ready_for_merge) or discard (suggest_discard); absent when it hasn't, or when a snapshot other than an answer's or a new question came after." - enum: [merge, discard] - x-enum-varnames: [TaskReadyMerge, TaskReadyDiscard] Todo: type: object diff --git a/docs/operations.md b/docs/operations.md index 6d45762..5e73a2c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -37,11 +37,11 @@ A task ends `merged` or `discarded`; either way its sessions are stopped, its sn A task's summary is what its agent writes, before the task closes, about the whole task: what it set out to do, what was built or tried, the key decisions and why, and what was left out. The agent sets it with the `set_task_summary` MCP tool (`internal/agents/summary.go`), any time while the task is active; each call replaces `tasks.summary` and records a `task.summary` event carrying it, which the transcript shows as a note. The Merge turn's prompt and the discard turn's prompt (`summaryRequest`) both ask for it. Turn summaries don't stand in for it: each covers only its own turn, and a merged task's last one is always the Merge turn's. Tasks closed before the column existed have none. `sqlite3 byre.db "SELECT id, status, summary FROM tasks WHERE status != 'active' ORDER BY closed_at DESC LIMIT 5"` shows recent ones. -An implementer says its task is ready to merge with the `ready_for_merge` MCP tool (`internal/agents/ready.go`), whose `overview` covers the whole task for a human switching between tasks: a summary, tradeoffs and compromises, what was not done, and future TODOs. Its description tells the agent to call it only once the review of its most recent changes has come back, never in the same turn as a change or while that review is pending. Each call records a `task.ready` event carrying it, which the lead gets as a signal and the transcript draws as a bubble (`ReadyBubble` in `mac/Sources/Byre/Chat.swift`) headed "ready for merge". The latest one, while the task is active and neither a question nor a snapshot other than an answer snapshot has come since it (a question, or newer work the overview doesn't cover), is drawn like an open question (the warn tint and border) and carries small Merge and Discard buttons that act like the header's, confirmation dialogs included; earlier ones are drawn like an answered question (the faint warn tint), without buttons. Recording it changes nothing else. byre asks for it (`readyPrompt`, sent as a byre message) when a task's state goes to `idle` or its workflow finishes (`Manager.Observe` calls `promptReady`) and the latest non-answer snapshot's review is `done` or `skipped`. It checks and sends under the session's delivery lock (the one human messages and answers take), and only to an agent that isn't running, so the prompt never queues behind a turn that could add unreviewed work. `tasks.ready_prompt_seq` holds the task's last event seq when the prompt's turn started; byre asks again only once a non-answer snapshot or a message from the human (a chat message or a question's answer, not one from byre, the agent or the lead) has come after it, so the agent's own turns that change no files don't ask again, and a human unblocking an agent that said it was waiting does. It doesn't ask while the task has an open question. It doesn't ask for a workflow stage task until its run has a `task.workflow_finished` event (by then the run's other stage tasks are being discarded, leaving the final one for the human), a task with a merge after review set, or a review that failed, and it skips the `reviewing` → `idle` change of a review with findings, since those go to the agent; the turn that answers them asks once it ends without new work. The prompt tells the agent not to call the tool, and to say what's left, when the task isn't ready. +An implementer says its task is ready to merge with the `ready_for_merge` MCP tool (`internal/agents/ready.go`), whose `overview` covers the whole task for a human switching between tasks: a summary, tradeoffs and compromises, what was not done, and future TODOs. Its description tells the agent to call it only once the review of its most recent changes has come back, never in the same turn as a change or while that review is pending. Each call records a `task.ready` event carrying it, which the lead gets as a signal and the transcript draws as a bubble (`ReadyBubble` in `mac/Sources/Byre/Chat.swift`) headed "ready for merge". The latest one, while the task is active and no question, `task.ready_cleared` or snapshot other than an answer snapshot has come since it (a question, a withdrawal or dismissal, or newer work the overview doesn't cover), is drawn like an open question (the warn tint and border) and carries small Dismiss, Merge and Discard buttons that act like the header's, confirmation dialogs included; earlier ones are drawn like an answered question (the faint warn tint), without buttons. Recording it changes nothing else. byre asks for it (`readyPrompt`, sent as a byre message) when a task's state goes to `idle`, `ready_to_merge` or `discard_suggested` (`readyState`) or its workflow finishes (`Manager.Observe` calls `promptReady`) and the latest non-answer snapshot's review is `done` or `skipped`. It checks and sends under the session's delivery lock (the one human messages and answers take), and only to an agent that isn't running, so the prompt never queues behind a turn that could add unreviewed work. `tasks.ready_prompt_seq` holds the task's last event seq when the prompt's turn started; byre asks again only once a non-answer snapshot or a message from the human (a chat message or a question's answer, not one from byre, the agent or the lead) has come after it, so the agent's own turns that change no files don't ask again, and a human unblocking an agent that said it was waiting does. It doesn't ask while the task has an open question. It doesn't ask for a workflow stage task until its run has a `task.workflow_finished` event (by then the run's other stage tasks are being discarded, leaving the final one for the human), a task in any other state (an open question, a held merge or a merge after review among them), or a review that failed, and it skips the `reviewing` → `idle` change of a review with findings, since those go to the agent; the turn that answers them asks once it ends without new work. The prompt tells the agent not to call the tool, and to say what's left, when the task isn't ready. When that review is `skipped` (the snapshot has no diff from the default branch), byre sends `readyPromptUnchanged` instead, which says there is nothing to merge and offers only `suggest_discard` and `ask_human`. An implementer that thinks its task shouldn't land at all (the approach failed, the task turned out unnecessary or already done) says so with the `suggest_discard` MCP tool (also `internal/agents/ready.go`), whose `reason` explains why; `readyPrompt` offers it as the alternative to `ready_for_merge`. Each call records a `task.discard_suggested` event, which the lead gets as a signal and the transcript draws with the same `ReadyBubble`, headed "suggests discarding". It takes part in "latest" with the ready-for-merge overviews, so whichever of the two came last is the current one, drawn and cleared under the same rules, and it carries only the Discard button. It only suggests: the task stays open until the human discards it. -A Task's `ready` (`tasks.ready`) is `merge` or `discard` after the latest of these two calls, set in the same transaction as its event, and is cleared (absent in the API) by the same things that clear the latest bubble: a snapshot other than an answer snapshot, or a new inbox item. The switcher's and the Project tab's task rows show it as a **Ready to merge** or **Ready to discard** state, in the warn colour with a warn dot like **Needs your answer**, when no question, merge or running work outranks it (`taskState` in `mac/Sources/Byre/Store.swift`). The app reloads a project's tasks on `task.ready`, `task.discard_suggested`, `snapshot.created` and `inbox.created`. +`tasks.ready` is `merge` or `discard` after the latest of these two calls, set in the same transaction as its event, and is cleared by the same things that clear the latest bubble: a snapshot other than an answer snapshot, a new inbox item, or a `task.ready_cleared` event. That event comes from the agent's `cancel_ready` MCP tool (also `internal/agents/ready.go`, for when its call no longer holds) or from `DELETE /api/tasks/{taskId}/ready`, which the current bubble's small Dismiss button sends; the lead gets it as a signal and the transcript draws it as a note. It feeds the task's state (`ready_to_merge` or `discard_suggested`, see "Task state in the store"), which the switcher's and the Project tab's task rows show as **Ready to merge** or **Ready to discard**, in the warn colour with a warn dot like **Needs your answer**. The app reloads a project's tasks on `task.ready`, `task.discard_suggested`, `task.ready_cleared`, `snapshot.created` and `task.state`. Agents find closed tasks with the `search_closed_tasks` MCP tool, which task agents get for their own project and the lead for one project (`project_id`) or all: a case-insensitive substring match of `query` on title, merge title, TODO item, summary and merge commit (`Store.SearchClosedTasks`), newest closed first, at most `limit` (default 20, at most 100), each with its id, outcome, merge commit, close time, creator, titles, TODO item, fork source and summary. The lead reads a closed task's transcript with `read_transcript`. There is no closed-task list in the UI; the human asks the lead. @@ -69,7 +69,7 @@ A Task's `merging` is true while `merging_by` or `finalizing` is set, and `disca ### Task state in the store -A Task's `state` (the `tasks.state` column) is what the task is doing, owned by the store: `merged` or `discarded` once closed, else `discarding`, `merging`, the state of the agent's latest non-completed implementer session when it isn't `idle` (`running`), `running` while that session's `subagents` count is above zero (see "Subagent count"), `reviewing` while a review is `running`, `review_pending` while `tasks.review_requests` is above zero, `idle`, or `no_agent` when the task has no live session (`taskStateExpr` in `internal/store/store.go`, earlier cases winning). Every write transaction (`Store.write`), after its own changes, recomputes the state of each task its events name and, when it changed, updates the column and emits `task.state` (`from`, `to`; actor that of the task's last event in the transaction) in the same transaction, so the state and the events that changed it commit and publish together, in seq order. The store publishes each transaction's events in one call (`onEvent` takes the list), which the lead's signals rely on. +A Task's `state` (the `tasks.state` column) is the one state the task is in, owned by the store and read as is by the app, the lead and the API: `merged` or `discarded` once closed, else `discarding`, `asking` while it has an open inbox item, `merging` while `merging_by`, `finalizing` or `parent_merge_by` is set, `merge_held` while `merge_queued_by` is set, `merge_after_review` while `merge_after_review_by` is set, the state of the agent's latest non-completed implementer session when it isn't `idle` (`running`), `running` while that session's `subagents` count is above zero (see "Subagent count"), `reviewing` while a review is `running`, `review_pending` while `tasks.review_requests` is above zero, `ready_to_merge` or `discard_suggested` while `tasks.ready` is `merge` or `discard`, `idle`, or `no_agent` when the task has no live session (`taskStateExpr` in `internal/store/store.go`, earlier cases winning). Every write transaction (`Store.write`), after its own changes, recomputes the state of each task its events name and, when it changed, updates the column and emits `task.state` (`from`, `to`; actor that of the task's last event in the transaction) in the same transaction, so the state and the events that changed it commit and publish together, in seq order. The store publishes each transaction's events in one call (`onEvent` takes the list), which the lead's signals rely on. Subagent count: Claude Code's subagents can run in the background past the `turn.ended` of the turn that started them, while the session is `idle`. A session's `subagents` (the `agent_sessions.subagents` column, on `AgentSession`) counts its running ones: `trackSubagents` in `internal/agents/manager.go` adds a `task_started` `system` event's `task_id` when its `task_type` is `local_agent` (background shell commands are `local_bash` and don't count), and drops it on the `task_notification` naming it. Each change is written with `Store.SetSubagents`, which emits `session.subagents` (`subagents`, the new count). It drops to 0 when the agent's process exits, and on startup for every session left above 0, since a restart ends every process. `sqlite3 byre.db "SELECT id, task_id, owner, subagents FROM agent_sessions WHERE subagents > 0"` shows the sessions with subagents running. @@ -280,7 +280,7 @@ Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks are grouped by state (`taskRank` in `mac/Sources/Byre/Store.swift`, which also orders the Project tab's task list, ties in the server's order, subtasks sorted among their siblings under their parent): with inbox items first, then **Ready to merge** or **Ready to discard**, then idle, then working or reviewing, and merging, held or discarding last; within each group, the task least recently visited comes first (never-visited tasks before all others, the right pane's own task after all others, ties in the server's order: projects by name, tasks newest first), so ⌘K Enter repeated cycles through the tasks. Visit times are SwiftData `VisitRecord`s synced through iCloud (`docs/macos.md`, "Persistence"), stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows the user's default model (see "Agent sessions"), the model the composer offers first for the new task's first message; the task itself starts with no session. Below each create row, a "Start in …" row creates the same task and starts its agent on the default model with the title as its first message, through `POST /api/projects/{id}/tasks` with `prompt` set to the title. Picking a task points the right pane at it on the Transcript tab, scrolled to its oldest open question when it has open inbox items, otherwise to its latest summary when that is unread (see below and "Lead agent"). -**Task state.** The switcher and the Project tab show one label per task in place of its raw session state, the first that applies (`taskState` in `mac/Sources/Byre/Store.swift`), with a dot and a tooltip saying why: **Needs your answer** (an open `question` inbox item), **Merging** (`merging` or `merging_into_parent`), **Merge held** (`merge_queued_at`), **Merge after review** (`merge_after_review`), **Reviewing** (the task's `state` is `review_pending` or `reviewing`), **Working** (`running`), **Discarding** (`discarding`; see "Task state in the store" for both), **Idle** (anything else, including no session). The Project tab, in its task list and TODO items, draws a spinner in place of the dot for **Reviewing**, **Working**, **Discarding** and **Merging**. The label follows the tasks and inbox state the app already holds, so it needs no extra fetch. +**Task state.** The switcher and the Project tab show one label per task for its `state` (see "Task state in the store"; `stateTitles` in `mac/Sources/Byre/Store.swift`), with a dot and a tooltip saying why: **Needs your answer** (`asking`), **Merging** (`merging`), **Merge held** (`merge_held`), **Merge after review** (`merge_after_review`), **Reviewing** (`review_pending` or `reviewing`), **Working** (`running`), **Discarding** (`discarding`), **Ready to merge** (`ready_to_merge`), **Ready to discard** (`discard_suggested`), **Idle** (`idle` or `no_agent`). The Project tab, in its task list and TODO items, draws a spinner in place of the dot for **Reviewing**, **Working**, **Discarding** and **Merging**. The label follows the tasks the app already holds, so it needs no extra fetch. **Unread summaries.** A Task's `summary_seq` is the seq of its latest `turn.ended` event with `summary: true` (0 for none), read through the partial index `events_summary`, so the app knows of unread summaries without loading transcripts; a `turn.ended` with `summary: true` on the global stream raises it in the app. The switcher's and the Project tab's task rows show a **new summary** state label in place of Idle while the task is idle and `summary_seq` is above the seq the user has read (`unreadSummary` in `mac/Sources/Byre/Store.swift`), except on tasks of a workflow run, which close too soon for their summaries to be read. A summary isn't an attention item: picking such a row opens the Transcript at the end, like any task without open questions, findings or a ready bubble. Read state is per user on the server: `summary_reads` holds one highest-read seq per user and task. `GET /api/me/summary-reads` returns the caller's, for active tasks, as a map from task id to seq, and `PUT /api/tasks/{taskId}/summary-read` with `{seq}` raises it, emitting `summary.read` (`task_id`, `seq`) owned by the user only when it went up, so every Mac the user has byre open on clears the label. A summary counts as read only when the summary turn's end row is on screen in a transcript while that server's window is the key window of the active app (`summaryVisible` in `mac/Sources/Byre/Chat.swift`), so selecting the task on another tab leaves it unread. `sqlite3 byre.db 'SELECT * FROM summary_reads'` shows the read seqs. diff --git a/internal/agents/lead.go b/internal/agents/lead.go index b163103..79e88ef 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -323,14 +323,14 @@ func (m *Manager) warnLeadContext(ctx context.Context, l *live) error { func (m *Manager) Observe(events []api.Event) { batch := []api.Event{} for _, ev := range events { - if ev.Kind == "task.state" && ev.Payload["to"] == string(api.TaskStateIdle) && ev.TaskId != nil { + if to, _ := ev.Payload["to"].(string); ev.Kind == "task.state" && readyState(api.TaskState(to)) && ev.TaskId != nil { go m.promptReady(*ev.TaskId, ev.Payload["from"]) } if ev.Kind == "task.workflow_finished" && ev.TaskId != nil { go m.promptReady(*ev.TaskId, nil) } switch ev.Kind { - case "inbox.created", "inbox.updated", "task.created", "task.merge_queued", "task.merge_after_review", "task.merge_after_review_cancelled", "task.merge_after_review_dropped", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state", "task.ready", "task.discard_suggested": + case "inbox.created", "inbox.updated", "task.created", "task.merge_queued", "task.merge_after_review", "task.merge_after_review_cancelled", "task.merge_after_review_dropped", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared": case "user.message": if ev.TaskId == nil || !humanActor(ev.Actor) { continue @@ -554,6 +554,12 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error) case "task.discard_suggested": return fmt.Sprintf("%s's agent suggests discarding the task:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["reason"]), 3000)), nil + case "task.ready_cleared": + if ev.Actor == agentActor { + return fmt.Sprintf("%s's agent withdrew its ready for merge or discard suggestion.", m.taskLabel(ctx, deref(ev.TaskId))), nil + } + return fmt.Sprintf("%s's ready for merge or discard suggestion was dismissed by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil + case "task.workflow_stage": return fmt.Sprintf("%s started stage %q of workflow %q.", m.taskLabel(ctx, deref(ev.TaskId)), fmt.Sprint(ev.Payload["stage"]), ev.Payload["workflow"]), nil diff --git a/internal/agents/manager.go b/internal/agents/manager.go index ab3e7d4..ea0681f 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -751,7 +751,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api return nil, err } l.notes = "\n\n" + taskNotesGuidance + "\n\n" + notes - tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l), m.suggestDiscardTool(l), m.respondToFindingTool(l)) + tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l), m.suggestDiscardTool(l), m.cancelReadyTool(l), m.respondToFindingTool(l)) if sess.Vendor == api.Claude { tools = append(tools, m.forkTool(l)) } diff --git a/internal/agents/ready.go b/internal/agents/ready.go index fcd5217..d39e8b5 100644 --- a/internal/agents/ready.go +++ b/internal/agents/ready.go @@ -13,10 +13,16 @@ import ( "g.fc.run/ai/byre/internal/store" ) +const readyPromptAsk = "If it isn't ready (work remains that you mean to do, or you're waiting on the human), call neither: say what's left in one line and end your turn. " + + "If you're waiting on the human for a decision or input, ask it with ask_human now, even if an earlier message already put it in prose, so it reaches their inbox; only an open ask_human question already counts as asked. Don't change any files this turn." + const readyPrompt = "Your turn has ended and the review of your latest work has finished. If the task is ready to merge, call ready_for_merge with an overview of the whole task, then end your turn with one line. " + "If the work shouldn't land at all (the approach failed, the task turned out unnecessary or already done), call suggest_discard with your reasoning instead, then end your turn with one line. " + - "If it isn't ready (work remains that you mean to do, or you're waiting on the human), call neither: say what's left in one line and end your turn. " + - "If you're waiting on the human for a decision or input, ask it with ask_human now, even if an earlier message already put it in prose, so it reaches their inbox; only an open ask_human question already counts as asked. Don't change any files this turn." + readyPromptAsk + +const readyPromptUnchanged = "Your turn has ended, and this task's branch has no changes from the default branch, so there is nothing to merge. " + + "If the task is finished, or shouldn't go further (it needed no changes, the approach failed, or it was already done), call suggest_discard with your reasoning, then end your turn with one line. " + + readyPromptAsk type readyArgs struct { Overview string `json:"overview"` @@ -97,6 +103,29 @@ func (m *Manager) suggestDiscardTool(l *live) mcp.Tool { } } +func (m *Manager) cancelReadyTool(l *live) mcp.Tool { + return mcp.Tool{ + Name: "cancel_ready", + Description: "Withdraw your standing ready_for_merge or suggest_discard, when it no longer holds: for example the human raised a problem, or you found more work. " + + "The human then no longer sees the task as ready to merge or as suggested for discard; call ready_for_merge or suggest_discard again once it holds.", + InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + err := m.store.ClearReady(ctx, l.taskID, agentActor) + if errors.Is(err, store.ErrNotFound) { + return "", fmt.Errorf("%w: this task has no standing ready_for_merge or suggest_discard", ErrConflict) + } + if err != nil { + return "", err + } + return "Withdrawn: the human no longer sees this task as ready to merge or suggested for discard.", nil + }, + } +} + +func readyState(s api.TaskState) bool { + return s == api.TaskStateIdle || s == api.TaskStateReadyToMerge || s == api.TaskStateDiscardSuggested +} + func (m *Manager) promptReady(taskID string, from any) { ctx := context.Background() err := m.tryPromptReady(ctx, taskID, from == string(api.TaskStateReviewing)) @@ -117,7 +146,7 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview if err != nil { return err } - if task.Status != api.TaskStatusActive || task.State != api.TaskStateIdle || task.MergeAfterReview { + if task.Status != api.TaskStatusActive || !readyState(task.State) { return nil } if task.WorkflowStage != nil { @@ -164,7 +193,11 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview if err != nil || len(open) > 0 { return err } - _, err = m.Send(ctx, sess.Id, "byre", readyPrompt, "") + prompt := readyPrompt + if review.State == api.ReviewStateSkipped { + prompt = readyPromptUnchanged + } + _, err = m.Send(ctx, sess.Id, "byre", prompt, "") if err != nil { return err } diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 13896ee..6e0f1ac 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -426,52 +426,49 @@ func (e StageAttemptState) Valid() bool { } } -// Defines values for TaskReady. -const ( - TaskReadyDiscard TaskReady = "discard" - TaskReadyMerge TaskReady = "merge" -) - -// Valid indicates whether the value is a known member of the TaskReady enum. -func (e TaskReady) Valid() bool { - switch e { - case TaskReadyDiscard: - return true - case TaskReadyMerge: - return true - default: - return false - } -} - // Defines values for TaskState. const ( - TaskStateDiscarded TaskState = "discarded" - TaskStateDiscarding TaskState = "discarding" - TaskStateIdle TaskState = "idle" - TaskStateMerged TaskState = "merged" - TaskStateMerging TaskState = "merging" - TaskStateNoAgent TaskState = "no_agent" - TaskStateReviewPending TaskState = "review_pending" - TaskStateReviewing TaskState = "reviewing" - TaskStateRunning TaskState = "running" + TaskStateAsking TaskState = "asking" + TaskStateDiscardSuggested TaskState = "discard_suggested" + TaskStateDiscarded TaskState = "discarded" + TaskStateDiscarding TaskState = "discarding" + TaskStateIdle TaskState = "idle" + TaskStateMergeAfterReview TaskState = "merge_after_review" + TaskStateMergeHeld TaskState = "merge_held" + TaskStateMerged TaskState = "merged" + TaskStateMerging TaskState = "merging" + TaskStateNoAgent TaskState = "no_agent" + TaskStateReadyToMerge TaskState = "ready_to_merge" + TaskStateReviewPending TaskState = "review_pending" + TaskStateReviewing TaskState = "reviewing" + TaskStateRunning TaskState = "running" ) // Valid indicates whether the value is a known member of the TaskState enum. func (e TaskState) Valid() bool { switch e { + case TaskStateAsking: + return true + case TaskStateDiscardSuggested: + return true case TaskStateDiscarded: return true case TaskStateDiscarding: return true case TaskStateIdle: return true + case TaskStateMergeAfterReview: + return true + case TaskStateMergeHeld: + return true case TaskStateMerged: return true case TaskStateMerging: return true case TaskStateNoAgent: return true + case TaskStateReadyToMerge: + return true case TaskStateReviewPending: return true case TaskStateReviewing: @@ -1338,10 +1335,7 @@ type Task struct { PortBase int `json:"port_base"` ProjectId string `json:"project_id"` - // Ready What the agent last told the human to do with the task: merge (ready_for_merge) or discard (suggest_discard); absent when it hasn't, or when a snapshot other than an answer's or a new question came after. - Ready *TaskReady `json:"ready,omitempty"` - - // State What the task is doing, kept by the store in the same transaction as every change to it (a task.state event carries each change): merged or discarded once closed, else discarding, merging, the state of its agent's latest live session when that isn't idle, reviewing while a review runs, review_pending between a turn's end and its snapshot's review starting or being skipped, idle, or no_agent. + // State The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; idle; no_agent when it has no live agent. State TaskState `json:"state"` Status TaskStatus `json:"status"` @@ -1363,10 +1357,7 @@ type Task struct { WorktreePath string `json:"worktree_path"` } -// TaskReady What the agent last told the human to do with the task: merge (ready_for_merge) or discard (suggest_discard); absent when it hasn't, or when a snapshot other than an answer's or a new question came after. -type TaskReady string - -// TaskState What the task is doing, kept by the store in the same transaction as every change to it (a task.state event carries each change): merged or discarded once closed, else discarding, merging, the state of its agent's latest live session when that isn't idle, reviewing while a review runs, review_pending between a turn's end and its snapshot's review starting or being skipped, idle, or no_agent. +// TaskState The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; idle; no_agent when it has no live agent. type TaskState string // TaskStatus defines model for Task.Status. @@ -1923,6 +1914,9 @@ type ServerInterface interface { // (DELETE /api/tasks/{taskId}/queue/{inputId}) WithdrawMessage(w http.ResponseWriter, r *http.Request, taskId string, inputId int64) + // (DELETE /api/tasks/{taskId}/ready) + DismissReady(w http.ResponseWriter, r *http.Request, taskId string) + // (GET /api/tasks/{taskId}/reviews) ListReviews(w http.ResponseWriter, r *http.Request, taskId string) @@ -3480,6 +3474,32 @@ func (siw *ServerInterfaceWrapper) WithdrawMessage(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// DismissReady operation middleware +func (siw *ServerInterfaceWrapper) DismissReady(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "taskId" ------------- + var taskId string + + err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DismissReady(w, r, taskId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListReviews operation middleware func (siw *ServerInterfaceWrapper) ListReviews(w http.ResponseWriter, r *http.Request) { @@ -4020,6 +4040,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/co-authors", wrapper.GetCoAuthors) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge", wrapper.MergeTask) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/merge-after-review", wrapper.CancelMergeAfterReview) + m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/ready", wrapper.DismissReady) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge-in", wrapper.MergeIn) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/discard", wrapper.DiscardTask) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/board", wrapper.PostToBoard) @@ -4070,285 +4091,287 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "iPLpclpv0YydpZoH9/m9qvW8f/9/BgA=", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/server.go b/internal/server/server.go index cc02cc9..607cd51 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -546,6 +546,20 @@ func (s *Server) CancelMergeAfterReview(w http.ResponseWriter, r *http.Request, writeJSON(w, http.StatusOK, t) } +func (s *Server) DismissReady(w http.ResponseWriter, r *http.Request, taskID string) { + err := s.store.ClearReady(r.Context(), taskID, actor(r)) + if err != nil { + writeStoreError(w, err) + return + } + t, err := s.store.GetTask(r.Context(), taskID) + if err != nil { + writeStoreError(w, err) + return + } + writeJSON(w, http.StatusOK, t) +} + func (s *Server) MergeIn(w http.ResponseWriter, r *http.Request, taskID string) { t, p, ok := s.activeTask(w, r, taskID) if !ok { diff --git a/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql b/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql new file mode 100644 index 0000000..28936b4 --- /dev/null +++ b/internal/store/migrations/066-fold-asking-held-and-ready-into-task-state.sql @@ -0,0 +1,22 @@ +UPDATE tasks SET state = CASE + WHEN tasks.status != 'active' THEN tasks.status + WHEN tasks.discard_state != '' THEN 'discarding' + WHEN EXISTS (SELECT 1 FROM inbox_items i WHERE i.task_id = tasks.id AND i.state = 'open') THEN 'asking' + WHEN tasks.merging_by != '' OR tasks.finalizing != '' OR tasks.parent_merge_by != '' THEN 'merging' + WHEN tasks.merge_queued_by != '' THEN 'merge_held' + WHEN tasks.merge_after_review_by != '' THEN 'merge_after_review' + ELSE coalesce(( + SELECT CASE + WHEN s.state != 'idle' THEN s.state + WHEN s.subagents > 0 THEN 'running' + WHEN EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = tasks.id AND r.state = 'running') THEN 'reviewing' + WHEN tasks.review_requests > 0 THEN 'review_pending' + WHEN tasks.ready = 'merge' THEN 'ready_to_merge' + WHEN tasks.ready = 'discard' THEN 'discard_suggested' + ELSE 'idle' + END + FROM agent_sessions s + WHERE s.task_id = tasks.id AND s.role != 'reviewer' AND s.state != 'completed' + ORDER BY s.state = 'running' DESC, s.subagents > 0 DESC, s.created_at DESC LIMIT 1 + ), 'no_agent') +END; diff --git a/internal/store/store.go b/internal/store/store.go index 7cc8ce3..ae02017 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -763,17 +763,31 @@ func (s *Store) SetReadyPromptSeq(ctx context.Context, id string, seq int64) err } func (s *Store) MarkReady(ctx context.Context, id, overview, actor string) error { - return s.setReady(ctx, id, actor, api.TaskReadyMerge, "task.ready", map[string]any{"id": id, "overview": overview}) + return s.setReady(ctx, id, actor, "merge", "task.ready", map[string]any{"id": id, "overview": overview}) } func (s *Store) SuggestDiscard(ctx context.Context, id, reason, actor string) error { - return s.setReady(ctx, id, actor, api.TaskReadyDiscard, "task.discard_suggested", map[string]any{"id": id, "reason": reason}) + return s.setReady(ctx, id, actor, "discard", "task.discard_suggested", map[string]any{"id": id, "reason": reason}) } -func (s *Store) setReady(ctx context.Context, id, actor string, ready api.TaskReady, kind string, payload map[string]any) error { +func (s *Store) ClearReady(ctx context.Context, id, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" - err := tx.QueryRowContext(ctx, `UPDATE tasks SET ready = ? WHERE id = ? AND status = ? RETURNING project_id`, string(ready), id, string(api.TaskStatusActive)).Scan(&projectID) + err := tx.QueryRowContext(ctx, `UPDATE tasks SET ready = '' WHERE id = ? AND status = ? AND ready != '' RETURNING project_id`, id, string(api.TaskStatusActive)).Scan(&projectID) + if errors.Is(err, sql.ErrNoRows) { + return ErrNotFound + } + if err != nil { + return err + } + return emit(api.Event{Kind: "task.ready_cleared", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id}}) + }) +} + +func (s *Store) setReady(ctx context.Context, id, actor, ready, kind string, payload map[string]any) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + projectID := "" + err := tx.QueryRowContext(ctx, `UPDATE tasks SET ready = ? WHERE id = ? AND status = ? RETURNING project_id`, ready, id, string(api.TaskStatusActive)).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } @@ -1425,13 +1439,18 @@ func settleTaskState(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, act const taskStateExpr = `CASE WHEN tasks.status != 'active' THEN tasks.status WHEN tasks.discard_state != '' THEN 'discarding' - WHEN tasks.merging_by != '' OR tasks.finalizing != '' THEN 'merging' + WHEN EXISTS (SELECT 1 FROM inbox_items i WHERE i.task_id = tasks.id AND i.state = 'open') THEN 'asking' + WHEN tasks.merging_by != '' OR tasks.finalizing != '' OR tasks.parent_merge_by != '' THEN 'merging' + WHEN tasks.merge_queued_by != '' THEN 'merge_held' + WHEN tasks.merge_after_review_by != '' THEN 'merge_after_review' ELSE coalesce(( SELECT CASE WHEN s.state != 'idle' THEN s.state WHEN s.subagents > 0 THEN 'running' WHEN EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = tasks.id AND r.state = 'running') THEN 'reviewing' WHEN tasks.review_requests > 0 THEN 'review_pending' + WHEN tasks.ready = 'merge' THEN 'ready_to_merge' + WHEN tasks.ready = 'discard' THEN 'discard_suggested' ELSE 'idle' END FROM agent_sessions s @@ -1493,7 +1512,7 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error { const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at` -const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), ready` +const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)` const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at, subagents` @@ -1528,16 +1547,11 @@ func scanTask(row scanner) (api.Task, error) { parentTaskID := "" parentMergeBy := "" taskState := "" - ready := "" - err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &ready) + err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq) if err != nil { return t, err } t.State = api.TaskState(taskState) - if ready != "" { - r := api.TaskReady(ready) - t.Ready = &r - } if parentTaskID != "" { t.ParentTaskId = &parentTaskID } diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index c1bba6b..d788850 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -138,7 +138,7 @@ final class ChatModel { var reviewing: Bool { let state = taskId.flatMap { store.findTask($0) }?.state - return state == .reviewing || state == .reviewPending + return state == .reviewing || state == .reviewPending || state == .mergeAfterReview } var subagents: Bool { @@ -1416,6 +1416,8 @@ private struct ReadyBubble: View { let latest: Bool let unmerged: Bool @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays + @ViewState private var dismissing = false var body: some View { let task = taskId.flatMap { store.findTask($0) } @@ -1434,6 +1436,20 @@ private struct ReadyBubble: View { let busy = store.taskBusy(task.id) HStack(spacing: 8) { Spacer(minLength: 0) + Button("Dismiss") { + dismissing = true + Task { + do { + _ = try await store.api.client.dismissReady(path: .init(taskId: task.id)).ok + } catch { + overlays.showAlert(errorText(error)) + } + dismissing = false + } + } + .buttonStyle(SmallLinkButtonStyle()) + .disabled(dismissing) + .help(discard ? "Drop this suggestion; the task stays open" : "Drop this ready call; the task stays open") if !discard { MergeButton(task: task, busy: busy, unmerged: unmerged, small: true) } diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index bf30e47..405c15d 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -33,11 +33,21 @@ struct TaskState { } } -private let workingTitles: [ByreTask.StatePayload: (label: String, title: String)] = [ - .running: ("Working", "The agent is mid-turn or its subagents are running"), - .reviewPending: ("Reviewing", "A review of the turn's work is about to start"), - .reviewing: ("Reviewing", "A review is running"), - .discarding: ("Discarding", "The agent is writing the task summary, then byre removes the task"), +private let stateTitles: [ByreTask.StatePayload: TaskState] = [ + .asking: TaskState(kind: .answer, label: "Needs your answer", title: "The agent asked a question"), + .merging: TaskState(kind: .merging, label: "Merging", title: "The merge is under way"), + .mergeHeld: TaskState(kind: .held, label: "Merge held", title: "Waiting for the merge in progress in this project to finish"), + .mergeAfterReview: TaskState(kind: .held, label: "Merge after review", title: "Merges when the running review finishes with no findings"), + .running: TaskState(kind: .working, label: "Working", title: "The agent is mid-turn or its subagents are running"), + .reviewPending: TaskState(kind: .working, label: "Reviewing", title: "A review of the turn's work is about to start"), + .reviewing: TaskState(kind: .working, label: "Reviewing", title: "A review is running"), + .discarding: TaskState(kind: .working, label: "Discarding", title: "The agent is writing the task summary, then byre removes the task"), + .readyToMerge: TaskState(kind: .ready, label: "Ready to merge", title: "The agent says the task is ready to merge"), + .discardSuggested: TaskState(kind: .ready, label: "Ready to discard", title: "The agent suggests discarding the task"), + .idle: TaskState(kind: .idle, label: "Idle", title: "Waiting for a message"), + .noAgent: TaskState(kind: .idle, label: "Idle", title: "Waiting for a message"), + .merged: TaskState(kind: .idle, label: "Merged", title: "The task was merged"), + .discarded: TaskState(kind: .idle, label: "Discarded", title: "The task was discarded"), ] @MainActor @@ -207,44 +217,12 @@ final class Store { } func taskState(_ task: ByreTask) -> TaskState { - if inbox.contains(where: { $0.taskId == task.id }) { - return TaskState(kind: .answer, label: "Needs your answer", title: "The agent asked a question") - } - let busy = progress(task) - if let busy, busy.kind == .merging { - return busy - } - 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 let busy { - return busy - } - switch task.ready { - case .merge: - return TaskState(kind: .ready, label: "Ready to merge", title: "The agent says the task is ready to merge") - case .discard: - return TaskState(kind: .ready, label: "Ready to discard", title: "The agent suggests discarding the task") - case nil: - break - } - return TaskState(kind: .idle, label: "Idle", title: "Waiting for a message") + stateTitles[task.state]! } func progress(_ task: ByreTask) -> TaskState? { - 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 let working = workingTitles[task.state] { - return TaskState(kind: .working, label: working.label, title: working.title) - } - return nil + let state = taskState(task) + return state.kind == .merging || state.kind == .working ? state : nil } func taskRank(_ task: ByreTask) -> Int { @@ -634,7 +612,7 @@ final class Store { 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", "task.ready", "task.discard_suggested", "snapshot.created": + 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", "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" && !["answer", "fork"].contains(str(ev.payload.value, "trigger"))) { clearTask(self, taskId) } diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift index db1bfb5..698732d 100644 --- a/mac/Sources/Byre/TranscriptBuilder.swift +++ b/mac/Sources/Byre/TranscriptBuilder.swift @@ -578,7 +578,7 @@ final class TranscriptBuilder { if (ev.kind == "task.ready" || ev.kind == "task.discard_suggested") && sink == nil { lastReady = row.id } - if ev.kind == "inbox.created" && sink == nil { + if (ev.kind == "inbox.created" || ev.kind == "task.ready_cleared") && sink == nil { lastReady = nil } put(row, keepHeld: ev.kind == "agent.thinking") @@ -801,6 +801,9 @@ final class TranscriptBuilder { case "task.discard_suggested": return Row(.ready(when: when, md: parseMarkdown(jsString(p["reason"])), discard: true), taskId: taskId) + case "task.ready_cleared": + return note([.text(ev.actor == "agent" ? "the agent withdrew its ready call" : "\(ev.actor) dismissed the ready call")]) + case "task.discard_started": return note([.text("\(ev.actor) asked to discard the task; the agent writes the task summary first")])