fix(orchestrator,plugins): contact cross-project corrélé + découvrabilité assignation plugin par agent — #284 (backend: registre des demandes actives clé projet/agent appelant + correlationId, refus corrélation vide/dupliquée, cancel routé après correspondance exacte sans redirection vers une autre cible, appelant projeté Waiting→Done dans le live-state de son propre projet avec correlationId exposé par idea_workstate_read, gardes RAII réconcilient l'abandon du futur, tools ask/cancel WriteAction deny-by-default dont le refus indique où activer la permission; frontend: action Attribuer aux agents depuis Settings projet > Plugins ouvrant le panneau Agents flottant/ancré/détaché, section Capabilities de plugins — {agent} explicitant attribution skills et tools MCP des plugins activés; QA verte: orchestrator_service 79/79, mcp_server 40/40, domain live_state 15/15, correlated_update_is_preserved_in_the_lean_projection 1/1, frontend ProjectPluginSettings 3 tests, npm run build frontend, cargo fmt --check, git diff --check; AppImage IdeA_0.3.0_amd64.AppImage rebuildée workflow build-appimage, validation live cross-project restant après remplacement/relance manuels; réserves: registre corrélation en mémoire non persisté, surface web unsupported, catalogue dynamique tools MCP = réserve #282)

This commit is contained in:
2026-09-08 17:35:04 +02:00
parent df5728a50a
commit 8bfcb3a534
13 changed files with 604 additions and 35 deletions

View File

@ -68,10 +68,20 @@ export function AgentPluginCapabilities({
return (
<div className="flex flex-col gap-4">
<div>
<h4 className="text-sm font-semibold text-content">
Capabilities de plugins — {agentName}
</h4>
<p className="mt-1 text-xs text-muted">
Attribuez à cet agent les skills et tools MCP fournis par les plugins activés
dans les paramètres du projet.
</p>
</div>
{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>
<h5 className="text-sm font-semibold text-content">Skills de plugins</h5>
{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>
) : (
@ -80,7 +90,7 @@ export function AgentPluginCapabilities({
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>
<h6 className="text-xs font-semibold text-content">{plugin.displayName}</h6>
{skills.map((skill) => {
const assigned = assignedSkills.has(`${plugin.id}:${skill.id}`);
return (
@ -110,7 +120,7 @@ export function AgentPluginCapabilities({
</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>
<h5 className="text-sm font-semibold text-content">Tools MCP de plugins</h5>
{assignedTools.length === 0 ? (
<p className="text-xs text-muted">Aucun tool MCP de plugin attribué à l’agent.</p>
) : (

View File

@ -38,9 +38,10 @@ interface InstallFlow {
export interface PluginsPanelProps {
projectId?: string;
onAssignAgents?: () => void;
}
export function PluginsPanel({ projectId }: PluginsPanelProps = {}) {
export function PluginsPanel({ projectId, onAssignAgents }: PluginsPanelProps = {}) {
const vm = usePlugins();
const pluginRuntime = usePluginRuntime();
const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);
@ -93,7 +94,7 @@ export function PluginsPanel({ projectId }: PluginsPanelProps = {}) {
return (
<div className="flex flex-col gap-4">
{projectId ? (
<ProjectPluginsPanel projectId={projectId} />
<ProjectPluginsPanel projectId={projectId} onAssignAgents={onAssignAgents} />
) : (
<Panel>
<p className="text-sm text-muted">

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { DIProvider } from "@/app/di";
import { createMockGateways, MockPluginGateway } from "@/adapters/mock";
@ -63,6 +63,22 @@ describe("project plugin settings #282", () => {
]);
});
it("offers an obvious entry point to assign plugin capabilities to agents", async () => {
const gateways = gatewaysWithPlugin();
const onAssignAgents = vi.fn();
render(
<DIProvider gateways={gateways}>
<ProjectPluginsPanel
projectId="project-1"
onAssignAgents={onAssignAgents}
/>
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Attribuer aux agents" }));
expect(onAssignAgents).toHaveBeenCalledOnce();
});
it("assigns plugin skills and MCP tools to an agent and keeps stale references removable", async () => {
const gateways = gatewaysWithPlugin();
gateways.plugin._seedProjectPluginSettings("project-1", {
@ -80,6 +96,7 @@ describe("project plugin settings #282", () => {
</DIProvider>,
);
expect(await screen.findByText("Capabilities de plugins — Dev")).toBeTruthy();
const skill = await screen.findByRole("checkbox", { name: /unity-debug/ });
fireEvent.click(skill);
await waitFor(() => expect((skill as HTMLInputElement).checked).toBe(true));

View File

@ -1,15 +1,19 @@
import { Panel, Spinner } from "@/shared";
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 }: ProjectPluginsPanelProps) {
export function ProjectPluginsPanel({
projectId,
onAssignAgents,
}: ProjectPluginsPanelProps) {
const vm = useProjectPlugins(projectId);
if (vm.loading && !vm.settings) {
@ -28,12 +32,19 @@ export function ProjectPluginsPanel({ projectId }: ProjectPluginsPanelProps) {
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 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>}

View File

@ -910,6 +910,12 @@ export function ProjectsView() {
section={settingsSection}
projectId={active?.id}
onSectionChange={setSettingsSection}
onAssignPluginCapabilities={() => {
setSettingsSection(null);
if (placementOf(placements, "agents") === "closed") {
setPlacement("agents", "floating");
}
}}
onClose={() => setSettingsSection(null)}
/>
) : active ? (

View File

@ -50,6 +50,7 @@ interface SettingsViewProps {
section: SettingsSection;
projectId?: string;
onSectionChange: (section: SettingsSection) => void;
onAssignPluginCapabilities?: () => void;
onClose: () => void;
}
@ -57,6 +58,7 @@ export function SettingsView({
section,
projectId,
onSectionChange,
onAssignPluginCapabilities,
onClose,
}: SettingsViewProps) {
return (
@ -99,7 +101,10 @@ export function SettingsView({
) : section === "deployment" ? (
<DeploymentSettings />
) : section === "plugins" ? (
<PluginsPanel projectId={projectId} />
<PluginsPanel
projectId={projectId}
onAssignAgents={onAssignPluginCapabilities}
/>
) : (
// No `onSessionEnded`: the desktop app hosts the server and is never
// itself a paired device, so it cannot revoke its own session.