diff --git a/services/hackbot-api/app/routers/runs.py b/services/hackbot-api/app/routers/runs.py index 083f5b0f7f..e89e57d400 100644 --- a/services/hackbot-api/app/routers/runs.py +++ b/services/hackbot-api/app/routers/runs.py @@ -109,9 +109,13 @@ async def create_run( @router.get("/runs", response_model=list[RunDoc]) async def list_runs( limit: int = Query(default=50, ge=1, le=100), + agent: str | None = Query(default=None), db: AsyncSession = Depends(get_db), ) -> list[RunDoc]: - result = await db.execute(select(Run).order_by(Run.created_at.desc()).limit(limit)) + query = select(Run).order_by(Run.created_at.desc()) + if agent is not None: + query = query.where(Run.agent == agent) + result = await db.execute(query.limit(limit)) return [RunDoc.model_validate(r) for r in result.scalars()] diff --git a/services/hackbot-ui/README.md b/services/hackbot-ui/README.md index c14c2d4828..519ca1ea1c 100644 --- a/services/hackbot-ui/README.md +++ b/services/hackbot-ui/README.md @@ -12,8 +12,9 @@ It lets you: artifacts written to the results bucket, each a **download link** (the browser is redirected to a short-lived signed GCS URL). -> This is a demo surface, not a system of record. It has no list-runs endpoint -> upstream, so the browser remembers the runs you triggered in `localStorage`. +> This is a demo surface, not a system of record. The recent-runs panel is +> sourced entirely from the upstream `GET /runs` endpoint and filtered to the +> agent selected in the trigger form (carried in the `?agent=` URL param). ## Architecture diff --git a/services/hackbot-ui/app/api/runs/route.ts b/services/hackbot-ui/app/api/runs/route.ts index 90ed1b6c06..97f781d61e 100644 --- a/services/hackbot-ui/app/api/runs/route.ts +++ b/services/hackbot-ui/app/api/runs/route.ts @@ -5,8 +5,8 @@ import { getAuthedEmail } from "@/lib/session"; export const dynamic = "force-dynamic"; -// GET /api/runs?limit=50 -// Returns the most recent runs from hackbot-api (no reconciliation). +// GET /api/runs?limit=50&agent=bug-fix +// Returns the most recent runs from hackbot-api, optionally filtered by agent. export async function GET(req: NextRequest) { if (!(await getAuthedEmail())) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -15,7 +15,8 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const raw = Number(searchParams.get("limit") ?? 50); const limit = Number.isFinite(raw) && raw > 0 ? raw : 50; - const runs = await listRuns(limit); + const agent = searchParams.get("agent") ?? undefined; + const runs = await listRuns(limit, agent); return NextResponse.json(runs); } catch (err) { const status = err instanceof HackbotError ? err.status : 500; diff --git a/services/hackbot-ui/app/page.tsx b/services/hackbot-ui/app/page.tsx index bc2183bbb0..ccc3577909 100644 --- a/services/hackbot-ui/app/page.tsx +++ b/services/hackbot-ui/app/page.tsx @@ -15,7 +15,9 @@ export default function HomePage() {

Recent runs

- + + +
); diff --git a/services/hackbot-ui/components/RecentRuns.tsx b/services/hackbot-ui/components/RecentRuns.tsx index dc29d03722..bfb7cbbeb0 100644 --- a/services/hackbot-ui/components/RecentRuns.tsx +++ b/services/hackbot-ui/components/RecentRuns.tsx @@ -1,27 +1,44 @@ "use client"; import Link from "next/link"; +import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -import { isTerminal } from "@/lib/types"; -import { loadRuns, type TrackedRun, updateRunStatus } from "@/lib/store"; +import { parseAgent } from "@/lib/agents"; +import { isTerminal, type RunStatus } from "@/lib/types"; import { StatusBadge } from "./StatusBadge"; -// Polls the status of any non-terminal tracked runs so the dashboard stays live. +// Polls while any run is non-terminal so the dashboard stays live. const POLL_MS = 5000; +interface RunRow { + run_id: string; + agent: string; + status: RunStatus; + // Human-readable summary of the inputs, e.g. "bug 1846789". + label: string; + created_at: string; +} + function labelFromInputs(inputs: Record): string { if (typeof inputs.bug_id === "number") return `bug ${inputs.bug_id}`; + if (typeof inputs.git_commit === "string") { + return `commit ${inputs.git_commit.slice(0, 12)}`; + } + if (typeof inputs.feature_name === "string") return inputs.feature_name; return "inline report"; } -async function fetchRuns(): Promise { - const res = await fetch("/api/runs?limit=50"); +// The runs list is sourced entirely from hackbot-api (no localStorage), filtered +// server-side to the agent selected in the trigger form (carried in `?agent=`). +async function fetchRuns(agent: string): Promise { + const params = new URLSearchParams({ limit: "50", agent }); + const res = await fetch(`/api/runs?${params.toString()}`); if (!res.ok) return []; const docs = (await res.json()) as Array<{ run_id: string; agent: string; - status: TrackedRun["status"]; + status: RunStatus; inputs: Record; created_at: string; }>; @@ -34,53 +51,33 @@ async function fetchRuns(): Promise { })); } -function mergeRuns( - apiRuns: TrackedRun[], - localRuns: TrackedRun[] -): TrackedRun[] { - const seen = new Set(apiRuns.map((r) => r.run_id)); - const extras = localRuns.filter((r) => !seen.has(r.run_id)); - return [...extras, ...apiRuns]; -} - export function RecentRuns() { - const [runs, setRuns] = useState(null); + const params = useSearchParams(); + const agent = parseAgent(params.get("agent")); + const [runs, setRuns] = useState(null); useEffect(() => { - fetchRuns().then((apiRuns) => { - setRuns(mergeRuns(apiRuns, loadRuns())); + let cancelled = false; + setRuns(null); + fetchRuns(agent).then((rows) => { + if (!cancelled) setRuns(rows); }); - }, []); + return () => { + cancelled = true; + }; + }, [agent]); useEffect(() => { if (!runs) return; - const active = runs.filter((r) => !isTerminal(r.status)); - if (active.length === 0) return; + if (!runs.some((r) => !isTerminal(r.status))) return; const timer = setInterval(async () => { - let changed = false; - for (const r of active) { - try { - const res = await fetch(`/api/runs/${r.run_id}`); - if (!res.ok) continue; - const doc = await res.json(); - if (doc.status && doc.status !== r.status) { - updateRunStatus(r.run_id, doc.status); - changed = true; - } - } catch { - // transient; try again next tick - } - } - if (changed) { - fetchRuns().then((apiRuns) => { - setRuns(mergeRuns(apiRuns, loadRuns())); - }); - } + const rows = await fetchRuns(agent); + setRuns(rows); }, POLL_MS); return () => clearInterval(timer); - }, [runs]); + }, [runs, agent]); if (runs === null) { return

Loading…

; @@ -89,7 +86,7 @@ export function RecentRuns() { if (runs.length === 0) { return (

- No runs yet. Use the form above to trigger an agent. + No runs yet for the {agent} agent. Use the form above to trigger one.

); } diff --git a/services/hackbot-ui/components/RunDetail.tsx b/services/hackbot-ui/components/RunDetail.tsx index 1c4466b518..2f5b98be94 100644 --- a/services/hackbot-ui/components/RunDetail.tsx +++ b/services/hackbot-ui/components/RunDetail.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { useCallback, useEffect, useRef, useState } from "react"; -import { updateRunStatus } from "@/lib/store"; import { isTerminal, type RunAction, type RunDoc } from "@/lib/types"; import { FindingsView } from "./FindingsView"; import { Markdown } from "./Markdown"; @@ -63,7 +62,6 @@ export function RunDetail({ runId }: { runId: string }) { const doc = body as RunDoc; setRun(doc); setError(null); - updateRunStatus(runId, doc.status); if (isTerminal(doc.status)) { setPolling(false); return false; diff --git a/services/hackbot-ui/components/TriggerForm.tsx b/services/hackbot-ui/components/TriggerForm.tsx index 8e63a06c71..bed4e5b19f 100644 --- a/services/hackbot-ui/components/TriggerForm.tsx +++ b/services/hackbot-ui/components/TriggerForm.tsx @@ -3,25 +3,9 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; -import { saveRun } from "@/lib/store"; +import { AGENTS, type AgentValue, parseAgent } from "@/lib/agents"; import type { RunRef } from "@/lib/types"; -const AGENTS = [ - { value: "bug-fix", label: "bug-fix" }, - { value: "autowebcompat-repro", label: "autowebcompat-repro" }, - { value: "build-repair", label: "build-repair" }, - { value: "frontend-triage", label: "frontend-triage" }, - { value: "test-plan-generator", label: "test-plan-generator" }, -] as const; - -type AgentValue = (typeof AGENTS)[number]["value"]; - -function parseAgent(value: string | null): AgentValue { - return AGENTS.some((a) => a.value === value) - ? (value as AgentValue) - : "bug-fix"; -} - export function TriggerForm() { const router = useRouter(); const params = useSearchParams(); @@ -54,6 +38,16 @@ export function TriggerForm() { const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + // Reflect the selected agent in the URL so the recent-runs panel can filter + // to it without any shared client state. + function selectAgent(value: AgentValue) { + setAgent(value); + setError(null); + const next = new URLSearchParams(params.toString()); + next.set("agent", value); + router.replace(`?${next.toString()}`, { scroll: false }); + } + const isReproAgent = agent === "autowebcompat-repro"; const isBuildRepairAgent = agent === "build-repair"; const isTestPlanAgent = agent === "test-plan-generator"; @@ -150,20 +144,6 @@ export function TriggerForm() { throw new Error(body?.error ?? `Request failed (${res.status})`); } const run = body as RunRef; - const label = isBuildRepairAgent - ? `commit ${gitCommit.trim().slice(0, 12)}` - : isTestPlanAgent - ? featureName.trim() - : hasBugId - ? `bug ${parsedBugId}` - : "inline report"; - saveRun({ - run_id: run.run_id, - agent: run.agent, - status: run.status, - label, - created_at: new Date().toISOString(), - }); router.push(`/runs/${run.run_id}`); } catch (err) { setError((err as Error).message); @@ -180,10 +160,7 @@ export function TriggerForm() {