How to register palette commands

A palette item is a row in the command palette. Use them for nav shortcuts, quick toggles, anything you want users to invoke with Ctrl+K + a few keys.

dddk.palette.addItem({
  id: 'go-billing',
  name: 'Open Billing',
  description: 'See invoices and payment history',
  section: 'Navigation',
  icon: '💳',
  handler: (p) => {
    p.close();
    yourRouter.push('/billing');
  },
});

That's the minimum. Drops into the palette, ranks against fuzzy matches, fires handler when chosen.


Field reference

interface PaletteItem {
  id: string;                        // unique key, used by HeatRank
  name: string;                      // displayed label
  description?: string;              // secondary line under the name
  icon?: string;                     // emoji, or any string that renders
  section?: string;                  // groups items in the list (Nav, AI, Settings…)
  prefix?: string | string[] | PaletteItemPrefix;
  prefixAcceptsAnyArg?: boolean;
  fallback?: boolean;
  agentTool?: PaletteAgentTool;
  handler?: (handle: PaletteHandle, arg?: string) => void;
  detail?: PieceNode;                // optional right-pane detail
}

The handle you receive:

interface PaletteHandle {
  close(): void;
  replace(items: PaletteItem[]): void; // swap the list (for sub-menus)
  setMode(mode: 'search' | 'chat'): void;
  input: string;
  setInput(value: string): void;
}

Pattern: nav shortcuts

const NAV = [
  { id: 'go-home',    name: 'Home',     icon: '🏠', path: '/' },
  { id: 'go-billing', name: 'Billing',  icon: '💳', path: '/billing' },
  { id: 'go-settings',name: 'Settings', icon: '⚙️', path: '/settings' },
];

for (const n of NAV) {
  dddk.palette.addItem({
    id: n.id,
    name: n.name,
    icon: n.icon,
    section: 'Navigation',
    handler: (p) => { p.close(); router.push(n.path); },
  });
}

Pattern: prefix routing — ask ai: ...

A prefix turns the item into a routing target. Anything typed after the prefix becomes arg for the handler. Useful for free-form input.

dddk.palette.addItem({
  id: 'ask-ai',
  name: 'Ask AI',
  description: 'Ask anything — powered by webagent',
  prefix: 'ask ai:',
  prefixAcceptsAnyArg: true,   // accept any non-empty arg
  fallback: true,              // also show when nothing else matches
  icon: '✨',
  section: 'AI',
  handler: (p, arg) => {
    p.close();
    if (arg) dddk.startAgent(arg);
  },
});

Multiple prefixes? Pass an array:

prefix: ['/ai', 'ask ai:', '?'],

Different display label vs. match key:

prefix: { match: ['/ai', 'ask:'], label: '/ai' },

Pattern: quick toggle

let darkMode = false;

dddk.palette.addItem({
  id: 'toggle-dark',
  name: () => darkMode ? 'Switch to light mode' : 'Switch to dark mode',
  section: 'Settings',
  icon: '🌓',
  handler: (p) => {
    p.close();
    darkMode = !darkMode;
    document.documentElement.dataset.theme = darkMode ? 'dark' : 'light';
  },
});

(For a polished version of this exact toggle, use the built-in ThemeToggleModule — see 04-how-to-toggle-features.)


Pattern: opt-in as agent tool

Mark a palette item with agentTool and webagent can invoke it during a multi-step task.

dddk.palette.addItem({
  id: 'refund-order',
  name: 'Refund order',
  prefix: 'refund:',
  prefixAcceptsAnyArg: true,
  icon: '↩',
  agentTool: {
    description: 'Refund the order whose ID is passed as `arg`. Requires confirmation.',
    parameters: {
      type: 'object',
      properties: { arg: { type: 'string', description: 'Order ID like ORD-1002' } },
      required: ['arg'],
    },
    requireConfirmation: true,
  },
  handler: async (p, arg) => {
    p.close();
    if (!arg) return;
    await fetch(`/api/orders/${arg}/refund`, { method: 'POST' });
  },
});

Now if the agent gets the task "refund Bob's order", it can find the order, then call refund-order with arg='ORD-1002'. Because requireConfirmation: true, the user gets a confirm dialog first.

Items WITHOUT agentTool are never exposed to the agent — opt-in by design.


Pattern: image attach (camera icon)

Show a camera icon at the right of the input — user picks a file or screenshots a tab.

const palette = new CommandPalette({
  // ...
  camera: {
    mode: 'upload',
    onCapture: (file, source) => {
      // do whatever — upload to your backend, attach to the next agent call, etc.
      console.log('got', source, file);
    },
    accept: 'image/*',
    capture: true,   // mobile: open camera directly
  },
});

Switch to mode: 'screenshot' to use getDisplayMedia() instead. Toggle at runtime via palette.setCameraMode('screenshot').


Pattern: sub-menu via replace()

dddk.palette.addItem({
  id: 'language',
  name: 'Change language',
  icon: '🌐',
  section: 'Settings',
  handler: (p) => {
    p.replace([
      { id: 'lang-en', name: 'English', handler: (h) => { setLocale('en'); h.close(); } },
      { id: 'lang-zh', name: '繁體中文', handler: (h) => { setLocale('zh-TW'); h.close(); } },
      { id: 'lang-ja', name: '日本語', handler: (h) => { setLocale('ja'); h.close(); } },
    ]);
  },
});

replace swaps the current items — close then returns to the original set.


Removing or updating items

dddk.palette.removeItem('go-billing');
dddk.palette.setItems(newList);    // full replace

If you add items dynamically (per route, per user role), use removeItem on the previous set first to avoid duplicates.