Add per-snapshot review link and snapshot picker to Reviews tab
This commit is contained in:
Vendored
+27
-8
@@ -1,7 +1,7 @@
|
||||
import { get } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { onEach, state } from "./state.js";
|
||||
import { onEach, pickReview, state } from "./state.js";
|
||||
const severities = ["critical", "high", "medium", "low", "info"];
|
||||
export function isReviewEvent(ev) {
|
||||
return ev.kind.startsWith("review.");
|
||||
@@ -38,7 +38,8 @@ function reviewResult(r, taskId) {
|
||||
: null);
|
||||
}
|
||||
export function mountReviews(root) {
|
||||
const list = h("div", { class: "review-list" });
|
||||
const head = h("header", { class: "reviews-header" });
|
||||
const body = h("div", { class: "review-list" });
|
||||
let reviews;
|
||||
let error = "";
|
||||
let target = 0;
|
||||
@@ -69,23 +70,40 @@ export function mountReviews(root) {
|
||||
};
|
||||
const render = () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
replace(head);
|
||||
head.hidden = true;
|
||||
if (!taskId) {
|
||||
replace(list, h("div", { class: "empty" }, "No task selected."));
|
||||
replace(body, h("div", { class: "empty" }, "No task selected."));
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
replace(list, h("div", { class: "empty error-text" }, error));
|
||||
replace(body, h("div", { class: "empty error-text" }, error));
|
||||
return;
|
||||
}
|
||||
if (!reviews) {
|
||||
replace(list, h("div", { class: "empty" }, "Loading…"));
|
||||
replace(body, 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."));
|
||||
replace(body, 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 n = state.reviewed ?? reviews.at(-1).snapshot_n;
|
||||
const shown = reviews.findLast((r) => r.snapshot_n === n);
|
||||
const ns = [...new Set([n, ...reviews.map((r) => r.snapshot_n)])].sort((a, b) => b - a);
|
||||
const picker = h("select", { title: "Snapshot whose review to show" }, ...ns.map((s) => h("option", { value: String(s) }, `snapshot #${s}`)));
|
||||
picker.value = String(n);
|
||||
picker.onchange = () => pickReview(Number(picker.value));
|
||||
const step = (to, label) => {
|
||||
const b = h("button", { type: "button", class: "snap-step", disabled: !ns.includes(to) }, label);
|
||||
b.onclick = () => pickReview(to);
|
||||
return b;
|
||||
};
|
||||
const next = ns.filter((s) => s > n).at(-1);
|
||||
const prev = ns.find((s) => s < n);
|
||||
head.hidden = false;
|
||||
replace(head, picker, step(prev ?? n - 1, `← #${prev ?? n - 1}`), step(next ?? n + 1, `#${next ?? n + 1} →`));
|
||||
replace(body, shown ? reviewCard(shown, taskId) : h("div", { class: "empty" }, `No review of snapshot #${n} yet.`));
|
||||
};
|
||||
const offs = [
|
||||
onEach(["selection"], () => {
|
||||
@@ -100,8 +118,9 @@ export function mountReviews(root) {
|
||||
void load();
|
||||
}
|
||||
}),
|
||||
onEach(["review"], render),
|
||||
];
|
||||
replace(root, h("div", { class: "reviews-view" }, list));
|
||||
replace(root, h("div", { class: "reviews-view" }, head, body));
|
||||
render();
|
||||
void load();
|
||||
return () => {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
@@ -9,6 +9,7 @@ export const state = {
|
||||
selectedTaskId: undefined,
|
||||
transcript: [],
|
||||
pinned: undefined,
|
||||
reviewed: undefined,
|
||||
connected: false,
|
||||
focus: { tabs: ["transcript"], tab: "transcript" },
|
||||
leadSessions: [],
|
||||
@@ -62,6 +63,14 @@ export function pinSnapshot(n) {
|
||||
state.pinned = n;
|
||||
notify("pin");
|
||||
}
|
||||
export function pickReview(n) {
|
||||
state.reviewed = n;
|
||||
notify("review");
|
||||
}
|
||||
export function showReview(n) {
|
||||
pickReview(n);
|
||||
focusTab("reviews");
|
||||
}
|
||||
export async function loadMe() {
|
||||
state.me = await get("/api/me");
|
||||
notify("me");
|
||||
@@ -203,6 +212,7 @@ export function selectTask(taskId) {
|
||||
taskFeed.reset();
|
||||
state.selectedTaskId = taskId;
|
||||
state.pinned = undefined;
|
||||
state.reviewed = undefined;
|
||||
notify("selection");
|
||||
if (!taskId) {
|
||||
return;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5
-5
@@ -3,7 +3,7 @@ import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./
|
||||
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 { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, showReview, state } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
export const models = [
|
||||
"claude-opus-5-5",
|
||||
@@ -776,8 +776,10 @@ function snapRow(snap, taskId) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
const review = h("button", { type: "button", title: `Show the review of snapshot ${snap.n} in the Reviews tab` }, "review");
|
||||
review.onclick = () => showReview(snap.n);
|
||||
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);
|
||||
return h("div", { class: "snap-row", title: `${snap.trigger.replace("_", " ")} · ${time}` }, `snapshot #${snap.n}`, changes, fork, review);
|
||||
}
|
||||
function runSummary(calls) {
|
||||
const counts = new Map();
|
||||
@@ -890,10 +892,8 @@ function renderEvent(ev, ctx) {
|
||||
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);
|
||||
return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when);
|
||||
}
|
||||
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);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+27
-8
@@ -1,7 +1,7 @@
|
||||
import { get, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { onEach, state } from "./state.js";
|
||||
import { onEach, pickReview, state } from "./state.js";
|
||||
|
||||
type Review = Schemas["Review"];
|
||||
|
||||
@@ -80,7 +80,8 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
|
||||
}
|
||||
|
||||
export function mountReviews(root: HTMLElement): () => void {
|
||||
const list = h("div", { class: "review-list" });
|
||||
const head = h("header", { class: "reviews-header" });
|
||||
const body = h("div", { class: "review-list" });
|
||||
let reviews: Review[] | undefined;
|
||||
let error = "";
|
||||
let target = 0;
|
||||
@@ -112,23 +113,40 @@ export function mountReviews(root: HTMLElement): () => void {
|
||||
|
||||
const render = () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
replace(head);
|
||||
head.hidden = true;
|
||||
if (!taskId) {
|
||||
replace(list, h("div", { class: "empty" }, "No task selected."));
|
||||
replace(body, h("div", { class: "empty" }, "No task selected."));
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
replace(list, h("div", { class: "empty error-text" }, error));
|
||||
replace(body, h("div", { class: "empty error-text" }, error));
|
||||
return;
|
||||
}
|
||||
if (!reviews) {
|
||||
replace(list, h("div", { class: "empty" }, "Loading…"));
|
||||
replace(body, 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."));
|
||||
replace(body, 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 n = state.reviewed ?? reviews.at(-1)!.snapshot_n;
|
||||
const shown = reviews.findLast((r) => r.snapshot_n === n);
|
||||
const ns = [...new Set([n, ...reviews.map((r) => r.snapshot_n)])].sort((a, b) => b - a);
|
||||
const picker = h("select", { title: "Snapshot whose review to show" }, ...ns.map((s) => h("option", { value: String(s) }, `snapshot #${s}`)));
|
||||
picker.value = String(n);
|
||||
picker.onchange = () => pickReview(Number(picker.value));
|
||||
const step = (to: number, label: string) => {
|
||||
const b = h("button", { type: "button", class: "snap-step", disabled: !ns.includes(to) }, label);
|
||||
b.onclick = () => pickReview(to);
|
||||
return b;
|
||||
};
|
||||
const next = ns.filter((s) => s > n).at(-1);
|
||||
const prev = ns.find((s) => s < n);
|
||||
head.hidden = false;
|
||||
replace(head, picker, step(prev ?? n - 1, `← #${prev ?? n - 1}`), step(next ?? n + 1, `#${next ?? n + 1} →`));
|
||||
replace(body, shown ? reviewCard(shown, taskId) : h("div", { class: "empty" }, `No review of snapshot #${n} yet.`));
|
||||
};
|
||||
|
||||
const offs = [
|
||||
@@ -144,9 +162,10 @@ export function mountReviews(root: HTMLElement): () => void {
|
||||
void load();
|
||||
}
|
||||
}),
|
||||
onEach(["review"], render),
|
||||
];
|
||||
|
||||
replace(root, h("div", { class: "reviews-view" }, list));
|
||||
replace(root, h("div", { class: "reviews-view" }, head, body));
|
||||
render();
|
||||
void load();
|
||||
return () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Topic =
|
||||
| "connection"
|
||||
| "event"
|
||||
| "pin"
|
||||
| "review"
|
||||
| "focus"
|
||||
| "lead.sessions"
|
||||
| "lead.transcript"
|
||||
@@ -28,6 +29,7 @@ export const state = {
|
||||
selectedTaskId: undefined as string | undefined,
|
||||
transcript: [] as ByreEvent[],
|
||||
pinned: undefined as number | undefined,
|
||||
reviewed: undefined as number | undefined,
|
||||
connected: false,
|
||||
focus: { tabs: ["transcript"], tab: "transcript" } as Focus,
|
||||
leadSessions: [] as AgentSession[],
|
||||
@@ -92,6 +94,16 @@ export function pinSnapshot(n: number | undefined): void {
|
||||
notify("pin");
|
||||
}
|
||||
|
||||
export function pickReview(n: number): void {
|
||||
state.reviewed = n;
|
||||
notify("review");
|
||||
}
|
||||
|
||||
export function showReview(n: number): void {
|
||||
pickReview(n);
|
||||
focusTab("reviews");
|
||||
}
|
||||
|
||||
export async function loadMe(): Promise<void> {
|
||||
state.me = await get("/api/me");
|
||||
notify("me");
|
||||
@@ -254,6 +266,7 @@ export function selectTask(taskId: string | undefined): void {
|
||||
taskFeed.reset();
|
||||
state.selectedTaskId = taskId;
|
||||
state.pinned = undefined;
|
||||
state.reviewed = undefined;
|
||||
notify("selection");
|
||||
|
||||
if (!taskId) {
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@ import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { reviewCounts } from "./reviews.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, state, type Topic } from "./state.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
|
||||
export type Usage = { used: number; window: number; model: string };
|
||||
@@ -907,8 +907,10 @@ function snapRow(snap: Snapshot, taskId: string | undefined): HTMLElement {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
const review = h("button", { type: "button", title: `Show the review of snapshot ${snap.n} in the Reviews tab` }, "review");
|
||||
review.onclick = () => showReview(snap.n);
|
||||
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);
|
||||
return h("div", { class: "snap-row", title: `${snap.trigger.replace("_", " ")} · ${time}` }, `snapshot #${snap.n}`, changes, fork, review);
|
||||
}
|
||||
|
||||
function runSummary(calls: Call[]): HTMLElement {
|
||||
@@ -1052,10 +1054,8 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
|
||||
case "review.finished": {
|
||||
const r = p as unknown as Review;
|
||||
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);
|
||||
return h("div", { class: `note${r.state === "failed" ? " error" : ""}` }, `review of snapshot #${r.snapshot_n}: ${outcome} `, when);
|
||||
}
|
||||
|
||||
case "task.merge_queued":
|
||||
|
||||
@@ -1612,6 +1612,18 @@ pre.file-text .line::before {
|
||||
background: var(--raised);
|
||||
}
|
||||
|
||||
.reviews-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.reviews-header[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card .review {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user