Merge pull request #718 from AssetOverflow/feat/wb-r2-runs-route

feat(workbench): Runs route — session evidence with trace cross-links (Wave R2)
This commit is contained in:
Shay 2026-06-12 21:07:37 -07:00 committed by GitHub
commit 1086415b36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 710 additions and 15 deletions

View file

@ -11,6 +11,8 @@ import type {
ProposalState,
ProposalSummary,
AuditEvent,
RunSummary,
RunDetail,
TurnJournalEntry,
TurnJournalSummary,
MathProposalSummary,
@ -139,6 +141,28 @@ export async function fetchAuditEvents(
return apiFetch<ItemsEnvelope<AuditEvent>>(query ? `/audit/events?${query}` : "/audit/events");
}
export async function fetchRuns(limit?: number, offset?: number): Promise<RunSummary[]> {
const params = new URLSearchParams();
if (limit !== undefined) params.set("limit", String(limit));
if (offset !== undefined) params.set("offset", String(offset));
const query = params.toString();
const envelope = await apiFetch<ItemsEnvelope<RunSummary>>(query ? `/runs?${query}` : "/runs");
return envelope.items;
}
export async function fetchRun(
sessionId: string,
turnLimit?: number,
turnOffset?: number,
): Promise<RunDetail> {
const params = new URLSearchParams();
if (turnLimit !== undefined) params.set("limit", String(turnLimit));
if (turnOffset !== undefined) params.set("offset", String(turnOffset));
const query = params.toString();
const path = `/runs/${encodeURIComponent(sessionId)}`;
return apiFetch<RunDetail>(query ? `${path}?${query}` : path);
}
export async function fetchMathProposals(): Promise<MathProposalSummary[]> {
const envelope = await apiFetch<ItemsEnvelope<MathProposalSummary>>("/math-proposals");
return envelope.items;

View file

@ -14,6 +14,8 @@ import {
fetchProposalDetail,
fetchProposals,
fetchAuditEvents,
fetchRuns,
fetchRun,
fetchTraceTurn,
fetchTraceTurns,
fetchMathProposals,
@ -31,6 +33,8 @@ import type {
ProposalSummary,
ProposalDetail,
AuditEvent,
RunSummary,
RunDetail,
TurnJournalEntry,
TurnJournalSummary,
EvalLaneSummary,
@ -142,6 +146,25 @@ export function useAuditEvents(limit?: number, offset?: number) {
});
}
export function useRuns(limit?: number, offset?: number) {
return useQuery<RunSummary[], WorkbenchApiError>({
queryKey: ["api", "runs", limit ?? null, offset ?? null],
queryFn: () => fetchRuns(limit, offset),
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}
export function useRun(sessionId?: string | null, turnLimit?: number) {
return useQuery<RunDetail, WorkbenchApiError>({
queryKey: ["api", "run", sessionId ?? null, turnLimit ?? null],
queryFn: () => fetchRun(sessionId as string, turnLimit),
enabled: typeof sessionId === "string" && sessionId.length > 0,
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}
export function useEvalLanes() {
return useQuery<EvalLaneSummary[]>({
queryKey: ["api", "evals"],

View file

@ -9,7 +9,7 @@ import { TraceRoute } from "./trace/TraceRoute";
import { AuditRoute } from "./audit/AuditRoute";
import { ReplayRoute } from "./replay/ReplayRoute";
import { EvalsRoute } from "./evals/EvalsRoute";
import { RunsRoutePlaceholder } from "../routes/RunsRoutePlaceholder";
import { RunsRoute } from "./runs/RunsRoute";
import { PacksRoutePlaceholder } from "../routes/PacksRoutePlaceholder";
import { VaultRoutePlaceholder } from "../routes/VaultRoutePlaceholder";
import { SettingsRoutePlaceholder } from "../routes/SettingsRoutePlaceholder";
@ -26,7 +26,7 @@ export function App() {
<Route path="replay/:artifactId?" element={<ReplayRoute />} />
<Route path="proposals/:proposalId?" element={<ProposalsRoute />} />
<Route path="evals/:laneId?" element={<EvalsRoute />} />
<Route path="runs/:sessionId?" element={<RunsRoutePlaceholder />} />
<Route path="runs/:sessionId?" element={<RunsRoute />} />
<Route path="packs/:packId?" element={<PacksRoutePlaceholder />} />
<Route path="vault" element={<VaultRoutePlaceholder />} />
<Route path="audit" element={<AuditRoute />} />

View file

@ -12,6 +12,7 @@ import { TraceRoute } from "./trace/TraceRoute";
import { AuditRoute } from "./audit/AuditRoute";
import { EvalsRoute } from "./evals/EvalsRoute";
import { ReplayRoute } from "./replay/ReplayRoute";
import { RunsRoute } from "./runs/RunsRoute";
/**
* ADR-0162 §6 route conformance executable, not aspirational.
@ -142,6 +143,15 @@ const MOUNT_ROUTES: MountRouteSpec[] = [
emptyStatement: "No eval lanes discovered.",
emptyCommand: "core eval --list",
},
{
name: "Runs",
element: <RunsRoute />,
path: "/runs/:sessionId?",
initialEntry: "/runs",
loadingLabel: "Loading runs...",
emptyStatement: "No runs recorded yet. Use Chat to create evidence.",
emptyCommand: "core chat",
},
{
name: "Replay",
element: <ReplayRoute />,

View file

@ -0,0 +1,246 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
MemoryRouter,
Route,
Routes,
useLocation,
useNavigationType,
} from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestQueryClient } from "../../test/createTestQueryClient";
import type { RunDetail, RunSummary } from "../../types/api";
import { EvidenceProvider } from "../evidenceContext";
import { RunsRoute } from "./RunsRoute";
const JOURNAL_RUN = "workbench_turn_journal";
const ENGINE_RUN = "engine_state_checkpoint";
const summaries: RunSummary[] = [
{
session_id: JOURNAL_RUN,
source: "turn_journal",
turn_count: 2,
started_at: "2026-06-12T18:00:00Z",
updated_at: "2026-06-12T18:05:00Z",
checkpoint_present: false,
checkpoint_revision: null,
artifact_refs: [],
evidence_gap: null,
},
{
session_id: ENGINE_RUN,
source: "engine_state_manifest",
turn_count: 0,
started_at: null,
updated_at: "2026-06-12T17:00:00Z",
checkpoint_present: true,
checkpoint_revision: "rev-abc123",
artifact_refs: [],
evidence_gap: "turn journal not found alongside checkpoint",
},
];
function detailFor(sessionId: string): RunDetail {
const summary = summaries.find((s) => s.session_id === sessionId) ?? summaries[0];
if (sessionId === JOURNAL_RUN) {
return {
...summary,
turns: [
{
turn_id: 1,
trace_hash: "sha256:111111111111abcdef",
timestamp: "2026-06-12T18:00:00Z",
trace_path: "/trace/1",
surface_excerpt: "First response",
},
{
turn_id: 2,
trace_hash: "sha256:222222222222abcdef",
timestamp: "2026-06-12T18:05:00Z",
trace_path: "/trace/2",
surface_excerpt: "Second response",
},
],
manifest: null,
};
}
return {
...summary,
turns: [],
manifest: { schema_version: 2, checkpoint_revision: "rev-abc123" },
};
}
function LocationProbe() {
const location = useLocation();
const navigationType = useNavigationType();
return (
<>
<span data-testid="location">{`${location.pathname}${location.search}`}</span>
<span data-testid="nav-type">{navigationType}</span>
</>
);
}
function renderRoute(initialEntry = "/runs") {
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[initialEntry]}>
<EvidenceProvider>
<Routes>
<Route
path="/runs/:sessionId?"
element={
<>
<RunsRoute />
<LocationProbe />
</>
}
/>
<Route path="/trace/:turnId?" element={<LocationProbe />} />
</Routes>
</EvidenceProvider>
</MemoryRouter>
</QueryClientProvider>,
);
}
function stubRunsFetch(items: RunSummary[] = summaries) {
const fetchMock = vi.fn((input: unknown) => {
const path = new URL(String(input)).pathname;
if (path === "/runs") {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items } }),
});
}
const match = path.match(/^\/runs\/(.+)$/);
if (match) {
return Promise.resolve({
json: () =>
Promise.resolve({
ok: true,
generated_at: "now",
data: detailFor(decodeURIComponent(match[1])),
}),
});
}
return Promise.resolve({
json: () =>
Promise.resolve({
ok: false,
generated_at: "now",
error: { code: "not_found", message: `unexpected path ${path}` },
}),
});
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
const offsetDescriptors = {
offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"),
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
};
describe("RunsRoute", () => {
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
configurable: true,
get: () => 560,
});
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
get: () => 480,
});
});
afterEach(() => {
if (offsetDescriptors.offsetHeight) {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", offsetDescriptors.offsetHeight);
}
if (offsetDescriptors.offsetWidth) {
Object.defineProperty(HTMLElement.prototype, "offsetWidth", offsetDescriptors.offsetWidth);
}
vi.restoreAllMocks();
localStorage.clear();
});
it("renders the session list with checkpoint and turn-count evidence", async () => {
stubRunsFetch();
renderRoute();
expect(await screen.findByText("Turn journal")).toBeInTheDocument();
expect(screen.getByText("Engine-state checkpoint")).toBeInTheDocument();
expect(screen.getByText(JOURNAL_RUN)).toBeInTheDocument();
expect(screen.getByText("2 turns")).toBeInTheDocument();
// checkpoint present vs absent are visually distinct
expect(screen.getByText("no checkpoint")).toBeInTheDocument();
expect(screen.getByText("rev-abc123")).toBeInTheDocument();
});
it("renders an evidence gap honestly in-row, never hidden", async () => {
stubRunsFetch();
renderRoute();
expect(
await screen.findByText(/evidence gap: turn journal not found alongside checkpoint/),
).toBeInTheDocument();
});
it("selecting a run writes /runs/<sessionId> with replace and shows detail", async () => {
stubRunsFetch();
const user = userEvent.setup();
renderRoute();
await user.click(await screen.findByText("Turn journal"));
await waitFor(() =>
expect(screen.getByTestId("location")).toHaveTextContent(`/runs/${JOURNAL_RUN}`),
);
expect(screen.getByTestId("nav-type")).toHaveTextContent("REPLACE");
});
it("every turn row cross-links to /trace/<turn_id> — the point of this route", async () => {
stubRunsFetch();
renderRoute(`/runs/${JOURNAL_RUN}`);
const link1 = await screen.findByRole("link", { name: /Turn #1/ });
expect(link1).toHaveAttribute("href", "/trace/1");
const link2 = screen.getByRole("link", { name: /Turn #2/ });
expect(link2).toHaveAttribute("href", "/trace/2");
// the trace hash is shown as a digest badge on each turn row
expect(screen.getByText("sha256:111111111111...")).toBeInTheDocument();
});
it("shows the engine-state manifest under the Manifest tab", async () => {
stubRunsFetch();
const user = userEvent.setup();
renderRoute(`/runs/${ENGINE_RUN}`);
// engine-state run has no cross-linkable turns
expect(
await screen.findByText(/records no cross-linkable turns/),
).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Manifest" }));
expect(await screen.findByText(/schema_version/)).toBeInTheDocument();
});
it("moves the session list focus with j/k through the VirtualizedList spine", async () => {
stubRunsFetch();
const user = userEvent.setup();
renderRoute();
const list = await screen.findByRole("listbox", { name: "Runs" });
list.focus();
expect(screen.getAllByRole("option")[0]).toHaveAttribute("aria-selected", "true");
await user.keyboard("j");
expect(screen.getAllByRole("option")[1]).toHaveAttribute("aria-selected", "true");
await user.keyboard("k");
expect(screen.getAllByRole("option")[0]).toHaveAttribute("aria-selected", "true");
});
});

View file

@ -0,0 +1,378 @@
import { useEffect, useMemo, useState } from "react";
import { ArrowUpRight, Eye } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { WorkbenchApiError } from "../../api/client";
import { useRun, useRuns } from "../../api/queries";
import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import { Button } from "../../design/components/primitives/Button";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
import type { RunDetail, RunSummary, RunTurnRef } from "../../types/api";
import { pushRecentItem } from "../commandRegistry";
import { subjectToUrl } from "../evidenceAddress";
import { useEvidenceSubject } from "../evidenceContext";
const RUN_TABS: readonly Tab[] = [
{ id: "turns", label: "Turns" },
{ id: "manifest", label: "Manifest" },
{ id: "raw", label: "Raw" },
];
// read_run() pages turns by limit/offset; the list grows by this step.
const TURN_PAGE = 100;
const SOURCE_LABEL: Record<string, string> = {
turn_journal: "Turn journal",
engine_state_manifest: "Engine-state checkpoint",
};
function sourceLabel(source: string): string {
return SOURCE_LABEL[source] ?? source;
}
function errorMessage(error: unknown) {
return error instanceof WorkbenchApiError ? error.message : "Runs request failed.";
}
function digestPayload(value: string | null | undefined): string | null {
if (!value) return null;
return value.replace(/^sha256:/, "");
}
function CheckpointBadge({
present,
revision,
}: {
present: boolean;
revision: string | null;
}) {
if (!present) {
return (
<span className="inline-flex h-6 items-center rounded-md border border-[var(--color-border-subtle)] px-2 text-xs text-[var(--color-text-muted)]">
no checkpoint
</span>
);
}
return (
<span className="inline-flex h-6 items-center gap-1 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-2 text-xs text-[var(--color-text-secondary)]">
checkpoint
{revision ? (
<span className="font-mono text-[var(--color-text-muted)]">{revision}</span>
) : null}
</span>
);
}
function RunRow({
run,
selected,
focused,
onSelect,
}: {
run: RunSummary;
selected: boolean;
focused: boolean;
onSelect: () => void;
}) {
return (
<div
role="button"
tabIndex={-1}
aria-current={selected ? "true" : undefined}
onClick={onSelect}
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-3 border-b border-[var(--color-border-subtle)] px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[var(--color-focus-ring)] ${
selected ? "bg-[var(--color-selected-bg)]" : ""
} ${
selected
? "border-l-2 border-l-[var(--color-selected-border)] pl-[10px]"
: focused
? "border-l-2 border-l-[var(--color-focus-ring)] pl-[10px]"
: "border-l-2 border-l-transparent pl-[10px]"
}`}
>
<span className="min-w-0">
<span className="block text-sm text-[var(--color-text-primary)]">
{sourceLabel(run.source)}
</span>
<span className="mt-1 block truncate font-mono text-xs text-[var(--color-text-muted)]">
{run.session_id}
</span>
<span className="mt-1 flex items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<span>{run.turn_count} turns</span>
{run.updated_at ? (
<>
<span aria-hidden className="text-[var(--color-text-muted)]">·</span>
<Timestamp iso={run.updated_at} format="relative" />
</>
) : null}
</span>
{run.evidence_gap ? (
<span className="mt-1 block text-xs text-[var(--color-state-warning-text)]">
evidence gap: {run.evidence_gap}
</span>
) : null}
</span>
<span className="justify-self-end">
<CheckpointBadge present={run.checkpoint_present} revision={run.checkpoint_revision} />
</span>
</div>
);
}
function TurnRefRow({ turn }: { turn: RunTurnRef }) {
const digest = digestPayload(turn.trace_hash);
return (
<Link
to={turn.trace_path}
className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-md border border-[var(--color-border-subtle)] px-3 py-2 no-underline transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
>
<span className="min-w-0">
<span className="flex items-center gap-1 text-sm text-[var(--color-text-primary)]">
Turn #{turn.turn_id}
<ArrowUpRight size={13} aria-hidden className="text-[var(--color-text-muted)]" />
</span>
<span className="mt-1 block text-xs text-[var(--color-text-secondary)]">
<Timestamp iso={turn.timestamp} format="relative" />
</span>
<span className="mt-1 block truncate text-xs text-[var(--color-text-muted)]">
{turn.surface_excerpt || "Not recorded."}
</span>
</span>
<span className="justify-self-end">
{digest ? (
<DigestBadge digest={digest} truncate={12} />
) : (
<span className="font-mono text-xs text-[var(--color-text-muted)]">no hash</span>
)}
</span>
</Link>
);
}
function TurnsTab({
detail,
onLoadMore,
canLoadMore,
}: {
detail: RunDetail;
onLoadMore: () => void;
canLoadMore: boolean;
}) {
if (detail.turns.length === 0) {
return (
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
This run records no cross-linkable turns. Engine-state checkpoints
expose their evidence under Manifest.
</p>
);
}
return (
<div className="grid gap-2">
{detail.turns.map((turn) => (
<TurnRefRow key={turn.turn_id} turn={turn} />
))}
{canLoadMore ? (
<Button type="button" variant="quiet" onClick={onLoadMore}>
Load more turns
</Button>
) : null}
</div>
);
}
function ManifestTab({ detail }: { detail: RunDetail }) {
if (!detail.manifest) {
return (
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
No engine-state manifest recorded for this run.
</p>
);
}
return <StableJsonViewer source={JSON.stringify(detail.manifest, null, 2)} />;
}
function RawTab({ detail }: { detail: RunDetail }) {
const [expanded, setExpanded] = useState(false);
return expanded ? (
<StableJsonViewer source={JSON.stringify(detail, null, 2)} />
) : (
<div className="grid justify-items-start gap-2">
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
Raw run JSON is collapsed by default.
</p>
<Button type="button" variant="quiet" onClick={() => setExpanded(true)}>
<Eye size={14} aria-hidden />
Expand raw JSON
</Button>
</div>
);
}
function RunDetailPanel({
detail,
onLoadMore,
canLoadMore,
}: {
detail: RunDetail;
onLoadMore: () => void;
canLoadMore: boolean;
}) {
const [activeTab, setActiveTab] = useState("turns");
return (
<Panel
title={sourceLabel(detail.source)}
toolbar={
<CheckpointBadge
present={detail.checkpoint_present}
revision={detail.checkpoint_revision}
/>
}
>
<TabBar tabs={RUN_TABS} activeTab={activeTab} onTabChange={setActiveTab}>
{activeTab === "turns" ? (
<TurnsTab detail={detail} onLoadMore={onLoadMore} canLoadMore={canLoadMore} />
) : null}
{activeTab === "manifest" ? <ManifestTab detail={detail} /> : null}
{activeTab === "raw" ? <RawTab detail={detail} /> : null}
</TabBar>
</Panel>
);
}
export function RunsRoute() {
const { sessionId } = useParams();
const selectedSessionId = sessionId && sessionId.length > 0 ? sessionId : null;
const navigate = useNavigate();
const { setSubject } = useEvidenceSubject();
const [search, setSearch] = useState("");
const [turnLimit, setTurnLimit] = useState(TURN_PAGE);
const runsQuery = useRuns();
const runQuery = useRun(selectedSessionId, turnLimit);
const runs = runsQuery.data ?? [];
const filteredRuns = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return runs;
return runs.filter(
(run) =>
run.session_id.toLowerCase().includes(q) ||
sourceLabel(run.source).toLowerCase().includes(q),
);
}, [search, runs]);
// Reset the turn window when the selected run changes.
useEffect(() => {
setTurnLimit(TURN_PAGE);
}, [selectedSessionId]);
useEffect(() => {
if (selectedSessionId === null) return;
setSubject({ kind: "run", sessionId: selectedSessionId, data: runQuery.data });
}, [selectedSessionId, setSubject, runQuery.data]);
function selectRun(run: RunSummary) {
const subject = { kind: "run" as const, sessionId: run.session_id };
const path = subjectToUrl(subject);
navigate(path, { replace: true });
pushRecentItem({ label: sourceLabel(run.source), path });
}
if (runsQuery.isLoading) {
return <LoadingState label="Loading runs..." />;
}
if (runsQuery.isError) {
return (
<ErrorState
whatFailed={errorMessage(runsQuery.error)}
mutationStatus="No runs mutation occurred."
reproducer="curl /runs"
retrySafety="Retry: safe"
/>
);
}
if (runs.length === 0) {
return (
<EmptyState
statement="No runs recorded yet. Use Chat to create evidence."
nextAction={{ kind: "cli", command: "core chat" }}
/>
);
}
const canLoadMore = !!runQuery.data && runQuery.data.turns.length === turnLimit;
return (
<div className="h-full min-h-0">
<SplitPane direction="horizontal" id="runs" defaultSplit={38} minSize={320}>
<Panel title="Sessions">
<div className="grid min-h-0 gap-3">
<SearchInput
placeholder="Filter by session or source"
value={search}
onChange={setSearch}
/>
{filteredRuns.length === 0 ? (
<EmptyState
statement="No runs match this filter."
nextAction={{ kind: "cli", command: "core chat" }}
/>
) : (
<VirtualizedList
ariaLabel="Runs"
estimateSize={92}
getKey={(run) => run.session_id}
height="calc(100vh - 14rem)"
initialRect={{ width: 480, height: 560 }}
items={filteredRuns}
onActivate={(run) => selectRun(run)}
renderItem={(run, _index, focused) => (
<RunRow
run={run}
selected={run.session_id === selectedSessionId}
focused={focused}
onSelect={() => selectRun(run)}
/>
)}
/>
)}
</div>
</Panel>
<section className="h-full min-h-0 overflow-y-auto pl-3">
{selectedSessionId === null ? (
<EmptyState
statement="Select a session to inspect its turns, manifest, and checkpoint."
nextAction={{ kind: "cli", command: "core chat" }}
/>
) : runQuery.isLoading ? (
<LoadingState label="Loading run detail..." />
) : runQuery.isError ? (
<ErrorState
whatFailed={errorMessage(runQuery.error)}
mutationStatus="No runs mutation occurred."
reproducer={`curl /runs/${selectedSessionId}`}
retrySafety="Retry: safe"
/>
) : runQuery.data ? (
<RunDetailPanel
detail={runQuery.data}
onLoadMore={() => setTurnLimit((n) => n + TURN_PAGE)}
canLoadMore={canLoadMore}
/>
) : null}
</section>
</SplitPane>
</div>
);
}

View file

@ -23,9 +23,6 @@ const NOT_YET_MIRRORED = new Set([
// R2-B backend read substrate (#712) — TS mirrors land with each R2 route:
"PackSummary",
"PackDetail",
"RunSummary",
"RunTurnRef",
"RunDetail",
"VaultSummary",
"VaultEntry",
// Wave R3 sealed turn replay backend — TS mirrors land with the frontend

View file

@ -1,10 +0,0 @@
import { EmptyState } from "../design/components/states/EmptyState";
export function RunsRoutePlaceholder() {
return (
<EmptyState
statement="Runs — no data loaded yet."
nextAction="Pending W-030"
/>
);
}

View file

@ -113,6 +113,33 @@ export interface TurnJournalEntry {
export type TurnEvidence = ChatTurnResult | TurnJournalEntry;
export type RunSource = "engine_state_manifest" | "turn_journal";
export interface RunSummary {
session_id: string;
source: RunSource;
turn_count: number;
started_at: string | null;
updated_at: string | null;
checkpoint_present: boolean;
checkpoint_revision: string | null;
artifact_refs: ArtifactRef[];
evidence_gap: string | null;
}
export interface RunTurnRef {
turn_id: number;
trace_hash: string | null;
timestamp: string;
trace_path: string;
surface_excerpt: string;
}
export interface RunDetail extends RunSummary {
turns: RunTurnRef[];
manifest: Record<string, unknown> | null;
}
export interface AuditEvent {
event_id: string;
source: AuditSource;