From 49364b08432b99401d2dca8385cb1ccccddf4540 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 2 Mar 2026 09:39:53 -0500 Subject: [PATCH] Add demo API server and wire React app to fetch from API Expand the OpenAPI spec from 11 to 39 endpoints covering Runs, Workflows, Verifications, Retros, Sessions, Insights, Settings, and Projects with ~45 schemas. Add `--demo` flag to `arc serve` that serves static demo data for all endpoints (auth disabled, read-only). Non-demo mode returns 501 for new endpoints while existing run handlers continue working. Regenerate the TypeScript API client and add `apiJson` helper. Wire all 19 React route files with server-side loaders that fetch from the API and map snake_case responses to camelCase UI types. Mock data kept as fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/arc-web/app/api-client.ts | 15 +- apps/arc-web/app/lib/format.ts | 35 + apps/arc-web/app/routes/insights.tsx | 51 +- apps/arc-web/app/routes/retros.tsx | 40 +- apps/arc-web/app/routes/run-configuration.tsx | 85 +- apps/arc-web/app/routes/run-detail.tsx | 40 +- apps/arc-web/app/routes/run-files-changed.tsx | 34 +- apps/arc-web/app/routes/run-graph.tsx | 88 +- apps/arc-web/app/routes/run-overview.tsx | 93 +- apps/arc-web/app/routes/run-retro.tsx | 16 +- apps/arc-web/app/routes/run-stages.tsx | 135 +- apps/arc-web/app/routes/run-usage.tsx | 57 +- apps/arc-web/app/routes/run-verifications.tsx | 25 +- apps/arc-web/app/routes/runs.tsx | 103 +- apps/arc-web/app/routes/session-detail.tsx | 67 +- apps/arc-web/app/routes/settings.tsx | 64 +- apps/arc-web/app/routes/start.tsx | 59 +- .../app/routes/verification-detail.tsx | 75 +- apps/arc-web/app/routes/verifications.tsx | 55 +- apps/arc-web/app/routes/workflow-detail.tsx | 37 +- apps/arc-web/app/routes/workflow-runs.tsx | 47 +- apps/arc-web/app/routes/workflows.tsx | 65 +- crates/arc-api/src/demo/mod.rs | 1225 +++++++++++++ crates/arc-api/src/lib.rs | 1 + crates/arc-api/src/serve.rs | 12 +- crates/arc-api/src/server.rs | 107 +- crates/arc-api/tests/openapi_conformance.rs | 3 + openapi/arc-api.yaml | 1511 ++++++++++++++++- .../src/.openapi-generator/FILES | 62 + packages/arc-api-client/src/api.ts | 7 + .../arc-api-client/src/api/insights-api.ts | 467 +++++ .../arc-api-client/src/api/projects-api.ts | 187 ++ packages/arc-api-client/src/api/retros-api.ts | 117 ++ packages/arc-api-client/src/api/runs-api.ts | 527 +++++- .../arc-api-client/src/api/sessions-api.ts | 408 +++++ .../arc-api-client/src/api/settings-api.ts | 117 ++ .../src/api/verifications-api.ts | 187 ++ .../arc-api-client/src/api/workflows-api.ts | 327 ++++ packages/arc-api-client/src/models/branch.ts | 21 + .../src/models/check-run-status.ts | 29 + .../arc-api-client/src/models/check-run.ts | 27 + .../src/models/control-detail.ts | 23 + .../arc-api-client/src/models/control-info.ts | 29 + .../src/models/control-performance.ts | 31 + .../src/models/create-session-request.ts | 22 + .../src/models/create-session-response.ts | 20 + .../arc-api-client/src/models/diff-file.ts | 21 + .../arc-api-client/src/models/diff-stats.ts | 21 + .../src/models/evaluation-result.ts | 27 + .../src/models/execute-query-request.ts | 20 + .../src/models/execute-query-response.ts | 23 + .../src/models/file-checkpoint.ts | 21 + .../arc-api-client/src/models/file-diff.ts | 24 + .../src/models/history-entry.ts | 24 + packages/arc-api-client/src/models/index.ts | 55 + packages/arc-api-client/src/models/project.ts | 21 + .../src/models/recent-control-result.ts | 29 + .../src/models/retro-list-item.ts | 34 + .../arc-api-client/src/models/retro-stats.ts | 25 + .../arc-api-client/src/models/run-files.ts | 31 + .../src/models/run-list-item-status.ts | 28 + .../src/models/run-list-item.ts | 42 + .../arc-api-client/src/models/run-stage.ts | 29 + .../arc-api-client/src/models/run-usage.ts | 31 + .../src/models/run-verification-control.ts | 31 + .../src/models/run-verification.ts | 31 + .../src/models/save-query-request.ts | 21 + .../arc-api-client/src/models/saved-query.ts | 22 + .../src/models/send-message-request.ts | 20 + .../src/models/session-detail.ts | 27 + .../src/models/session-group.ts | 24 + .../src/models/session-list-item.ts | 23 + .../arc-api-client/src/models/session-turn.ts | 35 + .../src/models/setting-field-type.ts | 27 + .../src/models/setting-field.ts | 30 + .../src/models/setting-group.ts | 26 + .../src/models/sibling-control.ts | 31 + .../src/models/smoothness-rating.ts | 29 + .../arc-api-client/src/models/stage-status.ts | 28 + .../arc-api-client/src/models/stage-turn.ts | 34 + .../src/models/steer-request.ts | 22 + .../src/models/steer-run200-response.ts | 20 + .../arc-api-client/src/models/tool-use.ts | 22 + .../src/models/usage-by-model.ts | 24 + .../arc-api-client/src/models/usage-stage.ts | 25 + .../arc-api-client/src/models/usage-totals.ts | 23 + .../src/models/verification-category.ts | 25 + .../src/models/verification-control.ts | 38 + .../models/verification-detail-response.ts | 39 + .../src/models/verification-mode.ts | 27 + .../src/models/verification-status.ts | 27 + .../src/models/verification-type.ts | 28 + .../src/models/workflow-detail.ts | 25 + .../src/models/workflow-list-item.ts | 25 + 94 files changed, 7577 insertions(+), 491 deletions(-) create mode 100644 apps/arc-web/app/lib/format.ts create mode 100644 crates/arc-api/src/demo/mod.rs create mode 100644 packages/arc-api-client/src/api/insights-api.ts create mode 100644 packages/arc-api-client/src/api/projects-api.ts create mode 100644 packages/arc-api-client/src/api/retros-api.ts create mode 100644 packages/arc-api-client/src/api/sessions-api.ts create mode 100644 packages/arc-api-client/src/api/settings-api.ts create mode 100644 packages/arc-api-client/src/api/verifications-api.ts create mode 100644 packages/arc-api-client/src/api/workflows-api.ts create mode 100644 packages/arc-api-client/src/models/branch.ts create mode 100644 packages/arc-api-client/src/models/check-run-status.ts create mode 100644 packages/arc-api-client/src/models/check-run.ts create mode 100644 packages/arc-api-client/src/models/control-detail.ts create mode 100644 packages/arc-api-client/src/models/control-info.ts create mode 100644 packages/arc-api-client/src/models/control-performance.ts create mode 100644 packages/arc-api-client/src/models/create-session-request.ts create mode 100644 packages/arc-api-client/src/models/create-session-response.ts create mode 100644 packages/arc-api-client/src/models/diff-file.ts create mode 100644 packages/arc-api-client/src/models/diff-stats.ts create mode 100644 packages/arc-api-client/src/models/evaluation-result.ts create mode 100644 packages/arc-api-client/src/models/execute-query-request.ts create mode 100644 packages/arc-api-client/src/models/execute-query-response.ts create mode 100644 packages/arc-api-client/src/models/file-checkpoint.ts create mode 100644 packages/arc-api-client/src/models/file-diff.ts create mode 100644 packages/arc-api-client/src/models/history-entry.ts create mode 100644 packages/arc-api-client/src/models/project.ts create mode 100644 packages/arc-api-client/src/models/recent-control-result.ts create mode 100644 packages/arc-api-client/src/models/retro-list-item.ts create mode 100644 packages/arc-api-client/src/models/retro-stats.ts create mode 100644 packages/arc-api-client/src/models/run-files.ts create mode 100644 packages/arc-api-client/src/models/run-list-item-status.ts create mode 100644 packages/arc-api-client/src/models/run-list-item.ts create mode 100644 packages/arc-api-client/src/models/run-stage.ts create mode 100644 packages/arc-api-client/src/models/run-usage.ts create mode 100644 packages/arc-api-client/src/models/run-verification-control.ts create mode 100644 packages/arc-api-client/src/models/run-verification.ts create mode 100644 packages/arc-api-client/src/models/save-query-request.ts create mode 100644 packages/arc-api-client/src/models/saved-query.ts create mode 100644 packages/arc-api-client/src/models/send-message-request.ts create mode 100644 packages/arc-api-client/src/models/session-detail.ts create mode 100644 packages/arc-api-client/src/models/session-group.ts create mode 100644 packages/arc-api-client/src/models/session-list-item.ts create mode 100644 packages/arc-api-client/src/models/session-turn.ts create mode 100644 packages/arc-api-client/src/models/setting-field-type.ts create mode 100644 packages/arc-api-client/src/models/setting-field.ts create mode 100644 packages/arc-api-client/src/models/setting-group.ts create mode 100644 packages/arc-api-client/src/models/sibling-control.ts create mode 100644 packages/arc-api-client/src/models/smoothness-rating.ts create mode 100644 packages/arc-api-client/src/models/stage-status.ts create mode 100644 packages/arc-api-client/src/models/stage-turn.ts create mode 100644 packages/arc-api-client/src/models/steer-request.ts create mode 100644 packages/arc-api-client/src/models/steer-run200-response.ts create mode 100644 packages/arc-api-client/src/models/tool-use.ts create mode 100644 packages/arc-api-client/src/models/usage-by-model.ts create mode 100644 packages/arc-api-client/src/models/usage-stage.ts create mode 100644 packages/arc-api-client/src/models/usage-totals.ts create mode 100644 packages/arc-api-client/src/models/verification-category.ts create mode 100644 packages/arc-api-client/src/models/verification-control.ts create mode 100644 packages/arc-api-client/src/models/verification-detail-response.ts create mode 100644 packages/arc-api-client/src/models/verification-mode.ts create mode 100644 packages/arc-api-client/src/models/verification-status.ts create mode 100644 packages/arc-api-client/src/models/verification-type.ts create mode 100644 packages/arc-api-client/src/models/workflow-detail.ts create mode 100644 packages/arc-api-client/src/models/workflow-list-item.ts diff --git a/apps/arc-web/app/api-client.ts b/apps/arc-web/app/api-client.ts index 340de4aac..92a469f4a 100644 --- a/apps/arc-web/app/api-client.ts +++ b/apps/arc-web/app/api-client.ts @@ -34,12 +34,23 @@ export async function apiFetch( throw new Error("ARC_API_BASE_URL environment variable is not set"); } - const token = await signToken(); const headers = new Headers(init?.headers); - headers.set("Authorization", `Bearer ${token}`); + if (ARC_JWT_PRIVATE_KEY) { + const token = await signToken(); + headers.set("Authorization", `Bearer ${token}`); + } return fetch(`${ARC_API_BASE_URL}${path}`, { ...init, headers, }); } + +/** + * Typed JSON fetch helper. Calls apiFetch and parses the JSON response. + */ +export async function apiJson(path: string, init?: RequestInit): Promise { + const res = await apiFetch(path, init); + if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`); + return res.json() as Promise; +} diff --git a/apps/arc-web/app/lib/format.ts b/apps/arc-web/app/lib/format.ts new file mode 100644 index 000000000..ce3213617 --- /dev/null +++ b/apps/arc-web/app/lib/format.ts @@ -0,0 +1,35 @@ +/** + * Format a number of seconds into a human-readable duration string. + * Examples: "23s", "7m", "2h 15m", "3d" + */ +export function formatElapsedSecs(secs: number): string { + if (secs < 60) return `${Math.round(secs)}s`; + const minutes = Math.floor(secs / 60); + if (minutes < 60) { + const remainSecs = Math.round(secs % 60); + return remainSecs > 0 ? `${minutes}m ${remainSecs}s` : `${minutes}m`; + } + const hours = Math.floor(minutes / 60); + if (hours < 24) { + const remainMin = minutes % 60; + return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`; + } + const days = Math.floor(hours / 24); + const remainHrs = hours % 24; + return remainHrs > 0 ? `${days}d ${remainHrs}h` : `${days}d`; +} + +/** + * Format seconds into a duration string for display (e.g., "1m 12s", "23s"). + */ +export function formatDurationSecs(secs: number): string { + if (secs < 60) return `${Math.round(secs)}s`; + const minutes = Math.floor(secs / 60); + const remainSecs = Math.round(secs % 60); + if (minutes < 60) { + return remainSecs > 0 ? `${minutes}m ${remainSecs}s` : `${minutes}m`; + } + const hours = Math.floor(minutes / 60); + const remainMin = minutes % 60; + return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`; +} diff --git a/apps/arc-web/app/routes/insights.tsx b/apps/arc-web/app/routes/insights.tsx index d7ca9550a..9a9b22fdb 100644 --- a/apps/arc-web/app/routes/insights.tsx +++ b/apps/arc-web/app/routes/insights.tsx @@ -1,5 +1,7 @@ import { Link, Outlet, useNavigate } from "react-router"; import { PlusIcon } from "@heroicons/react/24/outline"; +import { apiJson } from "../api-client"; +import type { SavedQuery as ApiSavedQuery, HistoryEntry as ApiHistoryEntry } from "@qltysh/arc-api-client"; import type { Route } from "./+types/insights"; export function meta({}: Route.MetaArgs) { @@ -26,35 +28,28 @@ export interface HistoryEntry { rowsReturned: number; } -// ── Mock data ── +export async function loader() { + const [apiQueries, apiHistory] = await Promise.all([ + apiJson("/insights/queries"), + apiJson("/insights/history"), + ]); + const savedQueries: SavedQuery[] = apiQueries.map((q) => ({ + id: q.id, + name: q.name, + sql: q.sql, + })); + const historyEntries: HistoryEntry[] = apiHistory.map((h) => ({ + id: h.id, + sql: h.sql, + timestamp: h.timestamp, + elapsed: h.elapsed, + rowsReturned: h.row_count, + })); + return { savedQueries, historyEntries }; +} -export const savedQueries: SavedQuery[] = [ - { - id: "1", - name: "Run duration by workflow", - sql: "SELECT workflow_name, AVG(duration_seconds) as avg_duration,\n COUNT(*) as run_count\nFROM runs\nGROUP BY workflow_name\nORDER BY avg_duration DESC\nLIMIT 20", - }, - { - id: "2", - name: "Daily failure rate", - sql: "SELECT date_trunc('day', created_at) as day,\n COUNT(*) FILTER (WHERE status = 'failed') as failures,\n COUNT(*) as total,\n ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'failed') / COUNT(*), 1) as failure_rate\nFROM runs\nGROUP BY 1\nORDER BY 1 DESC\nLIMIT 30", - }, - { - id: "3", - name: "Top repos by activity", - sql: "SELECT repo, COUNT(*) as runs, SUM(additions) as total_additions,\n SUM(deletions) as total_deletions\nFROM runs\nGROUP BY repo\nORDER BY runs DESC", - }, -]; - -export const historyEntries: HistoryEntry[] = [ - { id: "h1", sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1", timestamp: "2 min ago", elapsed: 0.342, rowsReturned: 6 }, - { id: "h2", sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100", timestamp: "8 min ago", elapsed: 0.127, rowsReturned: 23 }, - { id: "h3", sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1 ORDER BY 1", timestamp: "15 min ago", elapsed: 0.531, rowsReturned: 30 }, - { id: "h4", sql: "SELECT repo, AVG(duration_seconds) FROM runs GROUP BY repo", timestamp: "1 hr ago", elapsed: 0.089, rowsReturned: 12 }, - { id: "h5", sql: "DESCRIBE runs", timestamp: "1 hr ago", elapsed: 0.003, rowsReturned: 18 }, -]; - -export default function InsightsLayout() { +export default function InsightsLayout({ loaderData }: Route.ComponentProps) { + const { savedQueries, historyEntries } = loaderData; const navigate = useNavigate(); return ( diff --git a/apps/arc-web/app/routes/retros.tsx b/apps/arc-web/app/routes/retros.tsx index 670769d28..ac0226dc9 100644 --- a/apps/arc-web/app/routes/retros.tsx +++ b/apps/arc-web/app/routes/retros.tsx @@ -1,10 +1,36 @@ import { useState } from "react"; import { useNavigate } from "react-router"; import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; -import { allRetros, smoothnessConfig, formatDuration } from "../data/retros"; -import type { Retro, SmoothnessRating } from "../data/retros"; +import { smoothnessConfig, formatDuration } from "../data/retros"; +import type { SmoothnessRating } from "../data/retros"; +import { apiJson } from "../api-client"; +import type { RetroListItem } from "@qltysh/arc-api-client"; import type { Route } from "./+types/retros"; +interface RetroRow { + run_id: string; + workflow_name: string; + goal: string; + timestamp: string; + smoothness?: SmoothnessRating; + total_duration_ms: number; + friction_point_count: number; +} + +export async function loader() { + const apiRetros = await apiJson("/retros"); + const retros: RetroRow[] = apiRetros.map((r) => ({ + run_id: r.run_id, + workflow_name: r.workflow_name, + goal: r.goal, + timestamp: r.timestamp, + smoothness: r.smoothness as SmoothnessRating | undefined, + total_duration_ms: r.stats.total_duration_ms, + friction_point_count: r.friction_point_count, + })); + return { retros }; +} + export function meta({}: Route.MetaArgs) { return [{ title: "Retros \u2014 Arc" }]; } @@ -17,7 +43,7 @@ const smoothnessOptions: Array<{ value: SmoothnessRating; label: string }> = [ { value: "failed", label: "Failed" }, ]; -function SmoothnesssBadge({ smoothness }: { smoothness: Retro["smoothness"] }) { +function SmoothnesssBadge({ smoothness }: { smoothness: SmoothnessRating | undefined }) { if (!smoothness) { return --; } @@ -45,8 +71,8 @@ function truncate(text: string, maxLength: number): string { return text.slice(0, maxLength) + "\u2026"; } -export default function Retros() { - const retros = allRetros(); +export default function Retros({ loaderData }: Route.ComponentProps) { + const { retros } = loaderData; const navigate = useNavigate(); const [query, setQuery] = useState(""); const [smoothnessFilter, setSmoothnessFilter] = useState("all"); @@ -116,10 +142,10 @@ export default function Retros() { - {formatDuration(retro.stats.total_duration_ms)} + {formatDuration(retro.total_duration_ms)} - {retro.friction_points?.length ?? 0} + {retro.friction_point_count} {formatTimestamp(retro.timestamp)} diff --git a/apps/arc-web/app/routes/run-configuration.tsx b/apps/arc-web/app/routes/run-configuration.tsx index 6fc0116af..f765e668b 100644 --- a/apps/arc-web/app/routes/run-configuration.tsx +++ b/apps/arc-web/app/routes/run-configuration.tsx @@ -1,9 +1,11 @@ import { Link, useParams } from "react-router"; import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline"; -import { findRun } from "../data/runs"; -import { workflowData } from "./workflow-detail"; import { CollapsibleFile } from "../components/collapsible-file"; +import { apiFetch, apiJson } from "../api-client"; +import { formatDurationSecs } from "../lib/format"; +import type { RunStage } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-configuration"; export const handle = { wide: true }; @@ -16,13 +18,6 @@ interface Stage { duration: string; } -const stages: Stage[] = [ - { id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" }, - { id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" }, - { id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" }, - { id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" }, -]; - const statusConfig: Record = { completed: { icon: CheckCircleIcon, color: "text-mint" }, running: { icon: ArrowPathIcon, color: "text-teal-500" }, @@ -30,10 +25,24 @@ const statusConfig: Record(`/runs/${params.id}/stages`), + apiFetch(`/runs/${params.id}/configuration`), + ]); + const stages: Stage[] = apiStages.map((s) => ({ + id: s.id, + name: s.name, + status: s.status as StageStatus, + duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", + })); + const configText = configRes.ok ? await configRes.text() : null; + return { stages, configText }; +} + +export default function RunConfiguration({ loaderData }: Route.ComponentProps) { const { id } = useParams(); - const run = findRun(id ?? ""); - const workflow = run ? workflowData[run.workflow] : undefined; + const { stages, configText } = loaderData; return (
@@ -60,37 +69,35 @@ export default function RunConfiguration() {
- {workflow && ( -
-

Workflow

-
    -
  • - - - Run Configuration - -
  • -
  • - - - Workflow Graph - -
  • -
-
- )} +
+

Workflow

+
    +
  • + + + Run Configuration + +
  • +
  • + + + Workflow Graph + +
  • +
+
- {workflow ? ( + {configText ? ( ) : (

No configuration found.

diff --git a/apps/arc-web/app/routes/run-detail.tsx b/apps/arc-web/app/routes/run-detail.tsx index aa5487266..2cfcbe215 100644 --- a/apps/arc-web/app/routes/run-detail.tsx +++ b/apps/arc-web/app/routes/run-detail.tsx @@ -1,14 +1,17 @@ import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Link, Outlet, useLocation } from "react-router"; -import { findRun, statusColors } from "../data/runs"; -import { workflowData } from "./workflow-detail"; +import { statusColors } from "../data/runs"; +import type { ColumnStatus } from "../data/runs"; +import { apiJson } from "../api-client"; +import { formatElapsedSecs, formatDurationSecs } from "../lib/format"; +import type { RunListItem } from "@qltysh/arc-api-client"; import type { Route } from "./+types/run-detail"; const tabs = [ { name: "Overview", path: "", count: null }, - { name: "Stages", path: "/stages/detect-drift", count: 4 }, - { name: "Files Changed", path: "/files", count: 3 }, + { name: "Stages", path: "/stages/detect-drift", count: null }, + { name: "Files Changed", path: "/files", count: null }, { name: "Verifications", path: "/verifications", count: null }, { name: "Retro", path: "/retro", count: null }, { name: "Usage", path: "/usage", count: null }, @@ -16,13 +19,32 @@ const tabs = [ export const handle = { hideHeader: true }; -export function meta({ params }: Route.MetaArgs) { - const run = findRun(params.id); +export async function loader({ params }: Route.LoaderArgs) { + const apiRuns = await apiJson("/runs"); + const apiRun = apiRuns.find((r) => r.id === params.id); + if (!apiRun) return { run: null }; + return { + run: { + id: apiRun.id, + repo: apiRun.repo, + title: apiRun.title, + workflow: apiRun.workflow, + status: apiRun.status as ColumnStatus, + statusLabel: apiRun.status === "working" ? "Working" : apiRun.status === "pending" ? "Pending" : apiRun.status === "review" ? "Verify" : "Merge", + elapsed: apiRun.elapsed_secs != null ? formatElapsedSecs(apiRun.elapsed_secs) : undefined, + elapsedWarning: apiRun.elapsed_warning, + sandboxId: apiRun.sandbox_id, + }, + }; +} + +export function meta({ data }: Route.MetaArgs) { + const run = data?.run; return [{ title: run ? `${run.title} — Arc` : "Run — Arc" }]; } -export default function RunDetail({ params }: Route.ComponentProps) { - const run = findRun(params.id); +export default function RunDetail({ loaderData, params }: Route.ComponentProps) { + const { run } = loaderData; const { pathname } = useLocation(); const basePath = `/runs/${params.id}`; @@ -38,7 +60,7 @@ export default function RunDetail({ params }: Route.ComponentProps) { Runs - {workflowData[run.workflow]?.title ?? run.workflow} + {run.workflow} {run.title} diff --git a/apps/arc-web/app/routes/run-files-changed.tsx b/apps/arc-web/app/routes/run-files-changed.tsx index 4af9d56ac..2760ce08e 100644 --- a/apps/arc-web/app/routes/run-files-changed.tsx +++ b/apps/arc-web/app/routes/run-files-changed.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { useParams } from "react-router"; import { ChevronDownIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"; import { MultiFileDiff, @@ -6,18 +7,18 @@ import { type DiffLineAnnotation, } from "@pierre/diffs/react"; import { useTheme } from "../lib/theme"; +import { apiJson } from "../api-client"; +import type { RunFiles } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-files-changed"; export const handle = { wide: true }; -const checkpoints = [ - { id: "all", label: "All changes" }, - { id: "cp-4", label: "Checkpoint 4 — Apply Changes" }, - { id: "cp-3", label: "Checkpoint 3 — Review Changes" }, - { id: "cp-2", label: "Checkpoint 2 — Propose Changes" }, - { id: "cp-1", label: "Checkpoint 1 — Detect Drift" }, -]; +export async function loader({ params }: Route.LoaderArgs) { + const data = await apiJson(`/runs/${params.id}/files?checkpoint=all`); + return data; +} -const files = [ +const fallbackFiles = [ { oldFile: { name: "src/commands/run.ts", @@ -507,7 +508,20 @@ function buildAnnotationsForFile( return annotations; } -export default function RunFilesChanged() { +export default function RunFilesChanged({ loaderData }: Route.ComponentProps) { + const runFiles = loaderData; + const checkpoints = [ + { id: "all", label: "All changes" }, + ...runFiles.checkpoints.map((cp) => ({ id: cp.id, label: cp.label })), + ]; + const files = runFiles.files.length > 0 + ? runFiles.files.map((f) => ({ + oldFile: { name: f.old_file.name, contents: f.old_file.contents }, + newFile: { name: f.new_file.name, contents: f.new_file.contents }, + })) + : fallbackFiles; + const diffStats = runFiles.stats; + const [checkpoint, setCheckpoint] = useState(checkpoints[0].id); const [openSteers, setOpenSteers] = useState( () => new Map(), @@ -560,7 +574,7 @@ export default function RunFilesChanged() {
- +
- {workflow && ( -
-

Workflow

-
    -
  • - - - Run Configuration - -
  • -
  • - - - Workflow Graph - -
  • -
-
- )} +
+

Workflow

+
    +
  • + + + Run Configuration + +
  • +
  • + + + Workflow Graph + +
  • +
+
diff --git a/apps/arc-web/app/routes/run-overview.tsx b/apps/arc-web/app/routes/run-overview.tsx index ac4fa7765..71a89d492 100644 --- a/apps/arc-web/app/routes/run-overview.tsx +++ b/apps/arc-web/app/routes/run-overview.tsx @@ -3,10 +3,12 @@ import { Link, useParams } from "react-router"; import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline"; -import { findRun } from "../data/runs"; -import { workflowData } from "./workflow-detail"; import { useTheme } from "../lib/theme"; import { getGraphTheme } from "../lib/graph-theme"; +import { apiJson } from "../api-client"; +import { formatDurationSecs } from "../lib/format"; +import type { RunStage, RunListItem, WorkflowDetail } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-overview"; export const handle = { wide: true }; @@ -19,12 +21,29 @@ interface Stage { duration: string; } -const stages: Stage[] = [ - { id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" }, - { id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" }, - { id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" }, - { id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" }, -]; +export async function loader({ params }: Route.LoaderArgs) { + const [apiStages, runs] = await Promise.all([ + apiJson(`/runs/${params.id}/stages`), + apiJson("/runs"), + ]); + const stages: Stage[] = apiStages.map((s) => ({ + id: s.id, + name: s.name, + status: s.status as StageStatus, + duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", + })); + const run = runs.find((r) => r.id === params.id); + let graphDot: string | null = null; + if (run) { + try { + const workflow = await apiJson(`/workflows/${run.workflow}`); + graphDot = workflow.graph; + } catch { + // workflow not found — leave graphDot null + } + } + return { stages, graphDot }; +} const statusConfig: Record = { completed: { icon: CheckCircleIcon, color: "text-mint" }, @@ -35,6 +54,7 @@ const statusConfig: Record) { return ` + rankdir=LR bgcolor="transparent" pad=0.5 fontname="ui-monospace, monospace" @@ -265,10 +285,9 @@ function DotDiagram({ dot }: { dot: string }) { ); } -export default function RunOverview() { +export default function RunOverview({ loaderData }: Route.ComponentProps) { const { id } = useParams(); - const run = findRun(id ?? ""); - const workflow = run ? workflowData[run.workflow] : undefined; + const { stages, graphDot } = loaderData; return (
@@ -295,37 +314,35 @@ export default function RunOverview() {
- {workflow && ( -
-

Workflow

-
    -
  • - - - Run Configuration - -
  • -
  • - - - Workflow Graph - -
  • -
-
- )} +
+

Workflow

+
    +
  • + + + Run Configuration + +
  • +
  • + + + Workflow Graph + +
  • +
+
- {workflow ? ( + {graphDot ? (
- +
) : (

No workflow graph available.

diff --git a/apps/arc-web/app/routes/run-retro.tsx b/apps/arc-web/app/routes/run-retro.tsx index 5d1845508..8af0a75fc 100644 --- a/apps/arc-web/app/routes/run-retro.tsx +++ b/apps/arc-web/app/routes/run-retro.tsx @@ -1,16 +1,22 @@ import { Link } from "react-router"; import { - findRetro, smoothnessConfig, learningCategoryConfig, frictionKindConfig, openItemKindConfig, formatDuration, } from "../data/retros"; +import type { Retro } from "../data/retros"; +import { apiJson } from "../api-client"; import type { Route } from "./+types/run-retro"; -export function meta({ params }: Route.MetaArgs) { - const retro = findRetro(params.id); +export async function loader({ params }: Route.LoaderArgs) { + const retro = await apiJson(`/runs/${params.id}/retro`); + return { retro }; +} + +export function meta({ data }: Route.MetaArgs) { + const retro = data?.retro; return [{ title: retro ? `Retro: ${retro.goal} \u2014 Arc` : "Retro \u2014 Arc" }]; } @@ -19,8 +25,8 @@ function formatCost(cost: number | undefined): string { return `$${cost.toFixed(2)}`; } -export default function RunRetro({ params }: Route.ComponentProps) { - const retro = findRetro(params.id); +export default function RunRetro({ loaderData }: Route.ComponentProps) { + const { retro } = loaderData; if (!retro) { return

No retrospective found for this run.

; diff --git a/apps/arc-web/app/routes/run-stages.tsx b/apps/arc-web/app/routes/run-stages.tsx index 1b7cdea11..14b1cd6b8 100644 --- a/apps/arc-web/app/routes/run-stages.tsx +++ b/apps/arc-web/app/routes/run-stages.tsx @@ -3,8 +3,10 @@ import { Link, useParams } from "react-router"; import { ChevronRightIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import { DocumentTextIcon, MapIcon, CommandLineIcon, ChatBubbleLeftIcon, WrenchScrewdriverIcon } from "@heroicons/react/24/outline"; -import { findRun } from "../data/runs"; -import { workflowData } from "./workflow-detail"; +import { apiJson } from "../api-client"; +import { formatDurationSecs } from "../lib/format"; +import type { RunStage, StageTurn as ApiStageTurn } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-stages"; export const handle = { wide: true }; @@ -17,12 +19,24 @@ interface Stage { duration: string; } -const stages: Stage[] = [ - { id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" }, - { id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" }, - { id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" }, - { id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" }, -]; +export async function loader({ params }: Route.LoaderArgs) { + const apiStages = await apiJson(`/runs/${params.id}/stages`); + const stages: Stage[] = apiStages.map((s) => ({ + id: s.id, + name: s.name, + status: s.status as StageStatus, + duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", + })); + + // Fetch turns for the selected stage (first stage if none specified) + const selectedStageId = params.stageId ?? stages[0]?.id; + let turns: ApiStageTurn[] = []; + if (selectedStageId) { + turns = await apiJson(`/runs/${params.id}/stages/${selectedStageId}/turns`); + } + + return { stages, turns }; +} const statusConfig: Record = { completed: { icon: CheckCircleIcon, color: "text-mint" }, @@ -44,41 +58,6 @@ type TurnType = // selectedStage is resolved from the URL param in RunStages below -const turns: TurnType[] = [ - { - kind: "system", - content: `You are a drift detection agent. Compare the production and staging environments and identify any configuration or code drift.\n\nSource: production\nTarget: staging\nThreshold: warn`, - }, - { - kind: "assistant", - content: "I'll start by loading the environment configurations for both production and staging to compare them.", - }, - { - kind: "tool", - tools: [ - { - toolName: "read_file", - args: `{ "path": "environments/production/config.toml" }`, - result: `[redis]\nhost = "redis-prod.internal"\nport = 6379\nmax_connections = 200\ntls = true\n\n[iam]\nrole_arn = "arn:aws:iam::123456:role/prod-api"\nsession_duration = 3600`, - }, - { - toolName: "read_file", - args: `{ "path": "environments/staging/config.toml" }`, - result: `[redis]\nhost = "redis-staging.internal"\nport = 6379\nmax_connections = 100\ntls = false\n\n[iam]\nrole_arn = "arn:aws:iam::123456:role/staging-api"\nsession_duration = 1800`, - }, - { - toolName: "diff_configs", - args: `{ "source": "environments/production/config.toml", "target": "environments/staging/config.toml" }`, - result: `3 differences found:\n redis.max_connections: 200 → 100\n redis.tls: true → false\n iam.session_duration: 3600 → 1800`, - }, - ], - }, - { - kind: "assistant", - content: "I've detected drift in 3 resources between production and staging:\n\n1. **redis.max_connections** — production has 200, staging has 100\n2. **redis.tls** — enabled in production, disabled in staging\n3. **iam.session_duration** — production uses 3600s, staging uses 1800s\n\nThe TLS mismatch is the most critical — staging should match production's TLS configuration for accurate testing. The connection pool and session duration differences may be intentional for cost reasons but should be verified.", - }, -]; - function ToolRow({ tool }: { tool: ToolUse }) { const [open, setOpen] = useState(false); @@ -148,10 +127,24 @@ function AssistantBlock({ content }: { content: string }) { ); } -export default function RunStages() { +export default function RunStages({ loaderData }: Route.ComponentProps) { const { id, stageId } = useParams(); - const run = findRun(id ?? ""); - const workflow = run ? workflowData[run.workflow] : undefined; + const { stages, turns: apiTurns } = loaderData; + + const mappedTurns: TurnType[] = apiTurns.map((t) => { + if (t.kind === "tool" && t.tools) { + return { + kind: "tool" as const, + tools: t.tools.map((tu) => ({ + toolName: tu.tool_name, + args: tu.args, + result: tu.result, + })), + }; + } + return { kind: t.kind as "system" | "assistant", content: t.content ?? "" }; + }); + const selectedStage = stages.find((s) => s.id === stageId) ?? stages[0]; const selectedConfig = statusConfig[selectedStage.status]; const SelectedIcon = selectedConfig.icon; @@ -186,31 +179,29 @@ export default function RunStages() {
- {workflow && ( -
-

Workflow

-
    -
  • - - - Run Configuration - -
  • -
  • - - - Workflow Graph - -
  • -
-
- )} +
+

Workflow

+
    +
  • + + + Run Configuration + +
  • +
  • + + + Workflow Graph + +
  • +
+
@@ -220,7 +211,7 @@ export default function RunStages() { {selectedStage.duration}
- {turns.map((turn, i) => { + {mappedTurns.map((turn, i) => { switch (turn.kind) { case "system": return ; diff --git a/apps/arc-web/app/routes/run-usage.tsx b/apps/arc-web/app/routes/run-usage.tsx index 4bc41032f..baee0e166 100644 --- a/apps/arc-web/app/routes/run-usage.tsx +++ b/apps/arc-web/app/routes/run-usage.tsx @@ -1,35 +1,40 @@ -const stages = [ - { stage: "Detect Drift", model: "Opus 4.6", inputTokens: 12_480, outputTokens: 3_210, runtime: "1m 12s", cost: 0.48 }, - { stage: "Propose Changes", model: "Gemini 3.1", inputTokens: 28_640, outputTokens: 8_750, runtime: "2m 34s", cost: 0.72 }, - { stage: "Review Changes", model: "Codex 5.3", inputTokens: 9_120, outputTokens: 2_640, runtime: "0m 45s", cost: 0.19 }, - { stage: "Apply Changes", model: "Opus 4.6", inputTokens: 21_300, outputTokens: 6_480, runtime: "1m 58s", cost: 0.87 }, -]; +import { apiJson } from "../api-client"; +import { formatDurationSecs } from "../lib/format"; +import type { RunUsage } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-usage"; -const totalRuntime = "6m 29s"; -const totalCost = stages.reduce((sum, s) => sum + s.cost, 0); -const totalInput = stages.reduce((sum, s) => sum + s.inputTokens, 0); -const totalOutput = stages.reduce((sum, s) => sum + s.outputTokens, 0); - -const modelBreakdown = Object.values( - stages.reduce>( - (acc, s) => { - const entry = acc[s.model] ?? { model: s.model, inputTokens: 0, outputTokens: 0, cost: 0, stages: 0 }; - entry.inputTokens += s.inputTokens; - entry.outputTokens += s.outputTokens; - entry.cost += s.cost; - entry.stages += 1; - acc[s.model] = entry; - return acc; - }, - {}, - ), -).sort((a, b) => b.cost - a.cost); +export async function loader({ params }: Route.LoaderArgs) { + const usage = await apiJson(`/runs/${params.id}/usage`); + const stages = usage.stages.map((s) => ({ + stage: s.stage, + model: s.model, + inputTokens: s.input_tokens, + outputTokens: s.output_tokens, + runtime: formatDurationSecs(s.runtime_secs), + cost: s.cost, + })); + const totalRuntime = formatDurationSecs(usage.totals.runtime_secs); + const totalCost = usage.totals.cost; + const totalInput = usage.totals.input_tokens; + const totalOutput = usage.totals.output_tokens; + const modelBreakdown = usage.by_model + .map((m) => ({ + model: m.model, + stages: m.stages, + inputTokens: m.input_tokens, + outputTokens: m.output_tokens, + cost: m.cost, + })) + .sort((a, b) => b.cost - a.cost); + return { stages, totalRuntime, totalCost, totalInput, totalOutput, modelBreakdown }; +} function formatTokens(n: number) { return `${(n / 1000).toFixed(1)}k`; } -export default function RunUsage() { +export default function RunUsage({ loaderData }: Route.ComponentProps) { + const { stages, totalRuntime, totalCost, totalInput, totalOutput, modelBreakdown } = loaderData; return (
diff --git a/apps/arc-web/app/routes/run-verifications.tsx b/apps/arc-web/app/routes/run-verifications.tsx index 51e89d5f8..c35ec249c 100644 --- a/apps/arc-web/app/routes/run-verifications.tsx +++ b/apps/arc-web/app/routes/run-verifications.tsx @@ -10,7 +10,6 @@ import { ChevronRightIcon, } from "@heroicons/react/20/solid"; import { - verificationCategories, statusConfig, typeConfig, getCriteriaSummary, @@ -20,6 +19,25 @@ import type { VerificationType, VerificationCategory, } from "../data/verifications"; +import { apiJson } from "../api-client"; +import type { RunVerification } from "@qltysh/arc-api-client"; +import type { Route } from "./+types/run-verifications"; + +export async function loader({ params }: Route.LoaderArgs) { + const apiCategories = await apiJson(`/runs/${params.id}/verifications`); + const categories: VerificationCategory[] = apiCategories.map((cat) => ({ + name: cat.name, + question: cat.question, + status: cat.status as VerificationStatus, + criteria: cat.controls.map((c) => ({ + name: c.name, + description: c.description, + type: (c.type ?? null) as VerificationType | null, + status: c.status as VerificationStatus, + })), + })); + return { categories }; +} function StatusIcon({ status, @@ -117,10 +135,11 @@ function CategoryCard({ category }: { category: VerificationCategory }) { ); } -export default function RunVerifications() { +export default function RunVerifications({ loaderData }: Route.ComponentProps) { + const { categories } = loaderData; return (
- {verificationCategories.map((category) => ( + {categories.map((category) => ( ))}
diff --git a/apps/arc-web/app/routes/runs.tsx b/apps/arc-web/app/routes/runs.tsx index c16fdabd2..f7497cd42 100644 --- a/apps/arc-web/app/routes/runs.tsx +++ b/apps/arc-web/app/routes/runs.tsx @@ -18,14 +18,77 @@ import { arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { columns as staticColumns, ciConfig, statusColors, deriveCiStatus } from "../data/runs"; -import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus } from "../data/runs"; +import { ciConfig, statusColors, deriveCiStatus } from "../data/runs"; +import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs"; +import { apiJson } from "../api-client"; +import { formatElapsedSecs, formatDurationSecs } from "../lib/format"; +import type { RunListItem } from "@qltysh/arc-api-client"; import type { Route } from "./+types/runs"; export function meta({}: Route.MetaArgs) { return [{ title: "Runs — Arc" }]; } +function mapRunListItem(item: RunListItem): RunItem { + return { + id: item.id, + repo: item.repo, + title: item.title, + workflow: item.workflow, + number: item.number, + additions: item.additions, + deletions: item.deletions, + checks: item.checks?.map((c) => ({ + name: c.name, + status: c.status, + duration: c.duration_secs != null ? formatDurationSecs(c.duration_secs) : undefined, + })), + elapsed: item.elapsed_secs != null ? formatElapsedSecs(item.elapsed_secs) : undefined, + elapsedWarning: item.elapsed_warning, + resources: item.resources, + comments: item.comments, + question: item.question, + sandboxId: item.sandbox_id, + }; +} + +const columnConfig: { + id: ColumnStatus; + name: string; + accent: string; + iconColor: string; + iconType: "branch" | "pr"; + actions: string[]; +}[] = [ + { id: "working", name: "Working", accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] }, + { id: "pending", name: "Pending", accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: ["Answer Question"] }, + { id: "review", name: "Verify", accent: "bg-mint", iconColor: "text-mint", iconType: "pr", actions: ["Resolve"] }, + { id: "merge", name: "Merge", accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: ["Merge"] }, +]; + +export async function loader() { + const apiRuns = await apiJson("/runs"); + const items = apiRuns.map(mapRunListItem); + + const grouped = new Map(); + for (const cfg of columnConfig) { + grouped.set(cfg.id, []); + } + for (const item of items) { + const status = apiRuns.find((r) => r.id === item.id)?.status; + if (status && grouped.has(status)) { + grouped.get(status)?.push(item); + } + } + + const columns = columnConfig.map((cfg) => ({ + ...cfg, + items: grouped.get(cfg.id) ?? [], + })); + + return { columns }; +} + function GitBranchIcon({ className }: { className?: string }) { return ( @@ -186,24 +249,8 @@ function ChecksStatus({ checks }: { checks: CheckRun[] }) { ); } -const totalCards = staticColumns.reduce((sum, col) => sum + col.items.length, 0); -const totalPrs = staticColumns.reduce( - (sum, col) => sum + col.items.filter((item) => item.number != null).length, - 0, -); - export const handle = { wide: true, - headerExtra: ( -
- - {totalCards} runs - - - {totalPrs} PRs - -
- ), }; function PrCard({ @@ -359,7 +406,17 @@ function SortablePrCard({ ); } -function BoardColumn({ column }: { column: (typeof staticColumns)[number] }) { +type Column = { + id: ColumnStatus; + name: string; + accent: string; + iconColor: string; + iconType: "branch" | "pr"; + actions: string[]; + items: RunItem[]; +}; + +function BoardColumn({ column }: { column: Column }) { const Icon = iconMap[column.iconType]; return (
@@ -463,14 +520,14 @@ function SortableRunRow({ run }: { run: RunWithStatus }) { ); } -const allRepos = [...new Set(staticColumns.flatMap((col) => col.items.map((item) => item.repo)))].sort(); - -export default function Runs() { +export default function Runs({ loaderData }: Route.ComponentProps) { + const initialColumns = loaderData.columns; + const allRepos = [...new Set(initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => item.repo)))].sort(); const [query, setQuery] = useState(""); const [repoFilter, setRepoFilter] = useState("all"); const [view, setView] = useState("columns"); const [collapsed, setCollapsed] = useState>(new Set()); - const [columns, setColumns] = useState(staticColumns); + const [columns, setColumns] = useState(initialColumns); const lowerQuery = query.toLowerCase(); const sensors = useSensors( diff --git a/apps/arc-web/app/routes/session-detail.tsx b/apps/arc-web/app/routes/session-detail.tsx index 0673734ae..db51fc973 100644 --- a/apps/arc-web/app/routes/session-detail.tsx +++ b/apps/arc-web/app/routes/session-detail.tsx @@ -9,6 +9,8 @@ import { UserIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; +import { apiJson } from "../api-client"; +import type { SessionDetail as ApiSessionDetail, SessionGroup } from "@qltysh/arc-api-client"; import type { Route } from "./+types/session-detail"; export const handle = { hideHeader: true, wide: true }; @@ -17,6 +19,43 @@ export function meta({}: Route.MetaArgs) { return [{ title: "Session — Arc" }]; } +export async function loader({ params }: Route.LoaderArgs) { + const [apiSession, apiGroups] = await Promise.all([ + apiJson(`/sessions/${params.sessionId}`), + apiJson("/sessions"), + ]); + const session: Session = { + id: apiSession.id, + title: apiSession.title, + repo: apiSession.repo, + model: apiSession.model, + time: "", + turns: apiSession.turns.map((t) => { + if (t.kind === "tool" && t.tools) { + return { + kind: "tool" as const, + tools: t.tools.map((tu) => ({ + toolName: tu.tool_name, + args: tu.args, + result: tu.result, + })), + }; + } + return { kind: t.kind as "user" | "assistant", content: t.content ?? "", date: t.date }; + }), + }; + const sessionGroups = apiGroups.map((g) => ({ + label: g.label, + sessions: g.sessions.map((s) => ({ + id: s.id, + title: s.title, + repo: s.repo, + time: s.time, + })), + })); + return { session, sessionGroups }; +} + interface ToolUse { toolName: string; args: string; @@ -37,6 +76,7 @@ interface Session { turns: Turn[]; } +// Keep hardcoded sessions as fallback const sessions: Record = { s1: { id: "s1", @@ -191,7 +231,7 @@ function makeFallbackSession(id: string): Session { }; } -interface SessionGroup { +interface SessionGroupType { label: string; sessions: { id: string; title: string; repo: string; time: string }[]; } @@ -326,7 +366,7 @@ function AssistantBlock({ content, showCopy }: { content: string; showCopy: bool ); } -function SessionSidebar({ activeId }: { activeId: string }) { +function SessionSidebar({ activeId, groups }: { activeId: string; groups: SessionGroupType[] }) { return (