Commands
Commands are named actions available through ctx.commands. A plugin can run
them directly, and the host can expose them through menus, shortcuts, or a
command palette.
Keep each command focused on one action. Ask for confirmation or collect input
before running a command rather than putting interactive UI inside run.
Permission
Section titled “Permission”Plugins must request registry.command to access ctx.commands:
{ "recall": { "permissions": ["registry.command"] }}Register a command
Section titled “Register a command”type ArchiveNotePayload = { noteId: string;};
const registration = await ctx.commands.register<ArchiveNotePayload, boolean>({ id: "focus-tools.note.archive", name: "Archive note", description: "Moves a note into the archive.", async run(payload) { if (!payload || typeof payload.noteId !== "string") { throw new Error("A note id is required."); }
return archiveNote(payload.noteId); },});Command definitions contain:
| Field | Type | Description |
|---|---|---|
id | string | Stable id unique within the runtime. Prefix plugin-owned ids to avoid collisions. |
name | string | Human-readable label for menus, palettes, and diagnostics. |
description | string? | Optional explanation of the action. |
run | (payload) => result | Action executed when the command is invoked. It may return a promise. |
Recall supplies owner and registeredAt from the active plugin session; a
plugin does not provide those fields.
Duplicate ids are rejected. The registry does not validate a command’s
payload, so validate unknown input inside run before reading it.
Run a command
Section titled “Run a command”Pass the command id and its payload to run:
const archived = await ctx.commands.run<ArchiveNotePayload, boolean>( "focus-tools.note.archive", { noteId: "note_123" },);run rejects when the id is not registered or when the command throws. Handle
errors at the point where you can show useful feedback or recover safely.
Inspect commands
Section titled “Inspect commands”Use get for one id and list for a snapshot of the registry:
const archiveCommand = await ctx.commands.get("focus-tools.note.archive");const allCommands = await ctx.commands.list();Treat returned command definitions as read-only.
Lifecycle
Section titled “Lifecycle”register returns a disposable handle. Dispose it when the plugin unloads so
the command does not outlive the plugin:
await registration.dispose();ctx.commands.unregister(id) also removes a command by id. Prefer the
registration handle for plugin-owned cleanup because it represents that exact
registration. Dispose subscriptions separately when they are no longer needed.
Events
Section titled “Events”Subscribe to registry changes with on:
const subscription = await ctx.commands.on("listChange", ({ commands }) => { console.log(`${commands.length} commands available`);});
await subscription.dispose();| Event | Payload |
|---|---|
register | { command } for the new command. |
unregister | { command } for the removed command. |
listChange | { commands } containing the current command snapshot. |
API summary
Section titled “API summary”| Method | Purpose |
|---|---|
register(command) | Register a command and return its disposable handle. |
unregister(id) | Remove a command by id. |
get(id) | Read one registered command. |
list() | Read a snapshot of all commands. |
run(id, payload?) | Invoke a command and await its result. |
on(event, listener) | Subscribe to command registry events. |