feat(cli): dispatch du callback plugin slash-command vers le runtime registry — #166 (QA verte)

Boucle le gap end-to-end de #165 : à l'exécution d'une commande slash plugin
depuis le composer custom, l'UI traite l'effet pluginCallback et le dispatche
au PluginRuntimeRegistry pour que la callback du plugin s'exécute réellement.

- runtime/registry: runCommandStrict (échec explicite si handler absent) +
  propagation de la valeur de retour ; élargit les types de retour (unknown).
- runtime/loader: chargement de contributes.slashCommands au registre +
  propagation du retour du handler.
- features/plugins/usePluginMenus: runCommand retourne unknown.
- features/agents/CustomAgentChatView: handler de l'effet pluginCallback ->
  pluginRuntime.registry.runCommandStrict, avec retour utilisateur en cas
  d'échec (commande indisponible / callback absente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:47:04 +02:00
parent fbaca9d5cc
commit b0be5f04e4
6 changed files with 341 additions and 10 deletions

View File

@ -59,7 +59,7 @@ export interface Disposable {
dispose(): void;
}
export type PluginCommandHandler = (...args: unknown[]) => void | Promise<void>;
export type PluginCommandHandler = (...args: unknown[]) => unknown | Promise<unknown>;
/** Commands a plugin registers in `activate(ctx)`, dispatched by menu items. */
export class PluginCommandRegistry {
@ -74,7 +74,7 @@ export class PluginCommandRegistry {
if (!this.declaredCommandIds.has(commandId)) {
throw new Error(
`plugin "${this.pluginId}" tried to register command "${commandId}" ` +
"which is not declared by any menu item in its manifest",
"which is not declared by any command contribution in its manifest",
);
}
this.handlers.set(commandId, handler);
@ -86,11 +86,11 @@ export class PluginCommandRegistry {
}
/** Runs a registered command; a no-op (never throws) if none is registered. */
async run(commandId: string, ...args: unknown[]): Promise<void> {
async run(commandId: string, ...args: unknown[]): Promise<unknown> {
const handler = this.handlers.get(commandId);
if (!handler) return;
try {
await handler(...args);
return await handler(...args);
} catch (e) {
console.error(
`[plugin:${this.pluginId}] command "${commandId}" failed`,
@ -99,6 +99,17 @@ export class PluginCommandRegistry {
}
}
/** Runs a registered command and surfaces failures to the caller. */
async runStrict(commandId: string, ...args: unknown[]): Promise<unknown> {
const handler = this.handlers.get(commandId);
if (!handler) {
throw new Error(
`plugin "${this.pluginId}" has no registered command handler "${commandId}"`,
);
}
return await handler(...args);
}
has(commandId: string): boolean {
return this.handlers.has(commandId);
}
@ -256,7 +267,19 @@ export class PluginRuntimeRegistry {
);
}
async runCommand(pluginId: string, commandId: string, ...args: unknown[]): Promise<void> {
await this.loaded.get(pluginId)?.commands.run(commandId, ...args);
async runCommand(pluginId: string, commandId: string, ...args: unknown[]): Promise<unknown> {
return await this.loaded.get(pluginId)?.commands.run(commandId, ...args);
}
async runCommandStrict(
pluginId: string,
commandId: string,
...args: unknown[]
): Promise<unknown> {
const plugin = this.loaded.get(pluginId);
if (!plugin) {
throw new Error(`plugin "${pluginId}" is not loaded`);
}
return await plugin.commands.runStrict(commandId, ...args);
}
}