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.comonly (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
useFavoriteToolslist, first, when it isn't empty. - Recent — the last few tools actually opened, from the palette or from
/toolsitself (useRecentTools, most-recent-first), when it isn't empty. Local-first, best-effort account-synced via arecordRecentToolop on the same/api/settingsrowuseFavoriteToolsuses — seelib/recentTools.ts's header for why that field's validation/serialization stays in this app rather than moving intodebate-round, unlikefavoriteTools. - Go to —
/tools,/features,/news,/settings: destinations that aren't themselves a cataloged tool. - One group per
/toolsheading (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 storeKnown gaps
- Navigation only. The editor's own palette's
q/d/c/s/fprefixes (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) anddebate-editor'sWORKSPACE_LINKS. A tool added to/toolsand nowhere else now reaches this palette and favorites (both already readtool-groups.ts) but not/featuresor the Reason Editor's Workspace menu/tprefix, unless it's added there too. Guarded now:apps/debate-ai.com/lib/__tests__/tool-catalog-consistency.test.tscross-checkstool-groups.ts'sALL_TOOLSagainst bothfeature-catalog.ts'sAPP_FEATURESanddebate-editor'sWORKSPACE_LINKS(a test-only relative import intodebate-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 failsbun run testnaming 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 ownNarrowed further:/toolscard, a direct link, or the editor's Workspace menu doesn't promote it here./tools' grid cards and favorites chips go throughRecordVisitLink, so opening a tool from there promotes it too, and/toolsitself 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 ownt-prefix results (quick-card-search-ui.ts) now record a visit too, viapackages/debate-editor/src/editor/recent-tools.ts'srecordWorkspaceVisit— a package-local mirror oflib/recentTools.ts'slocalStorageshape andsaveRecentToolOp'sPUT /api/settingsop, sincedebate-editorcan'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 (Closed: alocalStorage, no account sync), so it resets on a new device.recordRecentToolop best-effort syncs each visit to the sameuser_settingsrowfavoriteToolsuses (apps/debate-ai.com/app/api/settings/route.ts,recent_toolsD1 column), read back on mount the same wayuseFavoriteToolshydrates — so recents now follow a signed-in user across devices,localStoragestaying the signed-out/first-paint source of truth.
