Command Palette
beta⌘K command palette for agent actions: fuzzy search, groups, recents, nested pages with async loading, and shortcut hints.
With a trigger
minimal
Agent actions, recents and nested pages
Controlled open state, ⌘K, recents, a nested “Switch agent…” page and an async “Run workflow…” page.
full
or pressControl KCtrlK
Nothing run yet
Server-side search
onQueryChange fetches results into `items` while `loading` shows skeleton rows.
composition
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/command-palette.jsonCopies the source into components/kura/command-palette with its dependencies (kura-theme, kbd, kura-keys, use-controllable-state, use-hotkey, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- A global launcher for agent actions: run/stop an agent, switch agents, add context, change settings.
- Navigating many commands or entities (files, threads, workflows) by typing.
- Multi-step choices as nested pages (“Change theme…” → Light / Dark), including lists fetched on demand.
Reach for something else when
- Commands typed inline inside a prompt — use slash-command-menu.
- Choosing a model — use model-picker.
- A handful of starter prompts in an empty chat — use prompt-suggestions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items* | readonly CommandPaletteItem[] | — | Items: { id, label, description?, icon?, group?, keywords?, shortcut?, onSelect?, children?, loadChildren?, placeholder?, disabled? }. children/loadChildren open a nested page. |
| open | boolean | — | Controlled open state. |
| defaultOpen | boolean | false | Initial open state. |
| onOpenChange | (open: boolean) => void | — | Open state changes. |
| hotkey | readonly KeyName[] | null | ["mod", "k"] | Global toggle shortcut (works in inputs). null disables it. |
| trigger | React.ReactElement | — | Element rendered as the dialog trigger, e.g. <CommandPaletteTrigger />. |
| recent | string[] | — | Controlled recent item ids (most recent first), shown as “Recent” when the query is empty. |
| defaultRecent | string[] | — | Initial recents when uncontrolled. |
| onRecentChange | (ids: string[]) => void | — | Called when an item is used (persist it to localStorage if you like). |
| recentLimit | number | 4 | Max recents shown. |
| onSelect | (item: CommandPaletteItem) => void | — | Called after any leaf item runs (in addition to item.onSelect). |
| onQueryChange | (query: string) => void | — | Every query change — fetch server results into `items` and toggle `loading`. |
| loading | boolean | false | Show skeleton rows and aria-busy (root page). Nested pages with loadChildren handle this themselves. |
| placeholder | string | "Search or run a command…" | Search placeholder on the root page. |
| emptyMessage | string | "No results" | Shown when nothing matches. |
| label | string | "Command palette" | Accessible dialog title (visually hidden). |
All native attributes of the root element, className and ref pass through.
Anatomy
- CommandPaletteModal dialog surface (translucent overlay, origin-top pop). data-page = current nested page id.
data-slot="command-palette" - CommandPaletteSearchSearch row with breadcrumbs for nested pages.
data-slot="command-palette-search" - CommandPaletteGroupGroup heading (Recent first).
data-slot="command-palette-group" - CommandPaletteItemrole=option row: icon, label, description, shortcut or chevron. data-active, data-disabled.
data-slot="command-palette-item" - CommandPaletteLoadingSkeleton rows while loading.
data-slot="command-palette-loading" - CommandPaletteFooterResult count and the Enter action (Run / Open).
data-slot="command-palette-footer" - CommandPaletteTriggerSearch-field-styled button with the shortcut; pass it as `trigger`.
data-slot="command-palette-trigger"
Accessibility
- Role:
dialog > combobox + listbox - A modal Radix dialog with a visually hidden title; focus moves to the search input on open and returns to the trigger on close.
- The input is a combobox using aria-activedescendant, so screen readers read the highlighted option while focus stays in the field.
- The listbox is aria-busy while loading; the footer's result count is a polite live region.
- Nested pages are announced through the input's label (“Search Change theme…”) and shown as breadcrumbs.
- Reduced motion: The dialog and overlay fade without scale; skeleton rows stop pulsing.
| Keys | Action |
|---|---|
| Mod+K | Toggle the palette (configurable) |
| ArrowDown / ArrowUp | Move the highlight (wraps, skips disabled) |
| Home / End | First / last item (empty query) |
| Enter | Run the item or open its page |
| Backspace (empty query) / Escape | Go back one page |
| Escape (root page) | Close |
Notes for agents
- Mount one palette near the app root; it portals itself. Only one instance should bind ⌘K — pass hotkey={null} to others.
- Use `children` for static sub-pages and `loadChildren` for fetched ones (the loading state is automatic).
- Persist recents with recent/onRecentChange (e.g. localStorage); recents resolve ids anywhere in the item tree.
- Shortcuts on items are display-only; bind them with useHotkey so they also work when the palette is closed.
- scoreCommand(text, query) is exported if you need the same fuzzy ranking elsewhere.