Commands & Keys

Two ways to reach something you built: type it in the : palette, or press a key.

Commands

A command is a name, an action, and a description.

commands.register("deploy", async () => {
    await commands.run("agent restart")
}, { description: "Restart and redeploy" })

It appears in the : palette beside the built-ins — filterable, tab-completable, with your description shown next to it.

Nested paths

Pass an array to nest. Commands sharing a prefix share the group automatically:

commands.register(["git", "push"], async () => { /* ... */ })
commands.register(["git", "pull"], async () => { /* ... */ })

:git now descends into a group with both. That is the same mechanism the built-in :provider and :open groups use — there is nothing to declare, the group exists because two paths share a segment.

Long-running commands

Return a promise and the palette holds a working row until it settles.

commands.register("sync", async signal => {
    await longRunningThing({ signal })
}, {
    description: "Sync everything",
    workingMessage: "syncing...",
})

The signal aborts when the user presses Escape mid-flight. An action that cannot be cancelled can ignore it; one that can should honour it.

Running a command from code

await commands.run("clear")
await commands.run(["agent", "restart"])

This reaches any command, including built-ins. It is the shortest path to functionality that has a command but no dedicated API — rather than re-implementing what :clear does, run :clear.

Keys

keys.register("ctrl+p", async () => {
    await palette.open("files")
})

Chords are written the way you read them everywhere else: ctrl+o, ctrl+shift+p, shift+tab, f5.

Handlers are fire-and-forget — nothing waits on your code while a key is held. If yours throws, the chord and the failure flash on the status row rather than disappearing silently.

Keys Axon keeps

Some chords belong to the terminal and cannot be rebound:

ReservedWhy
ctrl+c, ctrl+dQuit — a config must never be able to make Axon unquittable
escapeCancel, and back out of any mode
enter, tabSubmit and complete
arrow keys, backspace, deletePalette navigation and editing
: ~ / ^ * % # ? > !Built-in mode keys

Registering one of these throws immediately, naming the chord. It fails at registration rather than silently not firing — a binding that does nothing is indistinguishable from one that was never written, and that is a miserable thing to debug in your own config.

Sending a key

keys.send("ctrl+o")

Delivered as though you had pressed it. This is the escape hatch: anything the API does not expose directly is still reachable, because everything in the TUI is reachable from the keyboard.

When two things want the same name

Built-ins always win. Registering :clear or ctrl+c throws — the documented surface cannot be replaced from a config file.

Between your own registrations, the first one wins, and load order decides which is first:

main.ts (and anything it imports)
  → plugins/ alphabetically
    → each extension, in profile.config.ts order

Your own config loads first, so it beats every extension. Between two extensions, the one listed earlier in profile.config.ts wins.

A collision throws, naming both sides. Nothing is ever silently shadowed — "which of my two extensions bound ctrl+o" should never be a question you answer by bisecting your config.

Reference

commands.register(path, action, options?)  // → Disposer
commands.run(path)                          // → Promise<void>
commands.list()                             // → string[][]

keys.register(chord, handler)               // → Disposer
keys.send(chord)

Every register returns a function that undoes it. You rarely need it — Axon disposes everything your config registered before a reload — but it is there when you want to unbind something conditionally.

What's next

Palettes — your own lists, and asking the user a question.