diff --git a/workbench-ui/src/app/App.tsx b/workbench-ui/src/app/App.tsx
index 77ef23cf..fc055acc 100644
--- a/workbench-ui/src/app/App.tsx
+++ b/workbench-ui/src/app/App.tsx
@@ -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: ,
+ trace: ,
+ replay: ,
+ demos: ,
+ proposals: ,
+ runs: ,
+ vault: ,
+ audit: ,
+ evals: ,
+ calibration: ,
+ packs: ,
+ settings: ,
+};
export function App() {
return (
@@ -24,18 +44,13 @@ export function App() {
}>
} />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+ {WORKBENCH_ROUTES.map((route) => (
+
+ ))}
} />
diff --git a/workbench-ui/src/app/LeftNav.tsx b/workbench-ui/src/app/LeftNav.tsx
index 3baa8c94..c27ce656 100644
--- a/workbench-ui/src/app/LeftNav.tsx
+++ b/workbench-ui/src/app/LeftNav.tsx
@@ -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) => (
-
- [
- "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}
-
+ {NAV_SECTIONS.map(({ section, routes }) => (
+
+
+ {section}
+
+ {routes.map((route) => (
+
+ [
+ "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}
+
+ ))}
+
))}
);
diff --git a/workbench-ui/src/app/Shell.test.tsx b/workbench-ui/src/app/Shell.test.tsx
index 8b29718e..4b14a7a5 100644
--- a/workbench-ui/src/app/Shell.test.tsx
+++ b/workbench-ui/src/app/Shell.test.tsx
@@ -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",
]);
});
diff --git a/workbench-ui/src/app/routes.test.tsx b/workbench-ui/src/app/routes.test.tsx
new file mode 100644
index 00000000..2a282bc5
--- /dev/null
+++ b/workbench-ui/src/app/routes.test.tsx
@@ -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(
+
+ {}} />
+ ,
+ );
+ 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(
+
+ {}} />
+ ,
+ );
+ expect(screen.getByRole("button", { name: "Open Demos" })).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Open Calibration" }),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/workbench-ui/src/app/routes.ts b/workbench-ui/src/app/routes.ts
new file mode 100644
index 00000000..1213e14d
--- /dev/null
+++ b/workbench-ui/src/app/routes.ts
@@ -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 `` 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 `` 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 = 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(
+ 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;
diff --git a/workbench-ui/src/app/useGlobalKeyboard.ts b/workbench-ui/src/app/useGlobalKeyboard.ts
index a2ee455b..f4824eed 100644
--- a/workbench-ui/src/app/useGlobalKeyboard.ts
+++ b/workbench-ui/src/app/useGlobalKeyboard.ts
@@ -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 = {
- "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 = ROUTE_DIGIT_MAP;
interface GlobalKeyboardOptions {
onTogglePalette: () => void;
diff --git a/workbench-ui/src/app/workbenchPrefs.ts b/workbench-ui/src/app/workbenchPrefs.ts
index 62f3ce8a..6b28d728 100644
--- a/workbench-ui/src/app/workbenchPrefs.ts
+++ b/workbench-ui/src/app/workbenchPrefs.ts
@@ -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 {
diff --git a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx
index 21a26601..85091120 100644
--- a/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx
+++ b/workbench-ui/src/design/components/primitives/CommandPalette.keyboard.test.tsx
@@ -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
diff --git a/workbench-ui/src/design/components/primitives/CommandPalette.tsx b/workbench-ui/src/design/components/primitives/CommandPalette.tsx
index 459190f3..488f17d3 100644
--- a/workbench-ui/src/design/components/primitives/CommandPalette.tsx
+++ b/workbench-ui/src/design/components/primitives/CommandPalette.tsx
@@ -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 = {
- "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 = Object.fromEntries(
+ PALETTE_ROUTES.map((route) => [`nav-${route.id}`, route.path]),
+);
interface DisplayItem {
id: string;