155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
import { Button, Panel, Spinner } from "@/shared";
|
|
import { useProjectPlugins } from "./useProjectPlugins";
|
|
|
|
export interface ProjectPluginsPanelProps {
|
|
projectId: string;
|
|
onAssignAgents?: () => void;
|
|
}
|
|
|
|
function countLabel(count: number, singular: string, plural: string): string {
|
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
}
|
|
|
|
export function ProjectPluginsPanel({
|
|
projectId,
|
|
onAssignAgents,
|
|
}: ProjectPluginsPanelProps) {
|
|
const vm = useProjectPlugins(projectId);
|
|
|
|
if (vm.loading && !vm.settings) {
|
|
return (
|
|
<Panel aria-label="plugins du projet" className="flex items-center gap-2">
|
|
<Spinner size={14} />
|
|
<span className="text-sm text-muted">Chargement des plugins du projet…</span>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
const installedIds = new Set(vm.installed.map((plugin) => plugin.id));
|
|
const missing = (vm.settings?.plugins ?? []).filter(
|
|
(activation) => !installedIds.has(activation.pluginId),
|
|
);
|
|
|
|
return (
|
|
<section aria-label="plugins du projet" className="flex flex-col gap-3">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-sm font-semibold text-content">Plugins du projet</h2>
|
|
<p className="mt-1 text-xs text-muted">
|
|
Activez les plugins utilisés par ce projet, puis attribuez leurs skills et
|
|
tools MCP à chaque agent.
|
|
</p>
|
|
</div>
|
|
{onAssignAgents && (
|
|
<Button size="sm" variant="primary" onClick={onAssignAgents}>
|
|
Attribuer aux agents
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{vm.error && <p role="alert" className="text-sm text-danger">{vm.error}</p>}
|
|
|
|
{vm.installed.length === 0 && missing.length === 0 ? (
|
|
<Panel><p className="text-sm text-muted">Aucun plugin installé globalement.</p></Panel>
|
|
) : (
|
|
<ul className="flex flex-col gap-2">
|
|
{vm.installed.map((plugin) => {
|
|
const enabled = vm.settings?.plugins.some(
|
|
(entry) => entry.pluginId === plugin.id && entry.enabled,
|
|
) ?? false;
|
|
const skills = plugin.contributes.skills ?? [];
|
|
const servers = plugin.contributes.mcpServers ?? [];
|
|
return (
|
|
<li key={plugin.id}>
|
|
<Panel className="flex flex-col gap-3">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-semibold text-content">{plugin.displayName}</span>
|
|
<span className="text-xs text-faint">v{plugin.version}</span>
|
|
<span className="rounded bg-raised px-1.5 py-0.5 text-[0.65rem] text-muted">
|
|
Installé globalement
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-xs text-muted">
|
|
{countLabel(skills.length, "skill", "skills")} · {countLabel(servers.length, "serveur MCP", "serveurs MCP")}
|
|
</p>
|
|
</div>
|
|
<label className="flex shrink-0 items-center gap-2 text-xs text-content">
|
|
<input
|
|
type="checkbox"
|
|
checked={enabled}
|
|
disabled={vm.busy}
|
|
onChange={(event) => void vm.setEnabled(plugin.id, event.target.checked)}
|
|
/>
|
|
Activé pour ce projet
|
|
</label>
|
|
</div>
|
|
|
|
<details>
|
|
<summary className="cursor-pointer text-xs font-medium text-primary">
|
|
Voir les capabilities
|
|
</summary>
|
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-content">Skills fournis</h3>
|
|
{skills.length === 0 ? (
|
|
<p className="mt-1 text-xs text-muted">Aucun skill fourni.</p>
|
|
) : (
|
|
<ul className="mt-1 flex flex-col gap-1">
|
|
{skills.map((skill) => (
|
|
<li key={skill.id} className="text-xs text-muted">
|
|
<span className="font-medium text-content">{skill.name}</span>
|
|
{skill.description ? ` — ${skill.description}` : ""}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-content">Tools MCP fournis</h3>
|
|
{servers.length === 0 ? (
|
|
<p className="mt-1 text-xs text-muted">Aucun serveur MCP fourni.</p>
|
|
) : (
|
|
<ul className="mt-1 flex flex-col gap-1">
|
|
{servers.map((server) => (
|
|
<li key={server.id} className="text-xs text-muted">
|
|
<span className="font-medium text-content">{server.displayName}</span>
|
|
<span className="text-faint"> · {server.id}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</Panel>
|
|
</li>
|
|
);
|
|
})}
|
|
|
|
{missing.map((activation) => (
|
|
<li key={activation.pluginId}>
|
|
<Panel className="flex items-center justify-between gap-4 border-warning/40">
|
|
<div>
|
|
<span className="text-sm font-semibold text-content">{activation.pluginId}</span>
|
|
<p className="text-xs text-warning">Plugin manquant · configuration conservée</p>
|
|
</div>
|
|
<label className="flex items-center gap-2 text-xs text-muted">
|
|
<input
|
|
type="checkbox"
|
|
checked={activation.enabled}
|
|
disabled={vm.busy || !activation.enabled}
|
|
onChange={(event) => void vm.setEnabled(activation.pluginId, event.target.checked)}
|
|
/>
|
|
Activé pour ce projet
|
|
</label>
|
|
</Panel>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|