Review every new snapshot with an agent from the other vendor

This commit is contained in:
Ian Gulliver
2026-10-15 16:33:17 -07:00
parent a539ddab43
commit e9039cceb4
39 changed files with 1737 additions and 218 deletions
+2 -1
View File
@@ -3,6 +3,7 @@ import { mountChanges } from "./changes.js";
import { ago, errorText, h, replace, showAlert } from "./dom.js";
import { image, inlineMarkdown, markdown } from "./markdown.js";
import { diagram } from "./mermaid.js";
import { reviewCard } from "./reviews.js";
import { focusTab, onEach, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const checked = new Map();
@@ -83,7 +84,7 @@ function card(item) {
const p = item.payload;
const body = h("div", { class: "card-body" });
const actions = h("div", { class: "card-actions" });
const el = h("article", { class: `card ${item.kind} ${item.priority}` }, h("header", {}, h("span", { class: `priority ${item.priority}` }, item.priority), h("span", { class: "kind" }, item.kind.replace("_", " ")), h("time", { dateTime: item.created_at, title: item.created_at }, ago(item.created_at))), body, thread(item), actions);
const el = h("article", { class: `card ${item.kind} ${item.priority}` }, h("header", {}, h("span", { class: `priority ${item.priority}` }, item.priority), h("span", { class: "kind" }, item.kind.replace("_", " ")), h("time", { dateTime: item.created_at, title: item.created_at }, ago(item.created_at))), body, item.reviews.length > 0 ? h("section", { class: "card-reviews" }, ...item.reviews.map((r) => reviewCard(r, item.task_id))) : null, thread(item), actions);
const busy = () => el.classList.contains("submitting");
const lock = (on) => {
el.classList.toggle("submitting", on);
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -8,6 +8,7 @@ import { openSwitcher } from "./navbar.js";
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
import { mountPreview } from "./preview.js";
import { mountProject } from "./project.js";
import { mountReviews } from "./reviews.js";
import { mountTranscript } from "./task.js";
import { mountTerminal } from "./terminal.js";
const views = {
@@ -17,6 +18,7 @@ const views = {
terminal: { label: "Terminal", mount: mountTerminal },
preview: { label: "Preview", mount: mountPreview },
inbox: { label: "Inbox", mount: mountInbox },
reviews: { label: "Reviews", mount: mountReviews },
project: { label: "Project", mount: mountProject },
};
export function leadPane() {
+1 -1
View File
File diff suppressed because one or more lines are too long
+114
View File
@@ -0,0 +1,114 @@
import { get } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { markdown } from "./markdown.js";
import { onEach, state } from "./state.js";
const severities = ["critical", "high", "medium", "low", "info"];
export function isReviewEvent(ev) {
return ev.kind.startsWith("review.");
}
export function reviewCounts(r) {
const res = (r.result ?? {});
const findings = Array.isArray(res["findings"]) ? res["findings"] : [];
const parts = severities.map((s) => [s, findings.filter((f) => f["severity"] === s).length]).filter(([, n]) => n > 0);
return parts.length > 0 ? parts.map(([s, n]) => `${n} ${s}`).join(", ") : "no findings";
}
function stateText(r) {
switch (r.state) {
case "running":
return `reviewing on ${r.vendor} (${r.model})…`;
case "done":
return reviewCounts(r);
case "failed":
return "failed";
}
}
export function reviewCard(r, taskId) {
return h("article", { class: `review ${r.state}` }, h("header", {}, h("span", { class: "review-snapshot" }, `snapshot #${r.snapshot_n}`), h("span", { class: `review-state ${r.state}` }, stateText(r)), h("time", { dateTime: r.created_at, title: r.created_at }, ago(r.created_at))), r.state === "failed" ? h("p", { class: "error-text" }, r.error) : null, r.state === "done" ? reviewResult(r, taskId) : null);
}
function reviewResult(r, taskId) {
const res = (r.result ?? {});
const findings = Array.isArray(res["findings"]) ? res["findings"] : [];
const md = { taskId };
return h("div", { class: "review-result" }, markdown(String(res["summary"] ?? ""), "md review-summary", md), findings.length > 0
? h("ol", { class: "findings" }, ...findings.map((f) => {
const severity = String(f["severity"] ?? "");
const where = typeof f["path"] === "string" && f["path"] ? `${f["path"]}${typeof f["line"] === "number" && f["line"] > 0 ? `:${f["line"]}` : ""}` : "";
return h("li", { class: `finding ${severity}` }, h("div", { class: "finding-head" }, h("span", { class: `severity ${severity}` }, severity), typeof f["category"] === "string" && f["category"] ? h("span", { class: "finding-category" }, f["category"]) : null, h("span", { class: "finding-title" }, String(f["title"] ?? "")), where ? h("code", { class: "finding-where" }, where) : null), markdown(String(f["detail"] ?? ""), "md finding-detail", md));
}))
: null);
}
export function mountReviews(root) {
const list = h("div", { class: "review-list" });
let reviews;
let error = "";
let target = 0;
let disposed = false;
const load = async () => {
const taskId = state.selectedTaskId;
if (!taskId) {
render();
return;
}
const started = ++target;
try {
const fresh = await get("/api/tasks/{taskId}/reviews", { path: { taskId } });
if (started === target) {
reviews = fresh;
error = "";
}
}
catch (err) {
if (started === target) {
reviews = undefined;
error = errorText(err);
}
}
if (!disposed && started === target) {
render();
}
};
const render = () => {
const taskId = state.selectedTaskId;
if (!taskId) {
replace(list, h("div", { class: "empty" }, "No task selected."));
return;
}
if (error) {
replace(list, h("div", { class: "empty error-text" }, error));
return;
}
if (!reviews) {
replace(list, h("div", { class: "empty" }, "Loading…"));
return;
}
if (reviews.length === 0) {
replace(list, h("div", { class: "empty" }, "No reviews yet. Each new snapshot of the task is reviewed by an agent from the other vendor."));
return;
}
replace(list, ...[...reviews].reverse().map((r) => reviewCard(r, taskId)));
};
const offs = [
onEach(["selection"], () => {
target++;
reviews = undefined;
error = "";
render();
void load();
}),
onEach(["transcript"], (fresh) => {
if (fresh.some(isReviewEvent)) {
void load();
}
}),
];
replace(root, h("div", { class: "reviews-view" }, list));
render();
void load();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
//# sourceMappingURL=reviews.js.map
+1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -1,5 +1,5 @@
import { get, put, subscribe } from "./api.js";
export const tabs = ["transcript", "changes", "files", "terminal", "preview", "inbox", "project"];
export const tabs = ["transcript", "changes", "files", "terminal", "preview", "inbox", "reviews", "project"];
export const state = {
me: undefined,
projects: [],
@@ -251,6 +251,12 @@ export function handleEvent(ev) {
case "inbox.updated":
void loadInbox();
break;
case "review.started":
case "review.finished":
if (state.inbox.some((i) => i.task_id === ev.task_id && i.kind === "question")) {
void loadInbox();
}
break;
case "focus.updated":
if (ev.actor !== state.me?.login) {
applyFocus(ev.payload);
+1 -1
View File
File diff suppressed because one or more lines are too long
+17 -1
View File
@@ -2,6 +2,7 @@ import { get, post } from "./api.js";
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
import { markdown } from "./markdown.js";
import { previewURL } from "./preview.js";
import { reviewCounts } from "./reviews.js";
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
export const models = [
@@ -177,6 +178,7 @@ export function mountChat(root, src, makeHead) {
const newCtx = () => ({
tools: new Map(),
subagents: new Map(),
reviewers: new Set(),
run: undefined,
lastMessage: undefined,
held: undefined,
@@ -320,7 +322,7 @@ export function mountChat(root, src, makeHead) {
stream = undefined;
};
const appendDelta = (ev) => {
if (ctx.handoff) {
if (ctx.handoff || (ev.agent_session_id && ctx.reviewers.has(ev.agent_session_id))) {
return;
}
if (!stream) {
@@ -368,6 +370,13 @@ export function mountChat(root, src, makeHead) {
for (const ev of events) {
const p = ev.payload;
const parent = parentOf(p);
if (ev.kind === "session.created" && p["role"] === "reviewer" && ev.agent_session_id) {
ctx.reviewers.add(ev.agent_session_id);
continue;
}
if (ev.agent_session_id && ctx.reviewers.has(ev.agent_session_id)) {
continue;
}
if (ev.kind === "agent.vendor") {
claudeTaskStatus(p, ctx);
continue;
@@ -879,6 +888,13 @@ function renderEvent(ev, ctx) {
}
case "task.merge_title":
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "review.finished": {
const r = p;
const open = h("button", { type: "button", title: "Show the task's reviews" }, "reviews");
open.onclick = () => focusTab("reviews");
const outcome = r.state === "failed" ? `failed: ${r.error}` : reviewCounts(r);
return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when, " ", open);
}
case "task.merge_queued":
return h("div", { class: "note" }, `${ev.actor} asked to merge; held until the merge in progress in this project is done `, when);
case "preview.exposed": {
+1 -1
View File
File diff suppressed because one or more lines are too long