diff --git a/docs/commands-and-feedback.md b/docs/commands-and-feedback.md index 51be5d5..e30b327 100644 --- a/docs/commands-and-feedback.md +++ b/docs/commands-and-feedback.md @@ -5,14 +5,20 @@ background work. ## Contract -Every human menu click follows this sequence: +Every menu click or plugin slash command follows this sequence: ```text -menu item -> command id -> registered command handler -> optional task -> feedback surfaces +manifest contribution -> command id -> registered command handler -> optional task -> feedback surfaces ``` - A manifest menu item declares a `command` id; it does not run tools directly. +- A manifest slash command declares a slash `name`, autocomplete metadata and a + `command` id; it does not run tools directly. +- Menu items and slash commands may share the same `command` id, or point to + different handlers. The plugin owns that choice. - The command handler is the only place that decides whether work should start. +- The host slash-command registry only lists/filters metadata and returns a + callback dispatch effect. The plugin handler decides what the command does. - A launched process is represented by a background task returned from `ctx.services.tasks.runCommand()`. - A skipped command is represented by the command handler return value and logs, diff --git a/examples/hello-plugin/idea-plugin.json b/examples/hello-plugin/idea-plugin.json index cfc7dd7..6e088d0 100644 --- a/examples/hello-plugin/idea-plugin.json +++ b/examples/hello-plugin/idea-plugin.json @@ -32,6 +32,14 @@ "order": 10 } ], + "slashCommands": [ + { + "name": "/hello", + "shortDescription": "Run the hello-plugin callback", + "command": "hello-plugin", + "requiresConfirmation": false + } + ], "layouts": [ { "type": "hello-plugin.hello-world", diff --git a/src/manifest.ts b/src/manifest.ts index c3d4e6d..126aa30 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -16,6 +16,7 @@ export interface IdeAPluginManifest { contributes?: { menus?: IdeAPluginTopLevelMenuContribution[]; menuItems?: IdeAPluginMenuItemContribution[]; + slashCommands?: IdeAPluginSlashCommandContribution[]; layouts?: IdeAPluginLayoutContribution[]; mcpServers?: IdeAPluginMcpServerContribution[]; }; @@ -47,6 +48,19 @@ export interface IdeAPluginMenuItemContribution { when?: string; } +export interface IdeAPluginSlashCommandContribution { + /** Slash name shown in autocomplete. Must start with "/". */ + name: string; + /** Short autocomplete/help description. */ + shortDescription: string; + /** Command callback id registered through ctx.commands.registerCommand(). */ + command: string; + /** Ask for host confirmation before dispatching the callback. */ + requiresConfirmation?: boolean; + /** Reserved declarative condition for host-side availability. */ + when?: string; +} + export interface IdeAPluginLayoutContribution { type: string; label: string; @@ -196,6 +210,16 @@ function validateContributes(value: unknown, errors: string[]): void { optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`); optionalString(item, "when", errors, `contributes.menuItems[${index}].when`); }); + validateArray(value, "slashCommands", errors, (command, index) => { + requireString(command, "name", errors, `contributes.slashCommands[${index}].name`); + requireString(command, "shortDescription", errors, `contributes.slashCommands[${index}].shortDescription`); + requireString(command, "command", errors, `contributes.slashCommands[${index}].command`); + if (typeof command.name === "string" && !command.name.startsWith("/")) { + errors.push(`contributes.slashCommands[${index}].name must start with "/"`); + } + optionalBoolean(command, "requiresConfirmation", errors, `contributes.slashCommands[${index}].requiresConfirmation`); + optionalString(command, "when", errors, `contributes.slashCommands[${index}].when`); + }); validateArray(value, "layouts", errors, (layout, index) => { requireString(layout, "type", errors, `contributes.layouts[${index}].type`); requireString(layout, "label", errors, `contributes.layouts[${index}].label`);