144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import { Panel, Spinner } from "@/shared";
|
||
import { useProjectPlugins } from "./useProjectPlugins";
|
||
|
||
export interface ProjectPluginsPanelProps {
|
||
projectId: string;
|
||
}
|
||
|
||
function countLabel(count: number, singular: string, plural: string): string {
|
||
return `${count} ${count === 1 ? singular : plural}`;
|
||
}
|
||
|
||
export function ProjectPluginsPanel({ projectId }: 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>
|
||
<h2 className="text-sm font-semibold text-content">Plugins du projet</h2>
|
||
<p className="mt-1 text-xs text-muted">
|
||
Un plugin reste installé globalement. Activez-le ici avant d’attribuer ses
|
||
capabilities aux agents de ce projet.
|
||
</p>
|
||
</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>
|
||
);
|
||
}
|