feat(plugins): paramètres projet plugins + attribution skills/tools aux agents — #282 (manifeste accepte contributes.skills IdeA-native multi-harness; store .ideai/project-plugins.json v1: enable plugin par projet + assignments skills/tools par agent; refs + use cases settings/assign/unassign; resolver skills pour idea_skill_read et injection catalogue lancement agent; permissions MCP dynamiques projetées par harness Claude Code/Codex/OpenCode; tombstones uninstall, hot reload recalcule catalogue et marque assignments cassés; frontend: Settings projet Plugins, détail capabilities, attribution agent, catalogue permissions; bump pointeur IdeaSDK 6eb8db3; QA verte: cargo test -p application, project_plugin_settings 3/3, dto_plugins 2/2, structured_launch_d3 26/26, catalogue mcp 7/7, frontend 5 tests ProjectPluginSettings/plugin, npm build frontend)
This commit is contained in:
@ -24,6 +24,8 @@ import type {
|
||||
PluginProjectStructure,
|
||||
PluginReview,
|
||||
PluginRuntimeContributionCatalog,
|
||||
ProjectPluginSettings,
|
||||
ProjectPluginSettingsView,
|
||||
PluginUninstallResult,
|
||||
PluginWorkspaceBinaryFile,
|
||||
PluginWorkspaceDirectoryListing,
|
||||
@ -45,6 +47,8 @@ import type {
|
||||
PluginEventSubscribeInput,
|
||||
PluginEventUnsubscribeInput,
|
||||
PluginGateway,
|
||||
MutateAgentPluginSkillInput,
|
||||
MutateAgentPluginToolInput,
|
||||
PluginStorageGateway,
|
||||
PluginStorageGetInput,
|
||||
PluginStorageSetInput,
|
||||
@ -60,6 +64,7 @@ import type {
|
||||
PluginWorkspaceWriteTextInput,
|
||||
RemoteGateway,
|
||||
ReviewPluginPackageInput,
|
||||
SetProjectPluginEnabledInput,
|
||||
ViewWindowClosed,
|
||||
ViewWindowSnapshot,
|
||||
WindowGateway,
|
||||
@ -191,6 +196,24 @@ export class WebPluginGateway implements PluginGateway {
|
||||
// lets the bootstrap loader run unconditionally without a transport check.
|
||||
return { plugins: [] };
|
||||
}
|
||||
async listProjectSettings(_projectId: string): Promise<ProjectPluginSettingsView> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async setProjectEnabled(_input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async assignAgentSkill(_input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async unassignAgentSkill(_input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async assignAgentTool(_input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async unassignAgentTool(_input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
return unsupportedOnWeb("Project plugin settings");
|
||||
}
|
||||
async openPluginsFolder(_pluginId?: string): Promise<void> {
|
||||
return unsupportedOnWeb("Plugin management");
|
||||
}
|
||||
|
||||
@ -57,6 +57,9 @@ import type {
|
||||
PluginProjectStructureEntry,
|
||||
PluginReview,
|
||||
PluginRuntimeContributionCatalog,
|
||||
ProjectPluginManifest,
|
||||
ProjectPluginSettings,
|
||||
ProjectPluginSettingsView,
|
||||
PluginToolchainDiagnostic,
|
||||
PluginUninstallResult,
|
||||
PluginPublicEvent,
|
||||
@ -138,6 +141,8 @@ import type {
|
||||
PluginEventSubscribeInput,
|
||||
PluginEventUnsubscribeInput,
|
||||
PluginGateway,
|
||||
MutateAgentPluginSkillInput,
|
||||
MutateAgentPluginToolInput,
|
||||
PluginStorageGateway,
|
||||
PluginStorageGetInput,
|
||||
PluginStorageSetInput,
|
||||
@ -155,6 +160,7 @@ import type {
|
||||
ReattachAgentChatResult,
|
||||
RemoteGateway,
|
||||
ReviewPluginPackageInput,
|
||||
SetProjectPluginEnabledInput,
|
||||
SaveOpenCodeProviderProfileInput,
|
||||
SendAgentChatOptions,
|
||||
SkillGateway,
|
||||
@ -4004,6 +4010,8 @@ function isBlanketRule(rule: PermissionSet["rules"][number]) {
|
||||
*/
|
||||
export class MockPluginGateway implements PluginGateway {
|
||||
private plugins: PluginAdmin[] = [];
|
||||
private projectManifests: ProjectPluginManifest[] = [];
|
||||
private projectSettings = new Map<string, ProjectPluginSettings>();
|
||||
private seq = 0;
|
||||
|
||||
private summaryFor(_pluginId: string): PluginContributionSummary {
|
||||
@ -4015,6 +4023,24 @@ export class MockPluginGateway implements PluginGateway {
|
||||
this.plugins.push(plugin);
|
||||
}
|
||||
|
||||
/** Test/dev helper: seed a full manifest for project capability settings. */
|
||||
_seedProjectPluginManifest(manifest: ProjectPluginManifest): void {
|
||||
this.projectManifests.push(structuredClone(manifest));
|
||||
}
|
||||
|
||||
/** Test/dev helper: seed activation/assignment tombstones. */
|
||||
_seedProjectPluginSettings(projectId: string, settings: ProjectPluginSettings): void {
|
||||
this.projectSettings.set(projectId, structuredClone(settings));
|
||||
}
|
||||
|
||||
private settingsFor(projectId: string): ProjectPluginSettings {
|
||||
const existing = this.projectSettings.get(projectId);
|
||||
if (existing) return existing;
|
||||
const created: ProjectPluginSettings = { version: 1, plugins: [], agents: [] };
|
||||
this.projectSettings.set(projectId, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
async listPlugins(): Promise<PluginAdmin[]> {
|
||||
return structuredClone(this.plugins);
|
||||
}
|
||||
@ -4099,6 +4125,68 @@ export class MockPluginGateway implements PluginGateway {
|
||||
return { plugins: [] };
|
||||
}
|
||||
|
||||
async listProjectSettings(projectId: string): Promise<ProjectPluginSettingsView> {
|
||||
return {
|
||||
settings: structuredClone(this.settingsFor(projectId)),
|
||||
installed: structuredClone(this.projectManifests),
|
||||
};
|
||||
}
|
||||
|
||||
async setProjectEnabled(input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings> {
|
||||
if (input.enabled && !this.projectManifests.some((plugin) => plugin.id === input.pluginId)) {
|
||||
throw { code: "NOT_FOUND", message: `plugin ${input.pluginId} not found` } satisfies GatewayError;
|
||||
}
|
||||
const settings = this.settingsFor(input.projectId);
|
||||
const activation = settings.plugins.find((entry) => entry.pluginId === input.pluginId);
|
||||
if (activation) activation.enabled = input.enabled;
|
||||
else settings.plugins.push({ pluginId: input.pluginId, enabled: input.enabled });
|
||||
return structuredClone(settings);
|
||||
}
|
||||
|
||||
private assignmentsFor(projectId: string, agentId: string) {
|
||||
const settings = this.settingsFor(projectId);
|
||||
let assignments = settings.agents.find((entry) => entry.agentId === agentId);
|
||||
if (!assignments) {
|
||||
assignments = { agentId, skills: [], tools: [] };
|
||||
settings.agents.push(assignments);
|
||||
}
|
||||
return assignments;
|
||||
}
|
||||
|
||||
async assignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
const assignments = this.assignmentsFor(input.projectId, input.agentId);
|
||||
if (!assignments.skills.some((ref) => ref.pluginId === input.pluginId && ref.skillId === input.skillId)) {
|
||||
assignments.skills.push({ pluginId: input.pluginId, skillId: input.skillId });
|
||||
}
|
||||
return structuredClone(this.settingsFor(input.projectId));
|
||||
}
|
||||
|
||||
async unassignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
const assignments = this.assignmentsFor(input.projectId, input.agentId);
|
||||
assignments.skills = assignments.skills.filter(
|
||||
(ref) => ref.pluginId !== input.pluginId || ref.skillId !== input.skillId,
|
||||
);
|
||||
return structuredClone(this.settingsFor(input.projectId));
|
||||
}
|
||||
|
||||
async assignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
const assignments = this.assignmentsFor(input.projectId, input.agentId);
|
||||
if (!assignments.tools.some((ref) =>
|
||||
ref.pluginId === input.pluginId && ref.serverId === input.serverId && ref.toolName === input.toolName
|
||||
)) {
|
||||
assignments.tools.push({ pluginId: input.pluginId, serverId: input.serverId, toolName: input.toolName });
|
||||
}
|
||||
return structuredClone(this.settingsFor(input.projectId));
|
||||
}
|
||||
|
||||
async unassignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
const assignments = this.assignmentsFor(input.projectId, input.agentId);
|
||||
assignments.tools = assignments.tools.filter((ref) =>
|
||||
ref.pluginId !== input.pluginId || ref.serverId !== input.serverId || ref.toolName !== input.toolName
|
||||
);
|
||||
return structuredClone(this.settingsFor(input.projectId));
|
||||
}
|
||||
|
||||
async openPluginsFolder(_pluginId?: string): Promise<void> {
|
||||
// No filesystem in the mock — no-op.
|
||||
}
|
||||
|
||||
@ -47,4 +47,39 @@ describe("TauriPluginGateway invoke payloads", () => {
|
||||
expect(review.issues).toEqual([]);
|
||||
expect(review.installable).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the project-plugin command envelopes", async () => {
|
||||
const gateway = new TauriPluginGateway();
|
||||
await gateway.listProjectSettings("project-1");
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_list_project_settings", {
|
||||
projectId: "project-1",
|
||||
});
|
||||
|
||||
const projectInput = { projectId: "project-1", pluginId: "dev.acme.unity", enabled: true };
|
||||
await gateway.setProjectEnabled(projectInput);
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_set_project_enabled", { input: projectInput });
|
||||
|
||||
const skillInput = {
|
||||
projectId: "project-1",
|
||||
agentId: "agent-1",
|
||||
pluginId: "dev.acme.unity",
|
||||
skillId: "unity.debug",
|
||||
};
|
||||
await gateway.assignAgentSkill(skillInput);
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_assign_agent_skill", { input: skillInput });
|
||||
await gateway.unassignAgentSkill(skillInput);
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_unassign_agent_skill", { input: skillInput });
|
||||
|
||||
const toolInput = {
|
||||
projectId: "project-1",
|
||||
agentId: "agent-1",
|
||||
pluginId: "dev.acme.unity",
|
||||
serverId: "unity-tools",
|
||||
toolName: "unity_build",
|
||||
};
|
||||
await gateway.assignAgentTool(toolInput);
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_assign_agent_tool", { input: toolInput });
|
||||
await gateway.unassignAgentTool(toolInput);
|
||||
expect(invoke).toHaveBeenLastCalledWith("plugin_unassign_agent_tool", { input: toolInput });
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,9 +17,17 @@ import type {
|
||||
PluginInstallResult,
|
||||
PluginReview,
|
||||
PluginRuntimeContributionCatalog,
|
||||
ProjectPluginSettings,
|
||||
ProjectPluginSettingsView,
|
||||
PluginUninstallResult,
|
||||
} from "@/domain";
|
||||
import type { PluginGateway, ReviewPluginPackageInput } from "@/ports";
|
||||
import type {
|
||||
MutateAgentPluginSkillInput,
|
||||
MutateAgentPluginToolInput,
|
||||
PluginGateway,
|
||||
ReviewPluginPackageInput,
|
||||
SetProjectPluginEnabledInput,
|
||||
} from "@/ports";
|
||||
|
||||
type TauriPluginReviewDto = Omit<PluginReview, "issues" | "installable"> & {
|
||||
issues?: PluginReview["issues"];
|
||||
@ -66,6 +74,30 @@ export class TauriPluginGateway implements PluginGateway {
|
||||
return invoke<PluginRuntimeContributionCatalog>("plugin_list_runtime_contributions");
|
||||
}
|
||||
|
||||
listProjectSettings(projectId: string): Promise<ProjectPluginSettingsView> {
|
||||
return invoke<ProjectPluginSettingsView>("plugin_list_project_settings", { projectId });
|
||||
}
|
||||
|
||||
setProjectEnabled(input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings> {
|
||||
return invoke<ProjectPluginSettings>("plugin_set_project_enabled", { input });
|
||||
}
|
||||
|
||||
assignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
return invoke<ProjectPluginSettings>("plugin_assign_agent_skill", { input });
|
||||
}
|
||||
|
||||
unassignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
|
||||
return invoke<ProjectPluginSettings>("plugin_unassign_agent_skill", { input });
|
||||
}
|
||||
|
||||
assignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
return invoke<ProjectPluginSettings>("plugin_assign_agent_tool", { input });
|
||||
}
|
||||
|
||||
unassignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
|
||||
return invoke<ProjectPluginSettings>("plugin_unassign_agent_tool", { input });
|
||||
}
|
||||
|
||||
async openPluginsFolder(pluginId?: string): Promise<void> {
|
||||
await invoke("plugin_open_plugins_folder", { pluginId: pluginId ?? null });
|
||||
}
|
||||
|
||||
@ -1856,6 +1856,8 @@ export type PluginLifecycleState =
|
||||
|
||||
/** Counts of what a plugin declares, shown in the admin list (carnet §5). */
|
||||
export interface PluginContributionSummary {
|
||||
/** Agent skills shipped by the plugin. Absent on older backend DTOs. */
|
||||
skills?: number;
|
||||
topLevelMenus: number;
|
||||
menuItems: number;
|
||||
layouts: number;
|
||||
@ -1917,6 +1919,8 @@ export interface PluginUninstallResult {
|
||||
|
||||
/** Contribution declarations for one plugin, as consumed by the runtime loader. */
|
||||
export interface PluginContributionDto {
|
||||
/** Read-only, package-relative skills exposed to project agents. */
|
||||
skills?: PluginSkillContribution[];
|
||||
menus: PluginTopLevelMenuContribution[];
|
||||
menuItems: PluginMenuItemContribution[];
|
||||
slashCommands?: PluginSlashCommandContribution[];
|
||||
@ -1924,6 +1928,66 @@ export interface PluginContributionDto {
|
||||
mcpServers: PluginMcpServerSummary[];
|
||||
}
|
||||
|
||||
/** One agent skill declared by an IdeA plugin manifest. */
|
||||
export interface PluginSkillContribution {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
kind: SkillKind;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** Validated plugin manifest returned by the project-plugin settings catalogue. */
|
||||
export interface ProjectPluginManifest {
|
||||
ideaPluginManifestVersion: number;
|
||||
id: string;
|
||||
displayName: string;
|
||||
publisher?: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
engineIdea?: string;
|
||||
main: string;
|
||||
icon?: string;
|
||||
trustLevel: "full";
|
||||
capabilities: Array<"ui" | "mcp" | "tooling">;
|
||||
activationScope: "app" | "project";
|
||||
contributes: PluginContributionDto;
|
||||
}
|
||||
|
||||
export interface ProjectPluginActivation {
|
||||
pluginId: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface PluginSkillRef {
|
||||
pluginId: string;
|
||||
skillId: string;
|
||||
}
|
||||
|
||||
export interface PluginToolRef {
|
||||
pluginId: string;
|
||||
serverId: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export interface AgentPluginAssignments {
|
||||
agentId: string;
|
||||
skills: PluginSkillRef[];
|
||||
tools: PluginToolRef[];
|
||||
}
|
||||
|
||||
export interface ProjectPluginSettings {
|
||||
version: number;
|
||||
plugins: ProjectPluginActivation[];
|
||||
agents: AgentPluginAssignments[];
|
||||
}
|
||||
|
||||
/** Project settings plus the currently installed global plugin manifests. */
|
||||
export interface ProjectPluginSettingsView {
|
||||
settings: ProjectPluginSettings;
|
||||
installed: ProjectPluginManifest[];
|
||||
}
|
||||
|
||||
/** One entry of `plugin_list_runtime_contributions` (mirror of `PluginRuntimePluginDto`). */
|
||||
export interface PluginRuntimePlugin {
|
||||
id: string;
|
||||
@ -2266,5 +2330,11 @@ export interface CustomPluginLayoutCell {
|
||||
export interface PluginMcpServerSummary {
|
||||
id: string;
|
||||
displayName: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Array<[string, string]>;
|
||||
cwd?: string;
|
||||
transport?: "stdio";
|
||||
autoStart?: boolean;
|
||||
allowAbsoluteCommand?: boolean;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import { useEffect, useState } from "react";
|
||||
import { Button, Input, Panel, SmallDropdown, Spinner, cn } from "@/shared";
|
||||
import { TerminalView } from "@/features/terminals/TerminalView";
|
||||
import { AnnouncementsPreview } from "@/features/announcements";
|
||||
import { AgentPluginCapabilities } from "@/features/plugins/AgentPluginCapabilities";
|
||||
import { useDrift } from "@/features/templates/useDrift";
|
||||
import { templateProfileLabel } from "@/features/templates/templatePresentation";
|
||||
import { useGateways } from "@/app/di";
|
||||
@ -737,6 +738,16 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAgent && (
|
||||
<div className="border-t border-border p-4">
|
||||
<AgentPluginCapabilities
|
||||
projectId={projectId}
|
||||
agentId={selectedAgent.id}
|
||||
agentName={selectedAgent.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Context editor ── */}
|
||||
{selectedAgent && (
|
||||
<div className="flex flex-col gap-2 border-t border-border p-4">
|
||||
|
||||
148
frontend/src/features/plugins/AgentPluginCapabilities.tsx
Normal file
148
frontend/src/features/plugins/AgentPluginCapabilities.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Button, Input, Panel, SmallDropdown, Spinner } from "@/shared";
|
||||
import type { PluginSkillRef, PluginToolRef } from "@/domain";
|
||||
import { useProjectPlugins } from "./useProjectPlugins";
|
||||
|
||||
interface AgentPluginCapabilitiesProps {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
}
|
||||
|
||||
function skillKey(ref: PluginSkillRef): string {
|
||||
return `${ref.pluginId}:${ref.skillId}`;
|
||||
}
|
||||
|
||||
function toolKey(ref: PluginToolRef): string {
|
||||
return `${ref.pluginId}:${ref.serverId}:${ref.toolName}`;
|
||||
}
|
||||
|
||||
export function AgentPluginCapabilities({
|
||||
projectId,
|
||||
agentId,
|
||||
agentName,
|
||||
}: AgentPluginCapabilitiesProps) {
|
||||
const vm = useProjectPlugins(projectId);
|
||||
const [serverChoice, setServerChoice] = useState("");
|
||||
const [toolName, setToolName] = useState("");
|
||||
const assignments = vm.settings?.agents.find((entry) => entry.agentId === agentId);
|
||||
const assignedSkills = new Set((assignments?.skills ?? []).map(skillKey));
|
||||
const assignedTools = assignments?.tools ?? [];
|
||||
const enabledIds = new Set(
|
||||
(vm.settings?.plugins ?? []).filter((entry) => entry.enabled).map((entry) => entry.pluginId),
|
||||
);
|
||||
const enabledPlugins = vm.installed.filter((plugin) => enabledIds.has(plugin.id));
|
||||
const serverOptions = useMemo(
|
||||
() => enabledPlugins.flatMap((plugin) =>
|
||||
(plugin.contributes.mcpServers ?? []).map((server) => ({
|
||||
value: `${plugin.id}\u0000${server.id}`,
|
||||
label: `${plugin.displayName} — ${server.displayName}`,
|
||||
})),
|
||||
),
|
||||
[enabledPlugins],
|
||||
);
|
||||
|
||||
const knownSkillKeys = new Set(vm.installed.flatMap((plugin) =>
|
||||
(plugin.contributes.skills ?? []).map((skill) => `${plugin.id}:${skill.id}`),
|
||||
));
|
||||
const knownServerKeys = new Set(vm.installed.flatMap((plugin) =>
|
||||
(plugin.contributes.mcpServers ?? []).map((server) => `${plugin.id}:${server.id}`),
|
||||
));
|
||||
const staleSkills = (assignments?.skills ?? []).filter((ref) => !knownSkillKeys.has(skillKey(ref)));
|
||||
const staleTools = assignedTools.filter(
|
||||
(ref) => !knownServerKeys.has(`${ref.pluginId}:${ref.serverId}`),
|
||||
);
|
||||
|
||||
async function assignTool() {
|
||||
const [pluginId, serverId] = serverChoice.split("\u0000");
|
||||
const name = toolName.trim();
|
||||
if (!pluginId || !serverId || !name) return;
|
||||
await vm.setToolAssigned({ agentId, pluginId, serverId, toolName: name }, true);
|
||||
setToolName("");
|
||||
}
|
||||
|
||||
if (vm.loading && !vm.settings) {
|
||||
return <div className="flex items-center gap-2 text-xs text-muted"><Spinner size={12} /> Chargement des capabilities de plugins…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{vm.error && <p role="alert" className="text-xs text-danger">{vm.error}</p>}
|
||||
|
||||
<section aria-label={`Skills de plugins — ${agentName}`} className="flex flex-col gap-2">
|
||||
<h4 className="text-sm font-semibold text-content">Skills de plugins</h4>
|
||||
{enabledPlugins.every((plugin) => (plugin.contributes.skills ?? []).length === 0) ? (
|
||||
<p className="text-xs text-muted">Aucun skill fourni par les plugins activés pour ce projet.</p>
|
||||
) : (
|
||||
enabledPlugins.map((plugin) => {
|
||||
const skills = plugin.contributes.skills ?? [];
|
||||
if (skills.length === 0) return null;
|
||||
return (
|
||||
<Panel key={plugin.id} className="flex flex-col gap-2">
|
||||
<h5 className="text-xs font-semibold text-content">{plugin.displayName}</h5>
|
||||
{skills.map((skill) => {
|
||||
const assigned = assignedSkills.has(`${plugin.id}:${skill.id}`);
|
||||
return (
|
||||
<label key={skill.id} className="flex items-start gap-2 text-xs text-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assigned}
|
||||
disabled={vm.busy}
|
||||
onChange={(event) => void vm.setSkillAssigned(
|
||||
agentId, plugin.id, skill.id, event.target.checked,
|
||||
)}
|
||||
/>
|
||||
<span><span className="font-medium">{skill.name}</span>{skill.description ? ` — ${skill.description}` : ""}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</Panel>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{staleSkills.map((ref) => (
|
||||
<div key={skillKey(ref)} className="flex items-center justify-between gap-2 rounded border border-warning/40 px-3 py-2 text-xs">
|
||||
<span>{ref.pluginId} · {ref.skillId} <span className="text-warning">— Obsolète ou manquant</span></span>
|
||||
<Button size="sm" variant="ghost" disabled={vm.busy} onClick={() => void vm.setSkillAssigned(agentId, ref.pluginId, ref.skillId, false)}>Retirer</Button>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section aria-label={`Tools MCP de plugins — ${agentName}`} className="flex flex-col gap-2">
|
||||
<h4 className="text-sm font-semibold text-content">Tools MCP de plugins</h4>
|
||||
{assignedTools.length === 0 ? (
|
||||
<p className="text-xs text-muted">Aucun tool MCP de plugin attribué à l’agent.</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{assignedTools.map((ref) => {
|
||||
const stale = staleTools.some((candidate) => toolKey(candidate) === toolKey(ref));
|
||||
return (
|
||||
<li key={toolKey(ref)} className="flex items-center justify-between gap-2 rounded bg-raised px-3 py-2 text-xs text-content">
|
||||
<span>{ref.pluginId} · {ref.serverId} · <span className="font-medium">{ref.toolName}</span>{stale && <span className="text-warning"> — Obsolète ou manquant</span>}</span>
|
||||
<Button size="sm" variant="ghost" disabled={vm.busy} onClick={() => void vm.setToolAssigned({ agentId, ...ref }, false)}>Retirer</Button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{serverOptions.length > 0 && (
|
||||
<div className="flex items-end gap-2">
|
||||
<SmallDropdown
|
||||
aria-label="serveur MCP de plugin"
|
||||
value={serverChoice}
|
||||
onChange={setServerChoice}
|
||||
disabled={vm.busy}
|
||||
className="min-w-0 flex-1"
|
||||
size="md"
|
||||
options={[{ value: "", label: "— choisir un serveur MCP —" }, ...serverOptions]}
|
||||
/>
|
||||
<Input aria-label="nom du tool MCP" value={toolName} onChange={(event) => setToolName(event.target.value)} placeholder="Nom du tool" disabled={vm.busy} />
|
||||
<Button variant="primary" disabled={vm.busy || !serverChoice || !toolName.trim()} onClick={() => void assignTool()}>Attribuer</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -15,6 +15,7 @@ import { Button, Panel } from "@/shared";
|
||||
import { PluginConfirmDialog as ConfirmDialog } from "./PluginConfirmDialog";
|
||||
import { usePluginRuntime } from "./PluginRuntimeProvider";
|
||||
import { usePlugins } from "./usePlugins";
|
||||
import { ProjectPluginsPanel } from "./ProjectPluginsPanel";
|
||||
|
||||
const STATE_LABEL: Record<PluginLifecycleState, string> = {
|
||||
enabled: "Activé",
|
||||
@ -35,7 +36,11 @@ interface InstallFlow {
|
||||
review: PluginReview;
|
||||
}
|
||||
|
||||
export function PluginsPanel() {
|
||||
export interface PluginsPanelProps {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function PluginsPanel({ projectId }: PluginsPanelProps = {}) {
|
||||
const vm = usePlugins();
|
||||
const pluginRuntime = usePluginRuntime();
|
||||
const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);
|
||||
@ -87,8 +92,19 @@ export function PluginsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{projectId ? (
|
||||
<ProjectPluginsPanel projectId={projectId} />
|
||||
) : (
|
||||
<Panel>
|
||||
<p className="text-sm text-muted">
|
||||
Ouvrez un projet pour configurer les plugins utilisés par ce projet.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-content">Plugins</h2>
|
||||
<h2 className="text-sm font-semibold text-content">Installation globale</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => void startInstallFromArchive()} disabled={vm.busy}>
|
||||
Installer depuis une archive…
|
||||
@ -103,6 +119,7 @@ export function PluginsPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(vm.error || installFlowError) && (
|
||||
<p role="alert" className="text-sm text-danger">
|
||||
|
||||
105
frontend/src/features/plugins/ProjectPluginSettings.test.tsx
Normal file
105
frontend/src/features/plugins/ProjectPluginSettings.test.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { createMockGateways, MockPluginGateway } from "@/adapters/mock";
|
||||
import type { ProjectPluginManifest } from "@/domain";
|
||||
import { AgentPluginCapabilities } from "./AgentPluginCapabilities";
|
||||
import { ProjectPluginsPanel } from "./ProjectPluginsPanel";
|
||||
|
||||
function manifest(): ProjectPluginManifest {
|
||||
return {
|
||||
ideaPluginManifestVersion: 1,
|
||||
id: "dev.acme.unity",
|
||||
displayName: "Unity Plugin",
|
||||
publisher: "Acme",
|
||||
version: "1.2.3",
|
||||
main: "dist/index.js",
|
||||
trustLevel: "full",
|
||||
capabilities: ["ui", "mcp"],
|
||||
activationScope: "app",
|
||||
contributes: {
|
||||
skills: [{
|
||||
id: "unity.debug",
|
||||
name: "unity-debug",
|
||||
description: "Diagnostiquer les builds Unity.",
|
||||
kind: "workflow",
|
||||
path: "skills/unity-debug/SKILL.md",
|
||||
}],
|
||||
menus: [],
|
||||
menuItems: [],
|
||||
layouts: [],
|
||||
mcpServers: [{ id: "unity-tools", displayName: "Unity Tools", transport: "stdio" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gatewaysWithPlugin() {
|
||||
const plugin = new MockPluginGateway();
|
||||
plugin._seedProjectPluginManifest(manifest());
|
||||
return { ...createMockGateways(), plugin };
|
||||
}
|
||||
|
||||
describe("project plugin settings #282", () => {
|
||||
it("lists globally installed plugins, details their capabilities and toggles project activation", async () => {
|
||||
const gateways = gatewaysWithPlugin();
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<ProjectPluginsPanel projectId="project-1" />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Unity Plugin")).toBeTruthy();
|
||||
expect(screen.getByText("Installé globalement")).toBeTruthy();
|
||||
fireEvent.click(screen.getByText("Voir les capabilities"));
|
||||
expect(screen.getByText("unity-debug")).toBeTruthy();
|
||||
expect(screen.getByText("Unity Tools")).toBeTruthy();
|
||||
|
||||
const toggle = screen.getByRole("checkbox", { name: "Activé pour ce projet" });
|
||||
fireEvent.click(toggle);
|
||||
await waitFor(() => expect((toggle as HTMLInputElement).checked).toBe(true));
|
||||
expect((await gateways.plugin.listProjectSettings("project-1")).settings.plugins).toEqual([
|
||||
{ pluginId: "dev.acme.unity", enabled: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("assigns plugin skills and MCP tools to an agent and keeps stale references removable", async () => {
|
||||
const gateways = gatewaysWithPlugin();
|
||||
gateways.plugin._seedProjectPluginSettings("project-1", {
|
||||
version: 1,
|
||||
plugins: [{ pluginId: "dev.acme.unity", enabled: true }],
|
||||
agents: [{
|
||||
agentId: "agent-1",
|
||||
skills: [{ pluginId: "missing.plugin", skillId: "removed-skill" }],
|
||||
tools: [],
|
||||
}],
|
||||
});
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentPluginCapabilities projectId="project-1" agentId="agent-1" agentName="Dev" />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
const skill = await screen.findByRole("checkbox", { name: /unity-debug/ });
|
||||
fireEvent.click(skill);
|
||||
await waitFor(() => expect((skill as HTMLInputElement).checked).toBe(true));
|
||||
expect(screen.getByText(/removed-skill/).textContent).toContain("Obsolète ou manquant");
|
||||
|
||||
fireEvent.click(screen.getByLabelText("serveur MCP de plugin"));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Unity Plugin — Unity Tools" }));
|
||||
fireEvent.change(screen.getByLabelText("nom du tool MCP"), {
|
||||
target: { value: "unity_build" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Attribuer" }));
|
||||
expect(await screen.findByText("unity_build")).toBeTruthy();
|
||||
|
||||
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
||||
const agent = settings.agents.find((entry) => entry.agentId === "agent-1");
|
||||
expect(agent?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" });
|
||||
expect(agent?.tools).toContainEqual({
|
||||
pluginId: "dev.acme.unity",
|
||||
serverId: "unity-tools",
|
||||
toolName: "unity_build",
|
||||
});
|
||||
});
|
||||
});
|
||||
143
frontend/src/features/plugins/ProjectPluginsPanel.tsx
Normal file
143
frontend/src/features/plugins/ProjectPluginsPanel.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
export { PluginRuntimeProvider, usePluginRuntime, type PluginRuntimeContextValue } from "./PluginRuntimeProvider";
|
||||
export { PluginsPanel } from "./PluginsPanel";
|
||||
export { ProjectPluginsPanel } from "./ProjectPluginsPanel";
|
||||
export { AgentPluginCapabilities } from "./AgentPluginCapabilities";
|
||||
export { usePlugins, type PluginsViewModel } from "./usePlugins";
|
||||
export { resolveMenuItems, resolveTopLevelMenus, type ResolvedTopLevelMenu } from "./menus";
|
||||
export { usePluginMenus, type UsePluginMenusResult } from "./usePluginMenus";
|
||||
|
||||
105
frontend/src/features/plugins/useProjectPlugins.ts
Normal file
105
frontend/src/features/plugins/useProjectPlugins.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useGateways } from "@/app/di";
|
||||
import type {
|
||||
GatewayError,
|
||||
ProjectPluginManifest,
|
||||
ProjectPluginSettings,
|
||||
} from "@/domain";
|
||||
import type { MutateAgentPluginToolInput } from "@/ports";
|
||||
|
||||
function describe(error: unknown): string {
|
||||
if (error && typeof error === "object" && "message" in error) {
|
||||
return String((error as GatewayError).message);
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export interface ProjectPluginsViewModel {
|
||||
installed: ProjectPluginManifest[];
|
||||
settings: ProjectPluginSettings | null;
|
||||
loading: boolean;
|
||||
busy: boolean;
|
||||
error: string | null;
|
||||
refresh(): Promise<void>;
|
||||
setEnabled(pluginId: string, enabled: boolean): Promise<void>;
|
||||
setSkillAssigned(agentId: string, pluginId: string, skillId: string, assigned: boolean): Promise<void>;
|
||||
setToolAssigned(input: Omit<MutateAgentPluginToolInput, "projectId">, assigned: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export function useProjectPlugins(projectId: string): ProjectPluginsViewModel {
|
||||
const { plugin } = useGateways();
|
||||
const [installed, setInstalled] = useState<ProjectPluginManifest[]>([]);
|
||||
const [settings, setSettings] = useState<ProjectPluginSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const view = await plugin.listProjectSettings(projectId);
|
||||
setInstalled(view.installed);
|
||||
setSettings(view.settings);
|
||||
} catch (cause) {
|
||||
setError(describe(cause));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [plugin, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const mutate = useCallback(async (operation: () => Promise<ProjectPluginSettings>) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setSettings(await operation());
|
||||
} catch (cause) {
|
||||
setError(describe(cause));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setEnabled = useCallback(
|
||||
(pluginId: string, enabled: boolean) =>
|
||||
mutate(() => plugin.setProjectEnabled({ projectId, pluginId, enabled })),
|
||||
[mutate, plugin, projectId],
|
||||
);
|
||||
|
||||
const setSkillAssigned = useCallback(
|
||||
(agentId: string, pluginId: string, skillId: string, assigned: boolean) => {
|
||||
const input = { projectId, agentId, pluginId, skillId };
|
||||
return mutate(() =>
|
||||
assigned ? plugin.assignAgentSkill(input) : plugin.unassignAgentSkill(input),
|
||||
);
|
||||
},
|
||||
[mutate, plugin, projectId],
|
||||
);
|
||||
|
||||
const setToolAssigned = useCallback(
|
||||
(input: Omit<MutateAgentPluginToolInput, "projectId">, assigned: boolean) => {
|
||||
const scoped = { projectId, ...input };
|
||||
return mutate(() =>
|
||||
assigned ? plugin.assignAgentTool(scoped) : plugin.unassignAgentTool(scoped),
|
||||
);
|
||||
},
|
||||
[mutate, plugin, projectId],
|
||||
);
|
||||
|
||||
return {
|
||||
installed,
|
||||
settings,
|
||||
loading,
|
||||
busy,
|
||||
error,
|
||||
refresh,
|
||||
setEnabled,
|
||||
setSkillAssigned,
|
||||
setToolAssigned,
|
||||
};
|
||||
}
|
||||
@ -908,6 +908,7 @@ export function ProjectsView() {
|
||||
// while the menu bar above stays visible.
|
||||
<SettingsView
|
||||
section={settingsSection}
|
||||
projectId={active?.id}
|
||||
onSectionChange={setSettingsSection}
|
||||
onClose={() => setSettingsSection(null)}
|
||||
/>
|
||||
|
||||
@ -48,12 +48,14 @@ export const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
|
||||
interface SettingsViewProps {
|
||||
section: SettingsSection;
|
||||
projectId?: string;
|
||||
onSectionChange: (section: SettingsSection) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SettingsView({
|
||||
section,
|
||||
projectId,
|
||||
onSectionChange,
|
||||
onClose,
|
||||
}: SettingsViewProps) {
|
||||
@ -97,7 +99,7 @@ export function SettingsView({
|
||||
) : section === "deployment" ? (
|
||||
<DeploymentSettings />
|
||||
) : section === "plugins" ? (
|
||||
<PluginsPanel />
|
||||
<PluginsPanel projectId={projectId} />
|
||||
) : (
|
||||
// No `onSessionEnded`: the desktop app hosts the server and is never
|
||||
// itself a paired device, so it cannot revoke its own session.
|
||||
|
||||
@ -218,6 +218,7 @@ function layoutTypesFromContributes(contributes: PluginContributionDto): Set<str
|
||||
function normalizeContributes(entry: PluginRuntimePlugin): PluginContributionDto {
|
||||
const contributes = objectOrEmpty(entry.contributes) as Partial<PluginContributionDto>;
|
||||
return {
|
||||
skills: arrayOrEmpty(contributes?.skills),
|
||||
menus: arrayOrEmpty(contributes?.menus),
|
||||
menuItems: arrayOrEmpty(contributes?.menuItems),
|
||||
slashCommands: arrayOrEmpty(contributes?.slashCommands),
|
||||
|
||||
@ -59,6 +59,8 @@ import type {
|
||||
PluginProjectStructure,
|
||||
PluginReview,
|
||||
PluginRuntimeContributionCatalog,
|
||||
ProjectPluginSettings,
|
||||
ProjectPluginSettingsView,
|
||||
PluginSourceKind,
|
||||
PluginUninstallResult,
|
||||
PluginWorkspaceBinaryFile,
|
||||
@ -1549,6 +1551,27 @@ export interface ReviewPluginPackageInput {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface SetProjectPluginEnabledInput {
|
||||
projectId: string;
|
||||
pluginId: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MutateAgentPluginSkillInput {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
pluginId: string;
|
||||
skillId: string;
|
||||
}
|
||||
|
||||
export interface MutateAgentPluginToolInput {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
pluginId: string;
|
||||
serverId: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin system gateway (ticket #43, F1) — admin CRUD + the bootstrap catalog
|
||||
* the runtime loader consumes. Mirrors the Tauri commands in carnet §5
|
||||
@ -1562,6 +1585,12 @@ export interface PluginGateway {
|
||||
setEnabled(pluginId: string, enabled: boolean): Promise<PluginAdmin>;
|
||||
uninstall(pluginId: string): Promise<PluginUninstallResult>;
|
||||
listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog>;
|
||||
listProjectSettings(projectId: string): Promise<ProjectPluginSettingsView>;
|
||||
setProjectEnabled(input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings>;
|
||||
assignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings>;
|
||||
unassignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings>;
|
||||
assignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings>;
|
||||
unassignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings>;
|
||||
openPluginsFolder(pluginId?: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user