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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user