diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index dbd46c47a..712585815 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -16,7 +16,6 @@ domain, covers host-level detail for one hive. | `/jobs` | the controller's job graph — where agent creation and credential mints show progress | | `/schedules` | the scheduled prompts: add, edit, pause, resume and remove | | `/issues` | a cross-repo issue report | -| `/review-runs` | the review classifier's recent runs: which PRs, what it answered, which failed | | `/review-tryout` | try a review classification against a real PR | Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md). @@ -54,33 +53,6 @@ page. Selecting outside it, or Escape, closes it. You can stop or pause the agent, or link an account, without leaving the terminal. A subagent's tab opens its agent's details. -### Review runs - -`/review-runs` lists what the [review classifier](review-classifier.md) has -run, newest first, one card per run. A card shows the repo and PR, the head -commit, whether the run was a try-out or a requested review, how long ago it -started, how long it took, how many classifications ran, and the outcome as a -badge. Select a card to open it: it shows the same result table as the -[try-out](#review-try-out), the overall verdict, the reason when the outcome -has one, and a link to the posted review when there is one. - -An outcome can be more than approve or request changes: - -| badge | what happened | -| ------------------- | -------------------------------------------------- | -| `approved` | the run posted an approval | -| `changes requested` | the run posted a request for changes | -| `comment only` | there was no clean verdict, so it posted a comment | -| `queue full` | the queue was full, so it turned the run away | -| `post failed` | the run finished but posting the review failed | -| `done` | a try-out finished | -| `failed` | the run failed; the card says why | - -The page refreshes every 30 seconds, and a card you opened stays open when it -does. An empty list says there are no runs yet, and a failed load shows the reason the -controller gave. The page shows no token counts, cache counts or cost, and -stores and posts nothing. - ### Review try-out `/review-tryout` shows how the [review classifier](review-classifier.md) diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index d6432f103..44493a1ea 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -8,7 +8,6 @@ import { ComponentsPage } from "./pages/ComponentsPage.js"; import { JobsPage } from "./pages/JobsPage.js"; import { HivesPage } from "./pages/HivesPage.js"; import { IssueReportPage } from "./pages/IssueReportPage.js"; -import { ReviewRunsPage } from "./pages/review-runs/ReviewRunsPage.js"; import { ReviewTryOutPage } from "./pages/review-tryout/ReviewTryOutPage.js"; import { SchedulesPage } from "./pages/schedules/SchedulesPage.js"; import { Panel } from "./ui/panel/Panel.js"; @@ -35,7 +34,6 @@ export function App() { - diff --git a/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.css b/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.css deleted file mode 100644 index 75e49738f..000000000 --- a/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.css +++ /dev/null @@ -1,47 +0,0 @@ -/* The run list: one card per run, the head row a button that opens it. */ -.ui-runs-list { - display: flex; - flex-direction: column; - gap: 0.75em; - margin-top: 1em; -} -.ui-runs-head { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5em 0.75em; - padding: 0; - border: 0; - background: none; - color: inherit; - font: inherit; - text-align: left; - cursor: pointer; -} -.ui-runs-head:focus-visible { - outline: 2px solid var(--purple); - outline-offset: 2px; -} -.ui-runs-title { - display: inline-flex; - align-items: baseline; - gap: 0.5em; -} -.ui-runs-meta { - color: var(--muted); -} -.ui-runs-head .ui-runs-meta { - flex-basis: 100%; -} -.ui-runs-body { - display: flex; - flex-direction: column; - gap: 0.6em; - margin-top: 0.5em; -} -.ui-runs-body p { - margin: 0; -} -.ui-runs-empty { - color: var(--muted); -} diff --git a/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.tsx b/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.tsx deleted file mode 100644 index 6cabe1b04..000000000 --- a/frontend/packages/swarm-ui/src/pages/review-runs/ReviewRunsPage.tsx +++ /dev/null @@ -1,162 +0,0 @@ -// — the review classifier's recent runs, newest first -// (`docs/swarm/review-classifier.md`, "Recent runs"): which PRs it ran on, -// what it answered, and which runs failed or were turned away. One card per -// run; selecting a card expands it to the same result table the try-out page -// shows. Reads `GET /api/review-classifier/runs`. The page stores nothing and -// posts nothing, and shows no token counts, cache counts or cost. -import { useState } from "preact/hooks"; -import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; -import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; -import { Badge } from "@hive/shared/badge.js"; -import { Card } from "../../ui/card/Card.js"; -import { Panel } from "../../ui/panel/Panel.js"; -import { - RefreshIntervalPicker, - useRefreshInterval, - type RefreshIntervalMs, -} from "../../ui/refresh-interval/RefreshInterval.js"; -import { RelativeTime } from "../../ui/relative-time/RelativeTime.js"; -import { TryOutResult } from "../review-tryout/TryOutResult.js"; -import { - OUTCOME_LABEL, - OUTCOME_TONE, - TRIGGER_LABEL, - classificationCount, - durationLabel, - type ClassifierRun, -} from "./RunsTypes.js"; -import "./ReviewRunsPage.css"; - -const RUNS = "/api/review-classifier/runs"; -const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; - -function RunCard({ - run, - open, - onToggle, -}: { - run: ClassifierRun; - open: boolean; - onToggle: () => void; -}) { - const bodyId = `review-run-${run.id}`; - return ( - - - {open ? ( -
- {run.detail ?

{run.detail}

: null} - {run.rows && run.rows.length > 0 ? ( - - ) : null} - {run.review_url ? ( - - ) : null} - {run.requester ? ( -

Requested by {run.requester}.

- ) : null} -
- ) : null} -
- ); -} - -export function ReviewRunsPage() { - const [intervalMs, setIntervalMs] = - useState(DEFAULT_INTERVAL_MS); - const [runs, setRuns] = useState(null); - const [error, setError] = useState(null); - // Which cards are expanded. Survives a refresh, so a list that updates - // under you does not fold the card you are reading. - const [open, setOpen] = useState>(new Set()); - - async function load() { - try { - const res = await fetch(RUNS); - if (!res.ok) { - setError(await readApiError(res)); - return; - } - setRuns((await res.json()) as ClassifierRun[]); - setError(null); - } catch (e: unknown) { - setError({ detail: String(e) }); - } - } - - useRefreshInterval(intervalMs, () => void load()); - - function toggle(id: string) { - setOpen((cur) => { - const next = new Set(cur); - if (!next.delete(id)) next.add(id); - return next; - }); - } - - return ( - - } - > -

- The PRs the review classifier ran on, newest first: what it answered, - and which runs failed or were turned away. Select a run to see its - result table. -

- {error ? ( - - ) : null} - {runs !== null && runs.length === 0 && !error ? ( -

No runs yet.

- ) : null} -
- {(runs ?? []).map((run) => ( - toggle(run.id)} - /> - ))} -
-
- ); -} diff --git a/frontend/packages/swarm-ui/src/pages/review-runs/RunsTypes.ts b/frontend/packages/swarm-ui/src/pages/review-runs/RunsTypes.ts deleted file mode 100644 index c07563e10..000000000 --- a/frontend/packages/swarm-ui/src/pages/review-runs/RunsTypes.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Wire shape of the review classifier's run list -// (`GET /api/review-classifier/runs`), and how the page words it. Rows and -// the overall verdict are the try-out's own shapes. -import type { BadgeTone } from "@hive/shared/badge.js"; -import type { TryOutOverall, TryOutRow } from "../review-tryout/TryOutTypes.js"; - -export type RunTrigger = "try-out" | "review"; - -export type RunOutcome = - | "approved" - | "changes-requested" - | "no-verdict" - | "queue-full" - | "post-failed" - | "done" - | "failed"; - -/** One run, as the list returns it. Fields marked optional are absent when - * they do not apply to the run. */ -export interface ClassifierRun { - id: string; - trigger: RunTrigger; - repo: string; - pr: number; - requester: string; - head_sha?: string; - started_unix: number; - finished_unix: number; - outcome: RunOutcome; - /** Why, for an outcome that needs a reason. */ - detail?: string; - rows?: TryOutRow[]; - overall?: TryOutOverall; - /** The review the run posted, when it posted one. */ - review_url?: string; -} - -export const OUTCOME_LABEL: Record = { - approved: "approved", - "changes-requested": "changes requested", - "no-verdict": "comment only", - "queue-full": "queue full", - "post-failed": "post failed", - done: "done", - failed: "failed", -}; - -export const OUTCOME_TONE: Record = { - approved: "positive", - "changes-requested": "accent", - "no-verdict": "neutral", - "queue-full": "warning", - "post-failed": "negative", - done: "positive", - failed: "negative", -}; - -export const TRIGGER_LABEL: Record = { - "try-out": "try-out", - review: "requested review", -}; - -/** How long a run took, as `45s`, `3m 12s` or `1h 4m`. A clock that ran - * backwards reads as `0s`. */ -export function durationLabel(startedUnix: number, finishedUnix: number) { - const total = Math.max(0, Math.round(finishedUnix - startedUnix)); - if (total < 60) return `${total}s`; - const minutes = Math.floor(total / 60); - if (minutes < 60) return `${minutes}m ${total % 60}s`; - return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; -} - -/** How many classifications ran, in words. A run that never got as far as - * running any has no rows. */ -export function classificationCount(run: ClassifierRun): string { - const n = run.rows?.length ?? 0; - return `${n} ${n === 1 ? "classification" : "classifications"}`; -} diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css index 5419d9b4d..3d9b683db 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css @@ -62,3 +62,13 @@ margin: 0; color: var(--muted); } +.ui-tryout-row-error { + color: var(--red); +} +.ui-tryout-overall, +.ui-tryout-sha { + margin: 0.75em 0 0; +} +.ui-tryout-sha { + color: var(--muted); +} diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.css b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.css deleted file mode 100644 index 1c0aef8be..000000000 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.css +++ /dev/null @@ -1,11 +0,0 @@ -/* A finished run's table, the overall verdict under it and the head commit. */ -.ui-tryout-row-error { - color: var(--red); -} -.ui-tryout-overall, -.ui-tryout-sha { - margin: 0.75em 0 0; -} -.ui-tryout-sha { - color: var(--muted); -} diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx index 3c9ff858a..771f52eca 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx @@ -5,7 +5,6 @@ import { Badge } from "@hive/shared/badge.js"; import { Table, type TableColumn } from "../../ui/table/Table.js"; import type { TryOutResult as Result, TryOutRow } from "./TryOutTypes.js"; -import "./TryOutResult.css"; const VERDICT_LABEL: Record = { match: "match", @@ -45,21 +44,7 @@ const COLUMNS: TableColumn[] = [ }, ]; -/** What the table needs of a run: the try-out's answer, or a listed run's. The - * head commit is absent for a run that never read one. */ -export type TryOutAnswer = Pick & { - head_sha?: string; -}; - -export function TryOutResult({ - result, - filterKey = "swarm-ui:review-tryout:table-filters", -}: { - result: TryOutAnswer; - /** Where the column filters are remembered; a page showing several tables - * keeps them apart from the try-out's. */ - filterKey?: string; -}) { +export function TryOutResult({ result }: { result: Result }) { const rows = result.rows ?? []; return (
@@ -69,7 +54,7 @@ export function TryOutResult({ // By position: two classifications may share a name, and a shared key // would let one result render over the other. rowKey={(r) => String(rows.indexOf(r))} - storageKey={filterKey} + storageKey="swarm-ui:review-tryout:table-filters" emptyMessage="the run returned no classifications" /> {result.overall ? ( @@ -81,11 +66,9 @@ export function TryOutResult({ />

) : null} - {result.head_sha ? ( -

- Ran on head {result.head_sha.slice(0, 12)} -

- ) : null} +

+ Ran on head {result.head_sha.slice(0, 12)} +

); } diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index ad3a6dda5..7cab64c1c 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -52,7 +52,6 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ { href: "/jobs", label: "jobs", accent: "var(--pink)" }, { href: "/schedules", label: "schedules", accent: "var(--cyan)" }, { href: "/issues", label: "issues", accent: "var(--yellow)" }, - { href: "/review-runs", label: "review runs", accent: "var(--amber)" }, { href: "/review-tryout", label: "review try-out", accent: "var(--red)" }, { href: "/components", label: "components", accent: "var(--blue)" }, ]; @@ -64,12 +63,7 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ // modifier alongside the base class, not a replacement, so every other // route's layout is untouched. `/agents` joined for the same reason — // its own table view (plus the list+detail split) hits the same cap. -const WIDE_BODY_ROUTES = new Set([ - "/issues", - "/agents", - "/review-runs", - "/review-tryout", -]); +const WIDE_BODY_ROUTES = new Set(["/issues", "/agents", "/review-tryout"]); // `/agents/:name/terminal` (and a subagent's, under // `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills