Happy review

This commit is contained in:
Ian Gulliver
2026-10-15 20:07:36 -07:00
parent ccd55afe13
commit 35368242e3
14 changed files with 312 additions and 191 deletions
+6 -5
View File
@@ -1015,13 +1015,14 @@ components:
id: { type: string } id: { type: string }
task_id: { type: string } task_id: { type: string }
snapshot_n: { type: integer } snapshot_n: { type: integer }
agent_session_id: { type: string, description: The reviewer's session. } agent_session_id: { type: string, description: "The reviewer's session; empty when skipped." }
vendor: { type: string } vendor: { type: string, description: "Empty when skipped." }
model: { type: string } model: { type: string, description: "Empty when skipped." }
state: state:
type: string type: string
enum: [running, done, failed] enum: [running, done, failed, skipped]
x-enum-varnames: [ReviewStateRunning, ReviewStateDone, ReviewStateFailed] x-enum-varnames: [ReviewStateRunning, ReviewStateDone, ReviewStateFailed, ReviewStateSkipped]
description: "skipped: the snapshot's step changed no files, so no reviewer ran."
result: { type: object, description: "done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line." } result: { type: object, description: "done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line." }
error: { type: string, description: "failed: why." } error: { type: string, description: "failed: why." }
created_at: { type: string, format: date-time } created_at: { type: string, format: date-time }
+3 -3
View File
@@ -238,14 +238,14 @@ The fork's session guidance gets one more line (`forkGuidance` in `internal/agen
## Reviews ## Reviews
Every new snapshot of a task gets a review: a separate agent session on the vendor the task's agent doesn't use reads the change and reports on it (`internal/agents/review.go`, `internal/store/review.go`). A turn that changes no files makes no snapshot and so no review, and a snapshot whose change (the diff below) has no files, such as a task's first snapshot taken before its agent changed anything, gets no review either. Nothing else starts a review, and nothing waits for one: reviews ride along for the human to look at when they want. Every new snapshot of a task gets a review: a separate agent session on the vendor the task's agent doesn't use reads the change and reports on it (`internal/agents/review.go`, `internal/store/review.go`). A turn that changes no files makes no snapshot and so no review, and a snapshot whose change (the diff below) has no files, such as a task's first snapshot taken before its agent changed anything, runs no reviewer: it gets a `reviews` row in state `skipped`, with no session, vendor or model. Nothing else starts a review, and nothing waits for one: reviews ride along for the human to look at when they want.
- **Starting.** After `takeSnapshot` creates a snapshot, `afterSnapshot` runs `startReviewer`: it picks the vendor opposite the task's latest implementer session (`reviewVendor`) and that vendor's model in `reviewModels`, checks the snapshot's commit out detached at `worktrees/<project-id>/review-<review-id>`, creates an `agent_sessions` row with `role` `reviewer` on the task (created by the task's creator), a `reviews` row (`running`, one per snapshot), and sends `reviewPrompt` as its first turn (actor `byre`): the change as `git diff` from the previous snapshot (the snapshot commit's parent for the first) with its changed files, the merge-base diff of all the task's work for context, and the task's first message to its agent. - **Starting.** After `takeSnapshot` creates a snapshot, `afterSnapshot` runs `startReviewer`: it picks the vendor opposite the task's latest implementer session (`reviewVendor`) and that vendor's model in `reviewModels`, checks the snapshot's commit out detached at `worktrees/<project-id>/review-<review-id>`, creates an `agent_sessions` row with `role` `reviewer` on the task (created by the task's creator), a `reviews` row (`running`, one per snapshot), and sends `reviewPrompt` as its first turn (actor `byre`): the change as `git diff` from the previous snapshot (the snapshot commit's parent for the first) with its changed files, the merge-base diff of all the task's work for context, and the task's first message to its agent.
- **Running.** Reviewer sessions get `reviewGuidance` and only the `submit_review` tool, whose arguments (`reviewSchema`: `change`, a few sentences of Markdown on what the change does, required and non-empty; a Markdown `summary`, which the guidance and schema require to be exactly "No significant findings." when there are no findings and otherwise a bulleted list with one line per finding; `findings`, each with `severity`, `category`, `title`, `detail`, `path`, `line`, `end_line` and `images`; and top-level `images`) become the review's `result` and mark it `done`. The guidance encourages, without requiring, Mermaid blocks for a data flow, call graph, state machine or schema the change alters, and screenshots when the change alters something visible. - **Running.** Reviewer sessions get `reviewGuidance` and only the `submit_review` tool, whose arguments (`reviewSchema`: `change`, a few sentences of Markdown on what the change does, required and non-empty; a Markdown `summary`, which the guidance and schema require to be exactly "No significant findings." when there are no findings and otherwise a bulleted list with one line per finding; `findings`, each with `severity`, `category`, `title`, `detail`, `path`, `line`, `end_line` and `images`; and top-level `images`) become the review's `result` and mark it `done`. The guidance encourages, without requiring, Mermaid blocks for a data flow, call graph, state machine or schema the change alters, and screenshots when the change alters something visible.
- **Screenshots.** Reviewer sessions also get the Playwright MCP server (`playwrightMCP` in `internal/agents/review.go`: `npx @playwright/mcp` at a pinned version, headless Chrome, isolated profile, absolute file paths), passed to Claude as a `stdio` entry in `--mcp-config` and to Codex as `mcp_servers` in the thread's `config`. Its automatically named files go to `worktrees/<project-id>/review-<review-id>-media`, removed with the checkout. Each `images` entry is `{path, caption}`, the path absolute or relative to the checkout; at `submit_review` byre reads every one, refuses the whole submission (nothing recorded) for a missing file or one that isn't one of `store.ImageTypes` within `store.MaxBlobBytes`, then stores each as a blob and replaces its `path` with `blob`, since the files are deleted with the checkout. They run without the Stop-hook bounce, mid-turn delivery, end-of-turn commit, snapshots or turn summaries, and their `user.message` events carry no `head`. When the reviewer's turn ends byre completes its session, kills its process and removes the worktree; a review still `running` then fails with why (no submission, with the final message; an error; an interrupt). A process that exits mid-turn fails the review the same way. Merging or discarding the task fails its running reviews ("the task was closed") and ends their reviewers. On startup, a running reviewer resumes like any session, and a running review whose reviewer isn't `running` fails. - **Screenshots.** Reviewer sessions also get the Playwright MCP server (`playwrightMCP` in `internal/agents/review.go`: `npx @playwright/mcp` at a pinned version, headless Chrome, isolated profile, absolute file paths), passed to Claude as a `stdio` entry in `--mcp-config` and to Codex as `mcp_servers` in the thread's `config`. Its automatically named files go to `worktrees/<project-id>/review-<review-id>-media`, removed with the checkout. Each `images` entry is `{path, caption}`, the path absolute or relative to the checkout; at `submit_review` byre reads every one, refuses the whole submission (nothing recorded) for a missing file or one that isn't one of `store.ImageTypes` within `store.MaxBlobBytes`, then stores each as a blob and replaces its `path` with `blob`, since the files are deleted with the checkout. They run without the Stop-hook bounce, mid-turn delivery, end-of-turn commit, snapshots or turn summaries, and their `user.message` events carry no `head`. When the reviewer's turn ends byre completes its session, kills its process and removes the worktree; a review still `running` then fails with why (no submission, with the final message; an error; an interrupt). A process that exits mid-turn fails the review the same way. Merging or discarding the task fails its running reviews ("the task was closed") and ends their reviewers. On startup, a running reviewer resumes like any session, and a running review whose reviewer isn't `running` fails.
- **Questions.** A question's `reviews` is the review of the first snapshot the task took after the question was filed, the one at the end of the turn that asked it, computed when the item is read; the Inbox tab shows it under the question once it starts. - **Questions.** A question's `reviews` is the review of the first snapshot the task took after the question was filed, the one at the end of the turn that asked it, computed when the item is read; the Inbox tab shows it under the question once it starts.
- **Events.** `review.started` and `review.finished` (state `done` or `failed`) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions. - **Events.** `review.started` and `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions.
- **UI.** The **Reviews** tab (`web/src/reviews.ts`) shows one review at a time, picked by snapshot (see "Snapshots" for the picker): its snapshot, state or severity counts, error, `change`, summary, screenshots and findings. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot's step (`changes?snapshot=<n>&step=true`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Screenshots link to the full image. Each finding has a **fix** button (also on review cards in the inbox) that sends the finding's severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` when its latest live session is idle, starting a session when it has none, and refused with an alert while the agent is mid-turn. It loads when it opens, when the task changes, and on each `review.*` event in the task's stream. When the finished review of the highest-numbered snapshot has findings, the tab (or the More menu holding it) is highlighted with a badge of the finding count. Each snapshot row's **review** button gets the same treatment from that snapshot's `review.finished`: highlighted, with the count in a badge over its top-right corner. `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones. - **UI.** The **Reviews** tab (`web/src/reviews.ts`) shows one review at a time, picked by snapshot (see "Snapshots" for the picker): its snapshot, state or severity counts, error, `change`, summary, screenshots and findings. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot's step (`changes?snapshot=<n>&step=true`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Screenshots link to the full image. Each finding has a **fix** button (also on review cards in the inbox) that sends the finding's severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` when its latest live session is idle, starting a session when it has none, and refused with an alert while the agent is mid-turn. It loads when it opens, when the task changes, and on each `review.*` event in the task's stream. When the finished review of the highest-numbered snapshot has findings, the tab (or the More menu holding it) is highlighted with a badge of the finding count. Each snapshot row's **review** button carries a mark over its top-right corner from that snapshot's latest `review.*` event: a spinner while the review runs, a grey ● when it was skipped, a green ✓ when it finished with no findings, and, when it has findings, the button highlighted with the count in a badge. A failed review leaves the button unmarked. `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones.
## Events ## Events
+15
View File
@@ -142,6 +142,18 @@ func (m *Manager) startReviewer(ctx context.Context, task api.Task, snap api.Sna
return err return err
} }
if prompt == "" { if prompt == "" {
now := time.Now().UTC()
err = m.store.CreateReview(ctx, api.Review{
Id: store.NewID(),
TaskId: task.Id,
SnapshotN: snap.N,
State: api.ReviewStateSkipped,
CreatedAt: now,
FinishedAt: &now,
})
if err != nil {
return err
}
slog.Info("review skipped, snapshot has no diff", "task", task.Id, "snapshot", snap.N) slog.Info("review skipped, snapshot has no diff", "task", task.Id, "snapshot", snap.N)
return nil return nil
} }
@@ -494,6 +506,9 @@ func reviewLine(r api.Review) string {
if r.State == api.ReviewStateFailed { if r.State == api.ReviewStateFailed {
return fmt.Sprintf("review of snapshot %d failed: %s", r.SnapshotN, clip(r.Error, 300)) return fmt.Sprintf("review of snapshot %d failed: %s", r.SnapshotN, clip(r.Error, 300))
} }
if r.State == api.ReviewStateSkipped {
return fmt.Sprintf("review of snapshot %d skipped: no changes", r.SnapshotN)
}
res := map[string]any{} res := map[string]any{}
if r.Result != nil { if r.Result != nil {
res = *r.Result res = *r.Result
+173 -163
View File
@@ -278,6 +278,7 @@ const (
ReviewStateDone ReviewState = "done" ReviewStateDone ReviewState = "done"
ReviewStateFailed ReviewState = "failed" ReviewStateFailed ReviewState = "failed"
ReviewStateRunning ReviewState = "running" ReviewStateRunning ReviewState = "running"
ReviewStateSkipped ReviewState = "skipped"
) )
// Valid indicates whether the value is a known member of the ReviewState enum. // Valid indicates whether the value is a known member of the ReviewState enum.
@@ -289,6 +290,8 @@ func (e ReviewState) Valid() bool {
return true return true
case ReviewStateRunning: case ReviewStateRunning:
return true return true
case ReviewStateSkipped:
return true
default: default:
return false return false
} }
@@ -748,7 +751,7 @@ type ProtocolErrors struct {
// Review An agent session on the vendor the task's agent doesn't use, reviewing one snapshot's change. // Review An agent session on the vendor the task's agent doesn't use, reviewing one snapshot's change.
type Review struct { type Review struct {
// AgentSessionId The reviewer's session. // AgentSessionId The reviewer's session; empty when skipped.
AgentSessionId string `json:"agent_session_id"` AgentSessionId string `json:"agent_session_id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -756,17 +759,23 @@ type Review struct {
Error string `json:"error"` Error string `json:"error"`
FinishedAt *time.Time `json:"finished_at,omitempty"` FinishedAt *time.Time `json:"finished_at,omitempty"`
Id string `json:"id"` Id string `json:"id"`
Model string `json:"model"`
// Model Empty when skipped.
Model string `json:"model"`
// Result done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line. // Result done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line.
Result *map[string]interface{} `json:"result,omitempty"` Result *map[string]interface{} `json:"result,omitempty"`
SnapshotN int `json:"snapshot_n"` SnapshotN int `json:"snapshot_n"`
State ReviewState `json:"state"`
TaskId string `json:"task_id"` // State skipped: the snapshot's step changed no files, so no reviewer ran.
Vendor string `json:"vendor"` State ReviewState `json:"state"`
TaskId string `json:"task_id"`
// Vendor Empty when skipped.
Vendor string `json:"vendor"`
} }
// ReviewState defines model for Review.State. // ReviewState skipped: the snapshot's step changed no files, so no reviewer ran.
type ReviewState string type ReviewState string
// SessionCreate defines model for SessionCreate. // SessionCreate defines model for SessionCreate.
@@ -2715,163 +2724,164 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several // const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal. // times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
} }
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+12 -5
View File
@@ -10,18 +10,25 @@ import (
"g.fc.run/misc/byre/internal/api" "g.fc.run/misc/byre/internal/api"
) )
const reviewColumns = `id, task_id, snapshot_n, agent_session_id, vendor, model, state, result, error, created_at, finished_at` const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at`
func (s *Store) CreateReview(ctx context.Context, r api.Review) error { func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
finishedAt := ""
if r.FinishedAt != nil {
finishedAt = formatTime(*r.FinishedAt)
}
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, ` _, err := tx.ExecContext(ctx, `
INSERT INTO reviews (id, task_id, snapshot_n, agent_session_id, vendor, model, state, created_at) INSERT INTO reviews (id, task_id, snapshot_n, agent_session_id, vendor, model, state, created_at, finished_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, nullif(?, ''), ?, ?, ?, ?, ?)
`, r.Id, r.TaskId, r.SnapshotN, r.AgentSessionId, r.Vendor, r.Model, string(r.State), formatTime(r.CreatedAt)) `, r.Id, r.TaskId, r.SnapshotN, r.AgentSessionId, r.Vendor, r.Model, string(r.State), formatTime(r.CreatedAt), finishedAt)
if err != nil { if err != nil {
return err return err
} }
return emitReview(ctx, tx, emit, "review.started", r.Id) if r.State == api.ReviewStateRunning {
return emitReview(ctx, tx, emit, "review.started", r.Id)
}
return emitReview(ctx, tx, emit, "review.finished", r.Id)
}) })
} }
+19
View File
@@ -321,6 +321,25 @@ DROP TABLE vapid_keys;
`}, {"delete-report-inbox-items", ` `}, {"delete-report-inbox-items", `
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'report'); DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'report');
DELETE FROM inbox_items WHERE kind = 'report'; DELETE FROM inbox_items WHERE kind = 'report';
`}, {"allow-sessionless-reviews", `
CREATE TABLE reviews_new (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
snapshot_n INTEGER NOT NULL,
agent_session_id TEXT REFERENCES agent_sessions(id),
vendor TEXT NOT NULL,
model TEXT NOT NULL,
state TEXT NOT NULL,
result TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
finished_at TEXT NOT NULL DEFAULT '',
UNIQUE (task_id, snapshot_n)
);
INSERT INTO reviews_new SELECT * FROM reviews;
DROP TABLE reviews;
ALTER TABLE reviews_new RENAME TO reviews;
CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
`}} `}}
const ( const (
+2
View File
@@ -31,6 +31,8 @@ function stateText(r) {
return reviewCounts(r); return reviewCounts(r);
case "failed": case "failed":
return "failed"; return "failed";
case "skipped":
return "skipped: no changes";
} }
} }
export function reviewCard(r, taskId) { export function reviewCard(r, taskId) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -5
View File
@@ -360,11 +360,11 @@ export function mountChat(root, src, makeHead) {
put(snapRow(p, ctx)); put(snapRow(p, ctx));
continue; continue;
} }
if (ev.kind === "review.finished") { if (ev.kind === "review.started" || ev.kind === "review.finished") {
const r = p; const r = p;
const button = ctx.reviewButtons.get(r.snapshot_n); const button = ctx.reviewButtons.get(r.snapshot_n);
if (button) { if (button) {
markFindings(button, findingCount(r)); markReview(button, r);
} }
} }
if (ev.kind === "agent.tool_call" && isSubagentCall(p)) { if (ev.kind === "agent.tool_call" && isSubagentCall(p)) {
@@ -717,9 +717,26 @@ function claudeTaskStatus(p, ctx) {
return; return;
} }
} }
function markFindings(button, n) { function markReview(button, r) {
replace(button, "review", n > 0 && h("span", { class: "badge", title: `${n} findings` }, n)); const n = findingCount(r);
button.classList.toggle("attention", n > 0); button.classList.toggle("attention", n > 0);
if (r.state === "running") {
replace(button, "review", h("span", { class: "review-mark spinner", title: "review in progress" }));
return;
}
if (r.state === "failed") {
replace(button, "review");
return;
}
if (r.state === "skipped") {
replace(button, "review", h("span", { class: "review-mark skipped", title: "skipped: no changes" }, "●"));
return;
}
if (n === 0) {
replace(button, "review", h("span", { class: "review-mark clean", title: "no findings" }, "✓"));
return;
}
replace(button, "review", h("span", { class: "badge", title: `${n} findings` }, n));
} }
function snapRow(snap, ctx) { function snapRow(snap, ctx) {
const taskId = ctx.taskId; const taskId = ctx.taskId;
@@ -855,7 +872,7 @@ function renderEvent(ev, ctx) {
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when); return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "review.finished": { case "review.finished": {
const r = p; const r = p;
const outcome = r.state === "failed" ? `failed: ${r.error}` : reviewCounts(r); const outcome = r.state === "failed" ? `failed: ${r.error}` : r.state === "skipped" ? "skipped: no changes" : reviewCounts(r);
return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when); return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when);
} }
case "task.merge_queued": case "task.merge_queued":
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -3
View File
@@ -942,12 +942,17 @@ export interface components {
id: string; id: string;
task_id: string; task_id: string;
snapshot_n: number; snapshot_n: number;
/** @description The reviewer's session. */ /** @description The reviewer's session; empty when skipped. */
agent_session_id: string; agent_session_id: string;
/** @description Empty when skipped. */
vendor: string; vendor: string;
/** @description Empty when skipped. */
model: string; model: string;
/** @enum {string} */ /**
state: "running" | "done" | "failed"; * @description skipped: the snapshot's step changed no files, so no reviewer ran.
* @enum {string}
*/
state: "running" | "done" | "failed" | "skipped";
/** @description done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line. */ /** @description done: the reviewer's submit_review arguments: summary (Markdown) and findings, each with severity, category, title, detail, path and line. */
result?: Record<string, never>; result?: Record<string, never>;
/** @description failed: why. */ /** @description failed: why. */
+2
View File
@@ -40,6 +40,8 @@ function stateText(r: Review): string {
return reviewCounts(r); return reviewCounts(r);
case "failed": case "failed":
return "failed"; return "failed";
case "skipped":
return "skipped: no changes";
} }
} }
+22 -5
View File
@@ -439,11 +439,11 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
put(snapRow(p as unknown as Snapshot, ctx)); put(snapRow(p as unknown as Snapshot, ctx));
continue; continue;
} }
if (ev.kind === "review.finished") { if (ev.kind === "review.started" || ev.kind === "review.finished") {
const r = p as unknown as Review; const r = p as unknown as Review;
const button = ctx.reviewButtons.get(r.snapshot_n); const button = ctx.reviewButtons.get(r.snapshot_n);
if (button) { if (button) {
markFindings(button, findingCount(r)); markReview(button, r);
} }
} }
if (ev.kind === "agent.tool_call" && isSubagentCall(p)) { if (ev.kind === "agent.tool_call" && isSubagentCall(p)) {
@@ -846,9 +846,26 @@ function claudeTaskStatus(p: Record<string, unknown>, ctx: Ctx): void {
} }
} }
function markFindings(button: HTMLElement, n: number): void { function markReview(button: HTMLElement, r: Review): void {
replace(button, "review", n > 0 && h("span", { class: "badge", title: `${n} findings` }, n)); const n = findingCount(r);
button.classList.toggle("attention", n > 0); button.classList.toggle("attention", n > 0);
if (r.state === "running") {
replace(button, "review", h("span", { class: "review-mark spinner", title: "review in progress" }));
return;
}
if (r.state === "failed") {
replace(button, "review");
return;
}
if (r.state === "skipped") {
replace(button, "review", h("span", { class: "review-mark skipped", title: "skipped: no changes" }, "●"));
return;
}
if (n === 0) {
replace(button, "review", h("span", { class: "review-mark clean", title: "no findings" }, "✓"));
return;
}
replace(button, "review", h("span", { class: "badge", title: `${n} findings` }, n));
} }
function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement { function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement {
@@ -1015,7 +1032,7 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
case "review.finished": { case "review.finished": {
const r = p as unknown as Review; const r = p as unknown as Review;
const outcome = r.state === "failed" ? `failed: ${r.error}` : reviewCounts(r); const outcome = r.state === "failed" ? `failed: ${r.error}` : r.state === "skipped" ? "skipped: no changes" : reviewCounts(r);
return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when); return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when);
} }
+26
View File
@@ -957,6 +957,32 @@ button.compact.danger {
line-height: 14px; line-height: 14px;
} }
.snap-row button .review-mark {
position: absolute;
top: -7px;
right: -7px;
}
.snap-row button .review-mark.spinner {
margin: 0;
background: var(--muted);
}
.snap-row button .review-mark.clean,
.snap-row button .review-mark.skipped {
font-size: 11px;
font-weight: 700;
line-height: 1;
}
.snap-row button .review-mark.clean {
color: var(--ok);
}
.snap-row button .review-mark.skipped {
color: var(--muted);
}
.note.ask { .note.ask {
color: var(--accent); color: var(--accent);
} }