Questions tab, question bubbles, answer snapshots; errors to transcript

This commit is contained in:
Ian Gulliver
2026-10-18 15:06:06 -07:00
parent 1ef3594016
commit 10a80ccb22
31 changed files with 744 additions and 665 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ A server and macOS app for running many coding agents (Claude Code and Codex) in
- **Tasks.** Each task gets its own git worktree, branch and port block. Talk to its agent in a live transcript; messages sent mid-turn reach it after its next tool call.
- **Lead agent.** One long-running agent per user watches every task, tells you what needs you, points the right pane at it, and starts, messages, merges or discards tasks when you ask. It keeps notes on how you work and passes them to task agents.
- **Inbox.** Agents ask structured questions with a recommended answer; you answer by clicking. macOS notifications for questions, turn summaries and errors.
- **Questions.** Agents ask structured questions with a recommended answer; you answer by clicking. Each question shows in the transcript where it was asked, and clicking it shows it again with your answer. macOS notifications for questions, turn summaries and errors.
- **Cross-vendor reviews.** Every turn that changes files is snapshotted and reviewed by the other vendor's model. Findings go back to the agent, and you can fix or ignore each one.
- **Snapshots and forks.** Step through a task's snapshots, diff any of them, and fork a new task from any one, conversation included.
- **Merge.** One click: the agent merges main in and resolves conflicts, then byre squashes, pushes and closes the task, with co-author trailers and a git note recording the task. Merge after review, and one merge per project at a time with a queue.
+5
View File
@@ -17,6 +17,11 @@
# M7 — Inbox triage
- Answer a question again
From an answered question in the Questions tab, give a different answer, which forks the task: a new task starts from the snapshot taken just before the original answer was delivered (the answer's `snapshot_n`), its agent continuing the conversation as it stood there (as Fork does from a snapshot), and gets the new answer. The original task carries on untouched.
- Take both paths when unattended
An agent working unattended (no human to ask) that hits a question with two plausible answers forks at it: the task carries on with one answer and a fork, started the same way as answering again, carries on with the other, so the human later compares both results instead of the agent guessing.
# M8 — macOS client
- Phone app
+14 -15
View File
@@ -30,7 +30,7 @@ paths:
/api/me/notifications:
put:
operationId: updateNotificationPrefs
description: Sets which new inbox items notify the calling user in their open byre pages.
description: Sets which new questions, finished turns, errors and rate limits notify the calling user in their open byre pages.
requestBody:
required: true
content:
@@ -1125,13 +1125,13 @@ components:
NotificationPrefs:
type: object
description: Which new inbox items and finished turns notify this user in their open byre pages.
description: Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
required: [questions, reports, errors, rate_limits]
properties:
questions: { type: boolean, description: Questions. On by default. }
reports: { type: boolean, description: "Task turns that end with a summary (turn.ended with summary: true)." }
errors: { type: boolean, description: session_error items. }
rate_limits: { type: boolean, description: rate_limited items. }
errors: { type: boolean, description: session.error events on tasks. }
rate_limits: { type: boolean, description: session.rate_limited events on tasks. }
UserUpdate:
type: object
@@ -1469,9 +1469,9 @@ components:
event_seq: { type: integer, format: int64, description: "The task's latest event when the snapshot was started: the tree holds at least the work up to it." }
trigger:
type: string
enum: [message, turn_end, tool_call, ask_human, timer, requested]
x-enum-varnames: [SnapshotTriggerMessage, SnapshotTriggerTurnEnd, SnapshotTriggerToolCall, SnapshotTriggerAskHuman, SnapshotTriggerTimer, SnapshotTriggerRequested]
description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds."
enum: [message, turn_end, tool_call, ask_human, timer, requested, answer]
x-enum-varnames: [SnapshotTriggerMessage, SnapshotTriggerTurnEnd, SnapshotTriggerToolCall, SnapshotTriggerAskHuman, SnapshotTriggerTimer, SnapshotTriggerRequested, SnapshotTriggerAnswer]
description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds."
created_at: { type: string, format: date-time }
Preview:
@@ -1609,21 +1609,19 @@ components:
InboxItem:
type: object
required: [id, project_id, task_id, agent_session_id, thread_id, kind, payload, state, created_at, responses]
description: A question an agent asked the human with ask_human.
required: [id, project_id, task_id, agent_session_id, thread_id, payload, state, created_at, responses]
properties:
id: { type: string }
project_id: { type: string }
task_id: { type: string }
agent_session_id: { type: string }
thread_id: { type: string }
kind: { type: string, enum: [question, session_error, rate_limited] }
payload:
type: object
description: >
question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items),
context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items),
question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments.
session_error: error.
rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
state: { $ref: "#/components/schemas/InboxState" }
created_at: { type: string, format: date-time }
resolved_by: { type: string }
@@ -1634,7 +1632,7 @@ components:
InboxResponseType:
type: string
enum: [option, other, dismiss]
enum: [option, other]
InboxResponse:
type: object
@@ -1657,6 +1655,7 @@ components:
items: { $ref: "#/components/schemas/Attachment" }
created_at: { type: string, format: date-time }
delivered_at: { type: string, format: date-time }
snapshot_n: { type: integer, description: "The task's snapshot of the worktree as it stood just before the response was handed to the agent; absent until delivered, and when no snapshot could be taken." }
InboxResponseCreate:
type: object
@@ -1746,8 +1745,8 @@ components:
TaskTab:
type: string
enum: [transcript, board, changes, files, terminal, inbox, reviews, workflow, project]
x-enum-varnames: [TabTranscript, TabBoard, TabChanges, TabFiles, TabTerminal, TabInbox, TabReviews, TabWorkflow, TabProject]
enum: [transcript, board, changes, files, terminal, questions, reviews, workflow, project]
x-enum-varnames: [TabTranscript, TabBoard, TabChanges, TabFiles, TabTerminal, TabQuestions, TabReviews, TabWorkflow, TabProject]
TerminalSize:
type: object
+6 -6
View File
@@ -44,7 +44,7 @@ SwiftUI's own macros don't build here: the SDK declares `@State`, `@Entry`, `@An
| `Chat.swift`, `TranscriptBuilder.swift`, `Composer.swift`, `MergeDialog.swift` | Transcript and composer for tasks and the lead |
| `Markdown.swift`, `MarkdownView.swift` | byre's Markdown parser to a block tree, and its views |
| `Diagram.swift` | Native flowchart parser, layout and drawing for Mermaid blocks and inbox `diagram` attachments |
| `Inbox.swift` | One item at a time, choices, Other, Let the agent decide |
| `Questions.swift` | The Questions tab: one item at a time, choices, Other, Let the agent decide, answered items read-only |
| `Changes.swift` | Since main, This turn, pinned snapshots, per-file diffs, line comments |
| `Files.swift` | Read-only worktree browser |
| `Terminal.swift` | The terminal view and its WebSocket |
@@ -83,7 +83,7 @@ Focus lives in `Store`. In-app `#/` links go through `Store.route`: `#/task/<id>
One window. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity. The title bar is the standard one; no byre controls go in it or in a toolbar.
- **Left pane:** tabs Lead, Status, Settings, with "reconnecting…", the connection dot and the user's name at the end of the strip.
- **Right pane:** the task bar (task picker `<project> ▸ <title> ▾` and the preview links), then the task's tabs in the server's focus order, hiding Inbox, Reviews and Workflow when they have nothing, with a **More ▾** menu for tabs that don't fit, carrying their badges. The pane's accent is the selected task's colour.
- **Right pane:** the task bar (task picker `<project> ▸ <title> ▾` and the preview links), then the task's tabs in the server's focus order, hiding Questions, Reviews and Workflow when they have nothing, with a **More ▾** menu for tabs that don't fit, carrying their badges. The pane's accent is the selected task's colour.
Overlays (the switcher, alerts, confirmations, the merge dialog, keyboard help) are SwiftUI views drawn above the panes inside the window, not system sheets or alerts. The switcher and the Discard confirmation sit just below their anchor; the merge dialog is centred in the right pane.
@@ -93,13 +93,13 @@ Closing the window leaves the app running with its store and streams (`applicati
## Views
- **Transcript and Lead.** `TranscriptBuilder` is a pure function from events to a list of rows: user and agent messages, consecutive tool calls folded into one group with its summary ("Read file ×3"), subagents nested with their status, pending tool calls and running subagents (background ones included after the turn ends) outlined in the accent colour with a 0.6s fade, thinking, the byre prompt collapsed, held turns, `NO_REPLY` turns folded away, handoffs, snapshot rows with changes, fork and review, and notes for the other event kinds. It is incremental: a new batch extends the existing rows. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up. The stack isn't lazy because a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. While the user is scrolling (`onScrollPhaseChange`), content height changes only update whether it is stuck and never pull it back to the bottom. The lead pages back when the view comes within one screen of the top: the fetched page is held while the scroll phase is scrolling and inserted once it is idle (`ChatModel.older`, `prependOlder`), and the next geometry change moves the offset down by the height added, so the rows on screen stay put; a page that adds no rows (all folded `NO_REPLY` turns) fetches the next one at once. Each Markdown text run's `NSTextView` measures its height with one TextKit stack kept in its coordinator, and a repeat measurement at the same width and text returns the stored size. Pending sends show at the end until the server echoes them. A turn summary counts as read when its row is on screen and the app is active (`markSummaryRead`). The head carries Compact (with its context-usage fill; a long press opens a menu of models to continue on), Sync from main, Merge and Discard.
- **Transcript and Lead.** `TranscriptBuilder` is a pure function from events to a list of rows: user and agent messages, consecutive tool calls folded into one group with its summary ("Read file ×3"), subagents nested with their status, pending tool calls and running subagents (background ones included after the turn ends) outlined in the accent colour with a 0.6s fade, thinking, the byre prompt collapsed, held turns, `NO_REPLY` turns folded away, handoffs, snapshot rows with changes, fork and review, question bubbles that follow their item's `inbox.updated` events and open it in the Questions tab when clicked, and notes for the other event kinds. It is incremental: a new batch extends the existing rows. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up. The stack isn't lazy because a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. While the user is scrolling (`onScrollPhaseChange`), content height changes only update whether it is stuck and never pull it back to the bottom. The lead pages back when the view comes within one screen of the top: the fetched page is held while the scroll phase is scrolling and inserted once it is idle (`ChatModel.older`, `prependOlder`), and the next geometry change moves the offset down by the height added, so the rows on screen stay put; a page that adds no rows (all folded `NO_REPLY` turns) fetches the next one at once. Each Markdown text run's `NSTextView` measures its height with one TextKit stack kept in its coordinator, and a repeat measurement at the same width and text returns the stored size. Pending sends show at the end until the server echoes them. A turn summary counts as read when its row is on screen and the app is active (`markSummaryRead`). The head carries Compact (with its context-usage fill; a long press opens a menu of models to continue on), Sync from main, Merge and Discard.
- **Composer.** A `TextEditor`, with paste and drop handled as in "Gaps in SwiftUI". Text, the model menu (filtered to the live session's vendor), Interrupt and Send. Enter sends, Shift-Enter adds a newline, and Enter during IME composition does neither. Pasted or dropped PNG, JPEG, GIF and WebP images become thumbnails with ×, uploaded to `POST /api/blobs` on send; any other image `NSBitmapImageRep` decodes, such as TIFF, is converted to PNG first, its size checked after conversion. A failed send puts the text and images back and shows the error.
- **Inbox.** The selected task's oldest open item, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once) or checkbox (pick-any, with Submit), the "recommended" tag, image-choice grids, attachments, Let the agent decide, Other… and, for errors, Reply… and Dismiss. Arrow keys move between choices. A 409 shows its error. The Changes summary line sits above it.
- **Questions.** The question a transcript bubble picked (`Store.question`), read-only once it is no longer open, or else the selected task's oldest open item, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once) or checkbox (pick-any, with Submit), the "recommended" tag, image-choice grids, attachments, Let the agent decide and Other…. Arrow keys move between choices. A 409 shows its error. The Changes summary line sits above it.
- **Changes, Files, Reviews, Workflow, Project, Status, Settings.** Including the pinned-snapshot stepper, open-diff memory, per-task place in Files, finding diffs with Fix and Ignore, nested subtasks, TODO Start and Discuss, rate-limit pace bars and lead notes. The Workflow tab draws its stage graph natively from the `WorkflowRun` (stages as boxes coloured by state, `after` as edges, laid out top to bottom by depth) with the layout and drawing code from "Diagrams", fed from the run's data rather than Mermaid text.
- **Terminal.** A terminal view attached to `GET /api/tasks/{taskId}/terminal` with `URLSessionWebSocketTask`: binary input and output, `TerminalSize` JSON on resize, close code 4000 for `shell ended: <status>` with New shell, 1011 for a start failure, reconnect after 2 s while the task is active, one socket per open tab and selected task. Colours come from the palette. Ctrl combinations typed in it go to the shell. The cursor is a steady block (`setCursorStyle(.steadyBlock)` when the view is made): SwiftTerm's blinking cursor restarts its Core Animation fade on every keypress, scheduled 0.7 s out, and on a ProMotion (variable refresh rate) display macOS then holds the window's screen update until that fade starts, so each typed letter appears about 0.7 s late. A program in the shell can still ask for a blinking cursor with DECSCUSR.
- **Switcher.** The ⌘K overlay, also opened by clicking the task picker, centred below the picker: tasks with inbox items first, then least recently visited, the selected task last, the `name: title` project prefix, New task, Start in …, Start workflow… and its second step, projects, Escape stepping back, and the list scrolling to keep the highlighted row in view. Picking a task opens its Inbox when it has items, its unread summary when it has one, and its Transcript otherwise.
- **Merge and Discard.** The transcript head's Merge button is enabled once `GET /api/tasks/{taskId}/unmerged` reports commits or files, refetched 1.5 s after a tool result or turn end. `confirmMerge` (`MergeDialog.swift`): the editable commit title (at most 72 characters, kept across a warning link's detour), co-authors, warnings that refetch on the task's snapshot and review events and link to the Reviews or Inbox tab, the parent-or-default-branch choice for subtasks, and Merge now / After review while the latest snapshot's review is running. It is centred in the right pane. Discard confirms below its button, then clears the focused task.
- **Switcher.** The ⌘K overlay, also opened by clicking the task picker, centred below the picker: tasks with inbox items first, then least recently visited, the selected task last, the `name: title` project prefix, New task, Start in …, Start workflow… and its second step, projects, Escape stepping back, and the list scrolling to keep the highlighted row in view. Picking a task opens its Questions tab when it has open items, its unread summary when it has one, and its Transcript otherwise.
- **Merge and Discard.** The transcript head's Merge button is enabled once `GET /api/tasks/{taskId}/unmerged` reports commits or files, refetched 1.5 s after a tool result or turn end. `confirmMerge` (`MergeDialog.swift`): the editable commit title (at most 72 characters, kept across a warning link's detour), co-authors, warnings that refetch on the task's snapshot and review events and link to the Reviews or Questions tab, the parent-or-default-branch choice for subtasks, and Merge now / After review while the latest snapshot's review is running. It is centred in the right pane. Discard confirms below its button, then clears the focused task.
## Markdown
+39 -34
View File
File diff suppressed because one or more lines are too long
+6 -7
View File
@@ -34,14 +34,13 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api.
}
}
snaps, err := m.store.ListSnapshots(ctx, task.Id)
latest, err := m.store.LastSnapshotNot(ctx, task.Id, api.SnapshotTriggerAnswer)
if errors.Is(err, store.ErrNotFound) {
return api.Task{}, fmt.Errorf("%w: task %s has no snapshot under review", ErrConflict, task.Id)
}
if err != nil {
return api.Task{}, err
}
if len(snaps) == 0 {
return api.Task{}, fmt.Errorf("%w: task %s has no snapshot under review", ErrConflict, task.Id)
}
latest := snaps[len(snaps)-1]
reviews, err := m.store.ListReviews(ctx, task.Id)
if err != nil {
return api.Task{}, err
@@ -147,11 +146,11 @@ func (m *Manager) startMergeAfterReview(ctx context.Context, r api.Review, mar s
if task.Discarding {
return "the task is being discarded"
}
snaps, err := m.store.ListSnapshots(ctx, task.Id)
latest, err := m.store.LastSnapshotNot(ctx, task.Id, api.SnapshotTriggerAnswer)
if err != nil {
return err.Error()
}
if len(snaps) == 0 || snaps[len(snaps)-1].N != r.SnapshotN {
if latest.N != r.SnapshotN {
return "a newer snapshot was taken"
}
sess, found, err := m.liveSession(ctx, task.Id)
+2 -4
View File
@@ -85,10 +85,8 @@ func (m *Manager) takeBounce(ctx context.Context, l *live, final string) (bool,
if err != nil {
return false, err
}
for _, item := range open {
if item.Kind == api.Question {
return false, nil
}
if len(open) > 0 {
return false, nil
}
m.mu.Lock()
+32 -43
View File
@@ -127,7 +127,17 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
"attachments": attachments,
}
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, thread, api.Question, store.ToPayload(payload)))
err = m.store.CreateInboxItem(ctx, api.InboxItem{
Id: id,
ProjectId: l.projectID,
TaskId: l.taskID,
AgentSessionId: l.id,
ThreadId: thread,
Payload: store.ToPayload(payload),
State: api.Open,
CreatedAt: time.Now().UTC(),
Responses: []api.InboxResponse{},
})
if err != nil {
return "", err
}
@@ -176,7 +186,7 @@ func (m *Manager) cancelQuestion(ctx context.Context, l *live, raw json.RawMessa
if err != nil {
return "", err
}
if item.AgentSessionId != l.id || item.Kind != api.Question {
if item.AgentSessionId != l.id {
return "", fmt.Errorf("question_id %s is not a question you asked", args.QuestionID)
}
@@ -251,35 +261,18 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
return nil
}
func (m *Manager) fileError(l *live, msg string) {
id := store.NewID()
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.SessionError, map[string]any{"error": msg}))
func (m *Manager) recordError(l *live, msg string) {
err := m.emit(context.Background(), l, "byre", "session.error", map[string]any{"error": msg})
if err != nil {
slog.Error("file session error", "session", l.id, "err", err)
slog.Error("record session error", "session", l.id, "err", err)
}
}
func (m *Manager) fileRateLimited(l *live, info map[string]any, failure string) {
id := store.NewID()
func (m *Manager) recordRateLimited(l *live, info map[string]any, failure string) {
payload := map[string]any{"vendor": string(l.vendor), "rate_limit": info, "error": failure}
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.RateLimited, payload))
err := m.emit(context.Background(), l, "byre", "session.rate_limited", payload)
if err != nil {
slog.Error("file rate limit", "session", l.id, "err", err)
}
}
func (m *Manager) newItem(l *live, id, thread string, kind api.InboxItemKind, payload map[string]any) api.InboxItem {
return api.InboxItem{
Id: id,
ProjectId: l.projectID,
TaskId: l.taskID,
AgentSessionId: l.id,
ThreadId: thread,
Kind: kind,
Payload: payload,
State: api.Open,
CreatedAt: time.Now().UTC(),
Responses: []api.InboxResponse{},
slog.Error("record rate limit", "session", l.id, "err", err)
}
}
@@ -326,6 +319,12 @@ func (m *Manager) Deliver(ctx context.Context, sessionID string) error {
return err
}
target, err := m.sessionTarget(ctx, sess, task)
if err != nil {
return err
}
snapshotN := m.snapshotForAnswer(ctx, target, false)
images := []string{}
for _, p := range pending {
images = append(images, p.Response.Images...)
@@ -350,8 +349,8 @@ func (m *Manager) Deliver(ctx context.Context, sessionID string) error {
pid = l.sess.pid()
exited = l.sess.exited()
}
slog.Info("deliver sent", "session", sessionID, "state", after.State, "pid", pid, "exited", exited, "closing", closing, "responses", ids)
return m.store.MarkDelivered(ctx, ids)
slog.Info("deliver sent", "session", sessionID, "state", after.State, "pid", pid, "exited", exited, "closing", closing, "responses", ids, "snapshot", snapshotN)
return m.store.MarkDelivered(ctx, ids, snapshotN)
}
type midTurn struct {
@@ -454,7 +453,8 @@ func (m *Manager) responsesMidTurn(ctx context.Context, l *live) (string, []stri
return "", nil
}
err = m.store.MarkDelivered(ctx, ids)
snapshotN := m.snapshotForAnswer(ctx, liveTarget(l), m.ownsTask(l.id))
err = m.store.MarkDelivered(ctx, ids, snapshotN)
if err != nil {
slog.Error("mark responses delivered", "session", l.id, "err", err, "responses", ids)
return "", nil
@@ -465,7 +465,7 @@ func (m *Manager) responsesMidTurn(ctx context.Context, l *live) (string, []stri
if err != nil {
slog.Error("record mid-turn delivery", "session", l.id, "err", err)
}
slog.Info("deliver sent mid-turn", "session", l.id, "responses", ids)
slog.Info("deliver sent mid-turn", "session", l.id, "responses", ids, "snapshot", snapshotN)
return text, ids
}
@@ -485,14 +485,7 @@ func deliveryMessage(pending []store.Pending) string {
r := p.Response
b.WriteString("\n")
switch p.Item.Kind {
case api.Question:
fmt.Fprintf(b, "Your question %s: %s\n", p.Item.Id, p.Item.Payload["question"])
case api.SessionError:
fmt.Fprintf(b, "Your error %s: %s\n", p.Item.Id, p.Item.Payload["error"])
case api.RateLimited:
fmt.Fprintf(b, "Your rate-limited turn %s: %s\n", p.Item.Id, p.Item.Payload["error"])
}
fmt.Fprintf(b, "Your question %s: %s\n", p.Item.Id, p.Item.Payload["question"])
switch r.Type {
case api.Option:
@@ -512,12 +505,8 @@ func deliveryMessage(pending []store.Pending) string {
if r.Text != nil {
text = *r.Text
}
if p.Item.Kind == api.Question {
fmt.Fprintf(b, "%s answered (none of your options): %s\n", r.User, text)
fmt.Fprintf(b, "If this asks you something back, answer by calling ask_human with thread_id %q.\n", p.Item.ThreadId)
} else {
fmt.Fprintf(b, "%s replied: %s\n", r.User, text)
}
fmt.Fprintf(b, "%s answered (none of your options): %s\n", r.User, text)
fmt.Fprintf(b, "If this asks you something back, answer by calling ask_human with thread_id %q.\n", p.Item.ThreadId)
}
if len(r.Images) > 0 {
+33 -34
View File
@@ -15,13 +15,13 @@ import (
)
const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's app. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions and errors that task agents file, turns they finish with their final message, questions the human answered, tasks created, reviews started and finished, starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. The states are running, idle, review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, questions the human answered, tasks created, reviews started and finished, starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. The states are running, idle, review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Don't respond to what the human just did. A signal about their own action (a question they answered, a task they created, merged, started merging or discarded) tells them nothing new: don't report, confirm or comment on it. Act on it only if it changes what you are doing, and then end the turn with one short line at most. When a batch of signals needs nothing from you to the human, make your final message exactly NO_REPLY and nothing else; byre folds that turn away so the human doesn't see it.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions and errors, then finished turns, then tasks ready to merge), with one line saying what it is. If nothing needs them, show that project's project tab, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/3f2a9c1b7d4e/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors on the task's transcript tab, then finished turns, then tasks ready to merge), with one line saying what it is. If nothing needs them, show that project's project tab, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/3f2a9c1b7d4e/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- Whenever you suggest new work that isn't in TODO.md (a task you think is worth doing, an improvement to byre or to your own role), write it as a link to #/suggest/<project_id>/<title>/<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/3f2a9c1b7d4e/Lead%20tool%20for%20admin%20status/Add%20a%20lead%20tool%20that%20reads%20%2Fapi%2Fadmin%2Fstatus%20...). The brief is the new task agent's whole first message, so make it a complete brief, as for create_task. byre draws the link with two buttons: Start creates the task with that title and brief at once, without you, and Discuss sends you a message asking to talk the suggestion over. Once an active task has that title, it shows the task's state instead. Don't ask whether to start a suggestion; the buttons do that.
- Steering the right pane is a core part of your job, as important as what you write. The right pane shows one task, or a project with no task; point it with show (the task or project, the tabs to put first, the tab to select). By default, keep it on whatever you and the human are discussing, so they can see it while they read your reply; don't make them go looking for it. Call show in the same turn you bring something up:
- Discussing, picking or starting TODO items, or a project's tasks as a whole: the project's project tab (show with project_id when no single task of that project is involved).
- A question that needs the human: that task's inbox tab, with one line naming it and what you recommend.
- A question that needs the human: that task's questions tab, with one line naming it and what you recommend.
- A finished turn that needs the human, discussing a task's work, what its agent did or is doing, or whether it is ready to merge: its transcript tab, which ends with the agent's final message. A specific file in its worktree: its files tab.
- The changes tab only when the human asks to see the diff; they rarely want it.
- Show, don't tell. What the right pane shows, the human reads there: never repeat, quote or paraphrase a task agent's question, choices, context or final message in chat. Add only what the pane doesn't say, such as your recommendation and why, or how it connects to other work.
@@ -34,7 +34,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine.
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. Every stage of a workflow runs as its own task on the stage's model, once the stages it comes after have finished, so stages fan out (several stages after one) and fan in (one stage after several); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and when the final stage finishes byre discards the run's other stage tasks, leaving the final one to merge. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix".
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer, link to #/task/<task_id>/inbox; at a finished turn, to #/task/<task_id>/transcript.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer, link to #/task/<task_id>/questions; at a finished turn, to #/task/<task_id>/transcript.
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
- You are long-running. Your context window is large. byre sends you a signal once your context passes half of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff.
- Don't commit, push, or touch git branches yourself.`
@@ -326,6 +326,10 @@ func (m *Manager) Observe(events []api.Event) {
if ev.TaskId == nil || !humanActor(ev.Actor) {
continue
}
case "session.error", "session.rate_limited":
if ev.TaskId == nil {
continue
}
default:
continue
}
@@ -473,6 +477,12 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
}
return m.itemText(ctx, item), nil
case "session.error":
return fmt.Sprintf("%s hit an error: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["error"]), nil
case "session.rate_limited":
return fmt.Sprintf("%s was rate limited: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["error"]), nil
case "task.created":
task := api.Task{}
err := remarshal(ev.Payload, &task)
@@ -547,34 +557,26 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
func (m *Manager) itemText(ctx context.Context, item api.InboxItem) string {
label := m.taskLabel(ctx, item.TaskId)
p := item.Payload
switch item.Kind {
case api.Question:
b := &strings.Builder{}
fmt.Fprintf(b, "%s asked question %s: %v\n", label, item.Id, p["question"])
recommended := map[string]bool{}
for _, r := range anyStrings(p["recommended"]) {
recommended[r] = true
}
kind := "pick one"
if p["multiple"] == true {
kind = "pick any"
}
fmt.Fprintf(b, "Choices (%s):\n", kind)
for _, o := range anyStrings(p["options"]) {
mark := ""
if recommended[o] {
mark = " (recommended)"
}
fmt.Fprintf(b, "- %s%s\n", clip(o, 300), mark)
}
fmt.Fprintf(b, "Blocked on: %v\nThe full context is in read_inbox.", p["why_blocked"])
return b.String()
case api.SessionError:
return fmt.Sprintf("%s hit an error (item %s): %v", label, item.Id, p["error"])
case api.RateLimited:
return fmt.Sprintf("%s was rate limited (item %s): %v", label, item.Id, p["error"])
b := &strings.Builder{}
fmt.Fprintf(b, "%s asked question %s: %v\n", label, item.Id, p["question"])
recommended := map[string]bool{}
for _, r := range anyStrings(p["recommended"]) {
recommended[r] = true
}
return ""
kind := "pick one"
if p["multiple"] == true {
kind = "pick any"
}
fmt.Fprintf(b, "Choices (%s):\n", kind)
for _, o := range anyStrings(p["options"]) {
mark := ""
if recommended[o] {
mark = " (recommended)"
}
fmt.Fprintf(b, "- %s%s\n", clip(o, 300), mark)
}
fmt.Fprintf(b, "Blocked on: %v\nThe full context is in read_inbox.", p["why_blocked"])
return b.String()
}
func (m *Manager) answeredText(ctx context.Context, item api.InboxItem) string {
@@ -594,9 +596,6 @@ func (m *Manager) answeredText(ctx context.Context, item api.InboxItem) string {
if r.Text != nil && strings.TrimSpace(*r.Text) != "" {
answer = strings.TrimSpace(answer + " " + *r.Text)
}
if item.Kind != api.Question {
return fmt.Sprintf("%s: %s answered %s %s: %s", m.taskLabel(ctx, item.TaskId), r.User, item.Kind, item.Id, clip(answer, 600))
}
recommended := strings.Join(anyStrings(item.Payload["recommended"]), "; ")
return fmt.Sprintf("%s: %s answered question %s %q: %s\nThe agent recommended: %s", m.taskLabel(ctx, item.TaskId), r.User, item.Id, clip(fmt.Sprint(item.Payload["question"]), 300), clip(answer, 600), clip(recommended, 600))
}
+19 -15
View File
@@ -36,7 +36,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return append([]mcp.Tool{
tool("list_tasks",
"List every project and its active tasks (id, title, worktree path, the agent's state and model, and open inbox items), "+
"List every project and its active tasks (id, title, worktree path, the agent's state and model, and open questions), "+
"and the titles of its TODO.md items on the default branch that no active task was started from.",
`{"type": "object", "properties": {}}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
@@ -44,7 +44,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}),
tool("read_transcript",
"Read a task's transcript: messages, tool calls (briefly), tool results (cut short), turn ends and inbox filings, each with its event seq. "+
"Read a task's transcript: messages, tool calls (briefly), tool results (cut short), turn ends, questions asked, errors and rate limits, each with its event seq. "+
"Without after_seq it returns the last limit events; with it, the events after that seq.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
@@ -77,7 +77,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}),
tool("read_inbox",
"Read open inbox items as JSON, with each question's full context, choices and responses. Give task_id for one task's items.",
"Read open questions as JSON, with each one's full context, choices and responses. Give task_id for one task's questions.",
`{"type": "object", "properties": {"task_id": {"type": "string"}}}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
args := struct {
@@ -110,12 +110,12 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}),
tool("answer_question",
"Respond to an open inbox item on the human's behalf. Only when the answer is clear or the human told you it. "+
"Answer an open question on the human's behalf. Only when the answer is clear or the human told you it. "+
"type option: options holds the chosen choices' exact text (one for pick-one; pick-any may add text). "+
"type other: free text to a question or an error. type dismiss: clears an error.",
"type other: free text.",
`{"type": "object", "properties": {
"item_id": {"type": "string"},
"type": {"type": "string", "enum": ["option", "other", "dismiss"]},
"type": {"type": "string", "enum": ["option", "other"]},
"options": {"type": "array", "items": {"type": "string"}},
"text": {"type": "string"}
}, "required": ["item_id", "type"]}`,
@@ -388,7 +388,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"Merge a task as the human: its agent merges the default branch in, then byre squashes it into one commit, pushes it to origin and closes the task. "+
"While another task in the project is merging, byre holds the merge and starts it when that one is done. "+
"For a subtask, target parent instead asks its parent task's agent to merge it into the parent's branch, and the subtask closes once the parent has. "+
"Refused for a task with active subtasks. Only when the human asks. You get a signal when it is merged; failures are filed in the task's inbox.",
"Refused for a task with active subtasks. Only when the human asks. You get a signal when it is merged; failures show as errors in the task's transcript, and you get a signal for each.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"target": {"type": "string", "enum": ["main", "parent"], "description": "Default main."}
@@ -458,18 +458,18 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
tool("show",
"Point the human's right pane at a task, or at a project with no task: tabs are the tabs to put first, in order (the rest follow), and tab is the one to select. "+
"Whenever the human asks you to show something (\"show me that when you can\") or you want to show something, call show right away; never hold back because you expect a refusal, since only a refused show arranges byre's signal to try again. "+
"Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (a shell in the task's worktree), inbox (the task's open items), reviews (the task's reviews by reviewer agents), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
"Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (a shell in the task's worktree), questions (the task's oldest open question), reviews (the task's reviews by reviewer agents), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
"Omit both task_id and project_id to clear the pane. "+
"If the pane already shows that task (or project) on that tab, only the tab order changes and you are told so, even while the human is working in the pane. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; if a show was refused for it, byre signals you \"The move hold is over\" when those 10 seconds pass without another move, and sends no signal otherwise. "+
"Otherwise refused while the human is working in the right pane (focused in it, or with unsent text in it), unless it is on the project tab, or what it shows is finished (a merged or discarded task, or an inbox tab with nothing left), "+
"Otherwise refused while the human is working in the right pane (focused in it, or with unsent text in it), unless it is on the project tab, or what it shows is finished (a merged or discarded task, or a questions tab with nothing left open),"+
"or it is on the transcript tab of a task whose agent is mid-turn, the human has no unsent text there, and either the human sent the message that started that turn since the pane last moved or the pane moved there 10 seconds ago or more; byre signals you \"Your right pane is free\" when either happens. "+
"Also refused while the pane is on the project tab and the human started a TODO item there in the last 10 seconds, unless they switched task or tab or messaged you since; if a show was refused for it, byre signals you \"The start hold is over\" when those 10 seconds pass with the pane still on the project tab, and sends no signal otherwise.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"project_id": {"type": "string", "description": "Ignored when task_id is given."},
"tabs": {"type": "array", "items": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"]}},
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"]}
"tabs": {"type": "array", "items": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "questions", "reviews", "workflow", "project"]}},
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "questions", "reviews", "workflow", "project"]}
}, "required": ["tabs", "tab"]}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
f := api.Focus{}
@@ -640,7 +640,7 @@ func sameView(a, b api.Focus) bool {
return deref(a.ProjectId) == deref(b.ProjectId)
}
const inboxEmptied = "has nothing left in its inbox"
const inboxEmptied = "has no open questions left"
func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error) {
if f.TaskId == nil {
@@ -656,7 +656,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error)
if task.Merging {
return "is merging", nil
}
if f.Tab != api.TabInbox {
if f.Tab != api.TabQuestions {
return "", nil
}
items, err := m.store.ListInbox(ctx, store.InboxFilter{State: api.Open, TaskID: task.Id})
@@ -883,7 +883,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
open++
}
}
fmt.Fprintf(b, "- %s %q: %s; inbox %d open; worktree %s", t.Id, t.Title, agent, open, t.WorktreePath)
fmt.Fprintf(b, "- %s %q: %s; %d open questions; worktree %s", t.Id, t.Title, agent, open, t.WorktreePath)
if t.TodoItem != nil {
fmt.Fprintf(b, "; from TODO item %q", *t.TodoItem)
}
@@ -1023,7 +1023,11 @@ func transcriptLine(ev api.Event, root string) string {
}
return "turn ended"
case "inbox.created":
return fmt.Sprintf("filed %s %s", text("kind"), text("id"))
return fmt.Sprintf("asked question %s", text("id"))
case "session.error":
return "error: " + clip(text("error"), 300)
case "session.rate_limited":
return "rate limited: " + clip(text("error"), 300)
case "session.created":
return fmt.Sprintf("session started by %s", ev.Actor)
case "session.state":
+5 -5
View File
@@ -889,7 +889,7 @@ func (m *Manager) pump(l *live) {
if m.leadsTask(ctx, l.taskID, l.id) {
m.abandonMerge(l.taskID, l.projectID)
}
m.fileError(l, "The agent process exited mid-turn. Send a message to resume it.")
m.recordError(l, "The agent process exited mid-turn. Send a message to resume it.")
}
}
@@ -1104,7 +1104,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
sha, err := git.CommitAll(l.dir, commitMessage(res), l.gitName, l.gitEmail)
if err != nil {
slog.Error("commit turn", "session", l.id, "err", err)
m.fileError(l, fmt.Sprintf("byre couldn't commit the turn's work, so the turn counts as unfinished: %v", err))
m.recordError(l, fmt.Sprintf("byre couldn't commit the turn's work, so the turn counts as unfinished: %v", err))
finished = false
if owner {
err = git.AbortMerge(l.dir)
@@ -1152,9 +1152,9 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
switch {
case !res.isError || interrupted:
case rateLimited != nil:
m.fileRateLimited(l, rateLimited, failure)
m.recordRateLimited(l, rateLimited, failure)
default:
m.fileError(l, failure)
m.recordError(l, failure)
}
stageFinished := false
@@ -1217,7 +1217,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
case mergeBy != "":
go m.releaseMerge(l.projectID, l.taskID)
if !res.isError || interrupted {
m.fileError(l, "The Merge turn ended unfinished (interrupted, or with no final message), so byre didn't squash or push the task. Merge again to retry.")
m.recordError(l, "The Merge turn ended unfinished (interrupted, or with no final message), so byre didn't squash or push the task. Merge again to retry.")
}
default:
go m.nextMerge(l.projectID)
+3 -3
View File
@@ -392,7 +392,7 @@ func (m *Manager) fileTaskError(ctx context.Context, task api.Task, sessions []a
return
}
last := sessions[len(sessions)-1]
m.fileError(&live{id: last.Id, taskID: task.Id, projectID: task.ProjectId}, msg)
m.recordError(&live{id: last.Id, taskID: task.Id, projectID: task.ProjectId}, msg)
}
func (m *Manager) takeMerge(taskID string) string {
@@ -416,14 +416,14 @@ func (m *Manager) finishMerge(l *live, login string) {
slog.Error("finish merge", "task", l.taskID, "err", err)
unfinished := &unfinishedMergeError{}
if errors.As(err, &unfinished) {
m.fileError(l, "Merging the task failed: "+err.Error()+".")
m.recordError(l, "Merging the task failed: "+err.Error()+".")
return
}
cerr := m.store.ClearFinalizing(ctx, l.taskID)
if cerr != nil {
slog.Error("clear merge finalizing", "task", l.taskID, "err", cerr)
}
m.fileError(l, "Merging the task failed: "+err.Error())
m.recordError(l, "Merging the task failed: "+err.Error())
err = m.Deliver(ctx, l.id)
if err != nil {
slog.Error("deliver inbox responses", "session", l.id, "err", err)
+2 -8
View File
@@ -69,15 +69,9 @@ func (m *Manager) mergeNote(ctx context.Context, task api.Task, login string) (s
}
}
decisions := []api.InboxItem{}
for _, item := range answered {
if item.Kind == api.Question {
decisions = append(decisions, item)
}
}
if len(decisions) > 0 {
if len(answered) > 0 {
b.WriteString("\n## Decisions\n")
for _, item := range decisions {
for _, item := range answered {
fmt.Fprintf(b, "\n### %s\n", item.Payload["question"])
background, _ := item.Payload["context"].(string)
if strings.TrimSpace(background) != "" {
-13
View File
@@ -108,16 +108,12 @@ func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
text = strings.TrimSpace(*c.Text)
}
images := c.Images != nil && len(*c.Images) > 0
question := item.Kind == api.Question
bad := func(format string, args ...any) (api.InboxState, error) {
return "", fmt.Errorf("%w: item %s: "+format, append([]any{ErrInvalid, item.Id}, args...)...)
}
switch c.Type {
case api.Option:
if !question {
return bad("only questions have options")
}
if c.Options == nil || len(*c.Options) == 0 {
return bad("option needs the chosen options")
}
@@ -144,15 +140,6 @@ func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
return bad("other needs text or images")
}
return api.Answered, nil
case api.Dismiss:
if question {
return bad("questions need an option or other, not a dismissal")
}
if images {
return bad("a dismissal takes no images")
}
return api.Dismissed, nil
}
return bad("unknown response type %q", c.Type)
+41 -5
View File
@@ -145,7 +145,11 @@ func (m *Manager) takeSnapshot(ctx context.Context, t snapTarget, trigger api.Sn
if err != nil && !errors.Is(err, store.ErrNotFound) {
return nil, err
}
if err == nil && last.TreeSha == tree && !force {
reviewed, err := m.store.LastSnapshotNot(ctx, t.taskID, api.SnapshotTriggerAnswer)
if err != nil && !errors.Is(err, store.ErrNotFound) {
return nil, err
}
if err == nil && reviewed.TreeSha == tree && !force {
return nil, nil
}
@@ -179,7 +183,7 @@ func (m *Manager) takeSnapshot(ctx context.Context, t snapTarget, trigger api.Sn
}
func (m *Manager) RequestSnapshot(ctx context.Context, taskID string) error {
task, project, err := m.activeTask(ctx, taskID)
task, _, err := m.activeTask(ctx, taskID)
if err != nil {
return err
}
@@ -204,8 +208,7 @@ func (m *Manager) RequestSnapshot(ctx context.Context, taskID string) error {
if len(sessions) == 0 {
return fmt.Errorf("%w: task %s has no agent session to attribute a snapshot to", ErrConflict, task.Id)
}
sess := sessions[len(sessions)-1]
creator, err := m.store.GetUser(ctx, sess.CreatedBy)
t, err := m.sessionTarget(ctx, sessions[len(sessions)-1], task)
if err != nil {
return err
}
@@ -213,11 +216,44 @@ func (m *Manager) RequestSnapshot(ctx context.Context, taskID string) error {
if err != nil {
return err
}
t := snapTarget{taskID: task.Id, projectID: project.Id, sessionID: sess.Id, dir: task.WorktreePath, gitName: creator.GitName, gitEmail: creator.GitEmail}
m.snapshotSoon(t, api.SnapshotTriggerRequested, 0, true, true)
return nil
}
func (m *Manager) sessionTarget(ctx context.Context, sess api.AgentSession, task api.Task) (snapTarget, error) {
creator, err := m.store.GetUser(ctx, sess.CreatedBy)
if err != nil {
return snapTarget{}, err
}
return snapTarget{taskID: task.Id, projectID: task.ProjectId, sessionID: sess.Id, dir: task.WorktreePath, gitName: creator.GitName, gitEmail: creator.GitEmail}, nil
}
func (m *Manager) snapshotForAnswer(ctx context.Context, t snapTarget, held bool) int {
if !held {
unlock := m.lockTask(t.taskID)
defer unlock()
}
m.snapMu.Lock()
st := m.snapStateLocked(t.taskID)
m.snapMu.Unlock()
st.mu.Lock()
defer st.mu.Unlock()
m.snapMu.Lock()
closed := st.closed
m.snapMu.Unlock()
if closed {
return 0
}
snap, err := m.takeSnapshot(ctx, t, api.SnapshotTriggerAnswer, 0, true)
if err != nil {
slog.Error("take snapshot", "task", t.taskID, "trigger", api.SnapshotTriggerAnswer, "err", err)
m.recordError(&live{id: t.sessionID, taskID: t.taskID, projectID: t.projectID}, fmt.Sprintf("byre couldn't snapshot the worktree before handing over an answer, so the answer went to the agent without one: %v", err))
return 0
}
return snap.N
}
func (m *Manager) takeSnapshotTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "take_snapshot",
+1 -1
View File
@@ -111,7 +111,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool {
}),
tool("read_subtask",
"Read a subtask's transcript: messages, tool calls (briefly), tool results (cut short), turn ends and inbox filings, each with its event seq. "+
"Read a subtask's transcript: messages, tool calls (briefly), tool results (cut short), turn ends, questions asked, errors and rate limits, each with its event seq. "+
"Without after_seq it returns the last limit events; with it, the events after that seq.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
+1 -1
View File
@@ -315,7 +315,7 @@ func (m *Manager) advanceStage(l *live) {
_, err := m.nextStage(ctx, l.id)
if err != nil {
slog.Error("advance workflow stage", "session", l.id, "err", err)
m.fileError(l, fmt.Sprintf("The workflow didn't move on from this stage: %v. byre tries again when it next starts.", err))
m.recordError(l, fmt.Sprintf("The workflow didn't move on from this stage: %v. byre tries again when it next starts.", err))
}
m.afterTurn(ctx, l)
}
+277 -297
View File
@@ -231,39 +231,15 @@ func (e FindingState) Valid() bool {
}
}
// Defines values for InboxItemKind.
const (
Question InboxItemKind = "question"
RateLimited InboxItemKind = "rate_limited"
SessionError InboxItemKind = "session_error"
)
// Valid indicates whether the value is a known member of the InboxItemKind enum.
func (e InboxItemKind) Valid() bool {
switch e {
case Question:
return true
case RateLimited:
return true
case SessionError:
return true
default:
return false
}
}
// Defines values for InboxResponseType.
const (
Dismiss InboxResponseType = "dismiss"
Option InboxResponseType = "option"
Other InboxResponseType = "other"
Option InboxResponseType = "option"
Other InboxResponseType = "other"
)
// Valid indicates whether the value is a known member of the InboxResponseType enum.
func (e InboxResponseType) Valid() bool {
switch e {
case Dismiss:
return true
case Option:
return true
case Other:
@@ -323,6 +299,7 @@ func (e ReviewState) Valid() bool {
// Defines values for SnapshotTrigger.
const (
SnapshotTriggerAnswer SnapshotTrigger = "answer"
SnapshotTriggerAskHuman SnapshotTrigger = "ask_human"
SnapshotTriggerMessage SnapshotTrigger = "message"
SnapshotTriggerRequested SnapshotTrigger = "requested"
@@ -334,6 +311,8 @@ const (
// Valid indicates whether the value is a known member of the SnapshotTrigger enum.
func (e SnapshotTrigger) Valid() bool {
switch e {
case SnapshotTriggerAnswer:
return true
case SnapshotTriggerAskHuman:
return true
case SnapshotTriggerMessage:
@@ -464,8 +443,8 @@ const (
TabBoard TaskTab = "board"
TabChanges TaskTab = "changes"
TabFiles TaskTab = "files"
TabInbox TaskTab = "inbox"
TabProject TaskTab = "project"
TabQuestions TaskTab = "questions"
TabReviews TaskTab = "reviews"
TabTerminal TaskTab = "terminal"
TabTranscript TaskTab = "transcript"
@@ -481,10 +460,10 @@ func (e TaskTab) Valid() bool {
return true
case TabFiles:
return true
case TabInbox:
return true
case TabProject:
return true
case TabQuestions:
return true
case TabReviews:
return true
case TabTerminal:
@@ -866,14 +845,13 @@ type Focus struct {
TaskId *string `json:"task_id,omitempty"`
}
// InboxItem defines model for InboxItem.
// InboxItem A question an agent asked the human with ask_human.
type InboxItem struct {
AgentSessionId string `json:"agent_session_id"`
CreatedAt time.Time `json:"created_at"`
Id string `json:"id"`
Kind InboxItemKind `json:"kind"`
AgentSessionId string `json:"agent_session_id"`
CreatedAt time.Time `json:"created_at"`
Id string `json:"id"`
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
// Payload context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments.
Payload map[string]interface{} `json:"payload"`
ProjectId string `json:"project_id"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
@@ -884,9 +862,6 @@ type InboxItem struct {
ThreadId string `json:"thread_id"`
}
// InboxItemKind defines model for InboxItem.Kind.
type InboxItemKind string
// InboxRespond defines model for InboxRespond.
type InboxRespond struct {
Responses []InboxResponseCreate `json:"responses"`
@@ -900,12 +875,15 @@ type InboxResponse struct {
Id string `json:"id"`
// Images Blob ids sent to the agent as images alongside the response.
Images []string `json:"images"`
ItemId string `json:"item_id"`
Options *[]string `json:"options,omitempty"`
Text *string `json:"text,omitempty"`
Type InboxResponseType `json:"type"`
User string `json:"user"`
Images []string `json:"images"`
ItemId string `json:"item_id"`
Options *[]string `json:"options,omitempty"`
// SnapshotN The task's snapshot of the worktree as it stood just before the response was handed to the agent; absent until delivered, and when no snapshot could be taken.
SnapshotN *int `json:"snapshot_n,omitempty"`
Text *string `json:"text,omitempty"`
Type InboxResponseType `json:"type"`
User string `json:"user"`
}
// InboxResponseCreate defines model for InboxResponseCreate.
@@ -953,15 +931,15 @@ type MessageCreate struct {
Text string `json:"text"`
}
// NotificationPrefs Which new inbox items and finished turns notify this user in their open byre pages.
// NotificationPrefs Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
type NotificationPrefs struct {
// Errors session_error items.
// Errors session.error events on tasks.
Errors bool `json:"errors"`
// Questions Questions. On by default.
Questions bool `json:"questions"`
// RateLimits rate_limited items.
// RateLimits session.rate_limited events on tasks.
RateLimits bool `json:"rate_limits"`
// Reports Task turns that end with a summary (turn.ended with summary: true).
@@ -1094,11 +1072,11 @@ type Snapshot struct {
TaskId string `json:"task_id"`
TreeSha string `json:"tree_sha"`
// Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds.
// Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds.
Trigger SnapshotTrigger `json:"trigger"`
}
// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds.
// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds.
type SnapshotTrigger string
// Stage defines model for Stage.
@@ -1384,7 +1362,7 @@ type User struct {
GitName string `json:"git_name"`
Login string `json:"login"`
// NotificationPrefs Which new inbox items and finished turns notify this user in their open byre pages.
// NotificationPrefs Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
NotificationPrefs NotificationPrefs `json:"notification_prefs"`
}
@@ -3803,253 +3781,255 @@ 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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",
"ewA=",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+24 -26
View File
@@ -12,14 +12,6 @@ import (
"g.fc.run/misc/byre/internal/api"
)
func DeliverableResponse(t api.InboxResponseType) bool {
switch t {
case api.Option, api.Other:
return true
}
return false
}
func (s *Store) CreateInboxItem(ctx context.Context, item api.InboxItem) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
payload, err := json.Marshal(item.Payload)
@@ -28,9 +20,9 @@ func (s *Store) CreateInboxItem(ctx context.Context, item api.InboxItem) error {
}
_, err = tx.ExecContext(ctx, `
INSERT INTO inbox_items (id, project_id, task_id, agent_session_id, thread_id, kind, payload, state, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, item.Id, item.ProjectId, item.TaskId, item.AgentSessionId, item.ThreadId, string(item.Kind),
INSERT INTO inbox_items (id, project_id, task_id, agent_session_id, thread_id, payload, state, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, item.Id, item.ProjectId, item.TaskId, item.AgentSessionId, item.ThreadId,
string(payload), string(item.State), formatTime(item.CreatedAt))
if err != nil {
return err
@@ -220,10 +212,9 @@ func (s *Store) AwaitingHuman(ctx context.Context, sessionID string) (int, error
n := 0
err := s.db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM inbox_items i
WHERE i.agent_session_id = ? AND i.kind = ? AND i.state = ?
AND NOT EXISTS (SELECT 1 FROM inbox_responses r WHERE r.item_id = i.id AND r.type IN (?, ?))
`, sessionID, string(api.Question), string(api.Open),
string(api.Option), string(api.Other)).Scan(&n)
WHERE i.agent_session_id = ? AND i.state = ?
AND NOT EXISTS (SELECT 1 FROM inbox_responses r WHERE r.item_id = i.id)
`, sessionID, string(api.Open)).Scan(&n)
return n, err
}
@@ -235,9 +226,9 @@ type Pending struct {
func (s *Store) Undelivered(ctx context.Context, sessionID string) ([]Pending, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT r.id, r.item_id FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?)
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL
ORDER BY r.created_at
`, sessionID, string(api.Option), string(api.Other))
`, sessionID)
if err != nil {
return nil, err
}
@@ -274,11 +265,15 @@ func (s *Store) Undelivered(ctx context.Context, sessionID string) ([]Pending, e
return pending, nil
}
func (s *Store) MarkDelivered(ctx context.Context, responseIDs []string) error {
func (s *Store) MarkDelivered(ctx context.Context, responseIDs []string, snapshotN int) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
at := now()
var snap any
if snapshotN > 0 {
snap = snapshotN
}
for _, id := range responseIDs {
_, err := tx.ExecContext(ctx, `UPDATE inbox_responses SET delivered_at = ? WHERE id = ?`, at, id)
_, err := tx.ExecContext(ctx, `UPDATE inbox_responses SET delivered_at = ?, snapshot_n = ? WHERE id = ?`, at, snap, id)
if err != nil {
return err
}
@@ -290,7 +285,7 @@ func (s *Store) MarkDelivered(ctx context.Context, responseIDs []string) error {
func (s *Store) MarkUndelivered(ctx context.Context, responseIDs []string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
for _, id := range responseIDs {
_, err := tx.ExecContext(ctx, `UPDATE inbox_responses SET delivered_at = NULL WHERE id = ?`, id)
_, err := tx.ExecContext(ctx, `UPDATE inbox_responses SET delivered_at = NULL, snapshot_n = NULL WHERE id = ?`, id)
if err != nil {
return err
}
@@ -306,7 +301,6 @@ type querier interface {
func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
item := api.InboxItem{}
kind := ""
payload := ""
state := ""
createdAt := ""
@@ -314,16 +308,15 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
resolvedAt := sql.NullString{}
err := q.QueryRowContext(ctx, `
SELECT id, project_id, task_id, agent_session_id, thread_id, kind, payload, state,
SELECT id, project_id, task_id, agent_session_id, thread_id, payload, state,
created_at, resolved_by, resolved_at
FROM inbox_items WHERE id = ?
`, id).Scan(&item.Id, &item.ProjectId, &item.TaskId, &item.AgentSessionId, &item.ThreadId, &kind,
`, id).Scan(&item.Id, &item.ProjectId, &item.TaskId, &item.AgentSessionId, &item.ThreadId,
&payload, &state, &createdAt, &resolvedBy, &resolvedAt)
if err != nil {
return item, err
}
item.Kind = api.InboxItemKind(kind)
item.State = api.InboxState(state)
err = json.Unmarshal([]byte(payload), &item.Payload)
if err != nil {
@@ -347,7 +340,7 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxResponse, error) {
rows, err := q.QueryContext(ctx, `
SELECT id, item_id, user_login, type, options, text, images, attachments, created_at, delivered_at
SELECT id, item_id, user_login, type, options, text, images, attachments, created_at, delivered_at, snapshot_n
FROM inbox_responses WHERE item_id = ? ORDER BY created_at
`, itemID)
if err != nil {
@@ -365,8 +358,9 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
attachments := ""
createdAt := ""
delivered := sql.NullString{}
snap := sql.NullInt64{}
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &images, &attachments, &createdAt, &delivered)
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &images, &attachments, &createdAt, &delivered, &snap)
if err != nil {
return nil, err
}
@@ -383,6 +377,10 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
if text.Valid {
r.Text = &text.String
}
if snap.Valid {
n := int(snap.Int64)
r.SnapshotN = &n
}
err = json.Unmarshal([]byte(images), &r.Images)
if err != nil {
return nil, err
+8
View File
@@ -62,6 +62,14 @@ func (s *Store) LastSnapshot(ctx context.Context, taskID string) (api.Snapshot,
return snap, err
}
func (s *Store) LastSnapshotNot(ctx context.Context, taskID string, trigger api.SnapshotTrigger) (api.Snapshot, error) {
snap, err := scanSnapshot(s.db.QueryRowContext(ctx, `SELECT `+snapshotColumns+` FROM snapshots WHERE task_id = ? AND trigger != ? ORDER BY n DESC LIMIT 1`, taskID, string(trigger)))
if errors.Is(err, sql.ErrNoRows) {
return snap, ErrNotFound
}
return snap, err
}
func (s *Store) LastTaskSeq(ctx context.Context, taskID string) (int64, error) {
seq := int64(0)
err := s.db.QueryRowContext(ctx, `SELECT COALESCE(MAX(seq), 0) FROM events WHERE task_id = ?`, taskID).Scan(&seq)
+17
View File
@@ -479,6 +479,23 @@ ALTER TABLE users ADD COLUMN default_model TEXT NOT NULL DEFAULT 'claude-opus-5-
`}, {"add-team-members", `
ALTER TABLE agent_sessions ADD COLUMN member TEXT NOT NULL DEFAULT '';
ALTER TABLE task_inputs ADD COLUMN member TEXT NOT NULL DEFAULT '';
`}, {"rename-inbox-tab", `
UPDATE users SET focus = replace(focus, '"inbox"', '"questions"');
`}, {"add-inbox-response-snapshots", `
ALTER TABLE inbox_responses ADD COLUMN snapshot_n INTEGER;
`}, {"errors-to-transcript", `
UPDATE events SET kind = 'session.error', payload = json_object('error', json_extract(payload, '$.payload.error'))
WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'session_error';
UPDATE events SET kind = 'session.rate_limited', payload = json_object(
'vendor', json_extract(payload, '$.payload.vendor'),
'rate_limit', json(json_extract(payload, '$.payload.rate_limit')),
'error', json_extract(payload, '$.payload.error'))
WHERE kind = 'inbox.created' AND json_extract(payload, '$.kind') = 'rate_limited';
DELETE FROM events WHERE kind IN ('inbox.created', 'inbox.updated') AND json_extract(payload, '$.kind') != 'question';
UPDATE events SET payload = json_remove(payload, '$.kind') WHERE kind IN ('inbox.created', 'inbox.updated');
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind != 'question');
DELETE FROM inbox_items WHERE kind != 'question';
ALTER TABLE inbox_items DROP COLUMN kind;
`}}
const (
+67 -2
View File
@@ -1019,10 +1019,73 @@ struct RowView: View {
case .snapshot(let snap):
SnapshotRow(snap: snap, review: row.review, taskId: row.taskId)
case .question:
if let item = row.question {
QuestionBubble(item: item)
}
}
}
}
private struct QuestionBubble: View {
let item: InboxItem
@Environment(Store.self) private var store
@ViewState private var hover = false
var body: some View {
let open = item.state == .open
let tone = open ? Palette.warn : Palette.muted
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text("asked")
WhenText(when: item.createdAt)
}
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
MarkdownView(blocks: parseMarkdown(jsString(item.payload.value["question"] ?? "")), ctx: MDContext(taskId: item.taskId))
Text(status)
.font(Fonts.font(11, weight: open ? 700 : 400))
.foregroundStyle(tone)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(open ? Palette.warnTint : Palette.surface, in: RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(hover ? Palette.accent : open ? Palette.warn.opacity(0.55) : Palette.border))
.contentShape(Rectangle())
.onTapGesture {
store.showQuestion(item.taskId, item.id)
}
.onHover { hover = $0 }
.help("Show this question in the Questions tab")
}
private var status: String {
let answer = item.responses.last
switch item.state {
case .open:
return "waiting for your answer"
case .dismissed:
return item.resolvedBy == nil ? "withdrawn by the agent" : "dismissed by \(item.resolvedBy!)"
default:
guard let answer else {
return item.state.rawValue
}
if let options = answer.options, !options.isEmpty {
return "\(answer.user) chose: \(options.joined(separator: "; "))"
}
return "\(answer.user) answered: \(firstLine(answer.text ?? ""))"
}
}
private func firstLine(_ s: String) -> String {
let line = trimmed(s).components(separatedBy: "\n").first ?? ""
return line.count > 120 ? "\(line.prefix(120))…" : line
}
}
private struct ToolResult: View {
let result: String?
let unfinished: Bool
@@ -1109,7 +1172,7 @@ struct NoteView: View {
@ViewState private var cancelling = false
var body: some View {
let color = note.style == .error ? Palette.danger : note.style == .ask ? Palette.accent : Palette.muted
let color = note.style == .error ? Palette.danger : Palette.muted
HStack(alignment: .firstTextBaseline, spacing: 12) {
Text(text())
.font(Fonts.font(11))
@@ -1216,7 +1279,9 @@ private struct SnapshotRow: View {
.buttonStyle(SmallLinkButtonStyle())
.disabled(forking)
.help("Start a new task from this snapshot, its agent continuing the conversation as it stood here")
reviewButton
if snap.trigger != .answer {
reviewButton
}
Rectangle().fill(Palette.border).frame(height: 1)
}
.font(Fonts.font(11))
-2
View File
@@ -513,8 +513,6 @@ struct StateMark: View {
Spinner(color: color)
case .answer:
StateDot(filled: Palette.warn)
case .error:
StateDot(filled: Palette.danger)
default:
StateDot(filled: nil)
}
+3 -3
View File
@@ -160,7 +160,7 @@ private func reviewWarnings(_ taskId: String, _ store: Store, _ leave: @escaping
async let snapsCall = client.listSnapshots(path: .init(taskId: taskId)).ok.body.json
async let reviewsCall = client.listReviews(path: .init(taskId: taskId)).ok.body.json
let (snaps, reviews) = try await (snapsCall, reviewsCall)
guard let latest = snaps.last else {
guard let latest = snaps.last(where: { $0.trigger != .answer }) else {
return ([MergeWarning(text: "These changes haven't been reviewed")], false)
}
var items: [MergeWarning] = []
@@ -226,14 +226,14 @@ private struct MergeContent: View {
@FocusState private var focused: Bool
var body: some View {
let questions = store.inbox.filter { $0.taskId == model.taskId && $0.kind == .question }.count
let questions = store.inbox.filter { $0.taskId == model.taskId }.count
var warnings = model.reviewItems
if questions > 0 {
let taskId = model.taskId
let leave = model.leave
warnings.append(MergeWarning(text: questions == 1 ? "The agent's open question will be dismissed" : "The agent's \(questions) open questions will be dismissed", open: {
leave {
store.focusTask(taskId, .inbox)
store.focusTask(taskId, .questions)
}
}))
}
+14 -19
View File
@@ -41,23 +41,6 @@ final class Notifier: NSObject, UNUserNotificationCenterDelegate {
}
}
private func wants(_ p: NotificationPrefs, _ item: InboxItem) -> Bool {
switch item.kind {
case .question: p.questions
case .sessionError: p.errors
case .rateLimited: p.rateLimits
}
}
private func itemText(_ item: InboxItem) -> String {
let p = item.payload.value
switch item.kind {
case .question: return str(p, "question") ?? ""
case .sessionError: return "Error: \(str(p, "error") ?? "")"
case .rateLimited: return "Rate limited"
}
}
@MainActor
private var queue: Task<Void, Never>?
@@ -94,10 +77,22 @@ private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: Strin
@MainActor
func notifyInbox(_ store: Store, _ item: InboxItem) {
guard let me = store.me, wants(me.notificationPrefs, item) else {
guard let me = store.me, me.notificationPrefs.questions else {
return
}
show(store, item.taskId, item.id, itemText(item))
show(store, item.taskId, item.id, str(item.payload.value, "question") ?? "")
}
@MainActor
func notifyError(_ store: Store, _ ev: ByreEvent) {
guard let me = store.me, let taskId = ev.taskId, !taskId.isEmpty else {
return
}
let limited = ev.kind == "session.rate_limited"
if !(limited ? me.notificationPrefs.rateLimits : me.notificationPrefs.errors) {
return
}
show(store, taskId, "error-\(ev.seq)", limited ? "Rate limited" : "Error: \(str(ev.payload.value, "error") ?? "")")
}
@MainActor
+8 -8
View File
@@ -27,7 +27,7 @@ let tabLabels: [TaskTab: String] = [
.changes: "Changes",
.files: "Files",
.terminal: "Terminal",
.inbox: "Inbox",
.questions: "Questions",
.reviews: "Reviews",
.workflow: "Workflow",
.project: "Project",
@@ -211,8 +211,8 @@ private struct TaskPane: View {
for item in fresh {
seen.insert(item.id)
}
if store.focus.tab == .transcript && !store.paneFocus.drafting && fresh.contains(where: { $0.kind == .question && $0.taskId == store.selectedTaskId }) {
store.focusTab(.inbox)
if store.focus.tab == .transcript && !store.paneFocus.drafting && fresh.contains(where: { $0.taskId == store.selectedTaskId }) {
store.focusTab(.questions)
}
}
}
@@ -231,8 +231,8 @@ private struct TaskPane: View {
FilesView()
case .terminal:
TerminalTab()
case .inbox:
InboxView()
case .questions:
QuestionsView()
case .reviews:
ReviewsView()
case .workflow:
@@ -370,11 +370,11 @@ private struct TabStrip: View {
var reviewing: Bool
func count(_ t: TaskTab) -> Int {
t == .inbox ? waiting : t == .reviews && !reviewing ? findings : 0
t == .questions ? waiting : t == .reviews && !reviewing ? findings : 0
}
func badge(_ t: TaskTab) -> TabBadge {
count(t) > 0 ? .count(count(t), t == .inbox ? "inbox items" : "open findings in the latest review") : .none
count(t) > 0 ? .count(count(t), t == .questions ? "open questions" :"open findings in the latest review") : .none
}
}
@@ -398,7 +398,7 @@ private struct TabStrip: View {
}
switch t {
case .board: return team
case .inbox: return waiting > 0
case .questions: return waiting > 0
case .reviews: return hasReviews
case .workflow: return workflow
default: return true
+2 -2
View File
@@ -186,7 +186,7 @@ private struct TaskRow: View {
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
if count > 0 {
Badge(count: count).help("inbox items")
Badge(count: count).help("open questions")
}
if store.unreadSummary(task) {
Text("new summary")
@@ -197,7 +197,7 @@ private struct TaskRow: View {
}
Text(ts.label)
.font(Fonts.font(11))
.foregroundStyle(ts.kind == .answer ? Palette.warn : ts.kind == .error ? Palette.danger : Palette.muted)
.foregroundStyle(ts.kind == .answer ? Palette.warn : Palette.muted)
.help(ts.title)
}
}
@@ -9,25 +9,27 @@ private let delegateText = "Use your judgment for this decision and continue wit
private let otherStop = "\u{0}other"
private let httpURL = Re(#"^https?://"#, caseless: true)
struct InboxView: View {
struct QuestionsView: View {
@Environment(Store.self) private var store
var body: some View {
let taskId = store.selectedTaskId
let top = store.inbox.filter { $0.taskId == taskId }.sorted { $0.createdAt < $1.createdAt }.first
let picked = store.question.flatMap { $0.taskId == taskId ? store.questionItem($0.itemId) : nil }
let shown = picked ?? store.inbox.filter { $0.taskId == taskId }.sorted { $0.createdAt < $1.createdAt }.first
VStack(spacing: 0) {
ChangesView(compact: true)
if taskId == nil {
EmptyText(text: "No task selected.")
} else if let top {
} else if let shown {
let key = "\(shown.id)/\(shown.state.rawValue)/\(shown.responses.count)"
ScrollView {
InboxCard(item: top)
.id(top.id)
QuestionCard(item: shown)
.id(key)
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 16)
}
.id(top.id)
.id(shown.id)
} else {
EmptyText(text: "Nothing waiting on you in this task.")
}
@@ -121,8 +123,16 @@ private final class CardModel {
bool(payload, "multiple")
}
var answered: Bool {
item.state != .open
}
var chosen: [String] {
item.responses.flatMap { $0.options ?? [] }
}
var checked: [String] {
store.drafts?.checkedAnswer(item.id) ?? []
answered ? chosen : store.drafts?.checkedAnswer(item.id) ?? []
}
var hasImages: Bool {
@@ -146,7 +156,7 @@ private final class CardModel {
}
func pick(_ text: String) {
if busy {
if busy || answered {
return
}
if multiple {
@@ -169,8 +179,11 @@ private final class CardModel {
_ = try await store.api.client.respondInbox(body: .json(.init(responses: [r]))).ok
store.drafts?.clearAnswer(item.id)
store.paneFocus.setDraft(key, false)
if store.question?.itemId == item.id {
store.question = nil
}
let others = store.inbox.contains { $0.taskId == item.taskId && $0.id != item.id }
if !others && store.focus.tab == .inbox && store.selectedTaskId == item.taskId {
if !others && store.focus.tab == .questions && store.selectedTaskId == item.taskId {
store.focusTab(.transcript)
}
} catch {
@@ -191,16 +204,9 @@ private final class CardModel {
submit(InboxResponseCreate(itemId: item.id, _type: .other, text: store.drafts?.textAnswer(item.id) ?? ""), withImages: true)
}
}
func submitOther() {
if typed.isEmpty && !hasImages {
return
}
submit(InboxResponseCreate(itemId: item.id, _type: .other, text: store.drafts?.textAnswer(item.id) ?? ""), withImages: true)
}
}
private struct InboxCard: View {
private struct QuestionCard: View {
let item: InboxItem
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@@ -212,7 +218,7 @@ private struct InboxCard: View {
if let model {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(item.kind.rawValue.replacingOccurrences(of: "_", with: " "))
Text("question\(model.answered ? " · \(item.state.rawValue)" : "")")
Spacer(minLength: 0)
Text(ago(item.createdAt)).help(isoText(item.createdAt))
}
@@ -220,8 +226,10 @@ private struct InboxCard: View {
.foregroundStyle(Palette.muted)
CardBody(model: model, focus: $focus)
Thread(item: item)
CardActions(model: model, focus: $focus)
.padding(.top, 6)
if !model.answered {
CardActions(model: model, focus: $focus)
.padding(.top, 6)
}
}
.onKeyPress(keys: [.downArrow, .upArrow]) { press in
move(model, press.key == .downArrow ? 1 : -1)
@@ -264,32 +272,15 @@ private struct CardBody: View {
let p = model.payload
let item = model.item
VStack(alignment: .leading, spacing: 0) {
switch item.kind {
case .question:
MarkdownView(blocks: model.context, ctx: contextCtx)
.onGeometryChange(for: Bool.self) { $0.size.width > 480 } action: { wide = $0 }
.padding(.vertical, 10)
MarkdownView(blocks: parseMarkdown(jsString(p["question"] ?? "")), ctx: MDContext(taskId: item.taskId))
.padding(.top, 10)
.padding(.bottom, 4)
WhyLine(text: jsString(p["why_blocked"] ?? ""), taskId: item.taskId)
.padding(.bottom, 8)
Attachments(list: attachments(p["attachments"] ?? nil), taskId: item.taskId)
case .sessionError:
Text(jsString(p["error"] ?? ""))
.foregroundStyle(Palette.danger)
.textSelection(.enabled)
.padding(.vertical, 10)
case .rateLimited:
let limit = dict(p, "rate_limit") ?? [:]
let resets = (limit["resetsAt"] as? Double ?? (limit["resetsAt"] as? Int).map(Double.init)).map { Date(timeIntervalSince1970: $0).formatted(date: .numeric, time: .standard) } ?? "unknown"
VStack(alignment: .leading, spacing: 10) {
Text(jsString(p["error"] ?? "")).foregroundStyle(Palette.danger)
Text("\(jsString(p["vendor"] ?? "")) \(limit["rateLimitType"].map { jsString($0) } ?? "") limit resets \(resets)")
}
.textSelection(.enabled)
MarkdownView(blocks: model.context, ctx: contextCtx)
.onGeometryChange(for: Bool.self) { $0.size.width > 480 } action: { wide = $0 }
.padding(.vertical, 10)
}
MarkdownView(blocks: parseMarkdown(jsString(p["question"] ?? "")), ctx: MDContext(taskId: item.taskId))
.padding(.top, 10)
.padding(.bottom, 4)
WhyLine(text: jsString(p["why_blocked"] ?? ""), taskId: item.taskId)
.padding(.bottom, 8)
Attachments(list: attachments(p["attachments"] ?? nil), taskId: item.taskId)
}
}
@@ -350,7 +341,8 @@ private struct ChoiceBox: View {
private func box(_ text: String) -> some View {
let rec = model.recommended.contains(text)
let focused = focus.wrappedValue == text
let lit = (hover && !model.multiple) || focused
let picked = model.answered && model.chosen.contains(text)
let lit = (hover && !model.multiple && !model.answered) || focused || picked
let hasDetail = !item.inline.isEmpty || !item.blocks.isEmpty
let on = model.checked.contains(text)
let keys: Set<KeyEquivalent> = model.multiple ? [.space] : [.return, .space]
@@ -361,10 +353,16 @@ private struct ChoiceBox: View {
.toggleStyle(.checkbox)
.labelsHidden()
.tint(Palette.accent)
.disabled(model.busy)
.disabled(model.busy || model.answered)
}
InlineView(inlines: parseInline(text), ctx: MDContext(taskId: model.item.taskId, weight: 600))
.frame(maxWidth: .infinity, alignment: .leading)
if picked {
Text("chosen")
.font(Fonts.font(11))
.foregroundStyle(Palette.accent)
.allowsHitTesting(false)
}
if rec {
Text("recommended")
.font(Fonts.font(11))
@@ -408,7 +406,7 @@ private struct ChoiceBox: View {
}
.opacity(model.busy && !model.multiple ? 0.6 : 1)
.contentShape(Rectangle())
.focusable(!model.busy)
.focusable(!model.busy && !model.answered)
.focusEffectDisabled()
.focused(focus, equals: text)
.onKeyPress(keys: keys) { _ in
@@ -431,46 +429,30 @@ private struct CardActions: View {
var focus: FocusState<String?>.Binding
var body: some View {
if model.item.kind == .question {
let disabled = model.typed.isEmpty && !model.hasImages && (!model.multiple || model.checked.isEmpty)
VStack(alignment: .leading, spacing: 6) {
TextEntry(model: model, label: "Other…", placeholder: "Your answer, a question back, or new instructions", shared: true, focus: focus) {
model.submitQuestion()
}
HStack(spacing: 8) {
Button("Let the agent decide") {
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .other, text: delegateText))
}
.buttonStyle(WebButtonStyle())
Spacer()
Button("Submit") {
model.submitQuestion()
}
.buttonStyle(WebButtonStyle(primary: true))
.disabled(disabled)
}
.disabled(model.busy)
let disabled = model.typed.isEmpty && !model.hasImages && (!model.multiple || model.checked.isEmpty)
VStack(alignment: .leading, spacing: 6) {
TextEntry(model: model, focus: focus) {
model.submitQuestion()
}
} else {
HStack(alignment: .top, spacing: 8) {
TextEntry(model: model, label: "Reply…", placeholder: "Reply to the agent", shared: false, focus: focus) {
model.submitOther()
}
Button("Dismiss") {
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .dismiss))
HStack(spacing: 8) {
Button("Let the agent decide") {
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .other, text: delegateText))
}
.buttonStyle(WebButtonStyle())
.disabled(model.busy)
Spacer()
Button("Submit") {
model.submitQuestion()
}
.buttonStyle(WebButtonStyle(primary: true))
.disabled(disabled)
}
.disabled(model.busy)
}
}
}
private struct TextEntry: View {
let model: CardModel
let label: String
let placeholder: String
let shared: Bool
var focus: FocusState<String?>.Binding
let go: () -> Void
@Environment(Store.self) private var store
@@ -512,7 +494,7 @@ private struct TextEntry: View {
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
.overlay(alignment: .topLeading) {
if text.wrappedValue.isEmpty {
Text(placeholder)
Text("Your answer, a question back, or new instructions")
.foregroundStyle(Palette.muted)
.padding(.horizontal, 10)
.padding(.vertical, 6)
@@ -529,14 +511,6 @@ private struct TextEntry: View {
return true
}
}
if !shared {
HStack {
Spacer()
Button("Submit", action: go)
.buttonStyle(WebButtonStyle(primary: true))
.disabled(model.busy || (model.typed.isEmpty && !model.hasImages))
}
}
}
.onChange(of: editing) {
if !editing {
@@ -547,7 +521,7 @@ private struct TextEntry: View {
}
}
} else {
Text(label)
Text("Other…")
.foregroundStyle(Palette.muted)
.padding(.horizontal, 10)
.padding(.vertical, 4)
+25 -8
View File
@@ -2,7 +2,7 @@ import Foundation
import Observation
import SwiftData
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .terminal, .inbox, .reviews, .workflow, .project]
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .terminal, .questions, .reviews, .workflow, .project]
private let historyPage = 1000
private let leadPage = 50
@@ -10,7 +10,7 @@ private let routeHash = Re(#"^#/(task|project)/(\w+)(?:/(\w+))?$"#)
struct TaskState {
enum Kind {
case answer, merging, held, error, working, idle
case answer, merging, held, working, idle
}
let kind: Kind
@@ -37,6 +37,7 @@ final class Store {
private(set) var selectedTaskId: String?
var pinned: Int?
var reviewed: Int?
var question: (taskId: String, itemId: String)?
var connected = false
private(set) var focus = Focus(tabs: [.transcript], tab: .transcript)
var leadSessions: [AgentSession] = []
@@ -188,9 +189,8 @@ final class Store {
}
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 inbox.contains(where: { $0.taskId == task.id }) {
return TaskState(kind: .answer, label: "Needs your answer", title: "The agent asked a question")
}
if task.merging {
return TaskState(kind: .merging, label: "Merging", title: "The merge turn is running")
@@ -204,9 +204,6 @@ final class Store {
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)
}
@@ -234,6 +231,21 @@ final class Store {
focusTab(.reviews)
}
func showQuestion(_ taskId: String, _ itemId: String) {
focusTask(taskId, .questions)
question = (taskId, itemId)
}
func questionItem(_ itemId: String) -> InboxItem? {
if let open = inbox.first(where: { $0.id == itemId }) {
return open
}
guard let ev = transcript.last(where: { ($0.kind == "inbox.created" || $0.kind == "inbox.updated") && str($0.payload.value, "id") == itemId }) else {
return nil
}
return try! decodePayload(ev.payload, as: InboxItem.self)
}
var defaultModel: String {
me!.defaultModel
}
@@ -346,6 +358,7 @@ final class Store {
}
func focusTask(_ taskId: String, _ tab: TaskTab = .transcript) {
question = nil
spawn("set focus") {
try await self.setFocus(taskId: taskId, tab: tab)
}
@@ -358,6 +371,7 @@ final class Store {
}
func focusTab(_ tab: TaskTab) {
question = nil
spawn("set focus") {
try await self.setFocus(tab: tab)
}
@@ -370,6 +384,7 @@ final class Store {
let name = m?[3] ?? nil
let tab = taskTabs.first { $0.rawValue == name }
if kind == "task", let id {
question = nil
if id != focus.taskId || (tab != nil && tab != focus.tab) {
spawn("set focus") {
try await self.setFocus(taskId: id, tab: tab)
@@ -617,6 +632,8 @@ final class Store {
spawn("load inbox") {
try await self.loadInbox()
}
case "session.error", "session.rate_limited":
notifyError(self, ev)
case "turn.ended":
notifyTurn(self, ev)
if ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId {
+5 -5
View File
@@ -55,7 +55,7 @@ private let shortcuts: [(String, [(String, String)])] = [
("name: title", "Create the task in the project whose name starts with name"),
("Esc", "Back from the workflow list to the task list"),
]),
("Composer and inbox answers", [
("Composer and answers", [
("Enter", "Send"),
("Shift+Enter", "New line"),
("Enter / Space", "Pick the focused choice"),
@@ -261,7 +261,7 @@ final class SwitcherModel {
private func go(_ taskId: String) {
close()
if store.inbox.contains(where: { $0.taskId == taskId }) {
store.focusTask(taskId, .inbox)
store.focusTask(taskId, .questions)
} else if let task = store.findTask(taskId), store.unreadSummary(task) {
store.composerFocus = taskId
store.openSummary(task)
@@ -408,14 +408,14 @@ private struct SwitcherRowView: View {
case .task(let project, let task, let count):
let ts = store.taskState(task)
let color = taskPalette[task.color]
StateDot(filled: ts.kind == .working || ts.kind == .merging ? Palette.ok : ts.kind == .answer ? Palette.warn : ts.kind == .error ? Palette.danger : nil)
StateDot(filled: ts.kind == .working || ts.kind == .merging ? Palette.ok : ts.kind == .answer ? Palette.warn : nil)
.help(ts.title)
Text(taskTitle(project, task, color))
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
if count > 0 {
Badge(count: count).help("inbox items")
Badge(count: count).help("open questions")
}
if store.unreadSummary(task) {
Text("new summary")
@@ -426,7 +426,7 @@ private struct SwitcherRowView: View {
}
Text(ts.label)
.font(Fonts.font(11))
.foregroundStyle(ts.kind == .answer ? Palette.warn : ts.kind == .error ? Palette.danger : Palette.muted)
.foregroundStyle(ts.kind == .answer ? Palette.warn : Palette.muted)
.help(ts.title)
case .project(let p):
StateDot(filled: nil)
+22 -10
View File
@@ -23,7 +23,7 @@ enum NotePart {
}
enum NoteStyle {
case plain, error, ask
case plain, error
}
struct Note {
@@ -52,6 +52,7 @@ enum RowKind {
case note(Note)
case handoff
case snapshot(Snapshot)
case question
}
@MainActor
@@ -81,6 +82,7 @@ final class Row: Identifiable {
var calls: [Call] = []
var summary: Note?
var review: Review?
var question: InboxItem?
var text = ""
var md: [MDBlock] = []
var summarySeq: Int64?
@@ -171,6 +173,7 @@ private final class Ctx {
var subagents: [String: Subagent] = [:]
var reviewers: Set<String> = []
var reviewButtons: [Int: Row] = [:]
var questions: [String: Row] = [:]
var run: Run?
var held: RowList?
var open = false
@@ -463,6 +466,11 @@ final class TranscriptBuilder {
let r = try! decodePayload(ev.payload, as: Review.self)
ctx.reviewButtons[r.snapshotN]?.review = r
}
if ev.kind == "inbox.updated" {
let item = try! decodePayload(ev.payload, as: InboxItem.self)
ctx.questions[item.id]?.question = item
continue
}
let sub = ctx.subagents[parent]
if ev.kind == "agent.tool_call" && isSubagentCall(p) {
let row = renderSubagent(p)
@@ -660,15 +668,19 @@ final class TranscriptBuilder {
return note([.text("session \(jsString(p["state"])): \(error)")], .error, time: false)
case "inbox.created":
let q = dict(p, "payload") ?? [:]
switch str(p, "kind") {
case "question":
return note([.text("asked: \(jsString(q["question"]))")], .ask, time: false)
case "session_error", "rate_limited":
return note([.text(jsString(q["error"]))], .error, time: false)
default:
return nil
}
let row = Row(.question, taskId: taskId)
let item = try! decodePayload(ev.payload, as: InboxItem.self)
row.question = item
ctx.questions[item.id] = row
return row
case "session.error":
return note([.text(jsString(p["error"]))], .error)
case "session.rate_limited":
let limit = dict(p, "rate_limit") ?? [:]
let resets = (limit["resetsAt"] as? Double ?? (limit["resetsAt"] as? Int).map(Double.init)).map { Date(timeIntervalSince1970: $0).formatted(date: .numeric, time: .standard) } ?? "unknown"
return note([.text("\(jsString(p["error"])) (\(jsString(p["vendor"])) \(limit["rateLimitType"].map { jsString($0) } ?? "") limit resets \(resets))")], .error)
case "task.created":
if let from = str(p, "forked_from") {