Move questions and reviews into the transcript; agents answer findings
This commit is contained in:
+17
-3
@@ -1526,7 +1526,7 @@ components:
|
||||
Review:
|
||||
type: object
|
||||
description: An agent session on the vendor the task's agent doesn't use, reviewing one snapshot's change.
|
||||
required: [id, task_id, snapshot_n, agent_session_id, vendor, model, state, error, created_at, finding_states]
|
||||
required: [id, task_id, snapshot_n, agent_session_id, vendor, model, state, error, created_at, finding_states, finding_responses]
|
||||
properties:
|
||||
id: { type: string }
|
||||
task_id: { type: string }
|
||||
@@ -1547,6 +1547,20 @@ components:
|
||||
type: object
|
||||
description: "How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open."
|
||||
additionalProperties: { $ref: "#/components/schemas/FindingState" }
|
||||
finding_responses:
|
||||
type: object
|
||||
description: "How the task's agent answered each finding with respond_to_finding, keyed by its index in result.findings; its latest answer wins."
|
||||
additionalProperties: { $ref: "#/components/schemas/FindingResponse" }
|
||||
|
||||
FindingResponse:
|
||||
type: object
|
||||
required: [response, reason]
|
||||
properties:
|
||||
response:
|
||||
type: string
|
||||
enum: [fixing, ignoring]
|
||||
x-enum-varnames: [FindingFixing, FindingIgnoring]
|
||||
reason: { type: string, description: "What the agent is doing about it, or why it leaves it." }
|
||||
|
||||
FindingState:
|
||||
type: string
|
||||
@@ -1781,8 +1795,8 @@ components:
|
||||
|
||||
TaskTab:
|
||||
type: string
|
||||
enum: [transcript, board, changes, files, terminal, questions, reviews, workflow, project]
|
||||
x-enum-varnames: [TabTranscript, TabBoard, TabChanges, TabFiles, TabTerminal, TabQuestions, TabReviews, TabWorkflow, TabProject]
|
||||
enum: [transcript, board, changes, files, terminal, workflow, project]
|
||||
x-enum-varnames: [TabTranscript, TabBoard, TabChanges, TabFiles, TabTerminal, TabWorkflow, TabProject]
|
||||
|
||||
TerminalSize:
|
||||
type: object
|
||||
|
||||
+8
-8
@@ -46,11 +46,11 @@ 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 |
|
||||
| `Questions.swift` | The Questions tab: one item at a time, choices, Other, Let the agent decide, answered items read-only |
|
||||
| `Questions.swift` | The question card an open question shows in the transcript: choices, Other, Let the agent decide |
|
||||
| `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 |
|
||||
| `Reviews.swift` | Review cards, finding diffs, Fix and Ignore |
|
||||
| `Reviews.swift` | Finding rows, finding diffs, Fix and Ignore, Stop for a running review |
|
||||
| `Workflow.swift` | Stage graph and attempts |
|
||||
| `Project.swift` | Project tab and TODO items |
|
||||
| `Notes.swift`, `Workflows.swift`, `Status.swift`, `Settings.swift` | Left-pane tabs |
|
||||
@@ -87,7 +87,7 @@ One window (a `Window` scene, id `main`) shows one saved server at a time: `Serv
|
||||
At launch the window shows the oldest saved server. A CloudKit import that brings a server saved on another Mac starts its connection, and the window takes it while it shows an empty connection screen. A CloudKit import that removes the current server switches to the first saved one. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that earlier builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
|
||||
|
||||
- **Left pane:** tabs Lead, Status, Server, 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 their fixed 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.
|
||||
- **Right pane:** the task bar (task picker `<project> ▸ <title> ▾` and the preview links), then the task's tabs in their fixed order, hiding Board and Workflow when they have nothing, with a **More ▾** menu for tabs that don't fit. 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.
|
||||
|
||||
@@ -97,13 +97,13 @@ 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, 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 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. 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 (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. At the transcript's top right, the attention buttons (`AttentionBar`, from `ChatModel.attention`) count what needs the user: open questions, findings of the latest review that neither the user nor the agent has answered, and the current ready-for-merge overview; each click scrolls to the next one, wrapping. 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. 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. 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.
|
||||
- **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.
|
||||
- **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) 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.
|
||||
- **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 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.
|
||||
- **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 Transcript scrolled to its oldest open question when it has open items, to its unread summary when it has one, and at the end 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 jump to the review or the oldest open question in the transcript, 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
|
||||
|
||||
|
||||
+19
-15
File diff suppressed because one or more lines are too long
@@ -114,6 +114,9 @@ func codexToolCall(item codex.ThreadItem) (string, any, error) {
|
||||
return "", nil, err
|
||||
}
|
||||
args, err := decode(item.Arguments)
|
||||
if item.Type == codex.ThreadItemTypeDynamicToolCall && item.Namespace != "" {
|
||||
tool = "mcp__" + item.Namespace + "__" + tool
|
||||
}
|
||||
return tool, args, err
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
|
||||
- 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, and 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 questions tab, with one line naming it and what you recommend.
|
||||
- A question that needs the human: that task's transcript tab, where the question waits to be answered, 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.
|
||||
@@ -29,12 +29,12 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
|
||||
- Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, the human already decided it, or one of your notes settles it outright. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how, naming the note by its id when one decided it.
|
||||
- Notes: you learn how the human works and keep it with remember, forget and list_notes. byre adds every note to your instructions at the start of each lead session, so they outlast compaction; the human can read and delete them in the Settings tab. Write a note when the human shows a priority, a preference or the reasoning behind a choice that will recur: an answer that departs from the agent's recommendation (the answered signal shows both), a correction or overrule of you or a task agent, or a statement of how they want things done. Record the choice and the why, in their terms, general enough to apply to the next case and specific enough to act on, for example "Prefers replacing a state label over adding a tag beside it: keeps the picker uncluttered." File each note under the project it is about with project_id when it concerns that project's code, UI, docs or process, and with no project_id only when it holds for how the human works everywhere; apply a project's notes only to that project's tasks. Task agents start with their creator's every-project notes and their project's, and suggest new ones with suggest_note, which reaches you as a signal: save a suggestion only when it holds and your notes don't already cover it, choosing its project yourself, and otherwise drop it; neither needs a word to the human. Don't note one-off facts about a single task, and don't duplicate: update an existing note with remember and its id instead. When the human contradicts a note, update it or forget it. Before recommending anything, check your notes, and when one shaped the recommendation, cite its id.
|
||||
- Finished turns: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress.
|
||||
- Reviews: after each new snapshot of a task, a reviewer agent reviews the change, on the human's review model, or their second review model when the review model wrote the work. The results are on the task's reviews tab, and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding.
|
||||
- Reviews: after each new snapshot of a task, a reviewer agent reviews the change, on the human's review model, or their second review model when the review model wrote the work. The results are in the task's transcript, where its agent answers each finding with respond_to_finding (fixing, or ignoring with a reason), and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding.
|
||||
- Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message.
|
||||
- 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>/questions; 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 or a finished turn, link 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.`
|
||||
@@ -416,7 +416,7 @@ func (m *Manager) freedText(ctx context.Context, owner string, ev api.Event) (st
|
||||
return m.waitingText(ctx, owner, ev)
|
||||
}
|
||||
switch ev.Kind {
|
||||
case "inbox.updated", "task.merge_started", "task.merged", "task.discarded":
|
||||
case "task.merge_started", "task.merged", "task.discarded":
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
@@ -431,9 +431,6 @@ func (m *Manager) freedText(ctx context.Context, owner string, ev api.Event) (st
|
||||
if err != nil || finished == "" {
|
||||
return "", err
|
||||
}
|
||||
if ev.Kind == "inbox.updated" && finished != inboxEmptied {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("Your right pane is free: %s %s.", m.taskLabel(ctx, *f.TaskId), finished), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -457,17 +457,17 @@ 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, and select one of its tabs. The tabs' order is fixed. "+
|
||||
"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), 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. "+
|
||||
"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), 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, nothing changes and you are told so. "+
|
||||
"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 a questions tab with nothing left open),"+
|
||||
"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 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."},
|
||||
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "questions", "reviews", "workflow", "project"]}
|
||||
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "workflow", "project"]}
|
||||
}, "required": ["tab"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
f := api.Focus{}
|
||||
@@ -629,8 +629,6 @@ func sameView(a, b api.Focus) bool {
|
||||
return deref(a.ProjectId) == deref(b.ProjectId)
|
||||
}
|
||||
|
||||
const inboxEmptied = "has no open questions left"
|
||||
|
||||
func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error) {
|
||||
if f.TaskId == nil {
|
||||
return "", nil
|
||||
@@ -645,17 +643,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error)
|
||||
if task.Merging {
|
||||
return "is merging", nil
|
||||
}
|
||||
if f.Tab != api.TabQuestions {
|
||||
return "", nil
|
||||
}
|
||||
items, err := m.store.ListInbox(ctx, store.InboxFilter{State: api.Open, TaskID: task.Id})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(items) > 0 {
|
||||
return "", nil
|
||||
}
|
||||
return inboxEmptied, nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
const watchHold = 10 * time.Second
|
||||
|
||||
@@ -747,7 +747,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
return nil, err
|
||||
}
|
||||
l.notes = "\n\n" + taskNotesGuidance + "\n\n" + notes
|
||||
tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l))
|
||||
tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l), m.respondToFindingTool(l))
|
||||
}
|
||||
if sess.Role == api.Implementer && sess.Member != "" {
|
||||
sessions, err := m.store.ListSessions(ctx, task.Id)
|
||||
|
||||
@@ -364,9 +364,11 @@ func findingsMessage(r api.Review) string {
|
||||
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, act on the ones that hold up, and in your final message say briefly which you acted on and which you're leaving and why.", r.SnapshotN)
|
||||
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. "+
|
||||
"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)
|
||||
for _, f := range findings {
|
||||
for i, f := range findings {
|
||||
fm, _ := f.(map[string]any)
|
||||
where := ""
|
||||
if p, ok := fm["path"].(string); ok && p != "" {
|
||||
@@ -376,11 +378,74 @@ func findingsMessage(r api.Review) string {
|
||||
}
|
||||
where = " (`" + where + "`)"
|
||||
}
|
||||
fmt.Fprintf(b, "\n\n**[%v] %v**%s\n\n%s", fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"])))
|
||||
fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"])))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type findingResponseArgs struct {
|
||||
Snapshot int `json:"snapshot"`
|
||||
Finding int `json:"finding"`
|
||||
Response api.FindingResponseResponse `json:"response"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (m *Manager) respondToFindingTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "respond_to_finding",
|
||||
Description: "Answer one finding of a review of your work, as soon as you've decided what to do about it: fixing when you'll act on it, ignoring when you're leaving it. " +
|
||||
"The human sees your answer on the finding; an ignore keeps the human's fix button, so they can overrule you. Call it again to change your answer.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshot": {"type": "integer", "description": "The reviewed snapshot's number."},
|
||||
"finding": {"type": "integer", "description": "The finding's number in the review, from 1."},
|
||||
"response": {"type": "string", "enum": ["fixing", "ignoring"]},
|
||||
"reason": {"type": "string", "description": "One line: what you'll do about it, or why you're leaving it."}
|
||||
},
|
||||
"required": ["snapshot", "finding", "response", "reason"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
args := findingResponseArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed respond_to_finding call: %w", err)
|
||||
}
|
||||
if !args.Response.Valid() {
|
||||
return "", fmt.Errorf("%w: response must be fixing or ignoring", ErrInvalid)
|
||||
}
|
||||
if args.Finding < 1 {
|
||||
return "", fmt.Errorf("%w: finding numbers start at 1", ErrInvalid)
|
||||
}
|
||||
reason := strings.TrimSpace(args.Reason)
|
||||
if reason == "" {
|
||||
return "", fmt.Errorf("%w: reason must not be empty", ErrInvalid)
|
||||
}
|
||||
reviews, err := m.store.ListReviews(ctx, l.taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
reviewID := ""
|
||||
for _, r := range reviews {
|
||||
if r.SnapshotN == args.Snapshot && r.State == api.ReviewStateDone {
|
||||
reviewID = r.Id
|
||||
}
|
||||
}
|
||||
if reviewID == "" {
|
||||
return "", fmt.Errorf("%w: snapshot %d has no finished review", ErrInvalid, args.Snapshot)
|
||||
}
|
||||
err = m.store.SetFindingResponse(ctx, reviewID, args.Finding-1, api.FindingResponse{Response: args.Response, Reason: reason}, agentActor)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return "", fmt.Errorf("%w: the review of snapshot %d has no finding %d", ErrInvalid, args.Snapshot, args.Finding)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Recorded: %s finding %d of snapshot %d's review.", args.Response, args.Finding, args.Snapshot), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type reviewImage struct {
|
||||
entry map[string]any
|
||||
contentType string
|
||||
|
||||
+292
-265
@@ -219,6 +219,24 @@ func (e FileChangeStatus) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for FindingResponseResponse.
|
||||
const (
|
||||
FindingFixing FindingResponseResponse = "fixing"
|
||||
FindingIgnoring FindingResponseResponse = "ignoring"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the FindingResponseResponse enum.
|
||||
func (e FindingResponseResponse) Valid() bool {
|
||||
switch e {
|
||||
case FindingFixing:
|
||||
return true
|
||||
case FindingIgnoring:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for FindingState.
|
||||
const (
|
||||
FindingFixed FindingState = "fixed"
|
||||
@@ -471,8 +489,6 @@ const (
|
||||
TabChanges TaskTab = "changes"
|
||||
TabFiles TaskTab = "files"
|
||||
TabProject TaskTab = "project"
|
||||
TabQuestions TaskTab = "questions"
|
||||
TabReviews TaskTab = "reviews"
|
||||
TabTerminal TaskTab = "terminal"
|
||||
TabTranscript TaskTab = "transcript"
|
||||
TabWorkflow TaskTab = "workflow"
|
||||
@@ -489,10 +505,6 @@ func (e TaskTab) Valid() bool {
|
||||
return true
|
||||
case TabProject:
|
||||
return true
|
||||
case TabQuestions:
|
||||
return true
|
||||
case TabReviews:
|
||||
return true
|
||||
case TabTerminal:
|
||||
return true
|
||||
case TabTranscript:
|
||||
@@ -853,6 +865,16 @@ type FileDiff struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// FindingResponse defines model for FindingResponse.
|
||||
type FindingResponse struct {
|
||||
// Reason What the agent is doing about it, or why it leaves it.
|
||||
Reason string `json:"reason"`
|
||||
Response FindingResponseResponse `json:"response"`
|
||||
}
|
||||
|
||||
// FindingResponseResponse defines model for FindingResponse.Response.
|
||||
type FindingResponseResponse string
|
||||
|
||||
// FindingState fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning.
|
||||
type FindingState string
|
||||
|
||||
@@ -1069,6 +1091,9 @@ type Review struct {
|
||||
// Error failed: why.
|
||||
Error string `json:"error"`
|
||||
|
||||
// FindingResponses How the task's agent answered each finding with respond_to_finding, keyed by its index in result.findings; its latest answer wins.
|
||||
FindingResponses map[string]FindingResponse `json:"finding_responses"`
|
||||
|
||||
// FindingStates How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open.
|
||||
FindingStates map[string]FindingState `json:"finding_states"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
@@ -3877,265 +3902,267 @@ 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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",
|
||||
"HCINH4Qony6V+RbN2Fmhe3Cf36tI0/v3/2cA",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE reviews ADD COLUMN finding_responses TEXT NOT NULL DEFAULT '{}';
|
||||
UPDATE events SET payload = json_set(payload, '$.finding_responses', json('{}'))
|
||||
WHERE kind IN ('review.started', 'review.finished', 'review.finding') AND json_type(payload, '$.finding_responses') IS NULL;
|
||||
UPDATE users SET focus = json_set(focus, '$.tab', 'transcript') WHERE json_extract(focus, '$.tab') IN ('questions', 'reviews');
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states`
|
||||
const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses`
|
||||
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
|
||||
finishedAt := ""
|
||||
@@ -110,6 +110,33 @@ func (s *Store) SetFindingState(ctx context.Context, id string, index int, state
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) SetFindingResponse(ctx context.Context, id string, index int, resp api.FindingResponse, actor string) error {
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `
|
||||
UPDATE reviews SET
|
||||
finding_responses = json_set(finding_responses, '$."' || ?1 || '"', json(?2)),
|
||||
finding_states = CASE WHEN json_extract(finding_states, '$."' || ?1 || '"') = ?3
|
||||
THEN json_remove(finding_states, '$."' || ?1 || '"') ELSE finding_states END
|
||||
WHERE id = ?4 AND state = ?5 AND ?1 < json_array_length(nullif(result, ''), '$.findings')
|
||||
`, index, string(b), string(api.FindingFixed), id, string(api.ReviewStateDone))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return emitReview(ctx, tx, emit, "review.finding", id, actor)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ReviewerSessionIDs(ctx context.Context, taskID string) (map[string]bool, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id FROM agent_sessions WHERE task_id = ? AND role = ?`, taskID, string(api.Reviewer))
|
||||
if err != nil {
|
||||
@@ -177,7 +204,8 @@ func scanReview(row scanner) (api.Review, error) {
|
||||
createdAt := ""
|
||||
finishedAt := ""
|
||||
findingStates := ""
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt, &findingStates)
|
||||
findingResponses := ""
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt, &findingStates, &findingResponses)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
@@ -187,6 +215,11 @@ func scanReview(row scanner) (api.Review, error) {
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
r.FindingResponses = map[string]api.FindingResponse{}
|
||||
err = json.Unmarshal([]byte(findingResponses), &r.FindingResponses)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if result != "" {
|
||||
res := map[string]any{}
|
||||
err = json.Unmarshal([]byte(result), &res)
|
||||
|
||||
@@ -38,7 +38,6 @@ func untilCancelled() async {
|
||||
@Observable
|
||||
final class ChangesModel {
|
||||
let store: Store
|
||||
let compact: Bool
|
||||
private(set) var changes: TaskChanges?
|
||||
private(set) var error = ""
|
||||
private(set) var diffs: [String: Result<String, ApiError>] = [:]
|
||||
@@ -52,20 +51,19 @@ final class ChangesModel {
|
||||
@ObservationIgnored private var target = 0
|
||||
@ObservationIgnored private var fetching: Set<String> = []
|
||||
|
||||
init(_ store: Store, compact: Bool) {
|
||||
init(_ store: Store) {
|
||||
self.store = store
|
||||
self.compact = compact
|
||||
open = store.changesMemory.open
|
||||
range = store.changesMemory.range
|
||||
pinRange = store.changesMemory.pinRange
|
||||
}
|
||||
|
||||
var pinned: Int? {
|
||||
compact ? nil : store.pinned
|
||||
store.pinned
|
||||
}
|
||||
|
||||
var mode: ChangeMode {
|
||||
compact ? .main : pinned != nil ? pinRange : range
|
||||
pinned != nil ? pinRange : range
|
||||
}
|
||||
|
||||
var empty: String {
|
||||
@@ -217,7 +215,6 @@ final class ChangesModel {
|
||||
}
|
||||
|
||||
struct ChangesView: View {
|
||||
var compact = false
|
||||
@Environment(Store.self) private var store
|
||||
@ViewState private var model: ChangesModel?
|
||||
|
||||
@@ -225,14 +222,12 @@ struct ChangesView: View {
|
||||
VStack(spacing: 0) {
|
||||
if let model {
|
||||
ChangesHead(model: model)
|
||||
if !compact {
|
||||
ChangesList(model: model)
|
||||
}
|
||||
ChangesList(model: model)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: compact ? nil : .infinity, alignment: .top)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.task(id: store.selectedTaskId) {
|
||||
let m = model ?? ChangesModel(store, compact: compact)
|
||||
let m = model ?? ChangesModel(store)
|
||||
model = m
|
||||
m.retarget()
|
||||
let off = store.selectedFeed?.appended.add { [weak m] events in
|
||||
@@ -243,9 +238,7 @@ struct ChangesView: View {
|
||||
m.stop()
|
||||
}
|
||||
.onChange(of: store.pinned) {
|
||||
if !compact {
|
||||
model?.retarget()
|
||||
}
|
||||
model?.retarget()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,7 +260,7 @@ private struct ChangesHead: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
if !model.compact, let taskId = store.selectedTaskId, let drafts = store.drafts {
|
||||
if let taskId = store.selectedTaskId, let drafts = store.drafts {
|
||||
let n = drafts.lineComments(taskId).filter { !trimmed($0.text).isEmpty }.count
|
||||
if n > 0 {
|
||||
Button("Send \(n) \(n == 1 ? "comment" : "comments")") {
|
||||
@@ -317,29 +310,27 @@ private struct ChangesHead: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var toggle: some View {
|
||||
if !model.compact {
|
||||
HStack(spacing: 4) {
|
||||
if model.pinned != nil {
|
||||
RangeTabButton(label: "Since main", active: model.pinRange == .main) {
|
||||
model.setPinRange(.main)
|
||||
}
|
||||
RangeTabButton(label: "This step", active: model.pinRange == .step) {
|
||||
model.setPinRange(.step)
|
||||
}
|
||||
RangeTabButton(label: "Live", active: false, help: "Back to the live worktree") {
|
||||
store.pinSnapshot(nil)
|
||||
}
|
||||
} else {
|
||||
RangeTabButton(label: "Since main", active: model.range == .main) {
|
||||
model.setRange(.main)
|
||||
}
|
||||
RangeTabButton(label: "This turn", active: model.range == .turn) {
|
||||
model.setRange(.turn)
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
if model.pinned != nil {
|
||||
RangeTabButton(label: "Since main", active: model.pinRange == .main) {
|
||||
model.setPinRange(.main)
|
||||
}
|
||||
RangeTabButton(label: "This step", active: model.pinRange == .step) {
|
||||
model.setPinRange(.step)
|
||||
}
|
||||
RangeTabButton(label: "Live", active: false, help: "Back to the live worktree") {
|
||||
store.pinSnapshot(nil)
|
||||
}
|
||||
} else {
|
||||
RangeTabButton(label: "Since main", active: model.range == .main) {
|
||||
model.setRange(.main)
|
||||
}
|
||||
RangeTabButton(label: "This turn", active: model.range == .turn) {
|
||||
model.setRange(.turn)
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+237
-43
@@ -7,6 +7,13 @@ enum ChatKind: Equatable {
|
||||
case task(String?)
|
||||
}
|
||||
|
||||
struct AttentionGroup: Identifiable {
|
||||
let id: String
|
||||
let label: String
|
||||
let help: String
|
||||
let rows: [Int]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ChatModel {
|
||||
@@ -28,6 +35,7 @@ final class ChatModel {
|
||||
@ObservationIgnored private var activeObserver: NotificationCenter.ObservationToken?
|
||||
@ObservationIgnored private var keyObserver: NotificationCenter.ObservationToken?
|
||||
@ObservationIgnored private var started = false
|
||||
@ObservationIgnored private var cursors: [String: Int] = [:]
|
||||
|
||||
init(_ kind: ChatKind, _ store: Store) {
|
||||
self.kind = kind
|
||||
@@ -231,21 +239,112 @@ final class ChatModel {
|
||||
}
|
||||
|
||||
func jump() {
|
||||
guard let target = store.jump, target.taskId == taskId else {
|
||||
guard let j = store.jump, j.taskId == taskId else {
|
||||
return
|
||||
}
|
||||
let rows = builder.root.rows
|
||||
guard let i = rows.firstIndex(where: { $0.summarySeq == target.seq }) else {
|
||||
return
|
||||
switch j.target {
|
||||
case .summary(let seq):
|
||||
guard let i = rows.firstIndex(where: { $0.summarySeq == seq }) else {
|
||||
return
|
||||
}
|
||||
store.jump = nil
|
||||
stuck = false
|
||||
switch i > 0 ? rows[i - 1].kind : nil {
|
||||
case .agent, .subagentMessage:
|
||||
requestScroll(rows[i - 1].id, .top)
|
||||
default:
|
||||
requestScroll(rows[i].id, .bottom)
|
||||
}
|
||||
case .question(let id):
|
||||
guard let row = rows.first(where: { $0.question?.id == id }) else {
|
||||
return
|
||||
}
|
||||
store.jump = nil
|
||||
scrollTo(row.id)
|
||||
case .review(let n):
|
||||
guard let row = reviewRow(n) else {
|
||||
return
|
||||
}
|
||||
store.jump = nil
|
||||
scrollTo(row.id)
|
||||
}
|
||||
store.jump = nil
|
||||
}
|
||||
|
||||
private func reviewRow(_ n: Int) -> Row? {
|
||||
let rows = builder.root.rows
|
||||
return rows.first { $0.reviewOf == n } ?? rows.first { r in
|
||||
if case .snapshot(let s) = r.kind {
|
||||
return s.n == n
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func showReview(_ n: Int) {
|
||||
if let row = reviewRow(n) {
|
||||
scrollTo(row.id)
|
||||
}
|
||||
}
|
||||
|
||||
func showFinding(_ reviewId: String, _ index: Int) {
|
||||
let row = builder.root.rows.first { r in
|
||||
if case .finding(let i) = r.kind {
|
||||
return i == index && r.review?.id == reviewId
|
||||
}
|
||||
return false
|
||||
}
|
||||
if let row {
|
||||
scrollTo(row.id)
|
||||
}
|
||||
}
|
||||
|
||||
func attention() -> [AttentionGroup] {
|
||||
guard let taskId else {
|
||||
return []
|
||||
}
|
||||
let latest = latestReview(store.transcript)
|
||||
let active = store.findTask(taskId)?.status == .active
|
||||
var questions: [Int] = []
|
||||
var open: [Int] = []
|
||||
var ready: [Int] = []
|
||||
for row in builder.root.rows where !row.hidden {
|
||||
switch row.kind {
|
||||
case .question where row.question?.state == .open:
|
||||
questions.append(row.id)
|
||||
case .finding(let i):
|
||||
if let r = row.review, r.id == latest?.id, findingOpen(r, i) {
|
||||
open.append(row.id)
|
||||
}
|
||||
case .ready where row.id == builder.lastReady && active:
|
||||
ready.append(row.id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
var groups: [AttentionGroup] = []
|
||||
if !questions.isEmpty {
|
||||
groups.append(AttentionGroup(id: "questions", label: questions.count == 1 ? "1 question" : "\(questions.count) questions", help: "Scroll to the next open question", rows: questions))
|
||||
}
|
||||
if !open.isEmpty {
|
||||
groups.append(AttentionGroup(id: "findings", label: open.count == 1 ? "1 finding" : "\(open.count) findings", help: "Scroll to the next finding of the latest review that nobody has answered", rows: open))
|
||||
}
|
||||
if !ready.isEmpty {
|
||||
groups.append(AttentionGroup(id: "ready", label: "ready to merge", help: "Scroll to the agent's ready-for-merge overview", rows: ready))
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func cycle(_ g: AttentionGroup) {
|
||||
let next = (cursors[g.id] ?? -1) + 1
|
||||
let i = next < g.rows.count ? next : 0
|
||||
cursors[g.id] = i
|
||||
scrollTo(g.rows[i])
|
||||
}
|
||||
|
||||
private func scrollTo(_ id: Int) {
|
||||
stuck = false
|
||||
switch i > 0 ? rows[i - 1].kind : nil {
|
||||
case .agent, .subagentMessage:
|
||||
requestScroll(rows[i - 1].id, .top)
|
||||
default:
|
||||
requestScroll(rows[i].id, .bottom)
|
||||
}
|
||||
requestScroll(id, .top)
|
||||
}
|
||||
|
||||
private func requestScroll(_ id: Int, _ anchor: UnitPoint) {
|
||||
@@ -360,7 +459,7 @@ struct ChatView: View {
|
||||
.onDisappear {
|
||||
model?.stop()
|
||||
}
|
||||
.onChange(of: store.jump?.seq) {
|
||||
.onChange(of: store.jump) {
|
||||
model?.jump()
|
||||
}
|
||||
}
|
||||
@@ -702,6 +801,57 @@ private struct TranscriptScroll: View {
|
||||
Rectangle().stroke(running ? accent : .clear, lineWidth: 1)
|
||||
.animation(.easeInOut(duration: 0.3), value: running)
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
AttentionBar(model: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttentionBar: View {
|
||||
let model: ChatModel
|
||||
|
||||
var body: some View {
|
||||
let groups = model.attention()
|
||||
if !groups.isEmpty {
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
ForEach(groups) { g in
|
||||
AttentionButton(group: g) {
|
||||
model.cycle(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.trailing, 14)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttentionButton: View {
|
||||
let group: AttentionGroup
|
||||
let action: () -> Void
|
||||
@ViewState private var hover = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text(group.label)
|
||||
.font(Fonts.font(11, weight: 700))
|
||||
.foregroundStyle(Palette.warn)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background {
|
||||
ZStack {
|
||||
Palette.bg
|
||||
Palette.warnTint
|
||||
}
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.overlay(Capsule().stroke(hover ? Palette.accent : Palette.warn.opacity(0.55)))
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.fixedSize()
|
||||
.help("\(group.help); click again for the next")
|
||||
.onHover { hover = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,13 +1184,24 @@ struct RowView: View {
|
||||
case .review(let note, let blocks):
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
NoteView(note: note, cancelVisible: false)
|
||||
MarkdownView(blocks: blocks, ctx: md)
|
||||
.padding(.leading, 10)
|
||||
.overlay(alignment: .leading) {
|
||||
Rectangle().fill(Palette.border).frame(width: 1)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
MarkdownView(blocks: blocks, ctx: md)
|
||||
ReviewImages(value: row.review?.result?.value["images"] ?? nil)
|
||||
}
|
||||
.padding(.leading, 10)
|
||||
.overlay(alignment: .leading) {
|
||||
Rectangle().fill(Palette.border).frame(width: 1)
|
||||
}
|
||||
}
|
||||
|
||||
case .finding(let index):
|
||||
if let review = row.review {
|
||||
FindingRow(review: review, index: index, taskId: row.taskId)
|
||||
}
|
||||
|
||||
case .findingResponse(let snapshot, let index, let response, let reason):
|
||||
FindingResponseView(row: row, model: model, snapshot: snapshot, index: index, response: response, reason: reason)
|
||||
|
||||
case .ready(let when, let blocks):
|
||||
ReadyBubble(when: when, blocks: blocks, taskId: row.taskId, latest: row.id == model.builder.lastReady, unmerged: model.hasUnmerged)
|
||||
|
||||
@@ -1061,7 +1222,9 @@ struct RowView: View {
|
||||
}
|
||||
|
||||
case .snapshot(let snap):
|
||||
SnapshotRow(snap: snap, review: row.review, taskId: row.taskId)
|
||||
SnapshotRow(snap: snap, review: row.review, taskId: row.taskId) {
|
||||
model.showReview(snap.n)
|
||||
}
|
||||
|
||||
case .question:
|
||||
if let item = row.question {
|
||||
@@ -1073,39 +1236,34 @@ struct RowView: View {
|
||||
|
||||
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)
|
||||
Group {
|
||||
if open {
|
||||
QuestionCard(item: item)
|
||||
.id("\(item.id)/\(item.state.rawValue)/\(item.responses.count)")
|
||||
} else {
|
||||
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))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.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.warnFaint, 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)
|
||||
}
|
||||
.environment(\.choiceTap, { [store, item] in
|
||||
store.showQuestion(item.taskId, item.id)
|
||||
})
|
||||
.onHover { hover = $0 }
|
||||
.help("Show this question in the Questions tab")
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(open ? Palette.warn.opacity(0.55) : Palette.border))
|
||||
}
|
||||
|
||||
private var status: String {
|
||||
@@ -1132,6 +1290,37 @@ private struct QuestionBubble: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct FindingResponseView: View {
|
||||
let row: Row
|
||||
let model: ChatModel
|
||||
let snapshot: Int
|
||||
let index: Int
|
||||
let response: String
|
||||
let reason: String
|
||||
|
||||
var body: some View {
|
||||
let title = row.review.flatMap { r in findings(r).indices.contains(index) ? str(findings(r)[index], "title") : nil } ?? ""
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("\(response) finding \(index + 1)\(title.isEmpty ? "" : ", \(title),") of snapshot #\(snapshot)'s review: \(reason)")
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Spacer(minLength: 0)
|
||||
if let review = row.review, findings(review).indices.contains(index) {
|
||||
Button("show") {
|
||||
model.showFinding(review.id, index)
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Scroll to the finding")
|
||||
if response == FindingResponse.ResponsePayload.ignoring.rawValue, let taskId = row.taskId {
|
||||
FindingActions(taskId: taskId, review: review, index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReadyBubble: View {
|
||||
let when: Date
|
||||
let blocks: [MDBlock]
|
||||
@@ -1328,6 +1517,7 @@ private struct SnapshotRow: View {
|
||||
let snap: Snapshot
|
||||
let review: Review?
|
||||
let taskId: String?
|
||||
let showReview: () -> Void
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var forking = false
|
||||
@@ -1363,6 +1553,9 @@ private struct SnapshotRow: View {
|
||||
.help("Start a new task from this snapshot, its agent continuing the conversation as it stood here")
|
||||
if snap.trigger != .answer {
|
||||
reviewButton
|
||||
if let review, review.state == .running {
|
||||
StopReviewButton(reviewId: review.id)
|
||||
}
|
||||
}
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
}
|
||||
@@ -1374,7 +1567,7 @@ private struct SnapshotRow: View {
|
||||
private var reviewButton: some View {
|
||||
let n = review.map(findingCount) ?? 0
|
||||
return Button {
|
||||
store.showReview(snap.n)
|
||||
showReview()
|
||||
} label: {
|
||||
Text("review")
|
||||
.font(Fonts.font(11, weight: n > 0 ? 700 : 400))
|
||||
@@ -1408,6 +1601,7 @@ private struct SnapshotRow: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle(color: n > 0 ? Palette.warn : Palette.accent, fill: review?.state == .running ? Palette.accentTint : .clear))
|
||||
.help(review?.state == .running ? "Review in progress; show snapshot \(snap.n)'s review in the Reviews tab" : "Show the review of snapshot \(snap.n) in the Reviews tab")
|
||||
.disabled(review == nil || review?.state == .running)
|
||||
.help(review?.state == .running ? "Review in progress" : review == nil ? "Not reviewed" : "Scroll to the review of snapshot \(snap.n)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import SwiftUI
|
||||
struct MenuEntry: Identifiable {
|
||||
let id: String
|
||||
let label: String
|
||||
let badge: TabBadge
|
||||
let action: () -> Void
|
||||
}
|
||||
|
||||
@@ -312,15 +311,12 @@ private struct MenuRow: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 6) {
|
||||
Text(entry.label)
|
||||
TabBadgeView(badge: entry.badge)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(hover ? Palette.surface : .clear, in: RoundedRectangle(cornerRadius: 4))
|
||||
.contentShape(Rectangle())
|
||||
Text(entry.label)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(hover ? Palette.surface : .clear, in: RoundedRectangle(cornerRadius: 4))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hover = $0 }
|
||||
|
||||
@@ -166,8 +166,7 @@ private func reviewWarnings(_ taskId: String, _ store: Store, _ leave: @escaping
|
||||
var items: [MergeWarning] = []
|
||||
let open: @MainActor () -> Void = {
|
||||
leave {
|
||||
store.focusTask(taskId, .reviews)
|
||||
store.pickReview(latest.n)
|
||||
store.jumpTo(taskId, .review(latest.n))
|
||||
}
|
||||
}
|
||||
let r = reviews.first { $0.snapshotN == latest.n }
|
||||
@@ -233,7 +232,7 @@ private struct MergeContent: View {
|
||||
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, .questions)
|
||||
store.showQuestion(taskId)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -2,33 +2,12 @@ import AppKit
|
||||
import ByreAPI
|
||||
import SwiftUI
|
||||
|
||||
enum TabBadge {
|
||||
case none
|
||||
case count(Int, String)
|
||||
}
|
||||
|
||||
struct TabBadgeView: View {
|
||||
let badge: TabBadge
|
||||
|
||||
var body: some View {
|
||||
switch badge {
|
||||
case .none:
|
||||
EmptyView()
|
||||
case .count(let n, let help):
|
||||
Badge(count: n)
|
||||
.help(help)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tabLabels: [TaskTab: String] = [
|
||||
.transcript: "Transcript",
|
||||
.board: "Board",
|
||||
.changes: "Changes",
|
||||
.files: "Files",
|
||||
.terminal: "Terminal",
|
||||
.questions: "Questions",
|
||||
.reviews: "Reviews",
|
||||
.workflow: "Workflow",
|
||||
.project: "Project",
|
||||
]
|
||||
@@ -94,30 +73,24 @@ struct MainView: View {
|
||||
struct PaneTabButton: View {
|
||||
let label: String
|
||||
var active = false
|
||||
var attention = false
|
||||
var badge = TabBadge.none
|
||||
let action: () -> Void
|
||||
@Environment(\.paneAccent) private var accent
|
||||
@ViewState private var hover = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 6) {
|
||||
Text(label)
|
||||
TabBadgeView(badge: badge)
|
||||
}
|
||||
.font(Fonts.font(13, weight: attention ? 700 : 400))
|
||||
.foregroundStyle(hover ? Palette.text : attention ? Palette.warn : active ? Palette.text : Palette.muted)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.background(attention ? Palette.warnTint : .clear)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(active ? accent : attention ? Palette.warn : .clear)
|
||||
.frame(height: 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
Text(label)
|
||||
.font(Fonts.font(13))
|
||||
.foregroundStyle(hover || active ? Palette.text : Palette.muted)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(active ? accent : .clear)
|
||||
.frame(height: 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.fixedSize()
|
||||
@@ -196,7 +169,6 @@ private struct LeftPane: View {
|
||||
private struct TaskPane: View {
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var seen: Set<String> = []
|
||||
|
||||
var body: some View {
|
||||
let selected = store.selectedTaskId.flatMap { store.findTask($0) }
|
||||
@@ -219,18 +191,6 @@ private struct TaskPane: View {
|
||||
} action: { frame in
|
||||
overlays.paneFrame = frame
|
||||
}
|
||||
.onAppear {
|
||||
seen = Set(store.inbox.map(\.id))
|
||||
}
|
||||
.onChange(of: store.inbox.map(\.id)) {
|
||||
let fresh = store.inbox.filter { !seen.contains($0.id) }
|
||||
for item in fresh {
|
||||
seen.insert(item.id)
|
||||
}
|
||||
if store.focus.tab == .transcript && !store.paneFocus.drafting && fresh.contains(where: { $0.taskId == store.selectedTaskId }) {
|
||||
store.focusTab(.questions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -247,10 +207,6 @@ private struct TaskPane: View {
|
||||
FilesView()
|
||||
case .terminal:
|
||||
TerminalTab()
|
||||
case .questions:
|
||||
QuestionsView()
|
||||
case .reviews:
|
||||
ReviewsView()
|
||||
case .workflow:
|
||||
WorkflowView()
|
||||
case .project:
|
||||
@@ -381,27 +337,10 @@ private struct TabStrip: View {
|
||||
private struct Info {
|
||||
var order: [TaskTab]
|
||||
var tab: TaskTab
|
||||
var waiting: Int
|
||||
var findings: Int
|
||||
var reviewing: Bool
|
||||
|
||||
func count(_ t: TaskTab) -> Int {
|
||||
t == .questions ? waiting : t == .reviews && !reviewing ? findings : 0
|
||||
}
|
||||
|
||||
func badge(_ t: TaskTab) -> TabBadge {
|
||||
count(t) > 0 ? .count(count(t), t == .questions ? "open questions" :"open findings in the latest review") : .none
|
||||
}
|
||||
}
|
||||
|
||||
private func info() -> Info {
|
||||
let tab = store.focus.tab
|
||||
let waiting = store.inbox.filter { $0.taskId == store.selectedTaskId }.count
|
||||
let transcript = store.transcript
|
||||
let latest = latestReview(transcript)
|
||||
let reviewing = latest?.state == .running
|
||||
let findings = latest.map(findingCount) ?? 0
|
||||
let hasReviews = transcript.contains(where: isReviewEvent)
|
||||
let workflow = store.selectedTaskId.flatMap { store.findTask($0) }?.workflowRun != nil
|
||||
let team = store.selectedTaskId.map { !store.team($0).isEmpty } ?? false
|
||||
let order = taskTabs.filter { t in
|
||||
@@ -410,13 +349,11 @@ private struct TabStrip: View {
|
||||
}
|
||||
switch t {
|
||||
case .board: return team
|
||||
case .questions: return waiting > 0
|
||||
case .reviews: return hasReviews
|
||||
case .workflow: return workflow
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
return Info(order: order, tab: tab, waiting: waiting, findings: findings, reviewing: reviewing)
|
||||
return Info(order: order, tab: tab)
|
||||
}
|
||||
|
||||
private func kept(_ info: Info) -> [TaskTab] {
|
||||
@@ -444,17 +381,16 @@ private struct TabStrip: View {
|
||||
let info = info()
|
||||
let kept = kept(info)
|
||||
let hidden = info.order.filter { !kept.contains($0) }
|
||||
let flagged = hidden.filter { info.count($0) > 0 }
|
||||
HStack(spacing: 4) {
|
||||
ForEach(info.order.filter { kept.contains($0) }, id: \.self) { t in
|
||||
PaneTabButton(label: tabLabels[t]!, active: t == info.tab, attention: info.count(t) > 0, badge: info.badge(t)) {
|
||||
PaneTabButton(label: tabLabels[t]!, active: t == info.tab) {
|
||||
store.focusTab(t)
|
||||
}
|
||||
}
|
||||
if !hidden.isEmpty {
|
||||
MoreButton(badges: flagged.map(info.badge)) {
|
||||
MoreButton {
|
||||
overlays.menu = OverlayMenu(anchor: moreFrame, accent: accent, entries: hidden.map { t in
|
||||
MenuEntry(id: t.rawValue, label: tabLabels[t]!, badge: info.badge(t)) {
|
||||
MenuEntry(id: t.rawValue, label: tabLabels[t]!) {
|
||||
store.focusTab(t)
|
||||
}
|
||||
})
|
||||
@@ -473,14 +409,14 @@ private struct TabStrip: View {
|
||||
.background {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(info.order, id: \.self) { t in
|
||||
PaneTabButton(label: tabLabels[t]!, active: t == info.tab, attention: info.count(t) > 0, badge: info.badge(t)) {}
|
||||
PaneTabButton(label: tabLabels[t]!, active: t == info.tab) {}
|
||||
.onGeometryChange(for: CGFloat.self) { g in
|
||||
g.size.width
|
||||
} action: { w in
|
||||
widths[t] = w
|
||||
}
|
||||
}
|
||||
MoreButton(badges: []) {}
|
||||
MoreButton {}
|
||||
.onGeometryChange(for: CGFloat.self) { g in
|
||||
g.size.width
|
||||
} action: { w in
|
||||
@@ -502,28 +438,18 @@ private struct TabStrip: View {
|
||||
}
|
||||
|
||||
private struct MoreButton: View {
|
||||
let badges: [TabBadge]
|
||||
let action: () -> Void
|
||||
@ViewState private var hover = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 6) {
|
||||
Text("More ▾")
|
||||
ForEach(Array(badges.enumerated()), id: \.offset) { _, b in
|
||||
TabBadgeView(badge: b)
|
||||
}
|
||||
}
|
||||
.font(Fonts.font(13, weight: badges.isEmpty ? 400 : 700))
|
||||
.foregroundStyle(hover ? Palette.text : badges.isEmpty ? Palette.muted : Palette.warn)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.background(badges.isEmpty ? .clear : Palette.warnTint)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle().fill(badges.isEmpty ? .clear : Palette.warn).frame(height: 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
Text("More ▾")
|
||||
.font(Fonts.font(13))
|
||||
.foregroundStyle(hover ? Palette.text : Palette.muted)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.fixedSize()
|
||||
|
||||
@@ -9,35 +9,6 @@ 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 QuestionsView: View {
|
||||
@Environment(Store.self) private var store
|
||||
|
||||
var body: some View {
|
||||
let taskId = store.selectedTaskId
|
||||
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 shown {
|
||||
let key = "\(shown.id)/\(shown.state.rawValue)/\(shown.responses.count)"
|
||||
ScrollView {
|
||||
QuestionCard(item: shown)
|
||||
.id(key)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.id(shown.id)
|
||||
} else {
|
||||
EmptyText(text: "Nothing waiting on you in this task.")
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
|
||||
private func numberChoices(_ blocks: [MDBlock], _ next: inout Int) -> [MDBlock] {
|
||||
blocks.map { block in
|
||||
switch block {
|
||||
@@ -179,13 +150,6 @@ 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 == .questions && store.selectedTaskId == item.taskId {
|
||||
store.focusTab(.transcript)
|
||||
}
|
||||
} catch {
|
||||
store.images.restore(key, imgs)
|
||||
overlays.showAlert(errorText(error))
|
||||
@@ -206,7 +170,7 @@ private final class CardModel {
|
||||
}
|
||||
}
|
||||
|
||||
private struct QuestionCard: View {
|
||||
struct QuestionCard: View {
|
||||
let item: InboxItem
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
|
||||
+72
-213
@@ -2,18 +2,10 @@ import ByreAPI
|
||||
import SwiftUI
|
||||
|
||||
typealias FindingState = Components.Schemas.FindingState
|
||||
typealias FindingResponse = Components.Schemas.FindingResponse
|
||||
|
||||
private let findingHead = Re(#"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@"#)
|
||||
|
||||
private func stateText(_ r: Review) -> String {
|
||||
switch r.state {
|
||||
case .running: "reviewing on \(r.vendor) (\(r.model))…"
|
||||
case .done: reviewCounts(r)
|
||||
case .failed: "failed"
|
||||
case .skipped: "skipped: no changes"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private enum SnapshotPatches {
|
||||
static var changes: [String: Task<TaskChanges, any Error>] = [:]
|
||||
@@ -78,160 +70,30 @@ private func linesAround(_ patch: String, _ line: Int, _ end: Int) -> [(kind: Pa
|
||||
return lines.filter { $0.at >= line - 3 && $0.at <= end + 3 }
|
||||
}
|
||||
|
||||
struct ReviewsView: View {
|
||||
@Environment(Store.self) private var store
|
||||
@ViewState private var reviews: [Review]?
|
||||
@ViewState private var error = ""
|
||||
@ViewState private var target = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
content
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.task(id: store.selectedTaskId) {
|
||||
target += 1
|
||||
reviews = nil
|
||||
error = ""
|
||||
await load()
|
||||
let off = store.selectedFeed?.appended.add { events in
|
||||
if events.contains(where: isReviewEvent) {
|
||||
Task {
|
||||
await load()
|
||||
}
|
||||
}
|
||||
}
|
||||
await untilCancelled()
|
||||
off?()
|
||||
target += 1
|
||||
}
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
guard let taskId = store.selectedTaskId else {
|
||||
return
|
||||
}
|
||||
target += 1
|
||||
let started = target
|
||||
do {
|
||||
let fresh = try await store.api.client.listReviews(path: .init(taskId: taskId)).ok.body.json
|
||||
if started == target {
|
||||
reviews = fresh
|
||||
error = ""
|
||||
}
|
||||
} catch {
|
||||
if started == target {
|
||||
reviews = nil
|
||||
self.error = errorText(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if let taskId = store.selectedTaskId {
|
||||
if !error.isEmpty {
|
||||
EmptyText(text: error, error: true)
|
||||
} else if let reviews {
|
||||
if let lastReview = reviews.last {
|
||||
let n = store.reviewed ?? lastReview.snapshotN
|
||||
let shown = reviews.last { $0.snapshotN == n }
|
||||
let ns = Array(Set([n] + reviews.map(\.snapshotN))).sorted(by: >)
|
||||
let next = ns.filter { $0 > n }.last
|
||||
let prev = ns.first { $0 < n }
|
||||
HStack(spacing: 8) {
|
||||
Picker("", selection: Binding(get: { n }, set: { store.pickReview($0) })) {
|
||||
ForEach(ns, id: \.self) { s in
|
||||
Text("snapshot #\(s)").tag(s)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.menu)
|
||||
.fixedSize()
|
||||
.help("Snapshot whose review to show")
|
||||
step(prev ?? n - 1, "← #\(prev ?? n - 1)", ns)
|
||||
step(next ?? n + 1, "#\(next ?? n + 1) →", ns)
|
||||
}
|
||||
.modifier(HeaderBar())
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let shown {
|
||||
ReviewCard(review: shown, taskId: taskId)
|
||||
.id(shown.id)
|
||||
} else {
|
||||
Muted(text: "No review of snapshot #\(n) yet.")
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
} else {
|
||||
EmptyText(text: "No reviews yet. Each new snapshot of the task is reviewed by an agent from the other vendor.")
|
||||
}
|
||||
} else {
|
||||
EmptyText(text: "Loading…")
|
||||
}
|
||||
} else {
|
||||
EmptyText(text: "No task selected.")
|
||||
}
|
||||
}
|
||||
|
||||
private func step(_ to: Int, _ label: String, _ ns: [Int]) -> some View {
|
||||
Button(label) {
|
||||
store.pickReview(to)
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle(color: Palette.text))
|
||||
.disabled(!ns.contains(to))
|
||||
}
|
||||
func findingResponse(_ r: Review, _ index: Int) -> FindingResponse? {
|
||||
r.findingResponses.additionalProperties[String(index)]
|
||||
}
|
||||
|
||||
struct ReviewCard: View {
|
||||
let review: Review
|
||||
let taskId: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text("snapshot #\(review.snapshotN)")
|
||||
.font(Fonts.font(11, weight: 700))
|
||||
.foregroundStyle(Palette.text)
|
||||
Text(stateText(review))
|
||||
.foregroundStyle(review.state == .failed ? Palette.warn : Palette.muted)
|
||||
Spacer(minLength: 0)
|
||||
Text(ago(review.createdAt))
|
||||
.help(isoText(review.createdAt))
|
||||
if review.state == .running {
|
||||
StopReviewButton(reviewId: review.id)
|
||||
}
|
||||
}
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
if review.state == .failed {
|
||||
Text(review.error)
|
||||
.foregroundStyle(Palette.danger)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
if review.state == .done {
|
||||
ReviewResult(review: review, taskId: taskId)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border))
|
||||
}
|
||||
func findingOpen(_ r: Review, _ index: Int) -> Bool {
|
||||
r.findingStates.additionalProperties[String(index)] == nil && findingResponse(r, index) == nil
|
||||
}
|
||||
|
||||
private struct StopReviewButton: View {
|
||||
func findingWhere(_ f: JSONDict) -> String {
|
||||
let path = str(f, "path") ?? ""
|
||||
let line = max(0, int(f, "line") ?? 0)
|
||||
let endRaw = int(f, "end_line") ?? 0
|
||||
let end = endRaw > line ? endRaw : line
|
||||
return path.isEmpty ? "" : "\(path)\(line > 0 ? ":\(line)\(end > line ? "–\(end)" : "")" : "")"
|
||||
}
|
||||
|
||||
struct StopReviewButton: View {
|
||||
let reviewId: String
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var busy = false
|
||||
|
||||
var body: some View {
|
||||
Button("Stop") {
|
||||
Button("stop") {
|
||||
busy = true
|
||||
Task {
|
||||
do {
|
||||
@@ -242,51 +104,32 @@ private struct StopReviewButton: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(danger: true))
|
||||
.buttonStyle(SmallLinkButtonStyle(color: Palette.danger))
|
||||
.disabled(busy)
|
||||
.help("End the reviewer and record this review as failed")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReviewResult: View {
|
||||
let review: Review
|
||||
let taskId: String?
|
||||
|
||||
var body: some View {
|
||||
let res = review.result?.value ?? [:]
|
||||
let fs = findings(review)
|
||||
let md = MDContext(taskId: taskId)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
MarkdownView(blocks: parseMarkdown(jsString(res["summary"] ?? "")), ctx: md)
|
||||
ReviewImages(value: res["images"] ?? nil)
|
||||
if !fs.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(Array(fs.enumerated()), id: \.offset) { i, f in
|
||||
FindingRow(review: review, index: i, finding: f, taskId: taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FindingRow: View {
|
||||
struct FindingRow: View {
|
||||
let review: Review
|
||||
let index: Int
|
||||
let finding: JSONDict
|
||||
let taskId: String?
|
||||
|
||||
var body: some View {
|
||||
let f = finding
|
||||
let f = findings(review)[index]
|
||||
let severity = jsString(f["severity"] ?? "")
|
||||
let path = str(f, "path") ?? ""
|
||||
let line = max(0, int(f, "line") ?? 0)
|
||||
let endRaw = int(f, "end_line") ?? 0
|
||||
let end = endRaw > line ? endRaw : line
|
||||
let whereText = path.isEmpty ? "" : "\(path)\(line > 0 ? ":\(line)\(end > line ? "–\(end)" : "")" : "")"
|
||||
let whereText = findingWhere(f)
|
||||
let md = MDContext(taskId: taskId)
|
||||
let response = findingResponse(review, index)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("\(index + 1).")
|
||||
.font(Fonts.font(11, weight: 700))
|
||||
.foregroundStyle(Palette.muted)
|
||||
Text(severity)
|
||||
.font(Fonts.font(11, weight: 700))
|
||||
.foregroundStyle(severity == "critical" || severity == "high" ? Palette.danger : severity == "medium" ? Palette.warn : Palette.muted)
|
||||
@@ -301,23 +144,33 @@ private struct FindingRow: View {
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
if let taskId {
|
||||
FindingActions(taskId: taskId, review: review, index: index, finding: f, whereText: whereText)
|
||||
FindingActions(taskId: taskId, review: review, index: index)
|
||||
}
|
||||
}
|
||||
if let taskId, !path.isEmpty, line > 0 {
|
||||
FindingDiff(taskId: taskId, n: review.snapshotN, path: path, line: line, end: end)
|
||||
if let response {
|
||||
Text("agent \(response.response.rawValue): \(response.reason)")
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
MarkdownView(blocks: parseMarkdown(jsString(f["detail"] ?? "")), ctx: md)
|
||||
ReviewImages(value: f["images"] ?? nil)
|
||||
Group {
|
||||
if let taskId, !path.isEmpty, line > 0 {
|
||||
FindingDiff(taskId: taskId, n: review.snapshotN, path: path, line: line, end: end)
|
||||
}
|
||||
MarkdownView(blocks: parseMarkdown(jsString(f["detail"] ?? "")), ctx: md)
|
||||
ReviewImages(value: f["images"] ?? nil)
|
||||
}
|
||||
.opacity(response == nil ? 1 : 0.5)
|
||||
}
|
||||
.padding(.top, 6)
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
.padding(.leading, 10)
|
||||
.overlay(alignment: .leading) {
|
||||
Rectangle().fill(Palette.border).frame(width: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReviewImages: View {
|
||||
struct ReviewImages: View {
|
||||
let value: (any Sendable)?
|
||||
|
||||
var body: some View {
|
||||
@@ -399,36 +252,42 @@ private struct DiffBox: ViewModifier {
|
||||
}
|
||||
}
|
||||
|
||||
private struct FindingActions: View {
|
||||
struct FindingActions: View {
|
||||
let taskId: String
|
||||
let review: Review
|
||||
let index: Int
|
||||
let finding: JSONDict
|
||||
let whereText: String
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var done: FindingState?
|
||||
@ViewState private var busy = false
|
||||
|
||||
var body: some View {
|
||||
if let s = done ?? review.findingStates.additionalProperties[String(index)] {
|
||||
Text(s == .fixed ? "fix sent" : "ignored")
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
} else {
|
||||
HStack(spacing: 6) {
|
||||
Button("fix") {
|
||||
fix()
|
||||
let response = findingResponse(review, index)?.response
|
||||
let state = review.findingStates.additionalProperties[String(index)]
|
||||
Group {
|
||||
if let state {
|
||||
Text(state == .fixed ? "fix sent" : "ignored")
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
} else if response != .fixing {
|
||||
HStack(spacing: 6) {
|
||||
Button("fix") {
|
||||
fix()
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help(response == .ignoring ? "Overrule the agent: send this finding to it to fix" : "Send this finding to the task's agent to fix")
|
||||
if response == nil {
|
||||
Button("ignore") {
|
||||
ignore()
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Mark this finding done without fixing it; Merge won't warn about it")
|
||||
}
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Send this finding to the task's agent to fix")
|
||||
Button("ignore") {
|
||||
ignore()
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Mark this finding done without fixing it; Merge won't warn about it")
|
||||
.disabled(busy)
|
||||
}
|
||||
.disabled(busy)
|
||||
}
|
||||
.onChange(of: state) {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,8 +296,9 @@ private struct FindingActions: View {
|
||||
}
|
||||
|
||||
private func fix() {
|
||||
let f = finding
|
||||
let text = "Fix this finding from the review of snapshot #\(review.snapshotN):\n\n**[\(jsString(f["severity"] ?? ""))] \(jsString(f["title"] ?? ""))**\(whereText.isEmpty ? "" : " (`\(whereText)`)")\n\n\(jsString(f["detail"] ?? ""))"
|
||||
let f = findings(review)[index]
|
||||
let whereText = findingWhere(f)
|
||||
let text = "Fix finding \(index + 1) from the review of snapshot #\(review.snapshotN):\n\n**[\(jsString(f["severity"] ?? ""))] \(jsString(f["title"] ?? ""))**\(whereText.isEmpty ? "" : " (`\(whereText)`)")\n\n\(jsString(f["detail"] ?? ""))"
|
||||
busy = true
|
||||
Task {
|
||||
do {
|
||||
@@ -448,10 +308,10 @@ private struct FindingActions: View {
|
||||
overlays.showAlert(errorText(error))
|
||||
return
|
||||
}
|
||||
done = .fixed
|
||||
do {
|
||||
try await mark(.fixed)
|
||||
} catch {
|
||||
busy = false
|
||||
overlays.showAlert("fix sent, but not recorded: \(errorText(error))")
|
||||
}
|
||||
}
|
||||
@@ -462,7 +322,6 @@ private struct FindingActions: View {
|
||||
Task {
|
||||
do {
|
||||
try await mark(.ignored)
|
||||
done = .ignored
|
||||
} catch {
|
||||
busy = false
|
||||
overlays.showAlert(errorText(error))
|
||||
|
||||
@@ -2,7 +2,19 @@ import Foundation
|
||||
import Observation
|
||||
import SwiftData
|
||||
|
||||
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .terminal, .questions, .reviews, .workflow, .project]
|
||||
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .terminal, .workflow, .project]
|
||||
|
||||
enum JumpTarget: Equatable {
|
||||
case summary(Int64)
|
||||
case question(String)
|
||||
case review(Int)
|
||||
}
|
||||
|
||||
struct Jump: Equatable {
|
||||
let taskId: String
|
||||
let target: JumpTarget
|
||||
let n: Int
|
||||
}
|
||||
|
||||
private let historyPage = 1000
|
||||
private let leadPage = 50
|
||||
@@ -36,13 +48,12 @@ final class Store {
|
||||
var inbox: [InboxItem] = []
|
||||
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(tab: .transcript)
|
||||
var leadSessions: [AgentSession] = []
|
||||
let leadFeed: Feed
|
||||
var jump: (taskId: String, seq: Int64)?
|
||||
var jump: Jump?
|
||||
@ObservationIgnored private var jumps = 0
|
||||
var composerFocus: String?
|
||||
var summaryReads: [String: Int64] = [:]
|
||||
private(set) var leadMore = false
|
||||
@@ -224,28 +235,21 @@ final class Store {
|
||||
pinned = n
|
||||
}
|
||||
|
||||
func pickReview(_ n: Int) {
|
||||
reviewed = n
|
||||
func jumpTo(_ taskId: String, _ target: JumpTarget) {
|
||||
jumps += 1
|
||||
jump = Jump(taskId: taskId, target: target, n: jumps)
|
||||
focusTask(taskId, .transcript)
|
||||
}
|
||||
|
||||
func showReview(_ n: Int) {
|
||||
pickReview(n)
|
||||
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
|
||||
func showQuestion(_ taskId: String) {
|
||||
guard let item = inbox.filter({ $0.taskId == taskId }).min(by: { $0.createdAt < $1.createdAt }) else {
|
||||
focusTask(taskId, .transcript)
|
||||
return
|
||||
}
|
||||
guard let ev = transcript.last(where: { ($0.kind == "inbox.created" || $0.kind == "inbox.updated") && str($0.payload.value, "id") == itemId }) else {
|
||||
return nil
|
||||
if let member = sessions[taskId]?.first(where: { $0.id == item.agentSessionId })?.member, !member.isEmpty {
|
||||
pickedMembers[taskId] = member
|
||||
}
|
||||
return try! decodePayload(ev.payload, as: InboxItem.self)
|
||||
jumpTo(taskId, .question(item.id))
|
||||
}
|
||||
|
||||
var defaultModel: String {
|
||||
@@ -360,7 +364,6 @@ final class Store {
|
||||
}
|
||||
|
||||
func focusTask(_ taskId: String, _ tab: TaskTab = .transcript) {
|
||||
question = nil
|
||||
spawn("set focus") {
|
||||
try await self.setFocus(taskId: taskId, tab: tab)
|
||||
}
|
||||
@@ -373,7 +376,6 @@ final class Store {
|
||||
}
|
||||
|
||||
func focusTab(_ tab: TaskTab) {
|
||||
question = nil
|
||||
spawn("set focus") {
|
||||
try await self.setFocus(tab: tab)
|
||||
}
|
||||
@@ -386,7 +388,6 @@ 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)
|
||||
@@ -507,8 +508,7 @@ final class Store {
|
||||
let seq = task.summarySeq
|
||||
if let ev = taskFeeds[task.id]?.events.first(where: { $0.seq == seq }) {
|
||||
pickMember(task.id, ev)
|
||||
jump = (task.id, seq)
|
||||
focusTask(task.id, .transcript)
|
||||
jumpTo(task.id, .summary(seq))
|
||||
return
|
||||
}
|
||||
Task {
|
||||
@@ -520,8 +520,7 @@ final class Store {
|
||||
} catch {
|
||||
log.error("load summary event: \(errorText(error), privacy: .public)")
|
||||
}
|
||||
jump = (task.id, seq)
|
||||
focusTask(task.id, .transcript)
|
||||
jumpTo(task.id, .summary(seq))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +538,6 @@ final class Store {
|
||||
_ = taskFeed(taskId)
|
||||
}
|
||||
pinned = nil
|
||||
reviewed = nil
|
||||
dropClosedFeeds()
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ final class SwitcherModel {
|
||||
private func go(_ taskId: String) {
|
||||
close()
|
||||
if store.inbox.contains(where: { $0.taskId == taskId }) {
|
||||
store.focusTask(taskId, .questions)
|
||||
store.showQuestion(taskId)
|
||||
} else if let task = store.findTask(taskId), store.unreadSummary(task) {
|
||||
store.composerFocus = taskId
|
||||
store.openSummary(task)
|
||||
|
||||
@@ -51,6 +51,8 @@ enum RowKind {
|
||||
case turnEnd(error: String?)
|
||||
case note(Note)
|
||||
case review(note: Note, md: [MDBlock])
|
||||
case finding(index: Int)
|
||||
case findingResponse(snapshot: Int, index: Int, response: String, reason: String)
|
||||
case ready(when: Date, md: [MDBlock])
|
||||
case handoff
|
||||
case snapshot(Snapshot)
|
||||
@@ -84,6 +86,7 @@ final class Row: Identifiable {
|
||||
var calls: [Call] = []
|
||||
var summary: Note?
|
||||
var review: Review?
|
||||
var reviewOf: Int?
|
||||
var question: InboxItem?
|
||||
var text = ""
|
||||
var md: [MDBlock] = []
|
||||
@@ -175,6 +178,8 @@ private final class Ctx {
|
||||
var subagents: [String: Subagent] = [:]
|
||||
var reviewers: Set<String> = []
|
||||
var reviewButtons: [Int: Row] = [:]
|
||||
var reviews: [Int: Review] = [:]
|
||||
var reviewRows: [String: [Row]] = [:]
|
||||
var questions: [String: Row] = [:]
|
||||
var run: Run?
|
||||
var held: RowList?
|
||||
@@ -471,6 +476,17 @@ final class TranscriptBuilder {
|
||||
if ev.kind == "review.started" || ev.kind == "review.finished" || ev.kind == "review.finding" {
|
||||
let r = try! decodePayload(ev.payload, as: Review.self)
|
||||
ctx.reviewButtons[r.snapshotN]?.review = r
|
||||
ctx.reviews[r.snapshotN] = r
|
||||
for row in ctx.reviewRows[r.id] ?? [] {
|
||||
row.review = r
|
||||
}
|
||||
if ev.kind == "review.finished" {
|
||||
ctx.run = nil
|
||||
for row in renderReview(r, ev.createdAt) {
|
||||
put(row)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ev.kind == "inbox.updated" {
|
||||
let item = try! decodePayload(ev.payload, as: InboxItem.self)
|
||||
@@ -490,6 +506,20 @@ final class TranscriptBuilder {
|
||||
put(row, keepHeld: true)
|
||||
continue
|
||||
}
|
||||
if ev.kind == "agent.tool_call" && sub == nil && str(p, "name") == "mcp__byre__respond_to_finding" {
|
||||
let input = dict(p, "input") ?? [:]
|
||||
let n = int(input, "snapshot") ?? 0
|
||||
let row = Row(.findingResponse(snapshot: n, index: (int(input, "finding") ?? 0) - 1, response: str(input, "response") ?? "", reason: str(input, "reason") ?? ""), taskId: taskId)
|
||||
row.started = ev.createdAt
|
||||
ctx.tools[jsString(p["id"])] = row
|
||||
if let r = ctx.reviews[n], r.state == .done {
|
||||
row.review = r
|
||||
ctx.reviewRows[r.id, default: []].append(row)
|
||||
}
|
||||
ctx.run = nil
|
||||
put(row, keepHeld: true)
|
||||
continue
|
||||
}
|
||||
if ev.kind == "agent.tool_call" {
|
||||
let name = toolLabel(jsString(p["name"]))
|
||||
let row = renderToolCall(p)
|
||||
@@ -605,6 +635,22 @@ final class TranscriptBuilder {
|
||||
return row
|
||||
}
|
||||
|
||||
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)
|
||||
let head = fs.isEmpty
|
||||
? Row(.note(Note(parts: parts, when: when, style: r.state == .failed ? .error : .plain)), taskId: taskId)
|
||||
: Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: taskId)
|
||||
let rows = [head] + fs.indices.map { Row(.finding(index: $0), taskId: taskId) }
|
||||
for row in rows {
|
||||
row.review = r
|
||||
row.reviewOf = r.snapshotN
|
||||
}
|
||||
ctx.reviewRows[r.id, default: []] += rows
|
||||
return rows
|
||||
}
|
||||
|
||||
private func renderEvent(_ ev: ByreEvent) -> Row? {
|
||||
let p = ev.payload.value
|
||||
let when = ev.createdAt
|
||||
@@ -654,6 +700,9 @@ final class TranscriptBuilder {
|
||||
return Row(.orphan(body), taskId: taskId)
|
||||
}
|
||||
target.result = body
|
||||
if case .findingResponse = target.kind, bool(p, "is_error") {
|
||||
target.hidden = true
|
||||
}
|
||||
return nil
|
||||
|
||||
case "turn.ended":
|
||||
@@ -709,15 +758,6 @@ final class TranscriptBuilder {
|
||||
case "task.merge_title":
|
||||
return note([.text("\(ev.actor) set the merge commit title to \"\(jsString(p["merge_title"]))\"")])
|
||||
|
||||
case "review.finished":
|
||||
let r = try! decodePayload(ev.payload, as: Review.self)
|
||||
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)")]
|
||||
if findings(r).isEmpty {
|
||||
return note(parts, r.state == .failed ? .error : .plain)
|
||||
}
|
||||
return Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: taskId)
|
||||
|
||||
case "task.merge_queued":
|
||||
return note([.text("\(ev.actor) asked to merge; held until the merge in progress in this project is done")])
|
||||
|
||||
@@ -778,7 +818,7 @@ func findings(_ r: Review) -> [JSONDict] {
|
||||
}
|
||||
|
||||
func findingCount(_ r: Review) -> Int {
|
||||
r.state == .done ? findings(r).indices.filter { r.findingStates.additionalProperties[String($0)] == nil }.count : 0
|
||||
r.state == .done ? findings(r).indices.filter { findingOpen(r, $0) }.count : 0
|
||||
}
|
||||
|
||||
func latestReview(_ events: [ByreEvent]) -> Review? {
|
||||
|
||||
Reference in New Issue
Block a user