feat(workbench): Packs route + TreeView primitive — manifest & checksum evidence (Wave R2)

Replaces PacksRoutePlaceholder with the real triad route over the R2-B
/packs read substrate (#712).

- TS mirrors PackSource/PackSummary/PackDetail; PackSummary/PackDetail
  removed from NOT_YET_MIRRORED (drift gate now enforces them)
- new design primitive TreeView (src/design/components/TreeView): a
  deterministic, keyboard-driven JSON tree — object keys sorted, array
  indices in order, no animation; ←/→ collapse/expand, ↑/↓ + j/k move,
  Home/End jump, Enter toggles; registers its shortcuts only while focused
  (KeyboardHelp never advertises tree controls when no tree is on screen)
- list pane: VirtualizedList + useListNavigation (j/k) + SearchInput; rows
  show pack_id, source, version, language/modality, determinism_class pill
- detail pane: Panel + TabBar (Manifest / Checksums / Raw); Manifest renders
  via TreeView; Checksums is the verify affordance — manifest_digest and
  declared checksum as DigestBadges plus the per-field manifest checksums
  table (the checksum-hashes-the-bytes-on-disk doctrine made visible); Raw
  collapsed by default
- selection publishes the pack subject (R2-S), records /packs/<packId>
  (replace), pushRecentItem on visit
- Packs row added to routeConformance MOUNT_ROUTES (loading 'Loading
  packs...', error 'No packs mutation occurred.', empty 'No packs
  discovered.' + core pack validate <path>)
- tests: TreeView (sorted/collapsed/expand/collapse/focus/click/empty) +
  PacksRoute (list, replace-mode select + manifest tree, checksum verify
  affordance, j/k spine)

Token-only styling (hexScan green).
This commit is contained in:
Shay 2026-06-12 21:11:59 -07:00
parent 1086415b36
commit cfcd7dc9ec
11 changed files with 863 additions and 14 deletions

View file

@ -13,6 +13,8 @@ import type {
AuditEvent,
RunSummary,
RunDetail,
PackSummary,
PackDetail,
TurnJournalEntry,
TurnJournalSummary,
MathProposalSummary,
@ -209,3 +211,16 @@ export async function deferMathProposal(
},
);
}
export async function fetchPacks(limit?: number, offset?: number): Promise<PackSummary[]> {
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<PackSummary>>(query ? `/packs?${query}` : "/packs");
return envelope.items;
}
export async function fetchPack(packId: string): Promise<PackDetail> {
return apiFetch<PackDetail>(`/packs/${encodeURIComponent(packId)}`);
}

View file

@ -16,6 +16,8 @@ import {
fetchAuditEvents,
fetchRuns,
fetchRun,
fetchPacks,
fetchPack,
fetchTraceTurn,
fetchTraceTurns,
fetchMathProposals,
@ -35,6 +37,8 @@ import type {
AuditEvent,
RunSummary,
RunDetail,
PackSummary,
PackDetail,
TurnJournalEntry,
TurnJournalSummary,
EvalLaneSummary,
@ -272,3 +276,22 @@ export function useMathDefer() {
});
}
export function usePacks(limit?: number, offset?: number) {
return useQuery<PackSummary[], WorkbenchApiError>({
queryKey: ["api", "packs", limit ?? null, offset ?? null],
queryFn: () => fetchPacks(limit, offset),
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}
export function usePack(packId?: string | null) {
return useQuery<PackDetail, WorkbenchApiError>({
queryKey: ["api", "pack", packId ?? null],
queryFn: () => fetchPack(packId as string),
enabled: typeof packId === "string" && packId.length > 0,
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}

View file

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

View file

@ -0,0 +1,215 @@
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 { PackDetail, PackSummary } from "../../types/api";
import { EvidenceProvider } from "../evidenceContext";
import { PacksRoute } from "./PacksRoute";
const summaries: PackSummary[] = [
{
pack_id: "en_core_cognition_v1",
source: "language_pack",
manifest_path: "language_packs/data/en_core_cognition_v1/manifest.json",
version: "1",
language: "en",
modality: "text",
determinism_class: "deterministic",
checksum: "sha256:declared0000000000000000",
checksums: {},
},
{
pack_id: "safety_core_v1",
source: "runtime_pack",
manifest_path: "packs/runtime/safety_core_v1/manifest.json",
version: "1",
language: null,
modality: null,
determinism_class: "pinned",
checksum: null,
checksums: {},
},
];
function detailFor(packId: string): PackDetail {
const summary = summaries.find((s) => s.pack_id === packId) ?? summaries[0];
return {
...summary,
checksums: {
lexicon_sha256: "sha256:aaaaaaaaaaaaaaaaaaaaaaaa",
manifest_checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbb",
},
manifest_digest: "sha256:cccccccccccccccccccccccc",
manifest: {
pack_id: packId,
version: "1",
lexicon: { entries: 128, sealed: true },
},
};
}
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 = "/packs") {
return render(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[initialEntry]}>
<EvidenceProvider>
<Routes>
<Route
path="/packs/:packId?"
element={
<>
<PacksRoute />
<LocationProbe />
</>
}
/>
</Routes>
</EvidenceProvider>
</MemoryRouter>
</QueryClientProvider>,
);
}
function stubPacksFetch(items: PackSummary[] = summaries) {
const fetchMock = vi.fn((input: unknown) => {
const path = new URL(String(input)).pathname;
if (path === "/packs") {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items } }),
});
}
const match = path.match(/^\/packs\/(.+)$/);
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("PacksRoute", () => {
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 packs with source, version, and determinism class", async () => {
stubPacksFetch();
renderRoute();
expect(await screen.findByText("en_core_cognition_v1")).toBeInTheDocument();
expect(screen.getByText("safety_core_v1")).toBeInTheDocument();
expect(screen.getAllByText("Language pack").length).toBeGreaterThan(0);
expect(screen.getByText("deterministic")).toBeInTheDocument();
expect(screen.getByText("pinned")).toBeInTheDocument();
});
it("selecting a pack writes /packs/<packId> with replace and shows the manifest tree", async () => {
stubPacksFetch();
const user = userEvent.setup();
renderRoute();
await user.click(await screen.findByText("en_core_cognition_v1"));
await waitFor(() =>
expect(screen.getByTestId("location")).toHaveTextContent("/packs/en_core_cognition_v1"),
);
expect(screen.getByTestId("nav-type")).toHaveTextContent("REPLACE");
// manifest tree is the default tab — top-level keys render as treeitems
const tree = await screen.findByRole("tree");
expect(tree).toBeInTheDocument();
expect(screen.getByText("lexicon")).toBeInTheDocument();
});
it("surfaces the manifest checksums as the verify affordance", async () => {
stubPacksFetch();
const user = userEvent.setup();
renderRoute("/packs/en_core_cognition_v1");
await user.click(await screen.findByRole("tab", { name: "Checksums" }));
expect(await screen.findByText("manifest_digest")).toBeInTheDocument();
expect(screen.getByText("lexicon_sha256")).toBeInTheDocument();
expect(screen.getByText("manifest_checksum")).toBeInTheDocument();
// the manifest_digest renders as a sha256 digest badge (full value in aria-label)
expect(
screen.getByLabelText(/Digest sha256:cccccccccccccccccccccccc/),
).toBeInTheDocument();
// per-field checksum digests are present as the verify affordance
expect(
screen.getByLabelText(/Digest sha256:aaaaaaaaaaaaaaaaaaaaaaaa/),
).toBeInTheDocument();
});
it("moves the pack list focus with j/k through the VirtualizedList spine", async () => {
stubPacksFetch();
const user = userEvent.setup();
renderRoute();
const list = await screen.findByRole("listbox", { name: "Packs" });
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,301 @@
import { useEffect, useMemo, useState } from "react";
import { Eye } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { WorkbenchApiError } from "../../api/client";
import { usePack, usePacks } from "../../api/queries";
import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable";
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 { TreeView } from "../../design/components/TreeView/TreeView";
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 { PackDetail, PackSummary } from "../../types/api";
import { pushRecentItem } from "../commandRegistry";
import { subjectToUrl } from "../evidenceAddress";
import { useEvidenceSubject } from "../evidenceContext";
const PACK_TABS: readonly Tab[] = [
{ id: "manifest", label: "Manifest" },
{ id: "checksums", label: "Checksums" },
{ id: "raw", label: "Raw" },
];
const SOURCE_LABEL: Record<string, string> = {
language_pack: "Language pack",
runtime_pack: "Runtime pack",
};
function sourceLabel(source: string): string {
return SOURCE_LABEL[source] ?? source;
}
function errorMessage(error: unknown) {
return error instanceof WorkbenchApiError ? error.message : "Packs request failed.";
}
function digestPayload(value: string | null | undefined): string | null {
if (!value) return null;
return value.replace(/^sha256:/, "");
}
function Pill({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex h-6 items-center rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-inset)] px-2 text-xs text-[var(--color-text-secondary)]">
{children}
</span>
);
}
function PackRow({
pack,
selected,
focused,
onSelect,
}: {
pack: PackSummary;
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 truncate text-sm text-[var(--color-text-primary)]">
{pack.pack_id}
</span>
<span className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<span>{sourceLabel(pack.source)}</span>
{pack.version ? (
<>
<span aria-hidden className="text-[var(--color-text-muted)]">·</span>
<span className="font-mono">{pack.version}</span>
</>
) : null}
{pack.language ? <span className="text-[var(--color-text-muted)]">{pack.language}</span> : null}
{pack.modality ? <span className="text-[var(--color-text-muted)]">{pack.modality}</span> : null}
</span>
</span>
<span className="justify-self-end">
{pack.determinism_class ? <Pill>{pack.determinism_class}</Pill> : null}
</span>
</div>
);
}
function ChecksumsTab({ detail }: { detail: PackDetail }) {
const manifestDigest = digestPayload(detail.manifest_digest);
const declared = digestPayload(detail.checksum);
const fieldRows = Object.entries(detail.checksums).sort(([a], [b]) => a.localeCompare(b));
return (
<div className="grid gap-4">
<MetadataTable
rows={[
{
key: "manifest_digest",
value: manifestDigest ? (
<DigestBadge digest={manifestDigest} truncate={16} />
) : (
"not recorded"
),
},
{
key: "checksum",
value: declared ? <DigestBadge digest={declared} truncate={16} /> : "not declared",
},
]}
/>
<section>
<h3 className="m-0 mb-2 text-xs font-semibold text-[var(--color-text-secondary)]">
Manifest-declared checksums
</h3>
{fieldRows.length === 0 ? (
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
This manifest declares no per-field checksums.
</p>
) : (
<MetadataTable
rows={fieldRows.map(([field, value]) => ({
key: field,
value: <DigestBadge digest={digestPayload(value) ?? value} truncate={16} />,
}))}
/>
)}
</section>
</div>
);
}
function RawTab({ detail }: { detail: PackDetail }) {
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 pack 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 PackDetailPanel({ detail }: { detail: PackDetail }) {
const [activeTab, setActiveTab] = useState("manifest");
return (
<Panel
title={detail.pack_id}
toolbar={<Pill>{sourceLabel(detail.source)}</Pill>}
>
<TabBar tabs={PACK_TABS} activeTab={activeTab} onTabChange={setActiveTab}>
{activeTab === "manifest" ? (
<TreeView data={detail.manifest} ariaLabel={`${detail.pack_id} manifest`} />
) : null}
{activeTab === "checksums" ? <ChecksumsTab detail={detail} /> : null}
{activeTab === "raw" ? <RawTab detail={detail} /> : null}
</TabBar>
</Panel>
);
}
export function PacksRoute() {
const { packId } = useParams();
const selectedPackId = packId && packId.length > 0 ? packId : null;
const navigate = useNavigate();
const { setSubject } = useEvidenceSubject();
const [search, setSearch] = useState("");
const packsQuery = usePacks();
const packQuery = usePack(selectedPackId);
const packs = packsQuery.data ?? [];
const filteredPacks = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return packs;
return packs.filter(
(pack) =>
pack.pack_id.toLowerCase().includes(q) ||
sourceLabel(pack.source).toLowerCase().includes(q) ||
(pack.language ?? "").toLowerCase().includes(q),
);
}, [search, packs]);
useEffect(() => {
if (selectedPackId === null) return;
setSubject({ kind: "pack", packId: selectedPackId, data: packQuery.data });
}, [selectedPackId, setSubject, packQuery.data]);
function selectPack(pack: PackSummary) {
const subject = { kind: "pack" as const, packId: pack.pack_id };
const path = subjectToUrl(subject);
navigate(path, { replace: true });
pushRecentItem({ label: pack.pack_id, path });
}
if (packsQuery.isLoading) {
return <LoadingState label="Loading packs..." />;
}
if (packsQuery.isError) {
return (
<ErrorState
whatFailed={errorMessage(packsQuery.error)}
mutationStatus="No packs mutation occurred."
reproducer="curl /packs"
retrySafety="Retry: safe"
/>
);
}
if (packs.length === 0) {
return (
<EmptyState
statement="No packs discovered."
nextAction={{ kind: "cli", command: "core pack validate <path>" }}
/>
);
}
return (
<div className="h-full min-h-0">
<SplitPane direction="horizontal" id="packs" defaultSplit={38} minSize={320}>
<Panel title="Packs">
<div className="grid min-h-0 gap-3">
<SearchInput
placeholder="Filter by pack, source, or language"
value={search}
onChange={setSearch}
/>
{filteredPacks.length === 0 ? (
<EmptyState
statement="No packs match this filter."
nextAction={{ kind: "cli", command: "core pack validate <path>" }}
/>
) : (
<VirtualizedList
ariaLabel="Packs"
estimateSize={76}
getKey={(pack) => pack.pack_id}
height="calc(100vh - 14rem)"
initialRect={{ width: 480, height: 560 }}
items={filteredPacks}
onActivate={(pack) => selectPack(pack)}
renderItem={(pack, _index, focused) => (
<PackRow
pack={pack}
selected={pack.pack_id === selectedPackId}
focused={focused}
onSelect={() => selectPack(pack)}
/>
)}
/>
)}
</div>
</Panel>
<section className="h-full min-h-0 overflow-y-auto pl-3">
{selectedPackId === null ? (
<EmptyState
statement="Select a pack to inspect its manifest, checksums, and provenance."
nextAction={{ kind: "cli", command: "core pack validate <path>" }}
/>
) : packQuery.isLoading ? (
<LoadingState label="Loading pack detail..." />
) : packQuery.isError ? (
<ErrorState
whatFailed={errorMessage(packQuery.error)}
mutationStatus="No packs mutation occurred."
reproducer={`curl /packs/${selectedPackId}`}
retrySafety="Retry: safe"
/>
) : packQuery.data ? (
<PackDetailPanel detail={packQuery.data} />
) : null}
</section>
</SplitPane>
</div>
);
}

View file

@ -13,6 +13,7 @@ import { AuditRoute } from "./audit/AuditRoute";
import { EvalsRoute } from "./evals/EvalsRoute";
import { ReplayRoute } from "./replay/ReplayRoute";
import { RunsRoute } from "./runs/RunsRoute";
import { PacksRoute } from "./packs/PacksRoute";
/**
* ADR-0162 §6 route conformance executable, not aspirational.
@ -161,6 +162,15 @@ const MOUNT_ROUTES: MountRouteSpec[] = [
emptyStatement: "No artifacts available.",
emptyCommand: "core eval cognition",
},
{
name: "Packs",
element: <PacksRoute />,
path: "/packs/:packId?",
initialEntry: "/packs",
loadingLabel: "Loading packs...",
emptyStatement: "No packs discovered.",
emptyCommand: "core pack validate <path>",
},
];
describe.each(MOUNT_ROUTES)("route conformance: $name", (spec) => {

View file

@ -0,0 +1,82 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { TreeView } from "./TreeView";
const manifest = {
zeta: "last-alpha",
alpha: { nested_b: 2, nested_a: 1 },
items: ["x", "y"],
};
describe("TreeView", () => {
it("renders top-level keys in deterministic sorted order, collapsed", () => {
render(<TreeView data={manifest} ariaLabel="Manifest" />);
const items = screen.getAllByRole("treeitem");
// alpha, items, zeta — sorted ascending, children hidden until expanded
expect(items.map((el) => within(el).getAllByText(/.+/)[0].textContent)).toEqual([
"alpha",
"items",
"zeta",
]);
expect(screen.queryByText("nested_a")).not.toBeInTheDocument();
});
it("expands a branch with ArrowRight and reveals children sorted", async () => {
const user = userEvent.setup();
render(<TreeView data={manifest} ariaLabel="Manifest" />);
const tree = screen.getByRole("tree", { name: "Manifest" });
tree.focus();
// focus starts on the first node (alpha); expand it
await user.keyboard("{ArrowRight}");
const visible = screen.getAllByRole("treeitem").map((el) => el.getAttribute("aria-level"));
// alpha(1) now shows nested_a(2), nested_b(2) before items(1)
expect(screen.getByText("nested_a")).toBeInTheDocument();
expect(screen.getByText("nested_b")).toBeInTheDocument();
const labels = screen.getAllByRole("treeitem").map((el) => within(el).getAllByText(/.+/)[0].textContent);
expect(labels.slice(0, 3)).toEqual(["alpha", "nested_a", "nested_b"]);
expect(visible).toContain("2");
});
it("collapses an expanded branch with ArrowLeft", async () => {
const user = userEvent.setup();
render(<TreeView data={manifest} ariaLabel="Manifest" />);
const tree = screen.getByRole("tree", { name: "Manifest" });
tree.focus();
await user.keyboard("{ArrowRight}");
expect(screen.getByText("nested_a")).toBeInTheDocument();
await user.keyboard("{ArrowLeft}");
expect(screen.queryByText("nested_a")).not.toBeInTheDocument();
});
it("moves focus down with ArrowDown / j and marks the focused node selected", async () => {
const user = userEvent.setup();
render(<TreeView data={manifest} ariaLabel="Manifest" />);
const tree = screen.getByRole("tree", { name: "Manifest" });
tree.focus();
const items = () => screen.getAllByRole("treeitem");
expect(items()[0]).toHaveAttribute("aria-selected", "true");
await user.keyboard("j");
expect(items()[1]).toHaveAttribute("aria-selected", "true");
expect(items()[0]).toHaveAttribute("aria-selected", "false");
});
it("toggles a branch on click", async () => {
const user = userEvent.setup();
render(<TreeView data={manifest} ariaLabel="Manifest" />);
await user.click(screen.getByText("alpha"));
expect(screen.getByText("nested_a")).toBeInTheDocument();
await user.click(screen.getByText("alpha"));
expect(screen.queryByText("nested_a")).not.toBeInTheDocument();
});
it("renders an honest empty state for an empty manifest", () => {
render(<TreeView data={{}} ariaLabel="Manifest" />);
expect(screen.getByText("Empty manifest.")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,196 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { ChevronRight } from "lucide-react";
import { useRegisterShortcuts, type ShortcutEntry } from "../../../app/shortcutRegistry";
const TREE_NAV_SHORTCUTS: readonly ShortcutEntry[] = [
{ id: "tree-nav-move", keys: "↑ / ↓", action: "Move through tree", order: 42 },
{ id: "tree-nav-expand", keys: "← / →", action: "Collapse / expand node", order: 43 },
];
// Registered only while the tree holds focus, so KeyboardHelp never
// advertises tree controls when no tree is interactable.
function TreeNavShortcuts() {
useRegisterShortcuts(TREE_NAV_SHORTCUTS);
return null;
}
type Json = unknown;
interface FlatNode {
id: string;
depth: number;
label: string;
value: Json;
expandable: boolean;
expanded: boolean;
}
function isContainer(value: Json): value is Record<string, Json> | Json[] {
return value !== null && typeof value === "object";
}
// Deterministic child order: object keys sorted ascending, array indices in
// order. No layout depends on insertion order or animation.
function childEntries(value: Record<string, Json> | Json[]): [string, Json][] {
if (Array.isArray(value)) return value.map((item, i) => [String(i), item]);
return Object.keys(value)
.sort()
.map((key) => [key, (value as Record<string, Json>)[key]]);
}
function leafPreview(value: Json): string {
if (typeof value === "string") return value;
return JSON.stringify(value);
}
function containerPreview(value: Record<string, Json> | Json[]): string {
return Array.isArray(value) ? `[${value.length}]` : `{${Object.keys(value).length}}`;
}
function flatten(root: Json, expanded: ReadonlySet<string>): FlatNode[] {
const out: FlatNode[] = [];
const walk = (label: string, value: Json, id: string, depth: number) => {
const expandable = isContainer(value) && childEntries(value).length > 0;
const isOpen = expandable && expanded.has(id);
out.push({ id, depth, label, value, expandable, expanded: isOpen });
if (isOpen && isContainer(value)) {
for (const [childKey, childValue] of childEntries(value)) {
walk(childKey, childValue, `${id}.${childKey}`, depth + 1);
}
}
};
if (isContainer(root)) {
for (const [key, value] of childEntries(root)) walk(key, value, key, 0);
}
return out;
}
export function TreeView({ data, ariaLabel }: { data: Json; ariaLabel: string }) {
const [expanded, setExpanded] = useState<ReadonlySet<string>>(() => new Set());
const [focusIndex, setFocusIndex] = useState(0);
const [hasFocus, setHasFocus] = useState(false);
const ref = useRef<HTMLUListElement>(null);
const nodes = useMemo(() => flatten(data, expanded), [data, expanded]);
const focused = nodes[Math.min(focusIndex, nodes.length - 1)];
const toggle = useCallback((id: string, open: boolean) => {
setExpanded((prev) => {
const next = new Set(prev);
if (open) next.add(id);
else next.delete(id);
return next;
});
}, []);
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (nodes.length === 0) return;
const node = nodes[Math.min(focusIndex, nodes.length - 1)];
switch (e.key) {
case "ArrowDown":
case "j":
e.preventDefault();
setFocusIndex((i) => Math.min(i + 1, nodes.length - 1));
break;
case "ArrowUp":
case "k":
e.preventDefault();
setFocusIndex((i) => Math.max(i - 1, 0));
break;
case "ArrowRight":
e.preventDefault();
if (node.expandable && !node.expanded) toggle(node.id, true);
else if (node.expandable) setFocusIndex((i) => Math.min(i + 1, nodes.length - 1));
break;
case "ArrowLeft":
e.preventDefault();
if (node.expandable && node.expanded) toggle(node.id, false);
else {
const parentDepth = node.depth - 1;
for (let i = focusIndex - 1; i >= 0; i--) {
if (nodes[i].depth === parentDepth) {
setFocusIndex(i);
break;
}
}
}
break;
case "Home":
e.preventDefault();
setFocusIndex(0);
break;
case "End":
e.preventDefault();
setFocusIndex(nodes.length - 1);
break;
case "Enter":
e.preventDefault();
if (node.expandable) toggle(node.id, !node.expanded);
break;
default:
break;
}
},
[nodes, focusIndex, toggle],
);
if (nodes.length === 0) {
return (
<p className="m-0 text-sm text-[var(--color-text-secondary)]">Empty manifest.</p>
);
}
return (
<>
{hasFocus ? <TreeNavShortcuts /> : null}
<ul
ref={ref}
role="tree"
aria-label={ariaLabel}
tabIndex={0}
onFocus={() => setHasFocus(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) setHasFocus(false);
}}
onKeyDown={onKeyDown}
className="m-0 grid list-none gap-0.5 p-0 font-mono text-xs focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
>
{nodes.map((node, index) => (
<li
key={node.id}
role="treeitem"
aria-level={node.depth + 1}
aria-expanded={node.expandable ? node.expanded : undefined}
aria-selected={node === focused}
onClick={() => {
setFocusIndex(index);
if (node.expandable) toggle(node.id, !node.expanded);
}}
style={{ paddingLeft: `${node.depth * 16}px` }}
className={`flex cursor-default items-start gap-1 rounded px-1 py-0.5 ${
hasFocus && node === focused ? "bg-[var(--color-selected-bg)]" : ""
}`}
>
<span className="mt-0.5 w-3 shrink-0 text-[var(--color-text-muted)]">
{node.expandable ? (
<ChevronRight
size={11}
aria-hidden
className={node.expanded ? "rotate-90" : ""}
/>
) : null}
</span>
<span className="text-[var(--color-text-secondary)]">{node.label}</span>
<span className="text-[var(--color-text-muted)]">:</span>
<span className="min-w-0 break-all text-[var(--color-text-primary)]">
{node.expandable
? containerPreview(node.value as Record<string, Json> | Json[])
: leafPreview(node.value)}
</span>
</li>
))}
</ul>
</>
);
}

View file

@ -21,8 +21,6 @@ import { describe, expect, it } from "vitest";
const NOT_YET_MIRRORED = new Set([
// R2-B backend read substrate (#712) — TS mirrors land with each R2 route:
"PackSummary",
"PackDetail",
"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 PacksRoutePlaceholder() {
return (
<EmptyState
statement="Packs — no data loaded yet."
nextAction={{ kind: "cli", command: "core pack list" }}
/>
);
}

View file

@ -153,6 +153,25 @@ export interface AuditEvent {
payload: unknown;
}
export type PackSource = "language_pack" | "runtime_pack";
export interface PackSummary {
pack_id: string;
source: PackSource;
manifest_path: string;
version: string | null;
language: string | null;
modality: string | null;
determinism_class: string | null;
checksum: string | null;
checksums: Record<string, string>;
}
export interface PackDetail extends PackSummary {
manifest_digest: string;
manifest: Record<string, unknown>;
}
export type ArtifactKind =
| "trace"
| "eval_result"