feat(manifest): un plugin peut contribuer des commandes slash via callback — #165 (QA verte)
Étend le manifeste et la validation SDK : un plugin déclare des commandes slash (contributes.slashCommands) adossées à une callback (command id enregistré via ctx.commands.registerCommand). Métadonnées UI exposées : name, shortDescription, requiresConfirmation, when. Exemple hello-plugin mis à jour avec une commande /hello. Ces commandes transitent ensuite par le registry/contrat unifié (#162). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,14 +5,20 @@ background work.
|
|||||||
|
|
||||||
## Contract
|
## Contract
|
||||||
|
|
||||||
Every human menu click follows this sequence:
|
Every menu click or plugin slash command follows this sequence:
|
||||||
|
|
||||||
```text
|
```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 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 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
|
- A launched process is represented by a background task returned from
|
||||||
`ctx.services.tasks.runCommand()`.
|
`ctx.services.tasks.runCommand()`.
|
||||||
- A skipped command is represented by the command handler return value and logs,
|
- A skipped command is represented by the command handler return value and logs,
|
||||||
|
|||||||
@ -32,6 +32,14 @@
|
|||||||
"order": 10
|
"order": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"slashCommands": [
|
||||||
|
{
|
||||||
|
"name": "/hello",
|
||||||
|
"shortDescription": "Run the hello-plugin callback",
|
||||||
|
"command": "hello-plugin",
|
||||||
|
"requiresConfirmation": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"layouts": [
|
"layouts": [
|
||||||
{
|
{
|
||||||
"type": "hello-plugin.hello-world",
|
"type": "hello-plugin.hello-world",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export interface IdeAPluginManifest {
|
|||||||
contributes?: {
|
contributes?: {
|
||||||
menus?: IdeAPluginTopLevelMenuContribution[];
|
menus?: IdeAPluginTopLevelMenuContribution[];
|
||||||
menuItems?: IdeAPluginMenuItemContribution[];
|
menuItems?: IdeAPluginMenuItemContribution[];
|
||||||
|
slashCommands?: IdeAPluginSlashCommandContribution[];
|
||||||
layouts?: IdeAPluginLayoutContribution[];
|
layouts?: IdeAPluginLayoutContribution[];
|
||||||
mcpServers?: IdeAPluginMcpServerContribution[];
|
mcpServers?: IdeAPluginMcpServerContribution[];
|
||||||
};
|
};
|
||||||
@ -47,6 +48,19 @@ export interface IdeAPluginMenuItemContribution {
|
|||||||
when?: string;
|
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 {
|
export interface IdeAPluginLayoutContribution {
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -196,6 +210,16 @@ function validateContributes(value: unknown, errors: string[]): void {
|
|||||||
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
||||||
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
|
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) => {
|
validateArray(value, "layouts", errors, (layout, index) => {
|
||||||
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
||||||
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
||||||
|
|||||||
Reference in New Issue
Block a user