Skip to main content

Keybindings

A keybinding does not contain behaviour. It points at a command by id.

import { function command<TId extends string>(commandValue: CommandInput<TId>): Command<TId>
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
, function keybinding<TCommand extends Command | string>(input: KeybindingInput<TCommand>): Keybinding<TCommand>
Bind a keyboard shortcut to a command. Pass the `command` value itself when the command lives in the same mod, so the id is written once and a rename cannot break the binding. Pass a string only for a command someone else owns. A binding does not decide when the command is available. Give it the same {@link Condition } you gave the command's `when` so the chord goes quiet in the same situations the palette entry does.
@categoryState
keybinding
} from '@modular/sdk';
export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly commands: readonly [Command<string>];
    readonly keybindings: readonly [Keybinding<string>];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Hello";
        readonly description: "A first mod";
    };
    readonly commands: readonly [Command<string>];
    readonly keybindings: readonly [Keybinding<string>];
}): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
({
metadata: {
    readonly displayName: "Hello";
    readonly description: "A first mod";
}
metadata
: { displayName: "Hello"displayName: 'Hello', description: "A first mod"description: 'A first mod' },
commands: readonly [Command<string>]commands: [ command<string>(commandValue: CommandInput<string>): Command<string>
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
({
id: stringid: 'hello.greet', title: stringtitle: 'Greet', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { await ctx: CommandContextctx.ui: UiCapability
Notifications, dialogs, quick input, and progress.
ui
.UiCapability.notification: UiNotificationCapabilitynotification.UiNotificationCapability.info(request: UiNotificationRequest): Promise<UiActionResult>info({ UiNotificationRequest.message: stringmessage: 'Hello.' });
}, }), ], keybindings: readonly [Keybinding<string>]keybindings: [ keybinding<string>(input: KeybindingInput<string>): Keybinding<string>
Bind a keyboard shortcut to a command. Pass the `command` value itself when the command lives in the same mod, so the id is written once and a rename cannot break the binding. Pass a string only for a command someone else owns. A binding does not decide when the command is available. Give it the same {@link Condition } you gave the command's `when` so the chord goes quiet in the same situations the palette entry does.
@categoryState
keybinding
({
KeybindingInput<TCommand extends Command | string = string | Command<string>>.id: string
Identifier for the binding itself, distinct from the command it runs.
id
: 'hello.greet.key',
KeybindingInput<string>.command: string
The command value, or the id of a command declared elsewhere.
command
: 'hello.greet',
KeybindingInput<TCommand extends Command | string = string | Command<string>>.key: string
Default chord, for example `'ctrl+shift+u'`.
key
: 'ctrl+alt+g',
KeybindingInput<TCommand extends Command | string = string | Command<string>>.mac?: string | undefined
Chord used on macOS instead of `key`.
mac
: 'cmd+alt+g',
}), ], });

Members

Keybindingid, command, key, mac, win, linux, args, when, systemWide, kind.

  • key is the default chord. mac, win, and linux override it per platform — set them when the natural chord differs, not to restate the same thing three times.
  • args is passed to the command when the chord fires.
  • when gates the binding the same way it gates a command. A chord that is live in the wrong context is worse than no chord.
  • systemWide is the one to think twice about. It takes the chord outside the app, so it competes with every other program the person is running.

The id is a contract

command holds a command id as a string. Nothing type-checks that the target exists at author time, so renaming a command id and forgetting its keybinding leaves a binding pointing at nothing.