Palettes

The palette is the TUI's interaction model — filterable, tabbed, keyboard-driven, fast. Every built-in mode is one. You can build your own, and you get the same machinery: the filtering, the cursor, the tab strip, the scrolling, all of it.

That is what makes this the most useful thing in the extension API. A palette plus a key chord is a complete workflow, and it looks and behaves exactly like the interface it sits inside.

Creating one

palette.create("branches", {
    key: "&",
    description: "switch branch",

    async list(query, tab) {
        const branches = await readBranches()
        return branches.map(branch => ({
            id: branch.sha,
            label: branch.name,
            description: branch.subject,
            action: () => {
                tui.info(`checked out ${branch.name}`)
            },
        }))
    },
})

Three ways to open it, all equivalent:

await palette.open("branches")   // from code
keys.register("ctrl+b", () => palette.open("branches"))

...or press &, the mode key it claimed. It behaves exactly like a built-in mode key: only from an empty input, and pressing it again toggles back.

The list

list() is called with the current query and active tab, and may be async. While the first call is in flight the palette shows its own loading row, then swaps in your rows when it settles. Results are cached per query and tab.

You do not have to filter — Axon applies its own substring match over labels and descriptions. Return everything and let the palette narrow it. Set filter: false if your list() already narrows by query itself.

Rows

FieldWhat it does
idStable identity for the row
labelWhat is shown
descriptionDim text beside the label
actionRun on Enter
previewRun as the cursor moves over it
headerA non-selectable section title
separatorA blank spacer between groups

Headers and separators are structure, not results: a header only survives filtering when something under it matched, and a header that matches the query pulls its whole group in.

Tabs

palette.create("work", {
    tabs: [
        { id: "open", label: "Open" },
        { id: "done", label: "Done" },
    ],

    async list(query, tab) {
        return tab === "done" ? closedItems() : openItems()
    },
})

and switch tabs. Your list() is called again with the new tab.

Refreshing

const branches = palette.create("branches", { /* ... */ })

// later — the underlying data changed
branches.refresh()

refresh() drops the cache so the next open recomputes. It does not reopen anything.

Asking a question

Three verbs for when your code needs an answer:

const branch = await palette.pick(["main", "dev", "staging"])
const sure   = await palette.confirm("Deploy to production?")
const name   = await palette.prompt("Agent name?")

They return promises, and that is the entire design. A multi-step flow is sequential awaits — there is no wizard framework, because there does not need to be one:

commands.register("deploy", async () => {
    const env = await palette.pick(["staging", "production"])
    if (!env) return

    const sure = await palette.confirm(`Deploy to ${env}?`)
    if (!sure) return

    await runDeploy(env)
    tui.info(`deployed to ${env}`)
})

Escape is an answer

Every question resolves when the user walks away — undefined for pick and prompt, false for confirm. It is in the return type, so you cannot forget to handle it. A cancelled question is an ordinary outcome, not an error.

Picking objects

pick over strings returns the string. Pick over objects and you get the object back, not the label:

const branch = await palette.pick([
    { label: "main", description: "the trunk", value: { sha: "aaa1111" } },
    { label: "dev",  description: "in progress", value: { sha: "bbb2222" } },
])

if (branch) tui.info(branch.sha)

One at a time

Opening a question or a palette while one is already open throws. Stealing the palette out from under someone mid-navigation drops whatever they were answering, and queueing would let a config stack modal prompts in front of them. Check first if a keybind might collide:

if (palette.isOpen) return

Reference

palette.create(name, definition)   // → handle { open, refresh, dispose }
palette.get(name)                  // → handle | null
palette.open(name)                 // → Promise<void>
palette.close()
palette.isOpen                     // → boolean

palette.pick(options, opts?)       // → Promise<T | undefined>
palette.confirm(message)           // → Promise<boolean>
palette.prompt(message, opts?)     // → Promise<string | undefined>

What's next

Plugins & Hooks — running code when something happens, rather than when someone asks.