/** * `PluginsPanel` — the `Paramètres > Plugins` admin surface (ticket #43, F2). * * Pure presentation; all behaviour comes from {@link usePlugins}. List of * installed plugins (name/publisher/version/source/state/restartRequired/ * trust), install from archive/directory with a pre-install review step * (manifest summary + explicit full-trust mention), enable/disable/uninstall * with confirmation, and readable `pending`/`invalid`/error states. */ import { useState } from "react"; import type { PluginAdmin, PluginLifecycleState, PluginReview } from "@/domain"; import { Button, Panel } from "@/shared"; import { PluginConfirmDialog as ConfirmDialog } from "./PluginConfirmDialog"; import { usePluginRuntime } from "./PluginRuntimeProvider"; import { usePlugins } from "./usePlugins"; const STATE_LABEL: Record = { enabled: "Activé", disabled: "Désactivé", "pending-enable": "Activation en attente", "pending-disable": "Désactivation en attente", "pending-uninstall": "Désinstallation en attente", invalid: "Invalide", }; type PendingAction = | { kind: "disable"; plugin: PluginAdmin } | { kind: "uninstall"; plugin: PluginAdmin }; interface InstallFlow { sourceKind: "archive" | "directory"; path: string; review: PluginReview; } export function PluginsPanel() { const vm = usePlugins(); const pluginRuntime = usePluginRuntime(); const [pendingAction, setPendingAction] = useState(null); const [installFlow, setInstallFlow] = useState(null); const [installFlowError, setInstallFlowError] = useState(null); async function startInstallFromArchive() { setInstallFlowError(null); const path = await vm.pickArchiveFile(); if (!path) return; const review = await vm.reviewArchive(path); if (!review) { setInstallFlowError("La revue du paquet a échoué."); return; } setInstallFlow({ sourceKind: "archive", path, review }); } async function startInstallFromDirectory() { setInstallFlowError(null); const path = await vm.pickDirectory(); if (!path) return; const review = await vm.reviewDirectory(path); if (!review) { setInstallFlowError("La revue du paquet a échoué."); return; } setInstallFlow({ sourceKind: "directory", path, review }); } async function confirmInstall() { if (!installFlow) return; const ok = installFlow.sourceKind === "archive" ? await vm.installFromArchive(installFlow.path) : await vm.installFromDirectory(installFlow.path); if (ok) setInstallFlow(null); } async function confirmPendingAction() { if (!pendingAction) return; if (pendingAction.kind === "disable") { await vm.setEnabled(pendingAction.plugin.id, false); } else { await vm.uninstall(pendingAction.plugin.id); } setPendingAction(null); } return (

Plugins

{(vm.error || installFlowError) && (

{vm.error ?? installFlowError}

)} {pluginRuntime.failures.length > 0 && (

Certains plugins installés n'ont pas pu être chargés.

    {pluginRuntime.failures.map((failure) => (
  • {failure.pluginId} :{" "} {failure.reason}
  • ))}
)} {vm.plugins.length === 0 ? (

Aucun plugin installé.

) : (
    {vm.plugins.map((p) => (
  • {p.displayName} v{p.version} {p.publisher && · {p.publisher}} full-trust
    {STATE_LABEL[p.lifecycleState]} {p.restartRequired && ( Redémarrage requis )} {p.sourceLabel && · {p.sourceLabel}}
    {p.error && (

    {p.error}

    )}
    {p.lifecycleState === "invalid" ? ( ) : p.enabled ? ( ) : ( )}
  • ))}
)} {installFlow && ( { if (isInstallable(installFlow.review)) void confirmInstall(); else setInstallFlow(null); }} onCancel={() => setInstallFlow(null)} /> )} {pendingAction && ( void confirmPendingAction()} onCancel={() => setPendingAction(null)} /> )}
); } function reviewSummary(review: PluginReview): string { const { contributionSummary: c } = review; const parts = [ `Éditeur : ${review.publisher ?? "inconnu"}`, `Version : ${review.version}`, review.description ?? null, `Confiance : full-trust — ce plugin peut exécuter du code arbitraire dans IdeA.`, `Contributions : ${c.topLevelMenus} menu(s), ${c.menuItems} item(s), ${c.layouts} layout(s), ${c.mcpServers} serveur(s) MCP.`, ...(review.issues ?? []).map( (i) => `${i.severity === "error" ? "Erreur" : "Avertissement"} : ${i.message}`, ), ].filter((p): p is string => Boolean(p)); return parts.join("\n"); } function isInstallable(review: PluginReview): boolean { return review.installable ?? true; }