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

@ -98,7 +98,7 @@ describe("loadPlugins", () => {
);
expect(failures).toEqual([]);
expect((globalThis as Record<string, unknown>).__registerError).toMatch(
/not declared by any menu item/,
/not declared by any command contribution/,
);
});
@ -591,6 +591,13 @@ describe("loadPlugins", () => {
command: "hello-plugin",
},
],
slashCommands: [
{
name: "/hello",
shortDescription: "Run the hello-plugin callback",
command: "hello-plugin",
},
],
layouts: [
{
type: "hello-plugin.hello-world",
@ -613,6 +620,13 @@ describe("loadPlugins", () => {
},
]);
expect(registry.get("com.example.hello-plugin")?.contributes.mcpServers).toEqual([]);
expect(registry.get("com.example.hello-plugin")?.contributes.slashCommands).toEqual([
{
name: "/hello",
shortDescription: "Run the hello-plugin callback",
command: "hello-plugin",
},
]);
await registry.runCommand("com.example.hello-plugin", "hello-plugin");
expect((globalThis as Record<string, unknown>).__helloArchiveCommandRan).toBe(true);
const Layout = registry.layoutComponent(
@ -623,6 +637,39 @@ describe("loadPlugins", () => {
expect((Layout as unknown as () => string)()).toBe("hello-world");
});
it("allows command callbacks declared only by slashCommands", async () => {
const bundle = dataUrl(`
export function activate(ctx) {
ctx.commands.registerCommand("dev.acme.explain", () => {
globalThis.__slashOnlyCommandRan = true;
});
}
`);
const { registry, failures } = await loadPlugins(
[
entry({
id: "dev.acme.slash",
displayName: "Slash",
bundleUrl: bundle,
contributes: {
slashCommands: [
{
name: "/explain",
shortDescription: "Explain selection",
command: "dev.acme.explain",
},
],
} as unknown as PluginContributionDto,
}),
],
gateways,
);
expect(failures).toEqual([]);
await registry.runCommand("dev.acme.slash", "dev.acme.explain");
expect((globalThis as Record<string, unknown>).__slashOnlyCommandRan).toBe(true);
});
it("confines a malformed runtime catalog entry and still loads healthy plugins", async () => {
const bundle = dataUrl(`
export function activate(ctx) {