# Command palette

Search or jump: pages, threads, places and actions from one box (⌘K). Local groups filter as you type; remote groups are the service's results for the query, with a "Searching…" state while they're on the way and the failure said in words if the search failed. Closing it puts focus back where it was when it opened, even when a shortcut opened it. Choosing an item closes the palette, waits for focus to return, then runs it, so an action that moves focus (to a tab, a field) keeps it there.

- Registry name: `command-palette` (`@candor/command-palette`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/command-palette

## Install

```bash
npx candor-ui add command-palette
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/command-palette`.
It brings in: dialog, kbd, locale, use-shortcut, utils.
npm packages: cmdk@^1.1.1, lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Flag, GitBranch, MessageSquare, Plus, Settings, Table2 } from "lucide-react"
import { CommandPalette, CommandTrigger, matchEveryWord, type PaletteGroup } from "@/components/command-palette"

const recentThreads = [
  { id: "t1", label: "Add a region column to orders", detail: "Storefront · staging" },
  { id: "t2", label: "Why is checkout slow on Staging?", detail: "Storefront · staging" },
  { id: "t3", label: "Clean up stale feature flags", detail: "Storefront · Production" },
]

export default function Example() {
  const [open, setOpen] = React.useState(false)
  const [query, setQuery] = React.useState("")

  const groups: PaletteGroup[] = [
    {
      heading: "Actions",
      items: [
        { id: "new", label: "New thread", Icon: Plus, shortcut: "mod+shift+o", onSelect: () => {} },
        { id: "settings", label: "Open settings", Icon: Settings, onSelect: () => {} },
        { id: "deploy", label: "Promote Staging to Production", Icon: GitBranch, locked: "Only owners can promote to Production", onSelect: () => {} },
      ],
    },
    {
      heading: "Places",
      items: [
        { id: "flags", label: "Feature flags", Icon: Flag, keywords: ["toggles"], onSelect: () => {} },
        { id: "tables", label: "Tables", Icon: Table2, keywords: ["schema", "database"], onSelect: () => {} },
      ],
    },
    {
      // A real app fetches these from its search service as the query changes; here they're filtered locally.
      heading: "Threads",
      remote: true,
      items: query ? recentThreads.filter((t) => t.label.toLowerCase().includes(query.toLowerCase())).map((t) => ({ ...t, Icon: MessageSquare, onSelect: () => {} })) : [],
    },
  ]

  return (
    <div className="max-w-64">
      <CommandTrigger onClick={() => setOpen(true)} />
      {/* matchEveryWord: every word typed must start a word of the item, in any order ("flags feature"). */}
      <CommandPalette open={open} onOpenChange={setOpen} groups={groups} onQueryChange={setQuery} filter={matchEveryWord} />
    </div>
  )
}
```

## <CommandPalette>

Props:

- `open: boolean`: Whether the palette is showing.
- `onOpenChange: (open: boolean) => void`: Called when the palette opens or closes, including after an item is chosen.
- `groups: PaletteGroup[]`: The items, under their headings; local groups filter as the person types.
- `onQueryChange?: ((query: string) => void) | undefined`: Called as the query changes, to fetch remote groups.
- `searching?: boolean | undefined` (default `false`): Remote results are on the way; shows a spinner and "Searching…". Defaults to false.
- `searchError?: string | undefined`: Why the remote search failed, shown in place of "No results".
- `placeholder?: string | undefined`: Shown in the empty search box. Defaults to "Search or jump to…".
- `filter?: ((value: string, search: string, keywords?: string[]) => number) | undefined`: How local items match the query, passed to cmdk: `(value, search, keywords) => score`, 0 hiding the item. `keywords` holds the item's label, detail and keywords; `value` holds ids, so match on `keywords`. `matchEveryWord` is a strict "every word, any order" matcher. Defaults to cmdk's fuzzy match.
- `onCloseAutoFocus?: ((event: Event) => void) | undefined`: Called as the palette closes, just before focus returns to the element that had it when the palette opened. Call `event.preventDefault()` to put focus somewhere else yourself.

## <CommandTrigger>

The button that opens the palette, for a sidebar or top bar.

Props:

- `onClick: () => void`: Called when the button is pressed, usually to open the palette.
- `label?: string | undefined`: The button's text. Defaults to "Search or jump".
- `combo?: string | undefined` (default `"mod+k"`): The shortcut printed on the button, e.g. "mod+k" (the default). Printed, not bound.
- `className?: string | undefined`

## Functions

### matchEveryWord

```ts
matchEveryWord(_value: string, search: string, keywords?: string[])
```

A strict match for CommandPalette's `filter`: every word typed must start a word of the item's label, detail or keywords, in any order ("plan open" finds "Open the plan"). Case and accents are ignored. It reads cmdk's `keywords` only, never its `value`, which holds the group and item ids. Returns 1 for a match, 0 otherwise, so cmdk keeps the items in the order you gave them.

## Strings (`commandPalette`)

The fixed words, English by default. Override any of them under `commandPalette` in `<LocaleProvider strings={{ commandPalette: { … } }}>`; the table is exported as `commandPaletteStrings`.

- `title`: Search or jump
- `description`: Type to find pages, threads, places and actions.
- `placeholder`: Search or jump to…
- `search`: Search
- `results`: Results
- `searchingStatus`: Searching
- `searching`: Searching…
- `nothingMatches`: Nothing matches “{query}”.
- `nothingToShow`: Nothing to show.
- `searchFailed`: Search failed: {error}
- `noResults`: No results.
- `move`: move
- `open`: open
- `close`: close
- `trigger`: Search or jump

## Types

### PaletteItem

```ts
{ id: string label: string /** Shown after the label: a place, a date, a path. */ detail?: string | undefined Icon?: LucideIcon | undefined /** Extra words that should find this item. */ keywords?: string[] | undefined /** A combo that also does this, e.g. "mod+shift+o". Printed, not bound. */ shortcut?: string | undefined /** Why it can't be done now; the item stays listed but can't be chosen. */
```

### PaletteGroup

```ts
{ heading: string items: PaletteItem[] /** * Results the service already matched against the query (search across * threads, Know, the audit log). They're shown as given, not filtered again. */ remote?: boolean | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
