Skip to content

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…".

CommandPalette on createFixturePaletteHost

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 returnsThe palette
nothingcloses
{ 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:

SourceReadsLists
commandsSourcelistCommandsThe / commands of context.cwd. Running one sends /name, or inserts /name when it completes arguments
filesSourcelistFilesEvery file, found by name in the root list, and "Go to file…" to browse them
extensionsSourceextensionsExtension items. Running one goes back to its extension
settingsSourcesettings, setSettingsThe 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:

SourceReadsLists
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)NothingNo 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 chordsOne 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, repoSourceYour spaces, the setup wizard, a clone dialogSwitch to a space, Run setup, Import repository
prefsSource(prefs, labels)The appearance, editor and model picker prefs you passToggles 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:

OptionGives
StatusDotA 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.

MemberGives
listCommands({ cwd, hostId })The / commands of a folder
listFiles({ root, sessionId })The project's files, without gitignored ones
extensionslist, run, and optional subscribe and watch
settings, setSettingsThe settings in effect, as a readable store, and a patch
recentsWhere 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.