Track fixed and ignored review findings

This commit is contained in:
Ian Gulliver
2026-10-16 10:44:27 -07:00
parent 0f2415570c
commit 8200ffd17e
20 changed files with 569 additions and 243 deletions
+47 -14
View File
@@ -1,9 +1,10 @@
import { get, post, type ByreEvent, type Schemas } from "./api.js";
import { get, post, put, type ByreEvent, type Schemas } from "./api.js";
import { ago, errorText, h, replace, showAlert } from "./dom.js";
import { markdown } from "./markdown.js";
import { focusTask, onEach, pickReview, state } from "./state.js";
import { onEach, pickReview, state } from "./state.js";
type Review = Schemas["Review"];
type FindingState = Schemas["FindingState"];
const severities = ["critical", "high", "medium", "low", "info"];
@@ -17,7 +18,11 @@ function findingsOf(r: Review): Record<string, unknown>[] {
}
export function findingCount(r: Review): number {
return r.state === "done" ? findingsOf(r).length : 0;
return r.state === "done" ? findingsOf(r).filter((_, i) => !r.finding_states[String(i)]).length : 0;
}
export function unignoredFindings(r: Review): Record<string, unknown>[] {
return r.state === "done" ? findingsOf(r).filter((_, i) => r.finding_states[String(i)] !== "ignored") : [];
}
export function latestReview(events: ByreEvent[]): Review | undefined {
@@ -26,7 +31,10 @@ export function latestReview(events: ByreEvent[]): Review | undefined {
}
export function reviewCounts(r: Review): string {
const findings = findingsOf(r);
return severityCounts(findingsOf(r));
}
export function severityCounts(findings: Record<string, unknown>[]): string {
const parts = severities.map((s) => [s, findings.filter((f) => f["severity"] === s).length] as const).filter(([, n]) => n > 0);
return parts.length > 0 ? parts.map(([s, n]) => `${n} ${s}`).join(", ") : "no findings";
}
@@ -73,7 +81,7 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
? h(
"ol",
{ class: "findings" },
...findings.map((f) => {
...findings.map((f, i) => {
const severity = String(f["severity"] ?? "");
const path = typeof f["path"] === "string" ? f["path"] : "";
const line = typeof f["line"] === "number" && f["line"] > 0 ? f["line"] : 0;
@@ -89,7 +97,7 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
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,
taskId ? fixButton(taskId, r, f, where) : null,
taskId ? findingActions(taskId, r, i, f, where) : null,
),
taskId && path && line ? findingDiff(taskId, r.snapshot_n, path, line, end) : null,
markdown(String(f["detail"] ?? ""), "md finding-detail", md),
@@ -205,11 +213,21 @@ function findingDiff(taskId: string, n: number, path: string, line: number, end:
return box;
}
function fixButton(taskId: string, r: Review, f: Record<string, unknown>, where: string): HTMLElement {
const button = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
button.onclick = async () => {
function findingActions(taskId: string, r: Review, index: number, f: Record<string, unknown>, where: string): HTMLElement {
const box = h("span", { class: "finding-actions" });
const done = (s: FindingState) => replace(box, h("span", { class: "finding-done" }, s === "fixed" ? "fix sent" : "ignored"));
const current = r.finding_states[String(index)];
if (current) {
done(current);
return box;
}
const mark = (s: FindingState) => put("/api/reviews/{reviewId}/findings/{index}", { path: { reviewId: r.id, index }, body: { state: s } });
const fix = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
const ignore = h("button", { type: "button", class: "finding-ignore", title: "Mark this finding done without fixing it; Merge won't warn about it" }, "ignore");
fix.onclick = async () => {
const text = `Fix this finding from the review of snapshot #${r.snapshot_n}:\n\n**[${String(f["severity"] ?? "")}] ${String(f["title"] ?? "")}**${where ? ` (\`${where}\`)` : ""}\n\n${String(f["detail"] ?? "")}`;
button.disabled = true;
fix.disabled = true;
ignore.disabled = true;
try {
const sessions = await get("/api/tasks/{taskId}/sessions", { path: { taskId } });
const live = sessions.findLast((s) => s.state !== "completed");
@@ -218,14 +236,29 @@ function fixButton(taskId: string, r: Review, f: Record<string, unknown>, where:
} else {
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: text } });
}
replace(button, "sent");
focusTask(taskId);
} catch (err) {
button.disabled = false;
fix.disabled = false;
ignore.disabled = false;
void showAlert(errorText(err));
return;
}
done("fixed");
await mark("fixed").catch((err: unknown) => showAlert(`fix sent, but not recorded: ${errorText(err)}`));
};
ignore.onclick = async () => {
fix.disabled = true;
ignore.disabled = true;
try {
await mark("ignored");
done("ignored");
} catch (err) {
fix.disabled = false;
ignore.disabled = false;
void showAlert(errorText(err));
}
};
return button;
replace(box, fix, ignore);
return box;
}
export function mountReviews(root: HTMLElement): () => void {