Skip to content

Commands

This is a guide, not the contract. What the platform guarantees is specified under openspec/specs/. For this page: commands · host-services. Where this page and a specification disagree, the specification is right, and that is a defect in this page: change the behaviour there, then explain it here.

One behaviour, many triggers. Anything a rail item, menu entry, keybinding or the palette can run, your code can run too, through the same access check. A command your session may not use stays unavailable everywhere at once.

Do it

// src/app/… — inside an injection context
const commands = inject(CommandService);
commands.execute('shell.openSettings');
commands.execute('shell.tab.close', { tabId: 'doc/readme', closable: true }); // with a menu context
await commands.run(someCommand); // fire a resolved command and get what it answered

Read it

commands.commands(); // every registered command (signal)
commands.available(someCommand); // does the current session satisfy its `access`?
commands.shortcutOf(someCommand); // '⌘K' | 'Ctrl+K' | undefined
commands.triggerable(item); // can this item fire: a `command` that resolves, or a `run`
commands.trigger(item); // fire it the way a rail or bar item does

Triggerable is the shape every item shares, a command id or a run of its own, which is what lets one seam fire a rail item, a bar item and a view action alike.

The list of every registered command is commands(), the source list for a palette. Whether a command can run here and now is available(command): the session meets its access, and this window is one the command belongs in. The chord formatted for display is shortcutOf(command); it is undefined where there is none or shortcuts are switched off.

What asks about unsaved work

A command asks whatever its behaviour asks. execute('shell.tab.close', …) asks exactly as the × does, because it runs the same code; execute itself adds no question.

Switched off

commands.shortcuts takes the global key listener and every chord hint away from the user; execute and run keep working for you, and shortcutOf answers undefined so a hint never promises a dead key. commands.recentlyUsed governs only the palette’s section.

In depth

Fire or await. execute fires and forgets. run is the same one place the behaviour happens, but it answers what the command returned and rejects with what it threw, for a caller that has to tell the two apart. A plugin reaches the same thing through ctx.invokeCommand; see callable commands.

No-ops that protect your chrome. execute on an unknown id is a no-op with a console warning. On a command the session may not run it is a no-op too. So a command a plugin removed, or one the current user has no right to, cannot break your chrome.

The invoker seam. A plugin does not call CommandService. It calls through CommandInvoker, a seam with a name of its own. The seam is bound to the COMMAND_INVOKER token and implemented by CommandInvocationService. provideShell() binds it. An injector without the shell composed gets an invoker that reaches nothing and says so. You meet the seam only where you compose plugin runtimes yourself, and a distribution does that through provideShell() anyway.

Keybindings. KeybindingService binds every command’s shortcut globally; provideShell() starts it, and there is nothing to call yourself.

Rendering a chord. To show a shortcut in your own UI, use formatChord('mod+k'), exported from @loomweaver/shell. It returns ⌘K on macOS and Ctrl+K elsewhere, using the same platform detection as the shell, so your hint can never disagree with the binding.

Two searches, one component. The palette has two entry points. Under shell.commandPalette (mod+k) it lists commands. Under shell.quickOpen (mod+p) it lists content to navigate to: every open tab across all split content panes, plus every registered route you could open that takes no parameter and is not chromeless. Most recent comes first, with a relative-time hint. Enter reveals the tab where it already lives, so a tab in a secondary pane is activated in place rather than re-opened in the address pane; opens that tab’s context menu. Both are host commands, so omit and rebinding work the usual way. A pop-out window has no tab strip, so shell.quickOpen is not registered there.

Where the story is told