From c8336a250e759606894bbe95b79f25677130cf9f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 7 May 2026 08:36:04 -0700 Subject: [PATCH] feat: show archived runs on the runs page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend GET /api/v1/boards/runs with include_archived=true (matching the existing flag on listRuns), add an Archived BoardColumn that the server appends only when the flag is set, and surface a "Show archived" toggle on /runs that flips between request shapes. Default behavior is unchanged — archived runs stay hidden. Server: list_board_runs now takes ListRunsParams; board_column maps RunStatus::Archived to BoardColumn::Archived; board_columns(include_archived) appends the column conditionally. Two new handler tests cover the default and flag-on paths. Web: useBoardsRuns(includeArchived) keys requests so SWR refetches on toggle; columnStatuses + columnStatusDisplay + columnStyles get an "archived" entry; buildSkeletonColumns filters by the flag so the loading state matches the eventual response. Two new buildBoardColumns tests cover both column shapes. Touched generated TS client files include unrelated whitespace drift from openapi-generator-cli; including them keeps the working tree consistent with what `bun run generate` produces. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/data/runs.ts | 2 + apps/fabro-web/app/lib/queries.ts | 7 +- apps/fabro-web/app/lib/query-keys.ts | 5 +- apps/fabro-web/app/routes/runs.test.tsx | 52 +++++++ apps/fabro-web/app/routes/runs.tsx | 49 ++++--- docs/public/api-reference/fabro-api.yaml | 4 +- .../fabro-server/src/server/handler/runs.rs | 26 +++- lib/crates/fabro-server/src/server/tests.rs | 133 ++++++++++++++++++ .../src/api/human-in-the-loop-api.ts | 25 ++-- .../fabro-api-client/src/api/runs-api.ts | 30 ++-- .../src/models/board-column-definition.ts | 3 + .../src/models/board-column.ts | 3 +- .../src/models/run-billing-stage.ts | 3 + .../src/models/stage-projection.ts | 3 + .../src/models/steer-run-request.ts | 3 +- 15 files changed, 296 insertions(+), 52 deletions(-) diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index b397c5ecc..e1c235605 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -48,6 +48,7 @@ export const columnStatuses = [ BoardColumn.BLOCKED, BoardColumn.SUCCEEDED, BoardColumn.FAILED, + BoardColumn.ARCHIVED, ] as const satisfies readonly ColumnStatus[]; export const columnStatusDisplay: Record = { @@ -57,6 +58,7 @@ export const columnStatusDisplay: Record(queryKeys.boards.runs(), apiPaginatedFetcher); +export function useBoardsRuns(includeArchived: boolean = false) { + return useSWR( + queryKeys.boards.runs(includeArchived), + apiPaginatedFetcher, + ); } export function useRun(id: string | undefined) { diff --git a/apps/fabro-web/app/lib/query-keys.ts b/apps/fabro-web/app/lib/query-keys.ts index 402739897..1b1ffabf0 100644 --- a/apps/fabro-web/app/lib/query-keys.ts +++ b/apps/fabro-web/app/lib/query-keys.ts @@ -24,7 +24,10 @@ export const queryKeys = { attach: () => "/api/v1/attach", }, boards: { - runs: () => "/api/v1/boards/runs", + runs: (includeArchived = false) => + withQuery("/api/v1/boards/runs", { + include_archived: includeArchived ? "true" : null, + }), }, runs: { detail: (id: string) => `/api/v1/runs/${pathSegment(id)}`, diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 3cd9dec0a..140f9820e 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -44,6 +44,58 @@ describe("runs route board mapping", () => { expect(columns.find((column) => column.id === "blocked")?.items[0]?.question).toBe("Older unresolved question?"); }); + test("renders an archived column when the response includes one", () => { + const columns = buildBoardColumns({ + columns: [ + { id: "queued", name: "Queued" }, + { id: "initializing", name: "Initializing" }, + { id: "running", name: "Running" }, + { id: "blocked", name: "Blocked" }, + { id: "succeeded", name: "Succeeded" }, + { id: "failed", name: "Failed" }, + { id: "archived", name: "Archived" }, + ], + data: [ + boardRun("succeeded-run", "succeeded"), + boardRun("archived-run", "archived"), + ], + meta: { has_more: false }, + }); + + expect(columns.map((column) => column.id)).toEqual([ + "queued", + "initializing", + "running", + "blocked", + "succeeded", + "failed", + "archived", + ]); + expect( + columns.find((column) => column.id === "archived")?.items.map((item) => item.id), + ).toEqual(["archived-run"]); + expect( + columns.find((column) => column.id === "succeeded")?.items.map((item) => item.id), + ).toEqual(["succeeded-run"]); + }); + + test("omits the archived column when the response does not include it", () => { + const columns = buildBoardColumns({ + columns: [ + { id: "queued", name: "Queued" }, + { id: "initializing", name: "Initializing" }, + { id: "running", name: "Running" }, + { id: "blocked", name: "Blocked" }, + { id: "succeeded", name: "Succeeded" }, + { id: "failed", name: "Failed" }, + ], + data: [boardRun("succeeded-run", "succeeded")], + meta: { has_more: false }, + }); + + expect(columns.some((column) => column.id === "archived")).toBe(false); + }); + test("refreshes for blocked status and interview events", () => { expect(shouldRefreshBoardForEvent("run.queued")).toBe(true); expect(shouldRefreshBoardForEvent("run.blocked")).toBe(true); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 95801988a..2f3c30347 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Link } from "react-router"; -import { ChevronDownIcon, CommandLineIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { ArchiveBoxIcon, ChevronDownIcon, CommandLineIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { useSWRConfig } from "swr"; @@ -50,6 +50,7 @@ const columnStyles: Record = { blocked: { iconType: "branch", actions: ["Answer Question"] }, succeeded: { iconType: "pr", actions: [] }, failed: { iconType: "branch", actions: [] }, + archived: { iconType: "branch", actions: [] }, }; const defaultColumnStyle: ColumnStyle = { iconType: "branch", actions: [] }; @@ -71,18 +72,20 @@ type Column = { items: RunItem[]; }; -function buildSkeletonColumns(): Column[] { - return columnStatuses.map((id) => { - const colors = columnStatusDisplay[id]; - return { - id, - name: colors.label, - dot: colors.dot, - text: colors.text, - ...(columnStyles[id] ?? defaultColumnStyle), - items: [], - }; - }); +function buildSkeletonColumns(includeArchived: boolean): Column[] { + return columnStatuses + .filter((id) => includeArchived || id !== "archived") + .map((id) => { + const colors = columnStatusDisplay[id]; + return { + id, + name: colors.label, + dot: colors.dot, + text: colors.text, + ...(columnStyles[id] ?? defaultColumnStyle), + items: [], + }; + }); } export function buildBoardColumns(response: BoardRunsResponse): Column[] { @@ -750,7 +753,8 @@ function RunsLandingEmpty({ } export default function Runs() { - const boardRuns = useBoardsRuns(); + const [includeArchived, setIncludeArchived] = useState(false); + const boardRuns = useBoardsRuns(includeArchived); const authConfig = useAuthConfig(); const systemInfo = useSystemInfo(); const isLandingReady = @@ -758,8 +762,11 @@ export default function Runs() { authConfig.data !== undefined && systemInfo.data !== undefined; const initialColumns = useMemo( - () => boardRuns.data ? buildBoardColumns(boardRuns.data) : buildSkeletonColumns(), - [boardRuns.data], + () => + boardRuns.data + ? buildBoardColumns(boardRuns.data) + : buildSkeletonColumns(includeArchived), + [boardRuns.data, includeArchived], ); const hasGitHubAuth = authConfig.data?.methods.includes("github") === true; const serverUrl = systemInfo.data?.server_url; @@ -851,6 +858,16 @@ export default function Runs() { +