Command Palette

An app-wide Ctrl/Cmd-Shift-Space overlay that jumps straight to any tool, workspace, or settings page — the same shortcut the Reason Editor's own Search Everything palette already used, now working everywhere else too.

  • Route: none of its own — an overlay, opened from any page.
  • Nav: the shortcut itself, or the dock's gear-icon Settings menu → Command Palette.
  • Package: apps/debate-ai.com only (components/layout/GlobalCommandPalette.tsx, lib/ui/primitives/command.tsx).

Why this exists

Every feature doc in this repo has carried a "Nav: … in Ctrl/Cmd-Shift-Space's command palette" line for a while, but that was only ever true inside the Reason Editor: the shortcut is implemented entirely inside the CardMirror editor engine (packages/debate-editor/src/editor/quick-card-search-ui.ts), which only mounts on /reason-editor. Everywhere else in the app — /tools, /settings, /judges, both workspace hubs — the same keys did nothing, even though the app's ~50 surfaces and the editor's own Workspace menu made it look like a universal way to get around.

This closes that gap with a second, lighter palette that runs app-wide. It deliberately does not try to reproduce the editor engine's full palette (quick cards, ribbon commands, file search, settings deep-links) — those stay specific to editing a document. It only does the one thing that generalizes to the whole app: jump to a tool.

What it shows

Built from app/tools/tool-groups.ts's ALL_TOOLS/TOOL_GROUPS — the same catalog /tools renders and useFavoriteTools already keys its favorites list against — rather than the separate feature-catalog.ts copy /features uses (see features-page.md's "Three copies, hand-synced"; this is, pragmatically, a fourth). Picking ALL_TOOLS over that copy was so a palette result's star status, icon, and description always agree with what /tools and Settings' "Favorite tools" section already show for the same tool, with no new catalog to keep in sync.

  • Favorites — the signed-in-or-local useFavoriteTools list, first, when it isn't empty.
  • Recent — the last few tools actually opened, from the palette or from /tools itself (useRecentTools, most-recent-first), when it isn't empty. Local-first, best-effort account-synced via a recordRecentTool op on the same /api/settings row useFavoriteTools uses — see lib/recentTools.ts's header for why that field's validation/serialization stays in this app rather than moving into debate-round, unlike favoriteTools.
  • Go to — /tools, /features, /news, /settings: destinations that aren't themselves a cataloged tool.
  • One group per /tools heading (Workspaces, Community & Progress, Prep & Practice, Coaching & Analytics), in that order.

Typing filters on each tool's label, description, route, and highlights (cmdk's own fuzzy match against a built search string — see toolHaystack), so a query like "elo" or "paradigm" finds a tool whose visible label doesn't contain the word. Selecting a row navigates there and closes the palette.

Reachability

Mounted once per document, not once per app — AppShell.tsx renders it in both branches (the top-level shell and each framed dock destination), matching ToolRecordSyncProvider's reach, because a framed page (e.g. /videos, inside the dock's iframe) owns its own keyboard focus and a listener in the parent document would never see its keystrokes.

/reason-editor (and /reason-editor/[slug]) is the one place this palette stands down entirely (ownedByEditor): the CardMirror engine's own palette already owns Ctrl/Cmd-Shift-Space there and does strictly more (quick cards, commands, files), so this component neither binds the shortcut nor renders while that route is active, rather than the two racing over one key combo.

Data flow

app/tools/tool-groups.ts        — ALL_TOOLS / TOOL_GROUPS (existing; also
                                    used by /tools)
lib/hooks/useFavoriteTools.ts   — existing account-linked favorites list
lib/recentTools.ts              — pure push/parse/resolve/op helpers for the
                                    "Recent" group and /tools' matching strip,
                                    plus the recordRecentTool op /api/settings
                                    validates against
lib/hooks/useRecentTools.ts     — localStorage-first, cross-instance-synced,
                                    best-effort account-synced recent list
lib/ui/primitives/command.tsx   — cmdk wrapped in the app's Dialog primitive
                                    (the shadcn/ui-standard shape; cmdk had
                                    been a dependency with no caller yet)
  → components/layout/GlobalCommandPalette.tsx
                                 — the keydown listener, the editor stand-down
                                    check, the favorites/quick-actions/
                                    per-heading groups, and openGlobalCommandPalette()
                                    (a window CustomEvent so the dock's
                                    Settings menu can open it without prop
                                    drilling)
  → components/layout/AppShell.tsx        — mounted in both branches
  → components/layout/CategoryDock.tsx    — Settings menu's "Command Palette" row
  → components/tools/RecordVisitLink.tsx  — a next/link that also calls
                                    recordVisit(), used by /tools' grid cards
                                    and favorites chips so opening a tool
                                    from there counts as "Recent" too
  → components/tools/RecentlyOpenedTools.tsx
                                 — /tools' own "Recently opened" strip,
                                    reading the same store

Known gaps

  • Navigation only. The editor's own palette's q/d/c/s/f prefixes (quick cards, dropzone, ribbon commands, settings, files) have no app-wide equivalent, and this doc doesn't claim one — those are meaningfully specific to having a document open.
  • A fourth hand-maintained tool list, alongside feature-catalog.ts's three copies (features-page.md) and debate-editor's WORKSPACE_LINKS. A tool added to /tools and nowhere else now reaches this palette and favorites (both already read tool-groups.ts) but not /features or the Reason Editor's Workspace menu/t prefix, unless it's added there too. Guarded now: apps/debate-ai.com/lib/__tests__/tool-catalog-consistency.test.ts cross-checks tool-groups.ts's ALL_TOOLS against both feature-catalog.ts's APP_FEATURES and debate-editor's WORKSPACE_LINKS (a test-only relative import into debate-editor, which the app already depends on as a package), against an explicit, documented exception list for the few routes that should stay one-sided. A tool added to only one of the three now fails bun run test naming exactly which list is missing it, instead of silently under-reaching. The lists themselves still have to be edited by hand in three places — this catches the omission, it doesn't remove the edit.
  • The "Recent" group tracks palette selections only — a tool opened via its own /tools card, a direct link, or the editor's Workspace menu doesn't promote it here. Narrowed further: /tools' grid cards and favorites chips go through RecordVisitLink, so opening a tool from there promotes it too, and /tools itself shows a matching "Recently opened" strip (RecentlyOpenedTools.tsx) above the tool groups. The editor's own Workspace menu (MenuBar.tsx's Workspace category) and its palette's own t-prefix results (quick-card-search-ui.ts) now record a visit too, via packages/debate-editor/src/editor/recent-tools.ts's recordWorkspaceVisit — a package-local mirror of lib/recentTools.ts's localStorage shape and saveRecentToolOp's PUT /api/settings op, since debate-editor can't depend on either directly. Only a direct link (typed URL, bookmark, another page's own link to a tool) still doesn't record a visit — that would need a global click listener or per-link instrumentation this doc doesn't own. It's also still capped at 5. Per-browser (localStorage, no account sync), so it resets on a new device. Closed: a recordRecentTool op best-effort syncs each visit to the same user_settings row favoriteTools uses (apps/debate-ai.com/app/api/settings/route.ts, recent_tools D1 column), read back on mount the same way useFavoriteTools hydrates — so recents now follow a signed-in user across devices, localStorage staying the signed-out/first-paint source of truth.