From a7b2333535f03316feda5ae26e8c44873d371bdd Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 20 Oct 2026 13:04:17 -0700 Subject: [PATCH] Cut transcript noise: fold grey boxes, drop duplicate review rows --- TODO.md | 3 - api/openapi.yaml | 2 +- docs/macos.md | 2 +- docs/operations.md | 8 +- internal/agents/guidance.go | 2 +- internal/agents/inbox.go | 8 +- internal/agents/lead.go | 2 +- internal/agents/manager.go | 3 + internal/agents/queue.go | 6 +- internal/agents/review.go | 8 +- internal/api/api.gen.go | 464 +++++++++++------------ mac/Sources/Byre/Chat.swift | 2 +- mac/Sources/Byre/Questions.swift | 35 +- mac/Sources/Byre/TranscriptBuilder.swift | 57 ++- 14 files changed, 312 insertions(+), 290 deletions(-) diff --git a/TODO.md b/TODO.md index 216f2a8..00089fc 100644 --- a/TODO.md +++ b/TODO.md @@ -4,9 +4,6 @@ # M2 — Seeing the work -- Reduce transcript noise - Audit transcripts as they actually render for places where the noise can be cut. - # M3 — Second vendor and review - Continuous background review diff --git a/api/openapi.yaml b/api/openapi.yaml index 4c9693e..5b1a929 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1797,7 +1797,7 @@ components: type: object description: > 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. + question, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. state: { $ref: "#/components/schemas/InboxState" } created_at: { type: string, format: date-time } resolved_by: { type: string } diff --git a/docs/macos.md b/docs/macos.md index 9470e0a..82d5831 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -97,7 +97,7 @@ Connections belong to `Servers`, not to the window: every saved server's store a ## 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 stop while the review runs), question bubbles that follow their item's `inbox.updated` events, a finished review as its summary row followed by one row per finding, the agent's `respond_to_finding` calls as their own rows, and notes for the other event kinds. Rows that hold a review (`Row.review`) are kept per review id and updated by each later `review.*` event. It is incremental: a new batch extends the existing rows. The composer bar's status (`StatusButton`, from `ChatModel.status`) shows the task's closed state, its working state, or what needs the user (`ChatModel.attention`: open questions, findings of the latest review that neither the user nor the agent has answered, and the current ready-for-merge overview); a click on one of the latter scrolls to the next one, wrapping, and on any other to the end; and when one of those rows closes the transcript scrolls to the next remaining one (`ChatModel.closed`). Jumps from elsewhere (`Store.jumpTo` with a `JumpTarget`: a summary, a question or a snapshot's review) select the task's transcript and scroll there once the row exists. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up; while an attention row is held (`ChatModel.held`), being stuck instead keeps that row top-anchored, which the scroll view clamps to the bottom until the row reaches the top. 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 its server's window is the key window of the active app (`PaneFocus.windowKey`, `markSummaryRead`), checked again whenever the app activates or a window becomes key. An active task's 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; a closed task has no head, and its `task.merged` note is drawn with a ✓ in the ok colour. +- **Transcript and Lead.** `TranscriptBuilder` is a pure function from events to a list of rows: user and agent messages, consecutive tool calls, byre prompts and subagents 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 stop while the review runs), question bubbles that follow their item's `inbox.updated` events, a finished review as its summary row followed by one row per finding, the agent's `respond_to_finding` calls as their own rows, and notes for the other event kinds. Rows that hold a review (`Row.review`) are kept per review id and updated by each later `review.*` event. It is incremental: a new batch extends the existing rows. The composer bar's status (`StatusButton`, from `ChatModel.status`) shows the task's closed state, its working state, or what needs the user (`ChatModel.attention`: open questions, findings of the latest review that neither the user nor the agent has answered, and the current ready-for-merge overview); a click on one of the latter scrolls to the next one, wrapping, and on any other to the end; and when one of those rows closes the transcript scrolls to the next remaining one (`ChatModel.closed`). Jumps from elsewhere (`Store.jumpTo` with a `JumpTarget`: a summary, a question or a snapshot's review) select the task's transcript and scroll there once the row exists. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up; while an attention row is held (`ChatModel.held`), being stuck instead keeps that row top-anchored, which the scroll view clamps to the bottom until the row reaches the top. 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 its server's window is the key window of the active app (`PaneFocus.windowKey`, `markSummaryRead`), checked again whenever the app activates or a window becomes key. An active task's 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; a closed task has no head, and its `task.merged` note is drawn with a ✓ in the ok colour. - **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. The ⌄/⌃ button collapses the text box (with its thumbnails) into the bar and brings it back; it is collapsed by default while the task has an open question or is merged or discarded (`ChatModel.composerHiddenByDefault`), and the human's choice holds until that default changes or an explicit focus request (picking the task in the switcher, focusing the lead) opens it; the automatic focus of a task with no session yet only focuses a box that is shown. - **Questions.** An open question's bubble in the transcript holds its card, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once, with any Other… text as notes) 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. Once answered or dismissed, the bubble shrinks to the question and who answered what. - **Changes, Files, Workflow, Project, Status, Settings.** Including the pinned-snapshot stepper, open-diff memory, per-task place in Files, 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. diff --git a/docs/operations.md b/docs/operations.md index e348965..b8a3f50 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -278,7 +278,7 @@ Agent activity becomes events with `agent_session_id` set: `user.message`, `agen Event payloads are stored whole in `events.payload`, and `agent.tool_result` content (full command output, file reads, diffs) dominates the log's size; the app loads every active task's whole history when it starts (see "Events"), so this is what its startup costs. `sqlite3 byre.db "SELECT kind, count(*), sum(length(payload)) FROM events GROUP BY kind ORDER BY 3 DESC"` shows the split, and `sqlite3 byre.db "SELECT kind, count(*), sum(length(payload)) FROM events WHERE task_id = '' GROUP BY kind ORDER BY 3 DESC"` one task's. -Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and Merge prompts, Compact's handoff request) show collapsed like a tool call, labelled `byre` with the first line as the brief; expanding it shows the full text. Review findings forwarded to the agent come this way too, so the transcript shows them separately: a `review.finished` event with findings draws its counts note with the review's `summary` (one bullet per finding) expanded under it, behind a border-coloured rule, and then each finding as its own row (see "Reviews"). The transcript view has a border in the task's colour (see "Task colours"; the accent in the lead pane) while the session is `running` and its transcript has an open turn (`open` in `TranscriptBuilder`: a drawn turn-starting `user.message`, not mid-turn, with no `turn.ended` after it) or the task's state is `review_pending` or `reviewing` (see "Task state in the store"), and, only in the first case, a Cylon-style scanner in the same colour: a row of 2pt dots, 5pt apart along an 84pt track, where a glow sweeping back and forth every 1.2s lights up the dots under it, drawn below the last message, in the transcript's bottom padding, so it scrolls with the transcript and sits just above the composer's top border when scrolled to the bottom. It is an overlay 8pt below the transcript's last row, so showing or hiding it never changes the transcript's scroll height or position, and it is drawn with Core Animation (`ScannerView` in `mac/Sources/Byre/Chat.swift`: a gradient layer masked by the dots, its position animated by a repeating `CABasicAnimation`), so the render server animates it and SwiftUI never redraws the transcript for it. The session list (`GET /api/me/lead/sessions` or the task's sessions) is fetched separately from the event stream, so it says `running` a little before the turn's `user.message` arrives and a little after its `turn.ended`; requiring the open turn as well keeps the scanner and border from flashing on in those gaps. Consecutive tool calls collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. Thinking output appears as expanded Markdown text with a small muted "thinking" label and ends the current tool-call group just like an agent message, including inside subagent rows. While the scanner shows, a tool call still waiting for its result, and any group or subagent row holding one, gets a border in the task's colour, so a running tool reads apart from waiting on the model without expanding anything (`working` in `mac/Sources/Byre/Chat.swift`). The braille spinner the Project tab shows for task states is drawn with Core Animation too (`SpinnerView` in `mac/Sources/Byre/Format.swift`: its ten glyphs rendered once into images in the surrounding font and colour, cycled every 80ms by a repeating discrete `CAKeyframeAnimation` on the layer's contents), so it never makes SwiftUI redraw or lay out the views around it. When a turn ends, or a turn-starting `user.message` (not mid-turn) arrives, any call still without a result, other than those inside an active subagent row (below), reads "no result" instead of "running…", so an interrupted call never spins during a later turn. The second case covers a turn a restart cut off: it has no `turn.ended`, and the restart notice starts the next turn (`endTools` in `mac/Sources/Byre/TranscriptBuilder.swift`). byre's own tools show without their `mcp__byre__` prefix, in title case (`mcp__byre__create_task` shows as Create Task; `toolLabel` in `mac/Sources/Byre/TranscriptBuilder.swift`); other tools show under the name the agent used. A Claude Code subagent call (the `Agent` or `Task` tool) stands on its own, collapsed, labelled with its `subagent_type` and `description`; expanding it shows the prompt and the call's result. Its summary carries a status from the `agent.vendor` `system` events whose `tool_use_id` names it (`task_started`: running, or running in the background; `task_progress`: its `description`; `task_notification`: its `status`, red unless `completed`). From its `task_started` to its `task_notification` the row is active, and an active row gets the task-coloured border while the scanner shows or while the session's `subagents` count is above zero, as do the waiting tool calls inside it in the latter case; so a background subagent still reads as running after the turn that started it has ended. A turn's end leaves the waiting calls inside an active row waiting; the row's end (its `task_notification`, or a `session.subagents` event with 0, which ends every still-active row as "ended with its process", replayed history included) marks the calls inside it still waiting "no result". The subagent's own messages, thinking and tool calls (events whose `parent_tool_use_id` names the call) go inside that call's row, between the prompt and the result, so parallel subagents don't interleave in the main stream; its consecutive tool calls group there the same way, its messages get an accent rule down their left edge, and a subagent it starts nests inside it in turn. An event naming a subagent call the transcript never saw stays in the main stream marked `↳ subagent`. A subagent call ends the main agent's run of tool calls, so it never joins a group. A subagent's messages never end the main agent's live message, count as a turn's final message or hide a quiet turn. Claude Code streams a background subagent's activity, but for a foreground one only its prompt, so a foreground subagent shows only as its call, its status and its result. Selecting a task that has never had a session focuses the composer, and so does picking any task in the ⌘K switcher when it lands on the transcript (`Store.composerFocus`). In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task (see "Drafts" below), so it survives switching tabs or tasks and relaunching the app. The composer has no `@`-file mentions or path picker: text reaches the agent exactly as typed (stream-json doesn't expand `@path` the way the interactive CLI does), and the agent reads any file the message names with its own tools. The composer is the same whether or not the task has a live session, and stays enabled while a turn runs: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a bubble reading "queued…" while the session is running and "sending…" otherwise, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails. A queued message's `user.queued` event takes over its bubble (the oldest local one with the same text), and the transcript shows a "queued…" bubble below everything else for every `user.queued` not yet followed by a `user.message` with the same text, each `user.message` clearing the oldest such bubble first (queued messages are delivered oldest first), then a local one, and `task.merged` or `task.discarded` clearing them all, since closing the task deletes its queue, so a queued message stays visible across switching tasks, relaunching the app, and on every view. A bubble from a `user.queued` event has a × that withdraws the message (`ChatModel.withdraw`): it first downloads the message's images, so a failed download leaves the message queued, then sends the `DELETE`; on success the bubble goes, its text goes back into that task's composer ahead of whatever is there, separated by a blank line, and its images are added back as composer attachments. A `user.unqueued` event removes the bubble with its `id` on every other view. A refusal (the agent already has the message) shows as an alert and the bubble stays until the `user.message` clears it. +Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and Merge prompts, Compact's handoff request) show collapsed like a tool call, labelled `byre` with the first line as the brief; expanding it shows the full text. Review findings forwarded to the agent come this way too, but their `user.message` carries `findings: true` (`isFindingsMessage` in `internal/agents/review.go`) and the transcript never draws it, since it shows them itself: a `review.finished` event with findings draws its counts note with the review's `summary` (one bullet per finding) expanded under it, behind a border-coloured rule, and then each finding as its own row (see "Reviews"). The transcript view has a border in the task's colour (see "Task colours"; the accent in the lead pane) while the session is `running` and its transcript has an open turn (`open` in `TranscriptBuilder`: a drawn turn-starting `user.message`, not mid-turn, with no `turn.ended` after it) or the task's state is `review_pending` or `reviewing` (see "Task state in the store"), and, only in the first case, a Cylon-style scanner in the same colour: a row of 2pt dots, 5pt apart along an 84pt track, where a glow sweeping back and forth every 1.2s lights up the dots under it, drawn below the last message, in the transcript's bottom padding, so it scrolls with the transcript and sits just above the composer's top border when scrolled to the bottom. It is an overlay 8pt below the transcript's last row, so showing or hiding it never changes the transcript's scroll height or position, and it is drawn with Core Animation (`ScannerView` in `mac/Sources/Byre/Chat.swift`: a gradient layer masked by the dots, its position animated by a repeating `CABasicAnimation`), so the render server animates it and SwiftUI never redraws the transcript for it. The session list (`GET /api/me/lead/sessions` or the task's sessions) is fetched separately from the event stream, so it says `running` a little before the turn's `user.message` arrives and a little after its `turn.ended`; requiring the open turn as well keeps the scanner and border from flashing on in those gaps. Consecutive collapsed boxes (tool calls, byre's messages and subagent calls) fold into one group whose summary counts calls per tool and file, with `byre` for each of byre's messages, and file paths show relative to the worktree. A note the agent's own tool call caused (actor `agent`: "agent set the task title to …", the merge title, the summary, an exposed port) folds into the same group without adding to its summary. `ToolSearch` calls, Claude Code loading a tool's schema, are never drawn. Thinking output appears as expanded Markdown text with a small muted "thinking" label and ends the current tool-call group just like an agent message, including inside subagent rows. While the scanner shows, a tool call still waiting for its result, and any group or subagent row holding one, gets a border in the task's colour, so a running tool reads apart from waiting on the model without expanding anything (`working` in `mac/Sources/Byre/Chat.swift`). The braille spinner the Project tab shows for task states is drawn with Core Animation too (`SpinnerView` in `mac/Sources/Byre/Format.swift`: its ten glyphs rendered once into images in the surrounding font and colour, cycled every 80ms by a repeating discrete `CAKeyframeAnimation` on the layer's contents), so it never makes SwiftUI redraw or lay out the views around it. When a turn ends, or a turn-starting `user.message` (not mid-turn) arrives, any call still without a result, other than those inside an active subagent row (below), reads "no result" instead of "running…", so an interrupted call never spins during a later turn. The second case covers a turn a restart cut off: it has no `turn.ended`, and the restart notice starts the next turn (`endTools` in `mac/Sources/Byre/TranscriptBuilder.swift`). MCP tools show without their `mcp____` prefix, in title case (`mcp__byre__create_task` shows as Create Task, `mcp__hands__screen_list` as Screen List; `toolLabel` in `mac/Sources/Byre/TranscriptBuilder.swift`); other tools show under the name the agent used. A Claude Code subagent call (the `Agent` or `Task` tool) is collapsed, labelled with its `subagent_type` and `description`; expanding it shows the prompt and the call's result. Its summary carries a status from the `agent.vendor` `system` events whose `tool_use_id` names it (`task_started`: running, or running in the background; `task_progress`: its `description`; `task_notification`: its `status`, red unless `completed`). From its `task_started` to its `task_notification` the row is active, and an active row gets the task-coloured border while the scanner shows or while the session's `subagents` count is above zero, as do the waiting tool calls inside it in the latter case; so a background subagent still reads as running after the turn that started it has ended. A turn's end leaves the waiting calls inside an active row waiting; the row's end (its `task_notification`, or a `session.subagents` event with 0, which ends every still-active row as "ended with its process", replayed history included) marks the calls inside it still waiting "no result". The subagent's own messages, thinking and tool calls (events whose `parent_tool_use_id` names the call) go inside that call's row, between the prompt and the result, so parallel subagents don't interleave in the main stream; its consecutive tool calls group there the same way, its messages get an accent rule down their left edge, and a subagent it starts nests inside it in turn. An event naming a subagent call the transcript never saw stays in the main stream marked `↳ subagent`. A subagent's messages never end the main agent's live message, count as a turn's final message or hide a quiet turn. Claude Code streams a background subagent's activity, but for a foreground one only its prompt, so a foreground subagent shows only as its call, its status and its result. Selecting a task that has never had a session focuses the composer, and so does picking any task in the ⌘K switcher when it lands on the transcript (`Store.composerFocus`). In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task (see "Drafts" below), so it survives switching tabs or tasks and relaunching the app. The composer has no `@`-file mentions or path picker: text reaches the agent exactly as typed (stream-json doesn't expand `@path` the way the interactive CLI does), and the agent reads any file the message names with its own tools. The composer is the same whether or not the task has a live session, and stays enabled while a turn runs: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a bubble reading "queued…" while the session is running and "sending…" otherwise, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails. A queued message's `user.queued` event takes over its bubble (the oldest local one with the same text), and the transcript shows a "queued…" bubble below everything else for every `user.queued` not yet followed by a `user.message` with the same text, each `user.message` clearing the oldest such bubble first (queued messages are delivered oldest first), then a local one, and `task.merged` or `task.discarded` clearing them all, since closing the task deletes its queue, so a queued message stays visible across switching tasks, relaunching the app, and on every view. A bubble from a `user.queued` event has a × that withdraws the message (`ChatModel.withdraw`): it first downloads the message's images, so a failed download leaves the message queued, then sends the `DELETE`; on success the bubble goes, its text goes back into that task's composer ahead of whatever is there, separated by a blank line, and its images are added back as composer attachments. A `user.unqueued` event removes the bubble with its `id` on every other view. A refusal (the agent already has the message) shows as an alert and the bubble stays until the `user.message` clears it. **Drafts.** Unsent composer text (task and lead) and unsent inbox answers (checked choices and Other text) are kept in memory by `Drafts` (`mac/Sources/Byre/Drafts.swift`) and saved to a SwiftData `DraftRecord` per server and login (`docs/macos.md`, "Drafts"), one JSON object with `composer` (keyed by `message:` or `message:lead`, each `{text, images}`), `checked` and `texts` (both keyed by inbox item id), and `comments`, the Changes view's pending line comments keyed by task id, each `{anchor: {path, line, old, quote}, text}`. A comment shows only under a diff line whose path, line number, side and text all still match, so after the agent moves lines a comment never attaches to a different line. Every pending comment not shown under a line (its file closed or not loaded, or its line gone from the diff) is listed at the top of the Changes view with its `path:line`, the quoted line, its editable text and **remove**, so nothing is sent unseen. Writes are debounced (400 ms) and flushed at once when a composer or answer box loses focus, on send and on quit. A composer draft is cleared only once its send succeeds, and an inbox answer only once its response is accepted; each send in flight is tracked on its own and the stored draft holds the text of every unconfirmed send (oldest first, then the composer's current text, separated by blank lines) and counts its images, so a relaunch mid-send brings back everything not yet confirmed. A failed send's text goes back into the composer ahead of whatever is there now (text typed since, or earlier failures), separated by a blank line, rather than replacing it. The composer follows its `Drafts` text as it changes, so text put back from outside it (a withdrawn queued message, a draft synced in from another Mac) shows at once. Drafts for tasks that are no longer active and for inbox items no longer open are pruned whenever the task list or inbox reloads. A record that doesn't decode is logged and ignored, as is a write that fails; drafts then live only in memory. Pending images are not stored, only their count: after a relaunch the composer shows how many need re-attaching, until dismissed or the next successful send. @@ -321,7 +321,7 @@ The **Project** tab is the dashboard of the right pane's project (the focused ta ## Inbox -Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript, and its choices are written inline in that context as Markdown list items: `- ( ) text` for pick-one choices, with exactly one `- (*) text` marking the recommendation, or `- [ ] text` for pick-any choices, with `- [x] text` marking the recommended ones. There is no separate list of options. byre parses the choices out of `context` (`internal/agents/choices.go`; list items inside code fences and blockquotes don't count) and stores their text in the payload as `options`, with `multiple` (pick-any) and `recommended`. The UI renders the context as Markdown with a radio button or checkbox in place of each choice's marker. The tool's description tells agents to keep everything about one choice (what it does, its example, code, trade-offs) in lines indented beneath that choice rather than in text above the list; those lines render inside the option, and only the marker line's text is the option. When every item of a choice list has exactly one non-blank indented line and that line is only a Markdown image, the list renders as a grid of tiles (`grid`, set in `parseList` in `mac/Sources/Byre/Markdown.swift`): four across, or two across when the context is at most 480pt wide. The tool's description tells agents to write image choices this way. A malformed call (no `context` or `why_blocked`, fewer than two choices, pick-one and pick-any choices mixed, a pick-one question without exactly one recommendation, two choices with the same text, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. All questions are alike: the tool's description and its result tell the agent to end its turn after asking, and the answer arrives as its next message. When the task's worktree is behind the default branch (`git.Behind`: commits on the repo's local default branch that `HEAD` lacks) and the turn isn't a Merge or Sync turn that owns the worktree's merge, byre first commits the worktree itself (`commitBeforeSync`: `git.CommitAll` under the task's mutex) and takes an `ask_human` snapshot of it, so the agent never commits on its own and there is a forkable point just before the merge. The result then says how many commits behind it is and suggests merging the default branch in while the human answers: `git merge --no-ff --no-commit`, resolve, check, and leave it uncommitted for byre's end-of-turn commit to conclude, as **Sync from main** does, then end the turn. When the commit or snapshot fails, it is logged as `commit before merging default branch` and recorded as a `session.error`, and the result just says to end the turn. The call returns the item's id as `question_id`. An agent withdraws a question it no longer needs answered (the human answered it in a chat message, say) with the `cancel_question` MCP tool and that id: an `open` question of its own session becomes `dismissed` with no `resolved_by` (its `inbox.updated` event has actor `agent`), which also stops it from holding back delivery; anything else is rejected with a message saying why. The inbox holds only questions; errors go in the transcript (see "Errors"). +Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript, and its choices are written inline in that context as Markdown list items: `- ( ) text` for pick-one choices, with exactly one `- (*) text` marking the recommendation, or `- [ ] text` for pick-any choices, with `- [x] text` marking the recommended ones. There is no separate list of options. byre parses the choices out of `context` (`internal/agents/choices.go`; list items inside code fences and blockquotes don't count) and stores their text in the payload as `options`, with `multiple` (pick-any) and `recommended`. The UI renders the context as Markdown with a radio button or checkbox in place of each choice's marker. The tool's description tells agents to keep everything about one choice (what it does, its example, code, trade-offs) in lines indented beneath that choice rather than in text above the list; those lines render inside the option, and only the marker line's text is the option. When every item of a choice list has exactly one non-blank indented line and that line is only a Markdown image, the list renders as a grid of tiles (`grid`, set in `parseList` in `mac/Sources/Byre/Markdown.swift`): four across, or two across when the context is at most 480pt wide. The tool's description tells agents to write image choices this way. A malformed call (no `context` or `question`, fewer than two choices, pick-one and pick-any choices mixed, a pick-one question without exactly one recommendation, two choices with the same text, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. All questions are alike: the tool's description and its result tell the agent to end its turn after asking, and the answer arrives as its next message. When the task's worktree is behind the default branch (`git.Behind`: commits on the repo's local default branch that `HEAD` lacks) and the turn isn't a Merge or Sync turn that owns the worktree's merge, byre first commits the worktree itself (`commitBeforeSync`: `git.CommitAll` under the task's mutex) and takes an `ask_human` snapshot of it, so the agent never commits on its own and there is a forkable point just before the merge. The result then says how many commits behind it is and suggests merging the default branch in while the human answers: `git merge --no-ff --no-commit`, resolve, check, and leave it uncommitted for byre's end-of-turn commit to conclude, as **Sync from main** does, then end the turn. When the commit or snapshot fails, it is logged as `commit before merging default branch` and recorded as a `session.error`, and the result just says to end the turn. The call returns the item's id as `question_id`. An agent withdraws a question it no longer needs answered (the human answered it in a chat message, say) with the `cancel_question` MCP tool and that id: an `open` question of its own session becomes `dismissed` with no `resolved_by` (its `inbox.updated` event has actor `agent`), which also stops it from holding back delivery; anything else is rejected with a message saying why. The inbox holds only questions; errors go in the transcript (see "Errors"). `GET /api/inbox` is one query over open items, oldest first; `project_id` and `task_id` narrow it to a project or task view, and `state` picks another state. The right pane shows a task's questions in its transcript, where they were asked, and they are answered there (below). @@ -359,7 +359,7 @@ The running macOS app posts notifications for events on its SSE stream, raised b The left pane's **Settings** tab asks macOS for notification permission and edits the preferences, and also holds the Default model menu (see "Agent sessions") and the two Review models menus (see "Reviews"). -Each question shows in the task's transcript where it was asked, as a bubble (`QuestionBubble` in `mac/Sources/Byre/Chat.swift`). While it is open, the bubble, in the warn colours, holds the whole question card (`QuestionCard` in `mac/Sources/Byre/Questions.swift`), answered in place. Once it is answered or dismissed, the bubble shrinks, on a faint yellow background (`Palette.warnFaint`) that keeps past questions apart from the rest of the stream, to its `question` text and who chose which options or the first line of who answered what, "dismissed by" whoever dismissed it, or "withdrawn by the agent". Its ▸ disclosure expands it to the rest of the card (`QuestionDetails` in `mac/Sources/Byre/Questions.swift`): the context with its choices, chosen ones marked and none clickable, the "Blocked on:" line, attachments and the response thread. The transcript builds it from the item's `inbox.created` event and updates it from each later `inbox.updated` event of the same item. +Each question shows in the task's transcript where it was asked, as a bubble (`QuestionBubble` in `mac/Sources/Byre/Chat.swift`). While it is open, the bubble, in the warn colours, holds the whole question card (`QuestionCard` in `mac/Sources/Byre/Questions.swift`), answered in place. Once it is answered or dismissed, the bubble shrinks, on a faint yellow background (`Palette.warnFaint`) that keeps past questions apart from the rest of the stream, to its `question` text and who chose which options or the first line of who answered what, "dismissed by" whoever dismissed it, or "withdrawn by the agent". Its ▸ disclosure expands it to the rest of the card (`QuestionDetails` in `mac/Sources/Byre/Questions.swift`): the context with its choices, chosen ones marked and none clickable, attachments and the response thread. An open card shows the context up to its first choice list, then the `question`, then the choice list and the rest of the context. The transcript builds it from the item's `inbox.created` event and updates it from each later `inbox.updated` event of the same item. Each item is answered on its own; there is no batch submission. A pick-one question's choices are clickable blocks, and clicking one sends that answer at once, with whatever is in the Other text box (and its images) as notes. A choice's block (or a pick-any choice's checkbox label) holds the whole list item: its first line, which is the choice's text, and everything under it, such as an example's paragraph or code block. A pick-any question's choices are checkboxes, sent with the Submit button at the bottom of the card. Below the choices, an "Other…" block in the same style opens the only text field. Under it, every question has one submit row: a "Let the agent decide" button at its left, away from the choices so it isn't hit by accident, and the Submit button at its right. Let the agent decide sends at once an `other` answer reading "Use your judgment for this decision and continue within the existing task instructions." (`delegateText` in `mac/Sources/Byre/Questions.swift`), which shows on the answered bubble like any other; the `ask_human` description tells agents that this answer settles only that decision and never grants an authorization the owner must give explicitly. Beside it, "Ignore" (`POST /api/inbox/{id}/dismiss`) dismisses the question unanswered, for one the agent moved past without calling `cancel_question`: it becomes `dismissed` with the human as `resolved_by`, collapses to "dismissed by " and drops out of the attention bar, the agent isn't told, and answers held behind it are delivered. ArrowDown and ArrowUp move between the choice blocks, the checkboxes and the Other block. A question has exactly one Submit button, enabled once there is something to send: on a pick-one question it sends the Other text as `other`; on a pick-any question it sends the checked choices with the Other text alongside (one `option` response), or the Other text alone as `other` when nothing is checked. The Other text box sends with the Submit button or on Enter (Shift+Enter for a newline), like the message box, and folds back into its block when left empty with no images. Pasting or dropping an image into it adds a thumbnail, as in the composer (both keep pending images in `Store.images`, an `ImageDrafts`); images upload when the answer is submitted and are put back if the submission fails. Pending answer images are not kept across a relaunch. @@ -468,7 +468,7 @@ Every new snapshot of a task gets a review: a separate agent session on the vend - **Forwarding.** When a submitted review has findings, byre sends them to the task's live agent as a `byre` message (`forwardFindings` and `findingsMessage` in `internal/agents/review.go`): each finding's number, severity, title, location and detail, under a preamble asking the agent to check each one against the code and its brief and, before changing anything, answer each with `respond_to_finding` (see "Agent responses"). It goes through `Manager.Message`, so it is queued like any message while the agent is mid-turn. A task with no live agent session gets nothing, and the log says `review findings not forwarded`. Forwarding doesn't set any finding's state; **fix** and **ignore** stay available until the agent or the user answers. - **Screenshots.** Reviewer sessions also get the Playwright MCP server (`playwrightMCP` in `internal/agents/review.go`: `npx @playwright/mcp` at a pinned version, headless Chrome, isolated profile, absolute file paths), passed to Claude as a `stdio` entry in `--mcp-config` and to Codex as `mcp_servers` in the thread's `config`. Its automatically named files go to `worktrees//review--media`, removed with the checkout. Each `images` entry is `{path, caption}`, the path absolute or relative to the checkout; at `submit_review` byre reads every one, refuses the whole submission (nothing recorded) for a missing file or one that isn't one of `store.ImageTypes` within `store.MaxBlobBytes`, then stores each as a blob and replaces its `path` with `blob`, since the files are deleted with the checkout. They run without the Stop-hook bounce, mid-turn delivery, end-of-turn commit, snapshots or turn summaries, and their `user.message` events carry no `head`. When the reviewer's turn ends byre completes its session, kills its process and removes the worktree; a review still `running` then fails with why (no submission, with the final message; an error; an interrupt). A process that exits mid-turn fails the review the same way. `StopReview` ends the reviewer the same way and fails the review; `Watch` calls it for a reviewer whose process the health check finds `stuck` (no stream event for `StuckAfter`), which is how a reviewer whose connection died while the machine slept gets cleared. Reviews are of all the task's work, so a newer one makes older ones moot: once a snapshot's review starts, or is skipped for having no diff, byre fails the task's running reviews of earlier snapshots ("superseded by snapshot N") and ends their reviewers (`failRunningReviews`). Each snapshot's review starts on its own goroutine under `reviewMu`, so they can run out of order; `afterSnapshot` starts no review for a snapshot when the task already has a review of a later one. Merging or discarding the task fails its running reviews ("the task was closed") and ends their reviewers. On startup, a running reviewer resumes like any session, and a running review whose reviewer isn't `running` fails. - **Events.** `review.started`, `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) and `review.finding` (a finding's state set, with the user as actor) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions. -- **UI.** Reviews live in the task's transcript (`renderReview` in `mac/Sources/Byre/TranscriptBuilder.swift`, rows in `mac/Sources/Byre/Reviews.swift`). A finished review is a note at its `review.finished` event, "review of snapshot #n:" with its state or severity counts or error; with findings, the note carries the summary and screenshots, and each finding follows as its own row, numbered from 1 as in the message to the agent. The `change` field is not shown; it goes to the lead and into notes. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot against the default branch (`changes?snapshot=`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Clicking a screenshot opens it full size. A finding with no answer from the user or the agent has **fix** and **ignore** buttons. **fix** sends the finding's number, severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` to its latest live session (queued like any message while the agent is mid-turn, see "Agent sessions"), or starting a session when it has none; the view stays where it is, so several findings can be sent in a row. Then it, or **ignore** on its own, records the finding's state (`fixed` or `ignored`) with `PUT /api/reviews/{reviewId}/findings/{index}`, the index being the finding's position in `result.findings`; the review's `finding_states` (the `reviews.finding_states` column, a JSON object from index to state) holds them, and the buttons give way to "fix sent" or "ignored". Only a `done` review's findings take a state; the user can't clear one, and only the agent's answer clears a `fixed` one (below). +- **UI.** Reviews live in the task's transcript (`renderReview` in `mac/Sources/Byre/TranscriptBuilder.swift`, rows in `mac/Sources/Byre/Reviews.swift`). A finished review with findings, or a failed one, is a note at its `review.finished` event, "review of snapshot #n:" with its severity counts or error; one skipped or with no findings draws nothing, since the snapshot row's review badge says so. With findings, the note carries the summary and screenshots, and each finding follows as its own row, numbered from 1 as in the message to the agent. The `change` field is not shown; it goes to the lead and into notes. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot against the default branch (`changes?snapshot=`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Clicking a screenshot opens it full size. A finding with no answer from the user or the agent has **fix** and **ignore** buttons. **fix** sends the finding's number, severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` to its latest live session (queued like any message while the agent is mid-turn, see "Agent sessions"), or starting a session when it has none; the view stays where it is, so several findings can be sent in a row. Then it, or **ignore** on its own, records the finding's state (`fixed` or `ignored`) with `PUT /api/reviews/{reviewId}/findings/{index}`, the index being the finding's position in `result.findings`; the review's `finding_states` (the `reviews.finding_states` column, a JSON object from index to state) holds them, and the buttons give way to "fix sent" or "ignored". Only a `done` review's findings take a state; the user can't clear one, and only the agent's answer clears a `fixed` one (below). - **Agent responses.** The message forwarding findings to the agent numbers them and tells it to answer each with the task agent's `respond_to_finding` MCP tool (`respondToFindingTool` in `internal/agents/review.go`) before changing anything: `snapshot`, `finding` (from 1), `response` (`fixing` or `ignoring`) and a one-line `reason`. byre records it in the review's `finding_responses` (the `reviews.finding_responses` column, a JSON object from index to `{response, reason}`; a later call for the same finding replaces it) and emits `review.finding` with actor `agent`. The answer also clears the finding's `fixed` state, since it answers that fix request: an ignore after the user's **fix** brings **fix** back so the user can overrule it again. A user's `ignored` stays. A call naming a snapshot with no `done` review, or a finding it doesn't have, is refused. On the finding, an answer shows as "agent fixing: …" or "agent ignoring: …" and dims the finding's diff, detail and screenshots. Fixing takes away both buttons; ignoring keeps **fix**, so the user can overrule it, and drops **ignore**. The tool call itself is its own transcript row (not folded into a group of tool calls): what the agent answered for which finding, its title and reason, a **show** button that scrolls to the finding, and, for an ignore, the same **fix** button while the user hasn't answered the finding. A call that fails is hidden. The Merge confirmation's review warning counts findings the user hasn't ignored, whatever the agent answered. - **Snapshot row.** Each snapshot row's **review** button shows that snapshot's latest `review.*` event: a border in the task's colour while the review runs, and otherwise a mark over its top-right corner: a grey ● when it was skipped, a green ✓ when it finished with no open findings (none, or each answered by the user or the agent), and, when it has open findings, the button highlighted with their count in a badge. A failed review leaves the button unmarked. While any of a task's reviews is `running` and its agent is idle, the task's state is `reviewing` (see "Task state in the store"), which the task switcher and the Project tab show as **Reviewing** (see "Task state"). `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones. diff --git a/internal/agents/guidance.go b/internal/agents/guidance.go index 65e9643..158f746 100644 --- a/internal/agents/guidance.go +++ b/internal/agents/guidance.go @@ -24,7 +24,7 @@ const guidance = `You are running inside byre. The human reads your work in byre - TODO.md at the repo root is the project's list of work for byre; its format is the read_knowledge article todo-file.` const proseQuestionNudge = "Your final message asks the human something in prose. Ask it with byre's ask_human tool " + - "(context with the choices inline as `- ( ) choice` / `- (*) recommended` or `- [ ] choice` / `- [x] recommended` list items, question, why_blocked), " + + "(context with the choices inline as `- ( ) choice` / `- (*) recommended` or `- [ ] choice` / `- [x] recommended` list items, and question)," + "then end your turn with a short summary." var ( diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go index 7f7d4b9..74730da 100644 --- a/internal/agents/inbox.go +++ b/internal/agents/inbox.go @@ -24,7 +24,6 @@ var attachmentKinds = []api.AttachmentKind{ type askArgs struct { Context string `json:"context"` Question string `json:"question"` - WhyBlocked string `json:"why_blocked"` Attachments []api.Attachment `json:"attachments"` ThreadID string `json:"thread_id"` } @@ -58,7 +57,6 @@ func (m *Manager) askTool(l *live) mcp.Tool { "properties": { "context": {"type": "string", "description": "Everything the human needs to answer well without reading your transcript: what you are working on, what you found, the relevant facts and data shared by all the choices, and then the choices. Markdown is rendered. Each choice is a list item, at least two, all of one kind: '- ( ) Merge now' / '- (*) Wait for review' (pick one; exactly one (*) marks your recommendation), or '- [ ] Fix the tests' / '- [x] Update the docs' (pick any; [x] marks the ones you recommend). The choice's first line, after the marker, is a short name that comes back verbatim as the answer. Everything about that one choice goes in lines indented two spaces beneath it, not in a paragraph above the list: what it would concretely do, its worked example, any code block, command or sub-list, and its trade-offs. For example:\n\n- ( ) Retry in the client\n Wraps ` + "`fetch` in `retry(3)`" + `; a 500 shows the spinner up to 6s longer.\n ` + "```" + `ts\n await retry(3, () => api.get(path));\n ` + "```" + `\n- (*) Queue on the server\n The request returns at once; the write lands within 1s.\n\nPut long supporting detail (logs, full data, per-option analysis) in collapsed sections the human expands on demand: a line '
Title', the Markdown content, then a line '
'."}, "question": {"type": "string", "description": "The question itself, in one or two sentences. Markdown is rendered."}, - "why_blocked": {"type": "string", "description": "What you can't do until this is answered, and why you can't decide it yourself."}, "attachments": {"type": "array", "items": { "type": "object", "properties": { @@ -77,7 +75,7 @@ func (m *Manager) askTool(l *live) mcp.Tool { }}, "thread_id": {"type": "string"} }, - "required": ["context", "question", "why_blocked"] + "required": ["context", "question"] }`), Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { return m.ask(ctx, l, raw) @@ -121,7 +119,6 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string payload := map[string]any{ "context": args.Context, "question": args.Question, - "why_blocked": args.WhyBlocked, "options": options, "multiple": choices[0].checkbox, "recommended": recommended, @@ -259,9 +256,6 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error if strings.TrimSpace(args.Question) == "" { problems = append(problems, "question is empty") } - if strings.TrimSpace(args.WhyBlocked) == "" { - problems = append(problems, "why_blocked is empty: say what you can't do until this is answered and why you can't decide it yourself") - } choices := parseChoices(args.Context) if len(choices) < 2 { problems = append(problems, "context needs at least two choices, each a list item on its own line: `- ( ) choice` (pick one) or `- [ ] choice` (pick any)") diff --git a/internal/agents/lead.go b/internal/agents/lead.go index d864d90..f6b43f9 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -641,7 +641,7 @@ func (m *Manager) itemText(ctx context.Context, item api.InboxItem) string { } 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"]) + b.WriteString("The full context is in read_inbox.") return b.String() } diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 27708c6..1d5dfb0 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -723,6 +723,9 @@ func (m *Manager) recordMessage(ctx context.Context, sess api.AgentSession, task if isContinuation(actor, text) { payload["continuation"] = true } + if isFindingsMessage(actor, text) { + payload["findings"] = true + } err = m.store.RecordTurnMessage(ctx, sess.Id, newEvent(sess.Id, sess.Owner, task.ProjectId, task.Id, actor, "user.message", payload)) if err != nil { return nil, false, err diff --git a/internal/agents/queue.go b/internal/agents/queue.go index a954fc9..0fef360 100644 --- a/internal/agents/queue.go +++ b/internal/agents/queue.go @@ -256,7 +256,11 @@ func (m *Manager) recordQueued(ctx context.Context, l *live, msgs []queued) stri if l.implementer() && isMergeInPrompt(q.actor, q.text) { m.takeMergeTurn(l) } - err := m.emit(ctx, l, q.actor, "user.message", map[string]any{"text": q.text, "mid_turn": true}) + payload := map[string]any{"text": q.text, "mid_turn": true} + if isFindingsMessage(q.actor, q.text) { + payload["findings"] = true + } + err := m.emit(ctx, l, q.actor, "user.message", payload) if err != nil { slog.Error("record mid-turn message", "session", l.id, "err", err) } diff --git a/internal/agents/review.go b/internal/agents/review.go index 2ce4533..fcf2009 100644 --- a/internal/agents/review.go +++ b/internal/agents/review.go @@ -358,13 +358,19 @@ func (m *Manager) forwardFindings(taskID, reviewID string) { slog.Info("review findings forwarded", "review", reviewID, "session", s.Id) } +const findingsPreamble = "A reviewer looked at snapshot #" + +func isFindingsMessage(actor, text string) bool { + return actor == "byre" && strings.HasPrefix(text, findingsPreamble) +} + func findingsMessage(r api.Review) string { res := map[string]any{} if r.Result != nil { res = *r.Result } b := &strings.Builder{} - fmt.Fprintf(b, "A reviewer looked at snapshot #%d of your work and raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+ + fmt.Fprintf(b, findingsPreamble+"%d of your work and raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+ "Before you change anything, answer each finding with respond_to_finding (snapshot %d and the finding's number): fixing if it holds up and you'll act on it, ignoring with your reason if you're leaving it. "+ "The human sees your answers on the findings and can overrule an ignore.", r.SnapshotN, r.SnapshotN) findings, _ := res["findings"].([]any) diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index b3429e5..b2055b4 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -938,7 +938,7 @@ type InboxItem struct { CreatedAt time.Time `json:"created_at"` Id string `json:"id"` - // 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 context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, 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"` @@ -4197,237 +4197,237 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 58d425a..7dd646c 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -146,7 +146,7 @@ final class ChatModel { } func working(_ row: Row) -> Bool { - row.pending && (running || subagents && row.inActive) || row.active && (running || subagents) + row.pending && (running || subagents && row.inActive) || row.holdsActive && (running || subagents) } var hasOlder: Bool { diff --git a/mac/Sources/Byre/Questions.swift b/mac/Sources/Byre/Questions.swift index 216dd99..34fb4f1 100644 --- a/mac/Sources/Byre/Questions.swift +++ b/mac/Sources/Byre/Questions.swift @@ -300,19 +300,27 @@ private struct CardBody: View { var body: some View { let p = model.payload let item = model.item + let split = model.context.firstIndex(where: choiceList) ?? model.context.count VStack(alignment: .leading, spacing: 0) { - MarkdownView(blocks: model.context, ctx: contextCtx) - .onGeometryChange(for: Bool.self) { $0.size.width > 480 } action: { wide = $0 } - .padding(.vertical, 10) + MarkdownView(blocks: Array(model.context[.. 480 } action: { wide = $0 } + } + + private func choiceList(_ block: MDBlock) -> Bool { + if case .list(_, _, let items, _) = block { + return items.contains { $0.choice != nil } + } + return false } private func attachments(_ v: (any Sendable)?) -> [Attachment] { @@ -328,23 +336,6 @@ private struct CardBody: View { } } -private struct WhyLine: View { - let text: String - let taskId: String - @Environment(Store.self) private var store - - var body: some View { - var out = AttributedString("Blocked on: ") - out.foregroundColor = Palette.text - var rest = InlineRenderer(store: store, size: 13, taskId: taskId).text(parseInline(text)) - rest.foregroundColor = rest.foregroundColor ?? Palette.muted - return Text(out + rest) - .foregroundStyle(Palette.muted) - .textSelection(.enabled) - .fixedSize(horizontal: false, vertical: true) - } -} - private struct ChoiceBox: View { let model: CardModel let item: MDItem diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift index 9f20563..e119466 100644 --- a/mac/Sources/Byre/TranscriptBuilder.swift +++ b/mac/Sources/Byre/TranscriptBuilder.swift @@ -105,6 +105,13 @@ final class Row: Identifiable { } } + var holdsActive: Bool { + if case .group = kind { + return body!.rows.contains { $0.active } + } + return active + } + var inActive: Bool { var o = owner while let r = o { @@ -300,7 +307,7 @@ final class TranscriptBuilder { !parent.isEmpty && ctx.subagents[parent] == nil } - private func addToRun(_ row: Row, _ call: Call, _ parent: String) { + private func addToRun(_ row: Row, _ call: Call?, _ parent: String) { if let sub = ctx.subagents[parent] { sub.run = extendRun(sub.run, row, call, parent) { r in sub.row.body!.rows.append(r) @@ -313,12 +320,14 @@ final class TranscriptBuilder { } } - private func extendRun(_ run: Run?, _ row: Row, _ call: Call, _ parent: String, _ place: (Row) -> RowList) -> Run { + private func extendRun(_ run: Run?, _ row: Row, _ call: Call?, _ parent: String, _ place: (Row) -> RowList) -> Run { guard let run, run.parent == parent else { let list = place(row) - return Run(first: row, container: list, calls: [call], parent: parent) + return Run(first: row, container: list, calls: call.map { [$0] } ?? [], parent: parent) + } + if let call { + run.calls.append(call) } - run.calls.append(call) if run.group == nil { let group = Row(.group, taskId: rowTask, body: true) group.owner = ctx.subagents[parent]?.row @@ -383,6 +392,7 @@ final class TranscriptBuilder { private func openHandoff() { let row = Row(.handoff, taskId: rowTask, body: true) row.summary = Note(parts: [.text("writing a handoff…")]) + ctx.run = nil put(row) ctx.handoff = row } @@ -497,8 +507,11 @@ final class TranscriptBuilder { row.review = r } if ev.kind == "review.finished" { - ctx.run = nil - for row in renderReview(r, ev.createdAt) { + let rows = renderReview(r, ev.createdAt) + if !rows.isEmpty { + ctx.run = nil + } + for row in rows { put(row) } continue @@ -518,8 +531,7 @@ final class TranscriptBuilder { sub.row.body!.rows.append(row) continue } - ctx.run = nil - put(row, keepHeld: true) + addToRun(row, Call(name: jsString(p["name"]), target: ""), parent) continue } if ev.kind == "agent.tool_call" && sub == nil && str(p, "name") == "mcp__byre__respond_to_finding" { @@ -540,6 +552,9 @@ final class TranscriptBuilder { let name = toolLabel(jsString(p["name"])) let row = renderToolCall(p) row.started = ev.createdAt + if name == "ToolSearch" { + continue + } addToRun(row, Call(name: name, target: toolTarget(p["input"] ?? nil, worktree())), parent) continue } @@ -551,7 +566,11 @@ final class TranscriptBuilder { sub.row.body!.rows.append(row) continue } - ctx.run = nil + let prompt = if case .byrePrompt = row.kind { true } else { false } + let agentNote = if case .note = row.kind, ev.actor == "agent" { true } else { false } + if !prompt && !agentNote { + ctx.run = nil + } if ev.kind == "agent.message" && parent.isEmpty { ctx.drawn = nil ctx.reply = row @@ -577,6 +596,7 @@ final class TranscriptBuilder { ctx.drawn = nil } if ev.kind == "user.message" && ev.actor == "byre" && !(ev.owner ?? "").isEmpty && !bool(p, "mid_turn") && !bool(p, "handoff") { + ctx.run = nil ctx.held = RowList() ctx.open = false ctx.drawn = [] @@ -597,6 +617,11 @@ final class TranscriptBuilder { if (ev.kind == "inbox.created" || ev.kind == "task.ready_cleared") && sink == nil && own { lastReady = nil } + if prompt && !row.hidden || agentNote { + release() + addToRun(row, agentNote ? nil : Call(name: "byre", target: ""), parent) + continue + } put(row, keepHeld: ev.kind == "agent.thinking") } return contextChanged @@ -659,9 +684,12 @@ final class TranscriptBuilder { } private func renderReview(_ r: Review, _ when: Date) -> [Row] { - let outcome = r.state == .failed ? "failed: \(r.error)" : r.state == .skipped ? "skipped: no changes" : reviewCounts(r) - let parts: [NotePart] = [.text("review of snapshot #\(r.snapshotN): \(outcome)")] let fs = findings(r) + if fs.isEmpty && r.state != .failed { + return [] + } + let outcome = r.state == .failed ? "failed: \(r.error)" : reviewCounts(r) + let parts: [NotePart] = [.text("review of snapshot #\(r.snapshotN): \(outcome)")] let head = fs.isEmpty ? Row(.note(Note(parts: parts, when: when, style: r.state == .failed ? .error : .plain)), taskId: rowTask) : Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: rowTask) @@ -692,7 +720,7 @@ final class TranscriptBuilder { let first = text.components(separatedBy: "\n").first ?? "" let brief = first.count > 120 ? "\(first.prefix(120))…" : first let row = Row(.byrePrompt(brief: brief, when: when, md: parseMarkdown(text)), taskId: rowTask) - row.hidden = bool(p, "continuation") + row.hidden = bool(p, "continuation") || bool(p, "findings") return row } let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) } @@ -886,11 +914,10 @@ private func isSubagentCall(_ p: JSONDict) -> Bool { } func toolLabel(_ name: String) -> String { - let prefix = "mcp__byre__" - if !name.hasPrefix(prefix) { + guard name.hasPrefix("mcp__"), let sep = name.dropFirst(5).range(of: "__") else { return name } - return name.dropFirst(prefix.count).split(separator: "_", omittingEmptySubsequences: false).map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined(separator: " ") + return name[sep.upperBound...].split(separator: "_", omittingEmptySubsequences: false).map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined(separator: " ") } private func toolBrief(_ input: (any Sendable)?, _ root: String) -> String {