Define a command once for keys, palette, slash and agents
Use one typed command graph for keyboard shortcuts, a command palette, slash commands, help, menus and an agent view, with one execution outcome.
A keyboard shortcut, a palette row and an agent tool should not become three
implementations of the same action. Whistle keeps one command definition and
derives six immutable views: bindings, palette, slash, help, menu and agent.
Every caller uses the same typed execute door.
The host still owns key matching, rendering, transport, authorization and the real work. Whistle is a command graph, not a UI framework or an agent server.
Contribute one command
Section titled “Contribute one command”npm install @fungi.computer/whistleWhistle is ESM with Zod as its only runtime dependency. The core runs in Node, browsers and Workers without DOM or React state.
Illustrative: the handler returns a local result to demonstrate dispatch. Replace it with your authorized save operation; this example does not write a file.
import { createWhistle } from "@fungi.computer/whistle";
const whistle = createWhistle();const lease = whistle.contribute({ sourceId: "editor", namespace: "editor", commands: [ { id: "save", title: "Save file", category: "File", projections: { bindings: { values: ["mod+s"] }, palette: {}, slash: { aliases: ["save"] }, help: {}, menu: {}, agent: {}, }, handler: () => ({ saved: true }), }, ],});
const snapshot = whistle.snapshot();const outcome = await whistle.execute( { namespace: "editor", id: "save" }, { origin: "palette" },);
lease.dispose();whistle.close();Each projection row carries the same typed command key, { namespace, id }.
Use that key for execution. A row’s commandId is an opaque render key; do not
build or parse it. A command appears only on projections you explicitly name.
The contribution returns a lease. lease.update replaces that generation’s
commands atomically, and lease.dispose removes them. A stale lease cannot
remove a replacement. Subscribe to graph changes to refresh your UI rather than
maintaining a second command registry.
Send every surface through execute
Section titled “Send every surface through execute”The origin identifies the caller: agent, browser, keyboard, menu,
palette, programmatic, slash or terminal. It does not authorize the
request.
| Outcome | Meaning |
|---|---|
handled |
The handler finished; its return value is in result if supplied. |
unavailable |
The current availability reader refused dispatch, with a reason. |
failed |
The handler threw; a JSON error carries a code and message. |
missing |
No live command has that identity. |
invalid |
The command identity is blank. |
closed |
The runtime was already closed. |
handled does not prove a durable workflow or physical operation completed.
Report only what your handler’s result establishes.
An optional availability() reader keeps unavailable commands visible with
their reason. execute() reads it again before invoking the handler. This is a
convenience check, not authorization: the handler must still admit the real
request.
Connect Node, browser and Workers hosts
Section titled “Connect Node, browser and Workers hosts”Node or terminal. Import the core, render snapshot().palette, .help or
other rows through your own UI, and call
execute(row.command, { origin: "terminal" }). Whistle does not parse raw
terminal keys or slash text.
Browser. Render the same rows in your palette or menu. For DOM shortcuts,
install the optional @opentui/keymap@0.5.10 peer and use the HTML adapter.
Illustrative: the existing whistle runtime comes from your browser host and
already contains the command contribution above.
import { bindHtmlKeymap } from "@fungi.computer/whistle/html";
const keyboard = bindHtmlKeymap(document.body, whistle, "keyboard");// Dispose the Whistle layer and subscription when your host releases it.keyboard.dispose();The adapter refreshes the projected key layer as contributions change. Its disposer does not remove the underlying HTML keymap host’s document-lifetime DOM listeners.
Workers. Use the same core without importing /html. A backend can
advertise data-only definitions as a whistle.catalog/v1 catalog. A client uses
parseCatalog and catalogCommands(catalog, invoke) from /catalog; the
host’s invoke supplies transport and server authorization.
Expose app commands as agent tools
Section titled “Expose app commands as agent tools”The agent view retains semantic inputSchema and outputSchema from a
command’s action, but drops local presentation hints. Your agent bridge
discovers those rows and invokes the same command with origin: "agent".
Whistle does not start an MCP server or grant a model access.
scopeSnapshot(snapshot, scope) filters human and agent views to global
commands plus one focused scope. Discovery scope still does not replace
authorization at execution.
Commands that ask a question need an interaction. Human clients can register a
presenter; agent and programmatic callers get no interaction by default and must
supply one if required. Use /wire’s maintained schemas and parsers at
untrusted command, prompt and notice boundaries.
Read Whistle for catalogs, interactions, slash argument grammar and bounds. For a conversation UI that accepts the host’s Whistle runtime, see Parakeet.