Merge pull request #728 from AssetOverflow/feat/wb-route-registry
feat(workbench): unify route truth into one registry (Wave M B3.5-a)
This commit is contained in:
commit
658e677e26
9 changed files with 480 additions and 96 deletions
|
|
@ -16,6 +16,26 @@ import { VaultRoute } from "./vault/VaultRoute";
|
|||
import { CalibrationRoute } from "./calibration/CalibrationRoute";
|
||||
import { SettingsRoute } from "./settings/SettingsRoute";
|
||||
import { getWorkbenchPrefs } from "./workbenchPrefs";
|
||||
import { WORKBENCH_ROUTES, type RouteElementMap } from "./routes";
|
||||
|
||||
// The one place route id → element is bound (App owns the route-component
|
||||
// imports). Every WORKBENCH_ROUTES id must have an entry here; routes.test
|
||||
// asserts it, so a registry route without an element fails the suite instead
|
||||
// of rendering `undefined`.
|
||||
export const ROUTE_ELEMENTS: RouteElementMap = {
|
||||
chat: <ChatRoute />,
|
||||
trace: <TraceRoute />,
|
||||
replay: <ReplayRoute />,
|
||||
demos: <DemoTheaterRoute />,
|
||||
proposals: <ProposalsRoute />,
|
||||
runs: <RunsRoute />,
|
||||
vault: <VaultRoute />,
|
||||
audit: <AuditRoute />,
|
||||
evals: <EvalsRoute />,
|
||||
calibration: <CalibrationRoute />,
|
||||
packs: <PacksRoute />,
|
||||
settings: <SettingsRoute />,
|
||||
};
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
|
|
@ -24,18 +44,13 @@ export function App() {
|
|||
<Routes>
|
||||
<Route path="/" element={<Shell />}>
|
||||
<Route index element={<Navigate to={`/${getWorkbenchPrefs().landingRoute}`} replace />} />
|
||||
<Route path="chat" element={<ChatRoute />} />
|
||||
<Route path="trace/:turnId?" element={<TraceRoute />} />
|
||||
<Route path="replay/:turnId?" element={<ReplayRoute />} />
|
||||
<Route path="demos/:demoId?" element={<DemoTheaterRoute />} />
|
||||
<Route path="proposals/:proposalId?" element={<ProposalsRoute />} />
|
||||
<Route path="evals/:laneId?" element={<EvalsRoute />} />
|
||||
<Route path="runs/:sessionId?" element={<RunsRoute />} />
|
||||
<Route path="packs/:packId?" element={<PacksRoute />} />
|
||||
<Route path="vault" element={<VaultRoute />} />
|
||||
<Route path="calibration" element={<CalibrationRoute />} />
|
||||
<Route path="audit" element={<AuditRoute />} />
|
||||
<Route path="settings" element={<SettingsRoute />} />
|
||||
{WORKBENCH_ROUTES.map((route) => (
|
||||
<Route
|
||||
key={route.id}
|
||||
path={route.routePattern}
|
||||
element={ROUTE_ELEMENTS[route.id]}
|
||||
/>
|
||||
))}
|
||||
</Route>
|
||||
<Route path="/preview" element={<PreviewPage />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
import { NavLink } from "react-router-dom";
|
||||
import { leftNavSections } from "./routes";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: "Chat", to: "/chat" },
|
||||
{ label: "Trace", to: "/trace" },
|
||||
{ label: "Replay", to: "/replay" },
|
||||
{ label: "Demos", to: "/demos" },
|
||||
{ label: "Proposals", to: "/proposals" },
|
||||
{ label: "Evals", to: "/evals" },
|
||||
{ label: "Calibration", to: "/calibration" },
|
||||
{ label: "Runs", to: "/runs" },
|
||||
{ label: "Packs", to: "/packs" },
|
||||
{ label: "Vault", to: "/vault" },
|
||||
{ label: "Audit", to: "/audit" },
|
||||
{ label: "Settings", to: "/settings" },
|
||||
] as const;
|
||||
// Routes derive from the single registry (routes.ts), grouped by wayfinding
|
||||
// section. Adding a route in one place only is no longer possible — LeftNav,
|
||||
// the command palette, ⌘-digits, and the landing dropdown all read the same
|
||||
// list.
|
||||
const NAV_SECTIONS = leftNavSections();
|
||||
|
||||
export function LeftNav() {
|
||||
return (
|
||||
|
|
@ -22,21 +14,29 @@ export function LeftNav() {
|
|||
className="flex h-full flex-col gap-1 overflow-y-auto border-r border-[var(--color-border-subtle)] bg-[var(--color-surface-base)] p-2"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
"block rounded px-3 py-2 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]",
|
||||
isActive
|
||||
? "border-l-2 border-[var(--color-focus-ring)] pl-[10px] text-[var(--color-text-primary)] bg-[var(--color-surface-raised)]"
|
||||
: "border-l-2 border-transparent pl-[10px] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-raised)]",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
{NAV_SECTIONS.map(({ section, routes }) => (
|
||||
<div key={section} role="group" aria-label={section} className="flex flex-col gap-1">
|
||||
<div className="px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||
{section}
|
||||
</div>
|
||||
{routes.map((route) => (
|
||||
<NavLink
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
title={route.description}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
"block rounded px-3 py-2 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]",
|
||||
isActive
|
||||
? "border-l-2 border-[var(--color-focus-ring)] pl-[10px] text-[var(--color-text-primary)] bg-[var(--color-surface-raised)]"
|
||||
: "border-l-2 border-transparent pl-[10px] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-raised)]",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
{route.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -76,24 +76,26 @@ describe("Shell", () => {
|
|||
expect(document.querySelector('[data-region="statusfooter"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("LeftNav has exactly 12 items in order", () => {
|
||||
it("LeftNav has exactly 12 items in section-grouped order", () => {
|
||||
renderShell();
|
||||
const nav = document.querySelector('[data-region="leftnav"]')!;
|
||||
const links = nav.querySelectorAll("a");
|
||||
expect(links).toHaveLength(12);
|
||||
const labels = Array.from(links).map((l) => l.textContent);
|
||||
// Grouped by section (Converse → Cognition → Determinism → Evidence →
|
||||
// Discipline → Substrate → Settings), derived from the route registry.
|
||||
expect(labels).toEqual([
|
||||
"Chat",
|
||||
"Trace",
|
||||
"Replay",
|
||||
"Demos",
|
||||
"Proposals",
|
||||
"Evals",
|
||||
"Calibration",
|
||||
"Runs",
|
||||
"Packs",
|
||||
"Vault",
|
||||
"Audit",
|
||||
"Evals",
|
||||
"Calibration",
|
||||
"Packs",
|
||||
"Settings",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
113
workbench-ui/src/app/routes.test.tsx
Normal file
113
workbench-ui/src/app/routes.test.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { CommandPalette } from "../design/components/primitives/CommandPalette";
|
||||
import { ROUTE_ELEMENTS } from "./App";
|
||||
import {
|
||||
WORKBENCH_ROUTES,
|
||||
PALETTE_ROUTES,
|
||||
LANDING_ROUTE_IDS,
|
||||
ROUTE_DIGIT_MAP,
|
||||
ROUTE_SECTIONS,
|
||||
leftNavSections,
|
||||
} from "./routes";
|
||||
|
||||
/**
|
||||
* B3.5-a — the route registry is the single source of truth. These guards
|
||||
* fail loudly if a route is added to App without the registry, if a registry
|
||||
* route loses an element, or if a navigable route falls out of the command
|
||||
* palette (the prior Demos/Calibration drift).
|
||||
*/
|
||||
|
||||
describe("route registry ↔ App element map", () => {
|
||||
it("every route has exactly one element, and no element is orphaned", () => {
|
||||
const routeIds = WORKBENCH_ROUTES.map((r) => r.id).sort();
|
||||
const elementIds = Object.keys(ROUTE_ELEMENTS).sort();
|
||||
expect(elementIds).toEqual(routeIds);
|
||||
});
|
||||
|
||||
it("no route element is undefined (a registry route without an element)", () => {
|
||||
for (const route of WORKBENCH_ROUTES) {
|
||||
expect(ROUTE_ELEMENTS[route.id]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("route ids and paths are unique", () => {
|
||||
const ids = WORKBENCH_ROUTES.map((r) => r.id);
|
||||
const paths = WORKBENCH_ROUTES.map((r) => r.path);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(new Set(paths).size).toBe(paths.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyboard-digit assignment is honest", () => {
|
||||
it("digits are unique and within 0–9", () => {
|
||||
const digits = WORKBENCH_ROUTES.map((r) => r.keyboardDigit).filter(
|
||||
(d): d is string => d !== null,
|
||||
);
|
||||
expect(new Set(digits).size).toBe(digits.length);
|
||||
for (const d of digits) expect(d).toMatch(/^[0-9]$/);
|
||||
});
|
||||
|
||||
it("ROUTE_DIGIT_MAP maps each pinned digit to its route path", () => {
|
||||
for (const route of WORKBENCH_ROUTES) {
|
||||
if (route.keyboardDigit === null) continue;
|
||||
expect(ROUTE_DIGIT_MAP[route.keyboardDigit]).toBe(route.path);
|
||||
}
|
||||
const pinned = WORKBENCH_ROUTES.filter((r) => r.keyboardDigit !== null);
|
||||
expect(Object.keys(ROUTE_DIGIT_MAP)).toHaveLength(pinned.length);
|
||||
});
|
||||
|
||||
it("more routes than digits — at least one route is palette-only (honest model)", () => {
|
||||
const paletteOnly = WORKBENCH_ROUTES.filter((r) => r.keyboardDigit === null);
|
||||
expect(paletteOnly.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("landing routes derive from the registry", () => {
|
||||
it("includes Replay and Calibration (the prior drift)", () => {
|
||||
expect(LANDING_ROUTE_IDS).toContain("replay");
|
||||
expect(LANDING_ROUTE_IDS).toContain("calibration");
|
||||
});
|
||||
});
|
||||
|
||||
describe("leftNavSections", () => {
|
||||
it("covers every LeftNav route exactly once, in section order", () => {
|
||||
const flattened = leftNavSections().flatMap((g) => g.routes.map((r) => r.id));
|
||||
const expected = WORKBENCH_ROUTES.filter((r) => r.leftNavVisible).map((r) => r.id);
|
||||
expect(flattened).toEqual(expected);
|
||||
});
|
||||
|
||||
it("only emits known sections", () => {
|
||||
for (const group of leftNavSections()) {
|
||||
expect(ROUTE_SECTIONS).toContain(group.section);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("command palette reachability", () => {
|
||||
it("every palette-visible route is reachable as a Navigate command", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<CommandPalette open onOpenChange={() => {}} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
for (const route of PALETTE_ROUTES) {
|
||||
expect(
|
||||
screen.getByRole("button", { name: `Open ${route.label}` }),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it("Demos and Calibration are present (the regression they fell out of)", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<CommandPalette open onOpenChange={() => {}} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Open Demos" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Open Calibration" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
272
workbench-ui/src/app/routes.ts
Normal file
272
workbench-ui/src/app/routes.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import type { ReactElement } from "react";
|
||||
|
||||
/**
|
||||
* Single source of truth for workbench navigation routes (Wave M B3.5-a).
|
||||
*
|
||||
* Every route list in the app derives from {@link WORKBENCH_ROUTES}:
|
||||
* - App `<Route>` declarations (via {@link ROUTE_ELEMENTS})
|
||||
* - LeftNav (grouped by section)
|
||||
* - the command palette Navigate section
|
||||
* - ⌘-digit navigation shortcuts
|
||||
* - the landing-route preference dropdown
|
||||
* - route-conformance fixtures
|
||||
*
|
||||
* Adding a route in one place only was the prior failure mode: Demos and
|
||||
* Calibration shipped to LeftNav but never reached the command palette, and
|
||||
* Replay/Calibration were absent from the landing dropdown. With one list,
|
||||
* that drift is structurally impossible.
|
||||
*
|
||||
* Wayfinding `section`s group the flat route set by the organism's loop
|
||||
* (Converse → Cognition → Determinism → Evidence → Discipline → Substrate →
|
||||
* Settings). This is a display skin only — one workbench, one address space,
|
||||
* one Evidence Chain Rail; never a split into separate apps.
|
||||
*/
|
||||
|
||||
export const ROUTE_SECTIONS = [
|
||||
"Converse",
|
||||
"Cognition",
|
||||
"Determinism",
|
||||
"Evidence",
|
||||
"Discipline",
|
||||
"Substrate",
|
||||
"Settings",
|
||||
] as const;
|
||||
|
||||
export type RouteSection = (typeof ROUTE_SECTIONS)[number];
|
||||
|
||||
export interface WorkbenchRoute {
|
||||
/** Stable id; React key and digit-shortcut anchor. */
|
||||
id: string;
|
||||
/** Navigation target with no params, e.g. "/trace". */
|
||||
path: string;
|
||||
/** App `<Route path>` pattern; may carry optional params, e.g. "trace/:turnId?". */
|
||||
routePattern: string;
|
||||
/** LeftNav / palette label. */
|
||||
label: string;
|
||||
/** One line: what this route is for. */
|
||||
description: string;
|
||||
/** Wayfinding group. */
|
||||
section: RouteSection;
|
||||
/** Rendered in LeftNav. */
|
||||
leftNavVisible: boolean;
|
||||
/** Listed as a Navigate command in the palette. */
|
||||
commandPaletteVisible: boolean;
|
||||
/** Eligible as the landing route. */
|
||||
landingRouteAllowed: boolean;
|
||||
/**
|
||||
* Single-digit ⌘ shortcut, or `null` when the route is palette-only.
|
||||
* There are more routes than digits (1–9, 0) — the honest model pins the
|
||||
* first ten and leaves the rest searchable. KeyboardHelp says so; the
|
||||
* palette shows no chord for palette-only routes.
|
||||
*/
|
||||
keyboardDigit: string | null;
|
||||
/** Must pass ADR-0162 §6 route conformance (loading/error/empty). */
|
||||
routeConformanceRequired: boolean;
|
||||
}
|
||||
|
||||
// Array order is the LeftNav display order (already grouped by section).
|
||||
export const WORKBENCH_ROUTES: readonly WorkbenchRoute[] = [
|
||||
{
|
||||
id: "chat",
|
||||
path: "/chat",
|
||||
routePattern: "chat",
|
||||
label: "Chat",
|
||||
description: "Ask CORE a question and create turn evidence.",
|
||||
section: "Converse",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "1",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "trace",
|
||||
path: "/trace",
|
||||
routePattern: "trace/:turnId?",
|
||||
label: "Trace",
|
||||
description: "Inspect the cognitive turn pipeline for a turn.",
|
||||
section: "Cognition",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "2",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "replay",
|
||||
path: "/replay",
|
||||
routePattern: "replay/:turnId?",
|
||||
label: "Replay",
|
||||
description: "Re-run a turn and compare trace hashes.",
|
||||
section: "Determinism",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "3",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "demos",
|
||||
path: "/demos",
|
||||
routePattern: "demos/:demoId?",
|
||||
label: "Demos",
|
||||
description: "Run a registered determinism demo end to end.",
|
||||
section: "Determinism",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: null,
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "proposals",
|
||||
path: "/proposals",
|
||||
routePattern: "proposals/:proposalId?",
|
||||
label: "Proposals",
|
||||
description: "Review the teaching proposal queue and HITL ratification.",
|
||||
section: "Evidence",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "4",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "runs",
|
||||
path: "/runs",
|
||||
routePattern: "runs/:sessionId?",
|
||||
label: "Runs",
|
||||
description: "Browse recorded session runs.",
|
||||
section: "Evidence",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "6",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "vault",
|
||||
path: "/vault",
|
||||
routePattern: "vault",
|
||||
label: "Vault",
|
||||
description: "Inspect persisted session memory.",
|
||||
section: "Evidence",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "8",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "audit",
|
||||
path: "/audit",
|
||||
routePattern: "audit",
|
||||
label: "Audit",
|
||||
description: "Read the deterministic audit event log.",
|
||||
section: "Evidence",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "9",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "evals",
|
||||
path: "/evals",
|
||||
routePattern: "evals/:laneId?",
|
||||
label: "Evals",
|
||||
description: "Run eval lanes and read the wrong=0 ledger.",
|
||||
section: "Discipline",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "5",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "calibration",
|
||||
path: "/calibration",
|
||||
routePattern: "calibration",
|
||||
label: "Calibration",
|
||||
description: "See the gold-tether arena earn the right to guess.",
|
||||
section: "Discipline",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: null,
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "packs",
|
||||
path: "/packs",
|
||||
routePattern: "packs/:packId?",
|
||||
label: "Packs",
|
||||
description: "Browse language/identity packs (CORE-Logos studio).",
|
||||
section: "Substrate",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "7",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
path: "/settings",
|
||||
routePattern: "settings",
|
||||
label: "Settings",
|
||||
description: "Local workbench preferences (read-only to the engine).",
|
||||
section: "Settings",
|
||||
leftNavVisible: true,
|
||||
commandPaletteVisible: true,
|
||||
landingRouteAllowed: true,
|
||||
keyboardDigit: "0",
|
||||
routeConformanceRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
/** Routes shown in LeftNav, in display order. */
|
||||
export const LEFT_NAV_ROUTES = WORKBENCH_ROUTES.filter((r) => r.leftNavVisible);
|
||||
|
||||
/** Routes listed in the command palette Navigate section, in display order. */
|
||||
export const PALETTE_ROUTES = WORKBENCH_ROUTES.filter(
|
||||
(r) => r.commandPaletteVisible,
|
||||
);
|
||||
|
||||
/** Route ids eligible as the workbench landing route. */
|
||||
export const LANDING_ROUTE_IDS = WORKBENCH_ROUTES.filter(
|
||||
(r) => r.landingRouteAllowed,
|
||||
).map((r) => r.id);
|
||||
|
||||
/** ⌘-digit → navigation path, for the ten pinned routes. */
|
||||
export const ROUTE_DIGIT_MAP: Record<string, string> = Object.fromEntries(
|
||||
WORKBENCH_ROUTES.filter((r) => r.keyboardDigit !== null).map((r) => [
|
||||
r.keyboardDigit as string,
|
||||
r.path,
|
||||
]),
|
||||
);
|
||||
|
||||
/** LeftNav routes grouped by section, in section then route order. */
|
||||
export function leftNavSections(): {
|
||||
section: RouteSection;
|
||||
routes: WorkbenchRoute[];
|
||||
}[] {
|
||||
const groups = new Map<RouteSection, WorkbenchRoute[]>(
|
||||
ROUTE_SECTIONS.map((s) => [s, []]),
|
||||
);
|
||||
for (const route of LEFT_NAV_ROUTES) {
|
||||
groups.get(route.section)!.push(route);
|
||||
}
|
||||
return ROUTE_SECTIONS.map((section) => ({
|
||||
section,
|
||||
routes: groups.get(section)!,
|
||||
})).filter((group) => group.routes.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The element registry consumed by `App`. Keyed by route id so that adding a
|
||||
* route to {@link WORKBENCH_ROUTES} without giving it an element is caught by
|
||||
* `routes.test.tsx` rather than rendering `undefined`. Populated in `App.tsx`
|
||||
* (it owns the route-component imports); declared here so the contract — every
|
||||
* route id has an element — lives next to the route list.
|
||||
*/
|
||||
export type RouteElementMap = Record<string, ReactElement>;
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRegisterShortcuts, type ShortcutEntry } from "./shortcutRegistry";
|
||||
import { ROUTE_DIGIT_MAP } from "./routes";
|
||||
|
||||
const ROUTE_KEYS: Record<string, string> = {
|
||||
"1": "/chat",
|
||||
"2": "/trace",
|
||||
"3": "/replay",
|
||||
"4": "/proposals",
|
||||
"5": "/evals",
|
||||
"6": "/runs",
|
||||
"7": "/packs",
|
||||
"8": "/vault",
|
||||
"9": "/audit",
|
||||
"0": "/settings",
|
||||
};
|
||||
// digit → path for the ten pinned routes; derived from the single route
|
||||
// registry (routes.ts). Routes without a pinned digit (Demos, Calibration)
|
||||
// are reachable via the command palette, not a chord — and KeyboardHelp's
|
||||
// "global-routes" row stays honest about the 1–0 pinned set.
|
||||
const ROUTE_KEYS: Record<string, string> = ROUTE_DIGIT_MAP;
|
||||
|
||||
interface GlobalKeyboardOptions {
|
||||
onTogglePalette: () => void;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { LANDING_ROUTE_IDS } from "./routes";
|
||||
|
||||
// Local, single-operator workbench preferences (ADR-0160: local-only, no
|
||||
// cloud, no accounts). Persisted in localStorage; every pref here is read
|
||||
|
|
@ -7,20 +8,12 @@ import { useCallback, useEffect, useState } from "react";
|
|||
const PREFS_KEY = "core-workbench-prefs";
|
||||
const PREFS_EVENT = "core-workbench-prefs-change";
|
||||
|
||||
export const LANDING_ROUTES = [
|
||||
"chat",
|
||||
"trace",
|
||||
"demos",
|
||||
"proposals",
|
||||
"evals",
|
||||
"runs",
|
||||
"packs",
|
||||
"vault",
|
||||
"audit",
|
||||
"settings",
|
||||
] as const;
|
||||
// Landing-eligible routes derive from the single route registry (routes.ts),
|
||||
// so the Settings dropdown can never drift from the real route set. The prior
|
||||
// hand-maintained tuple was missing Replay and Calibration.
|
||||
export const LANDING_ROUTES: readonly string[] = LANDING_ROUTE_IDS;
|
||||
|
||||
export type LandingRoute = (typeof LANDING_ROUTES)[number];
|
||||
export type LandingRoute = string;
|
||||
|
||||
export interface WorkbenchPrefs {
|
||||
/** Route the workbench opens to (consumed by the App index redirect). */
|
||||
|
|
@ -35,7 +28,7 @@ export const DEFAULT_PREFS: WorkbenchPrefs = {
|
|||
};
|
||||
|
||||
function isLandingRoute(value: unknown): value is LandingRoute {
|
||||
return typeof value === "string" && (LANDING_ROUTES as readonly string[]).includes(value);
|
||||
return typeof value === "string" && LANDING_ROUTES.includes(value);
|
||||
}
|
||||
|
||||
export function getWorkbenchPrefs(): WorkbenchPrefs {
|
||||
|
|
|
|||
|
|
@ -52,8 +52,11 @@ describe("CommandPalette keyboard contract", () => {
|
|||
expect(screen.getByRole("button", { name: "Open Trace" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Open Replay" })).toBeInTheDocument();
|
||||
|
||||
// One Navigate command per palette-visible route (12), derived from the
|
||||
// route registry — Demos and Calibration are now included (the prior
|
||||
// hand-maintained list of 10 dropped them).
|
||||
const items = dialog.querySelectorAll('[role="option"]');
|
||||
expect(items.length).toBe(10);
|
||||
expect(items.length).toBe(12);
|
||||
const lastIndex = items.length - 1;
|
||||
|
||||
// Initially first item (index 0) is focused — check aria-selected
|
||||
|
|
|
|||
|
|
@ -10,32 +10,24 @@ import {
|
|||
type Command,
|
||||
type RecentItem,
|
||||
} from "../../../app/commandRegistry";
|
||||
import { PALETTE_ROUTES } from "../../../app/routes";
|
||||
|
||||
const NAV_COMMANDS: Command[] = [
|
||||
{ id: "nav-chat", label: "Open Chat", section: "Navigate", kind: "navigate", shortcut: "⌘1", action: () => {} },
|
||||
{ id: "nav-trace", label: "Open Trace", section: "Navigate", kind: "navigate", shortcut: "⌘2", action: () => {} },
|
||||
{ id: "nav-replay", label: "Open Replay", section: "Navigate", kind: "navigate", shortcut: "⌘3", action: () => {} },
|
||||
{ id: "nav-proposals", label: "Open Proposals", section: "Navigate", kind: "navigate", shortcut: "⌘4", action: () => {} },
|
||||
{ id: "nav-evals", label: "Open Evals", section: "Navigate", kind: "navigate", shortcut: "⌘5", action: () => {} },
|
||||
{ id: "nav-runs", label: "Open Runs", section: "Navigate", kind: "navigate", shortcut: "⌘6", action: () => {} },
|
||||
{ id: "nav-packs", label: "Open Packs", section: "Navigate", kind: "navigate", shortcut: "⌘7", action: () => {} },
|
||||
{ id: "nav-vault", label: "Open Vault", section: "Navigate", kind: "navigate", shortcut: "⌘8", action: () => {} },
|
||||
{ id: "nav-audit", label: "Open Audit", section: "Navigate", kind: "navigate", shortcut: "⌘9", action: () => {} },
|
||||
{ id: "nav-settings", label: "Open Settings", section: "Navigate", kind: "navigate", shortcut: "⌘0", action: () => {} },
|
||||
];
|
||||
// Navigate commands derive from the single route registry (routes.ts), so
|
||||
// every palette-visible route is searchable here. The prior hand-maintained
|
||||
// list dropped Demos and Calibration; deriving makes that drift impossible.
|
||||
// Palette-only routes (no pinned ⌘-digit) appear with no chord — honest.
|
||||
const NAV_COMMANDS: Command[] = PALETTE_ROUTES.map((route) => ({
|
||||
id: `nav-${route.id}`,
|
||||
label: `Open ${route.label}`,
|
||||
section: "Navigate",
|
||||
kind: "navigate",
|
||||
shortcut: route.keyboardDigit ? `⌘${route.keyboardDigit}` : undefined,
|
||||
action: () => {},
|
||||
}));
|
||||
|
||||
const NAV_PATHS: Record<string, string> = {
|
||||
"nav-chat": "/chat",
|
||||
"nav-trace": "/trace",
|
||||
"nav-replay": "/replay",
|
||||
"nav-proposals": "/proposals",
|
||||
"nav-evals": "/evals",
|
||||
"nav-runs": "/runs",
|
||||
"nav-packs": "/packs",
|
||||
"nav-vault": "/vault",
|
||||
"nav-audit": "/audit",
|
||||
"nav-settings": "/settings",
|
||||
};
|
||||
const NAV_PATHS: Record<string, string> = Object.fromEntries(
|
||||
PALETTE_ROUTES.map((route) => [`nav-${route.id}`, route.path]),
|
||||
);
|
||||
|
||||
interface DisplayItem {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue