Tighten review format, badge finding counts, add per-finding fix button
This commit is contained in:
+12
-13
@@ -8,7 +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 { isReviewEvent, mountReviews } from "./reviews.js";
|
||||
import { isReviewEvent, latestFindingCount, mountReviews } from "./reviews.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTerminal } from "./terminal.js";
|
||||
|
||||
@@ -60,6 +60,10 @@ export function taskPane(): HTMLElement {
|
||||
let buttons: { tab: TaskTab; button: HTMLButtonElement }[] = [];
|
||||
let drawn = "";
|
||||
let waiting = 0;
|
||||
let findings = 0;
|
||||
|
||||
const count = (t: TaskTab): number => (t === "inbox" ? waiting : t === "reviews" ? findings : 0);
|
||||
const badge = (t: TaskTab) => count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "findings in the latest review" }, count(t));
|
||||
|
||||
const layout = () => {
|
||||
for (const { button } of buttons) {
|
||||
@@ -90,13 +94,13 @@ export function taskPane(): HTMLElement {
|
||||
for (const { tab, button } of buttons) {
|
||||
button.hidden = hidden.includes(tab);
|
||||
}
|
||||
const attention = hidden.includes("inbox");
|
||||
more.classList.toggle("attention", attention);
|
||||
replace(more, "More ▾", attention && h("span", { class: "badge", title: "inbox items" }, waiting));
|
||||
const flagged = hidden.filter((t) => count(t) > 0);
|
||||
more.classList.toggle("attention", flagged.length > 0);
|
||||
replace(more, "More ▾", ...flagged.map(badge));
|
||||
replace(
|
||||
menu,
|
||||
...hidden.map((t) => {
|
||||
const b = h("button", { type: "button" }, views[t].label, t === "inbox" && h("span", { class: "badge", title: "inbox items" }, waiting));
|
||||
const b = h("button", { type: "button" }, views[t].label, badge(t));
|
||||
b.onclick = () => {
|
||||
menu.hidePopover();
|
||||
focusTab(t);
|
||||
@@ -126,18 +130,13 @@ export function taskPane(): HTMLElement {
|
||||
const first = state.focus.tabs.filter((t, i, all) => tabs.includes(t) && all.indexOf(t) === i);
|
||||
const tab = state.focus.tab;
|
||||
waiting = state.inbox.filter((item) => item.task_id === state.selectedTaskId).length;
|
||||
findings = latestFindingCount(state.transcript);
|
||||
const order = [...first, ...tabs.filter((t) => !first.includes(t))].filter(available);
|
||||
const key = JSON.stringify([order, tab, waiting]);
|
||||
const key = JSON.stringify([order, tab, waiting, findings]);
|
||||
if (key !== drawn) {
|
||||
drawn = key;
|
||||
buttons = order.map((t) => {
|
||||
const attention = t === "inbox" && waiting > 0;
|
||||
const button = h(
|
||||
"button",
|
||||
{ class: [t === tab ? "active" : "", attention ? "attention" : ""].join(" ").trim() },
|
||||
views[t].label,
|
||||
attention && h("span", { class: "badge", title: "inbox items" }, waiting),
|
||||
);
|
||||
const button = h("button", { class: [t === tab ? "active" : "", count(t) > 0 ? "attention" : ""].join(" ").trim() }, views[t].label, badge(t));
|
||||
button.onclick = () => focusTab(t);
|
||||
return { tab: t, button };
|
||||
});
|
||||
|
||||
+47
-5
@@ -1,5 +1,5 @@
|
||||
import { get, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { get, post, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { onEach, pickReview, state } from "./state.js";
|
||||
|
||||
@@ -11,9 +11,23 @@ export function isReviewEvent(ev: ByreEvent): boolean {
|
||||
return ev.kind.startsWith("review.");
|
||||
}
|
||||
|
||||
export function reviewCounts(r: Review): string {
|
||||
function findingsOf(r: Review): Record<string, unknown>[] {
|
||||
const res = (r.result ?? {}) as Record<string, unknown>;
|
||||
const findings = Array.isArray(res["findings"]) ? (res["findings"] as Record<string, unknown>[]) : [];
|
||||
return Array.isArray(res["findings"]) ? (res["findings"] as Record<string, unknown>[]) : [];
|
||||
}
|
||||
|
||||
export function findingCount(r: Review): number {
|
||||
return r.state === "done" ? findingsOf(r).length : 0;
|
||||
}
|
||||
|
||||
export function latestFindingCount(events: ByreEvent[]): number {
|
||||
const finished = events.filter((ev) => ev.kind === "review.finished").map((ev) => ev.payload as unknown as Review);
|
||||
const top = finished.reduce<Review | undefined>((best, r) => (!best || r.snapshot_n >= best.snapshot_n ? r : best), undefined);
|
||||
return top ? findingCount(top) : 0;
|
||||
}
|
||||
|
||||
export function reviewCounts(r: Review): string {
|
||||
const findings = findingsOf(r);
|
||||
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";
|
||||
}
|
||||
@@ -47,7 +61,7 @@ export function reviewCard(r: Review, taskId: string | undefined): HTMLElement {
|
||||
|
||||
function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
|
||||
const res = (r.result ?? {}) as Record<string, unknown>;
|
||||
const findings = Array.isArray(res["findings"]) ? (res["findings"] as Record<string, unknown>[]) : [];
|
||||
const findings = findingsOf(r);
|
||||
const md = { taskId };
|
||||
return h(
|
||||
"div",
|
||||
@@ -70,6 +84,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,
|
||||
),
|
||||
markdown(String(f["detail"] ?? ""), "md finding-detail", md),
|
||||
);
|
||||
@@ -79,6 +94,33 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
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 () => {
|
||||
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;
|
||||
try {
|
||||
const sessions = await get("/api/tasks/{taskId}/sessions", { path: { taskId } });
|
||||
const live = sessions.findLast((s) => s.state !== "completed");
|
||||
if (live && live.state !== "idle") {
|
||||
button.disabled = false;
|
||||
void showAlert("The agent is mid-turn. Send this once its turn ends.");
|
||||
return;
|
||||
}
|
||||
if (live) {
|
||||
await post("/api/sessions/{sessionId}/messages", { path: { sessionId: live.id }, body: { text } });
|
||||
} else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: text } });
|
||||
}
|
||||
replace(button, "sent");
|
||||
} catch (err) {
|
||||
button.disabled = false;
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
export function mountReviews(root: HTMLElement): () => void {
|
||||
const head = h("header", { class: "reviews-header" });
|
||||
const body = h("div", { class: "review-list" });
|
||||
|
||||
+19
-3
@@ -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 { reviewCounts } from "./reviews.js";
|
||||
import { findingCount, reviewCounts } from "./reviews.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
|
||||
@@ -68,6 +68,7 @@ type Ctx = {
|
||||
tools: Map<string, HTMLElement>;
|
||||
subagents: Map<string, Subagent>;
|
||||
reviewers: Set<string>;
|
||||
reviewButtons: Map<number, HTMLElement>;
|
||||
run: Run | undefined;
|
||||
root: () => string;
|
||||
taskId: string | undefined;
|
||||
@@ -251,6 +252,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
tools: new Map(),
|
||||
subagents: new Map(),
|
||||
reviewers: new Set(),
|
||||
reviewButtons: new Map(),
|
||||
run: undefined,
|
||||
lastMessage: undefined,
|
||||
held: undefined,
|
||||
@@ -479,9 +481,16 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
}
|
||||
if (ev.kind === "snapshot.created") {
|
||||
ctx.run = undefined;
|
||||
put(snapRow(p as unknown as Snapshot, ctx.taskId));
|
||||
put(snapRow(p as unknown as Snapshot, ctx));
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "review.finished") {
|
||||
const r = p as unknown as Review;
|
||||
const button = ctx.reviewButtons.get(r.snapshot_n);
|
||||
if (button) {
|
||||
markFindings(button, findingCount(r));
|
||||
}
|
||||
}
|
||||
if (ev.kind === "agent.tool_call" && isSubagentCall(p)) {
|
||||
ctx.run = undefined;
|
||||
put(renderSubagent(p, ctx));
|
||||
@@ -888,7 +897,13 @@ function claudeTaskStatus(p: Record<string, unknown>, ctx: Ctx): void {
|
||||
}
|
||||
}
|
||||
|
||||
function snapRow(snap: Snapshot, taskId: string | undefined): HTMLElement {
|
||||
function markFindings(button: HTMLElement, n: number): void {
|
||||
replace(button, "review", n > 0 && h("span", { class: "badge", title: `${n} findings` }, n));
|
||||
button.classList.toggle("attention", n > 0);
|
||||
}
|
||||
|
||||
function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement {
|
||||
const taskId = ctx.taskId;
|
||||
const changes = h("button", { type: "button", title: `Show snapshot ${snap.n} in the Changes tab` }, "changes");
|
||||
changes.onclick = () => {
|
||||
pinSnapshot(snap.n);
|
||||
@@ -910,6 +925,7 @@ function snapRow(snap: Snapshot, taskId: string | undefined): HTMLElement {
|
||||
};
|
||||
const review = h("button", { type: "button", title: `Show the review of snapshot ${snap.n} in the Reviews tab` }, "review");
|
||||
review.onclick = () => showReview(snap.n);
|
||||
ctx.reviewButtons.set(snap.n, review);
|
||||
const time = new Date(snap.created_at).toLocaleTimeString([], { hour12: false });
|
||||
return h("div", { class: "snap-row", title: `${snap.trigger.replace("_", " ")} · ${time}` }, `snapshot #${snap.n}`, changes, fork, review);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user