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:
@ -26,6 +26,7 @@ import type {
|
||||
SlashCommand,
|
||||
} from "@/domain";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
||||
import { Button, Spinner, cn } from "@/shared";
|
||||
import type { ChatAttachmentInput } from "@/ports";
|
||||
|
||||
@ -66,6 +67,11 @@ interface ProfileCommandDialogState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface SlashCommandInvocation {
|
||||
name: string;
|
||||
arguments: unknown[];
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
@ -274,6 +280,17 @@ function slashCommandPrefix(draft: string): string | null {
|
||||
return draft;
|
||||
}
|
||||
|
||||
function slashCommandInvocation(text: string): SlashCommandInvocation | null {
|
||||
if (!text.startsWith("/")) return null;
|
||||
const [name, ...argumentParts] = text.split(/\s+/);
|
||||
if (!name) return null;
|
||||
const rawArguments = argumentParts.join(" ").trim();
|
||||
return {
|
||||
name,
|
||||
arguments: rawArguments ? [rawArguments] : [],
|
||||
};
|
||||
}
|
||||
|
||||
function isSlashCommandAvailable(command: SlashCommand): boolean {
|
||||
return command.availability.status === "available";
|
||||
}
|
||||
@ -328,6 +345,7 @@ export function CustomAgentChatView({
|
||||
onConversationId,
|
||||
}: CustomAgentChatViewProps) {
|
||||
const { agent, profile: profileGateway, system } = useGateways();
|
||||
const pluginRuntime = usePluginRuntime();
|
||||
const [turns, setTurns] = useState<ChatTurn[]>([]);
|
||||
const [currentSession, setCurrentSession] = useState(sessionId);
|
||||
const [externalSessionId, setExternalSessionId] = useState(sessionId);
|
||||
@ -756,6 +774,45 @@ export function CustomAgentChatView({
|
||||
}
|
||||
}
|
||||
|
||||
async function executeSlashCommand(invocation: SlashCommandInvocation) {
|
||||
if (!agent.executeSlashCommand) {
|
||||
setError(`Commande ${invocation.name} indisponible dans ce runtime.`);
|
||||
return;
|
||||
}
|
||||
setDraft("");
|
||||
setError(null);
|
||||
try {
|
||||
const sid =
|
||||
currentSession ??
|
||||
(await recoverStructuredSession({
|
||||
applyScrollback: false,
|
||||
retryAttachNotFound: true,
|
||||
}));
|
||||
const result = await agent.executeSlashCommand(invocation.name, {
|
||||
sessionId: sid,
|
||||
arguments: invocation.arguments,
|
||||
});
|
||||
if (result.effect.kind !== "pluginCallback") {
|
||||
throw new Error(
|
||||
`La commande ${invocation.name} n'a pas renvoyé d'effet pluginCallback.`,
|
||||
);
|
||||
}
|
||||
await pluginRuntime.registry.runCommandStrict(
|
||||
result.effect.pluginId,
|
||||
result.effect.commandId,
|
||||
...result.effect.arguments,
|
||||
);
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{ role: "tool", label: `Commande plugin ${result.command.name} exécutée.` },
|
||||
]);
|
||||
} catch (e) {
|
||||
const message = `Commande ${invocation.name} échouée: ${describe(e)}`;
|
||||
setError(message);
|
||||
setTurns((prev) => [...prev, { role: "error", text: message }]);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickAttachment() {
|
||||
const path = await system.pickFile();
|
||||
if (path) {
|
||||
@ -804,6 +861,11 @@ export function CustomAgentChatView({
|
||||
await openProfileCommandFlow();
|
||||
return;
|
||||
}
|
||||
const slashInvocation = slashCommandInvocation(text);
|
||||
if (slashInvocation) {
|
||||
await executeSlashCommand(slashInvocation);
|
||||
return;
|
||||
}
|
||||
const outgoingAttachments = attachments;
|
||||
const attachmentInputs = outgoingAttachments.map((item) => item.input);
|
||||
const attachmentLabels = outgoingAttachments.map((item) => item.label);
|
||||
|
||||
Reference in New Issue
Block a user