# Command palette components

> The desktop app's Cmd+K palette as React components, fed by sources you register and a host for commands, files and settings.

The command palette is the desktop app's Cmd+K list: actions, `/` commands, files, settings and extension items, ranked as you type. `zidane-ui/palette` ships it as React components. Your app registers sources, and a `PaletteHost` gives the built-in sources their data. The preview runs on a fixture host. Type `res`, or open "Go to file…".

## Mount it

Wrap the app in a `PaletteProvider`. Register each source with `usePaletteSource`, from the component that owns its state. Render `CommandPalette` in a layer while the palette is open.

```tsx
import { Overlay } from 'zidane-ui'
import { CommandPalette, PaletteProvider, usePalette, usePaletteController, usePaletteHost, usePaletteLabels, usePaletteSource } from 'zidane-ui/palette'
import { commandsSource, filesSource } from 'zidane-ui/palette/headless'

function Sources({ openFile }) {
  const host = usePaletteHost()
  const labels = usePaletteLabels()
  usePaletteSource(commandsSource(host, labels))
  usePaletteSource(filesSource(host, labels, { open: openFile }))
  return null
}

function Palette({ context, sendPrompt }) {
  const { isOpen, close } = usePalette()
  const controller = usePaletteController({ context, onPrompt: sendPrompt, onInsert: sendPrompt })
  return (
    <Overlay open={isOpen} onClose={close}>
      {() => <CommandPalette controller={controller} />}
    </Overlay>
  )
}

export function App({ host }) {
  return (
    <PaletteProvider host={host}>
      <Sources openFile={path => console.log(path)} />
      <Palette context={{ host: 'gui', cwd: '/my/project' }} sendPrompt={text => console.log(text)} />
    </PaletteProvider>
  )
}
```

`usePaletteActions().toggle` is the Cmd+K handler. It never re-renders its caller. `AppHostProvider` wraps the app in a `PaletteProvider` when the `AppHost` has a `palette` host.

## Sources

A source is a `PaletteProvider` from `zidane/chat/pure`: an `id`, `items(context)` (an array or a promise), `run(itemId, context)`, and an optional `subscribe`. Items are data, never React nodes or callbacks. The palette lists sources in the order they registered.

| `run` returns | The palette |
| - | - |
| nothing | closes |
| `{ kind: 'items' }` | opens a nested picker. Backspace on an empty query goes back |
| `{ kind: 'prompt' }`, `{ kind: 'insert' }` | closes, and hands the text to `onPrompt` or `onInsert` |
| `{ kind: 'notify' }` | shows the text under the list, and stays open |

The built-in sources in `zidane-ui/palette/headless` read the host:

| Source | Reads | Lists |
| - | - | - |
| `commandsSource` | `listCommands` | The `/` commands of `context.cwd`. Running one sends `/name`, or inserts `/name ` when it completes arguments |
| `filesSource` | `listFiles` | Every file, found by name in the root list, and "Go to file…" to browse them |
| `extensionsSource` | `extensions` | Extension items. Running one goes back to its extension |
| `settingsSource` | `settings`, `setSettings` | The conversation preset, Compact transcript, Run details in the sidebar |

## App sources

The desktop app's own sources ship in `zidane-ui/palette/headless` too. Each one reads what your app hands it, when the palette lists its rows:

| Source | Reads | Lists |
| - | - | - |
| `sessionsSource(sessions, activeId, select, rankOf, labels)` | Your loaded sessions and how each ranks (`0` needs you, `1` running, `2` the rest) | Six sessions on an empty query, without the current one. The rest are found by title |
| `conversationsSource(open)` | Nothing | No rows of its own. Running a match from `useConversationSearch` calls `open({ sessionId, query })` |
| `actionsSource(handlers, bindings, labels)` | Your key action handlers (or a getter, read when it lists and runs) and their live chords | One row per action you handle |
| `panelsSource(read, bindings, labels)` | Your side panel (`PalettePanels`) | Open or close the panel, and one toggle per tab with its badge |
| `spacesSource`, `setupSource`, `repoSource` | Your spaces, the setup wizard, a clone dialog | Switch to a space, Run setup, Import repository |
| `prefsSource(prefs, labels)` | The appearance, editor and model picker prefs you pass | Toggles and cycles for each, and Editor / Appearance settings when you pass `openSettings` |

Two hooks in `zidane-ui/palette` mount the common ones for you. `useActionsPaletteSource()` lists the key command registry's actions (`useKeyCommands`, `zidane-ui/keybindings`): whatever has a shortcut is in the palette, registered again only when the set of actions changes. `useSessionRank(peekRunning)` is the `rankOf` for `sessionsSource`, from the pending approvals and questions, presence and your running flag.

## Rows and the dialog

`createPaletteRows(options)` returns the app rows for `CommandPalette`. A session row shows a live status word, the folder, the age and a Current tag. A conversation match shows its excerpt with the query marked. A file row shows its type badge. Call it once, at module scope. The hooks you pass run inside the rows. Every option is optional:

| Option | Gives |
| - | - |
| `StatusDot` | A session's live dot, in the icon slot |
| `useSessionStatus(id)` | `running`, `pending`, `attention` or `error`, worded next to the title |
| `useSession(id)` | The session's last update and whether it is the one on show |
| `highlight(excerpt, query)` | Cuts the excerpt around its first match and marks the terms |

`PaletteDialog` is the palette in your app's modal slot (`ModalProvider` from `zidane-ui/app`). It adds the "In conversations" group (`useConversationSearch`, over the host's `searchConversations`) after the ranked rows. `usePaletteModalSlot({ context, cwd, render })` opens it while the palette is open. It also closes the palette when another modal takes the slot:

```tsx
const rows = createPaletteRows({ useSessionStatus, useSession })

function PaletteSlot({ context, sessions }) {
  usePaletteModalSlot({ context, render: ctx => <PaletteDialog context={ctx} sessions={sessions} rows={rows} /> })
  return null
}
```

## The host

Every `PaletteHost` member is optional. A source whose data the host leaves out lists nothing.

| Member | Gives |
| - | - |
| `listCommands({ cwd, hostId })` | The `/` commands of a folder |
| `listFiles({ root, sessionId })` | The project's files, without gitignored ones |
| `extensions` | `list`, `run`, and optional `subscribe` and `watch` |
| `settings`, `setSettings` | The settings in effect, as a readable store, and a patch |
| `recents` | Where recently run items persist. `createStorageRecents(localStorage, key)` keeps them in a `Storage` |
| `searchConversations({ query, sessionIds, archive })` | Matches inside conversations: the excerpt and how many turns matched |

`createServerPaletteHost({ client })` (`zidane-ui/adapters`) builds one on zidane-server: `commands.list`, `sessions.workspaceListFiles`, `history.searchAll` and `settings.get` / `settings.set` (an admin token). `extensions` runs the server's extension items (`palette.list`, `palette.run`, `palette.watch` while the palette is open and `palette.unwatch` once it closes, `palette:changed`). `createFixturePaletteHost()` (`zidane-ui/palette/testing`) holds demo data in memory. Every host runs the same suite, `describePaletteHostContract` from `zidane-ui/palette/testing/contract`.

## Large catalogs

A project's files can be thousands of rows. Pass `createRankWorker` to the provider and the palette ranks a big list in a worker, so typing never waits:

```tsx
<PaletteProvider host={host} createRankWorker={() => new Worker(new URL('zidane-ui/palette/rank.worker', import.meta.url), { type: 'module' })}>
```

Under Vite, `import RankWorker from 'zidane-ui/palette/rank.worker?worker'` works too. Without a worker, the palette ranks every query in place.

## Strings

The palette's own strings (the placeholder, the built-in sources' rows) are in `DEFAULT_PALETTE_LABELS`. Pass overrides as `PaletteProvider`'s `labels`. The surface's strings (filter, back, the verbs) are the design system's labels.
