Command Palette
block / layout
⌘K launcher that searches navigation, actions and recents in one result set, with awaited commands, scoped child lists and async results.
- type
- block
- files
- 2
- registry deps
- button, command, dialog
- npm deps
- lucide-react
installs
- lib/automindz/commands.ts
- components/automindz/command-palette.tsx
- Commands are data — nav, actions and recents in one result set
- ⌘K / Ctrl+K, and never while the user is typing in a field
- Awaited `run`: pending on the row, failure in place, no optimistic close
- One level of scoped child commands, Escape or Backspace to leave
- Async `search` with its own loading and error states
Commands are data: pass commands (contract in lib/automindz/commands.ts). Mount it once, high in the tree. run is awaited — the row stays busy and a failure shows in place, so never close optimistically. href goes through onNavigate; pass onNavigate={(href) => router.push(href)} for client-side routing. Always render CommandPaletteTrigger as well: a palette reachable only by ⌘K is invisible to anyone who was not told the hotkey. Set hotkey={false} if the host app already owns ⌘K.
Variations
one state per section
- 01The launcher, opened two ways
- 02A command that runs, and one that fails
- 03A scoped child list
- 04Async results, with their own states
- 05Blocked, with the reason on the row
- 06Nothing configured
The launcher, opened two ways
Press ⌘K (Ctrl+K) anywhere on this page, or the button — a palette only a hotkey can reach is invisible to anyone who has not been told the hotkey. Open with nothing typed and you get recents and the groups, never a blank box; recents vanish the moment you type, because a recent row plus its own group row is one command listed twice. Groups sit in the order the array declares them, not alphabetically. Type inv to reach Billing through a keyword that is not in its label, zzz for a no-results message that names what you typed. Focus a field first and ⌘K does nothing: the keystroke belongs to whoever is typing. This is the only preview on the page with the hotkey bound.
Command palette
Search navigation, actions and recents in one list.
A command that runs, and one that fails
run is awaited. Press “Rebuild search index”: the row spins, everything else recedes, the palette refuses to close, and it dismisses only once the work settles. Press “Deploy to production”: the rejection renders on its own row with the reason and turns it into a retry — icon, label and colour all change, so it never depends on red alone. A palette that closes optimistically and swallows that error is worse than no palette.
Command palette
Search navigation, actions and recents in one list.
A scoped child list
“Change status →” pushes its own list. One level, capped in the type rather than left to a recursion the row height cannot carry. Escape goes up before it closes the palette, Backspace on an empty query does the same, and the breadcrumb’s back button is there for the pointer — with focus handed back to the input, so the arrow keys keep working.
Command palette
Search navigation, actions and recents in one list.
Async results, with their own states
The search hook is for rows a client fetches. It is debounced, runs only with a query, and lands **below** the local groups so late arrivals never shove the list out from under the cursor. Three distinct outcomes, all pressable: type sa for results, zz for “no matches”, and fail for the service error — loading, failed and nothing-matched are three messages, never one.
Command palette
Search navigation, actions and recents in one list.
Blocked, with the reason on the row
A command the current user cannot run stays in the list, disabled, with the reason as its second line rather than a tooltip — hover-only text is unreachable for a keyboard, and a palette is a keyboard surface. Arrow keys skip them, so the block is real and not just visual.
Command palette
Search navigation, actions and recents in one list.
Nothing configured
The fourth message: no commands at all is not the same as nothing matched, not the same as still loading, and not the same as a failed fetch. commands={[]} says so instead of showing an empty frame.
Command palette
Search navigation, actions and recents in one list.
