Track fixed and ignored review findings
This commit is contained in:
@@ -518,6 +518,27 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/reviews/{reviewId}/findings/{index}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
reviewId: string;
|
||||
/** @description The finding's position in the review's findings, from 0. */
|
||||
index: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
/** @description Records that the human addressed a finding of a done review, by sending it to the agent to fix or by ignoring it. */
|
||||
put: operations["setFindingState"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/tasks/{taskId}/snapshots/{n}/fork": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1061,6 +1082,18 @@ export interface components {
|
||||
created_at: string;
|
||||
/** Format: date-time */
|
||||
finished_at?: string;
|
||||
/** @description How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open. */
|
||||
finding_states: {
|
||||
[key: string]: components["schemas"]["FindingState"];
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @description fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning.
|
||||
* @enum {string}
|
||||
*/
|
||||
FindingState: "fixed" | "ignored";
|
||||
FindingStateUpdate: {
|
||||
state: components["schemas"]["FindingState"];
|
||||
};
|
||||
AgentSession: {
|
||||
id: string;
|
||||
@@ -2115,6 +2148,36 @@ export interface operations {
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
setFindingState: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
reviewId: string;
|
||||
/** @description The finding's position in the review's findings, from 0. */
|
||||
index: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["FindingStateUpdate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The review. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Review"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Error"];
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
forkSnapshot: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ export function taskPane(): HTMLElement {
|
||||
if (t === "reviews" && reviewing) {
|
||||
return h("span", { class: "spinner", title: "review in progress" });
|
||||
}
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "findings in the latest review" }, count(t));
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "open findings in the latest review" }, count(t));
|
||||
};
|
||||
|
||||
const layout = () => {
|
||||
|
||||
+47
-14
@@ -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 {
|
||||
|
||||
@@ -351,6 +351,11 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
void loadInbox();
|
||||
}
|
||||
break;
|
||||
case "review.finding":
|
||||
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 as unknown as Focus);
|
||||
|
||||
+6
-6
@@ -2,7 +2,7 @@ import { get, post, type AgentSession, type ByreEvent, type Participant, type Sc
|
||||
import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { findingCount, reviewCounts } from "./reviews.js";
|
||||
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
|
||||
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
@@ -458,7 +458,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
put(snapRow(p as unknown as Snapshot, ctx));
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished") {
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished" || ev.kind === "review.finding") {
|
||||
const r = p as unknown as Review;
|
||||
const button = ctx.reviewButtons.get(r.snapshot_n);
|
||||
if (button) {
|
||||
@@ -932,10 +932,10 @@ function markReview(button: HTMLElement, r: Review): void {
|
||||
return;
|
||||
}
|
||||
if (n === 0) {
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: "no findings" }, "✓"));
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: Object.keys(r.finding_states).length > 0 ? "every finding addressed" : "no findings" }, "✓"));
|
||||
return;
|
||||
}
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} findings` }, n));
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} open findings` }, n));
|
||||
}
|
||||
|
||||
function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement {
|
||||
@@ -1341,8 +1341,8 @@ async function reviewWarnings(taskId: string, leave: (go: () => void) => void):
|
||||
items.push(mergeWarning("Review of the latest snapshot is still running", open));
|
||||
} else if (r.state === "failed") {
|
||||
items.push(mergeWarning("Review of the latest snapshot failed", open));
|
||||
} else if (findingCount(r) > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${reviewCounts(r)}`, open));
|
||||
} else if (unignoredFindings(r).length > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${severityCounts(unignoredFindings(r))}`, open));
|
||||
}
|
||||
const changes = await get("/api/tasks/{taskId}/changes", { path: { taskId }, query: { since: latest.commit_sha } });
|
||||
if (changes.files.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user