feat(plugins): seam slash-commands contribuées par plugin via callback — #165 (QA verte)

Permet à un plugin de contribuer des commandes slash adossées à une callback,
transitant par le registry/contrat unifié (#162) :

- sdk/IdeaSDK: manifeste contributes.slashCommands (déclaratif) — pointer bumpé.
- domain: source Plugin étendue + effet PluginCallback (la commande ne
  s'exécute pas directement : elle renvoie une référence à la callback du plugin).
- application: registration des commandes plugin dans le registry + exécution
  renvoyant l'effet PluginCallback ; métadonnées UI suffisantes pour l'autocomplete.
- backend + app-tauri: DTO + commandes Tauri listant/invoquant les commandes plugin.
- frontend (contrat): types adapters/domain/ports/mock pour pluginCallback et
  l'invocation avec arguments.

L'exécution effective de la callback côté runtime UI est livrée par #166.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:46:52 +02:00
parent ff7696b724
commit fbaca9d5cc
17 changed files with 514 additions and 34 deletions

View File

@ -248,7 +248,7 @@ describe("TauriAgentGateway invoke payloads", () => {
});
expect(invoke).toHaveBeenCalledWith("execute_slash_command", {
request: { name: "/clean", sessionId: "chat-session-1" },
request: { name: "/clean", sessionId: "chat-session-1", arguments: [] },
});
expect(out.effect.kind).toBe("cleanConversation");
});

View File

@ -286,12 +286,13 @@ export class TauriAgentGateway implements AgentGateway {
async executeSlashCommand(
name: string,
options: { sessionId?: string | null } = {},
options: { sessionId?: string | null; arguments?: unknown[] } = {},
): Promise<ExecuteSlashCommandResult> {
return invoke<ExecuteSlashCommandResult>("execute_slash_command", {
request: {
name,
sessionId: options.sessionId ?? null,
arguments: options.arguments ?? [],
},
});
}

View File

@ -906,7 +906,7 @@ export class MockAgentGateway implements AgentGateway {
async executeSlashCommand(
name: string,
options: { sessionId?: string | null } = {},
options: { sessionId?: string | null; arguments?: unknown[] } = {},
): Promise<ExecuteSlashCommandResult> {
const normalized = name.trim().startsWith("/") ? name.trim() : `/${name.trim()}`;
const command = this.slashCommands.find((item) => item.name === normalized);
@ -951,6 +951,18 @@ export class MockAgentGateway implements AgentGateway {
},
};
}
if (command.plugin) {
return {
command: structuredClone(command),
effect: {
kind: "pluginCallback",
pluginId: command.plugin.pluginId,
commandId: command.plugin.commandId,
sessionId: options.sessionId ?? undefined,
arguments: structuredClone(options.arguments ?? []),
},
};
}
return {
command: structuredClone(command),
effect: {

View File

@ -1698,12 +1698,23 @@ export interface SlashCommand {
availability: SlashCommandAvailability;
source: SlashCommandSource;
native?: NativeSlashCommand;
plugin?: {
pluginId: string;
commandId: string;
};
}
export type SlashCommandEffect =
| { kind: "help"; commands: SlashCommand[] }
| { kind: "cleanConversation"; sessionId: string; clearedChunks: number }
| { kind: "profileSwitch"; sessionId: string };
| { kind: "profileSwitch"; sessionId: string }
| {
kind: "pluginCallback";
pluginId: string;
commandId: string;
sessionId?: string;
arguments: unknown[];
};
export interface ExecuteSlashCommandResult {
command: SlashCommand;
@ -1844,6 +1855,7 @@ export interface PluginUninstallResult {
export interface PluginContributionDto {
menus: PluginTopLevelMenuContribution[];
menuItems: PluginMenuItemContribution[];
slashCommands?: PluginSlashCommandContribution[];
layouts: PluginLayoutContribution[];
mcpServers: PluginMcpServerSummary[];
}
@ -2125,6 +2137,15 @@ export interface PluginMenuItemContribution {
when?: string;
}
/** Manifest declaration of a plugin-backed slash command. */
export interface PluginSlashCommandContribution {
name: string;
shortDescription: string;
command: string;
requiresConfirmation?: boolean;
when?: string;
}
/** A menu item resolved for rendering — enablement already evaluated (carnet §7.2). */
export interface ResolvedPluginMenuItem {
id: string;

View File

@ -315,7 +315,7 @@ export interface AgentGateway {
/** Executes a selected slash command and returns the planned UI effect. */
executeSlashCommand?(
name: string,
options?: { sessionId?: string | null },
options?: { sessionId?: string | null; arguments?: unknown[] },
): Promise<ExecuteSlashCommandResult>;
/** Interrupts only the current turn of a live structured session. */
cancelAgentChat?(sessionId: string): Promise<void>;