feat(agents,plugins): surface unifiée Capacités de l'agent — skills et tools MCP natifs + plugins #296 (domain: PluginMcpToolMetadata dans PluginMcpServerContribution, validation hôte alignée SDK — name requis non vide, description optionnelle non vide, doublons rejetés; application: McpToolPermissionCatalogue.vue structurée additive tools {name, description, access read|writeAction} enrichie des descriptions canoniques infrastructure orchestrator mcp tools, use case SetAgentMcpToolAllowed — toggle atomique idempotente d'un tool natif dans l'allowlist explicite d'un agent, refus unknown tool sans mutation, overrides vides retirés; backend DTO: catalogue.tools additive + SetAgentMcpToolAllowedRequestDto, wire camelCase; app-tauri + web-server: commande set_agent_mcp_tool_allowed; frontend: AgentPluginCapabilities devient la surface unifiée — skills natifs assign/unassign, tools natifs via setAgentMcpToolAllowed (catalogue structuré avec fallback legacy), skills/tools plugins avec metadata statiques mcpServers[].tools et saisie manuelle fallback pour serveurs sans catalogue, tombstones indisponibles, bannière redémarrage requis si agent actif; AgentsPanel retire l'ancienne section Skills séparée au profit de la surface unifiée; ProjectPluginsPanel recentré enable/disable avec renvoi Capacités des agents; adapters Tauri/HTTP/mock + permission.test.ts; QA verte reconfirmée par Git: application mcp_tool_permissions 5/5 + plugin::tests 37/37, backend ciblés 3/3 + checks domain/application/backend, app-tauri dto 33/33 + dto_agents 30/30 + dto_plugins 2/2, check web-server, npm test 35/35 + 65/65, npm run build; suites complètes rouges classées non bloquantes par QA: web-server parallèle too many open files + 7 PTY série passant individuellement, frontend 32 échecs sur 6 fichiers web non modifiés reproduits isolément)
This commit is contained in:
@ -68,50 +68,6 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
void templateGw.listTemplates().then(setTemplates).catch(() => {});
|
||||
}
|
||||
|
||||
// Skills available for assignment (global + project), loaded lazily once.
|
||||
const skillGw = gateways.skill ?? null;
|
||||
const [skills, setSkills] = useState<import("@/domain").Skill[]>([]);
|
||||
const [skillsLoaded, setSkillsLoaded] = useState(false);
|
||||
const [skillToAssign, setSkillToAssign] = useState("");
|
||||
|
||||
async function refreshSkills() {
|
||||
if (!skillGw) return;
|
||||
try {
|
||||
const [globals, projects] = await Promise.all([
|
||||
skillGw.listSkills(projectId, "global"),
|
||||
skillGw.listSkills(projectId, "project"),
|
||||
]);
|
||||
setSkills([...projects, ...globals]);
|
||||
} catch {
|
||||
// Skills are optional — leave the list empty on failure.
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillsLoaded && skillGw) {
|
||||
setSkillsLoaded(true);
|
||||
void refreshSkills();
|
||||
}
|
||||
|
||||
async function handleAssignSkill() {
|
||||
if (!skillGw || !vm.selectedAgentId || !skillToAssign) return;
|
||||
const target = skills.find((s) => s.id === skillToAssign);
|
||||
if (!target) return;
|
||||
await skillGw.assignSkill(
|
||||
projectId,
|
||||
vm.selectedAgentId,
|
||||
target.id,
|
||||
target.scope,
|
||||
);
|
||||
setSkillToAssign("");
|
||||
await vm.refresh();
|
||||
}
|
||||
|
||||
async function handleUnassignSkill(skillId: string) {
|
||||
if (!skillGw || !vm.selectedAgentId) return;
|
||||
await skillGw.unassignSkill(projectId, vm.selectedAgentId, skillId);
|
||||
await vm.refresh();
|
||||
}
|
||||
|
||||
// Context editor state — local copy before Save
|
||||
const [editedContext, setEditedContext] = useState(vm.context);
|
||||
|
||||
@ -668,82 +624,16 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Skill assignment ── */}
|
||||
{selectedAgent && skillGw && (
|
||||
<div className="flex flex-col gap-2 border-t border-border p-4">
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
Skills — {selectedAgent.name}
|
||||
</h4>
|
||||
|
||||
{/* Assigned skills */}
|
||||
{selectedAgent.skills.length === 0 ? (
|
||||
<p className="text-xs text-muted">No skills assigned.</p>
|
||||
) : (
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{selectedAgent.skills.map((ref) => {
|
||||
const name =
|
||||
skills.find((s) => s.id === ref.skillId)?.name ?? ref.skillId;
|
||||
return (
|
||||
<li
|
||||
key={ref.skillId}
|
||||
className="flex items-center gap-1 rounded-full bg-raised px-2 py-0.5 text-xs text-content"
|
||||
>
|
||||
<span>{name}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`unassign ${name}`}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void handleUnassignSkill(ref.skillId)}
|
||||
className="text-faint hover:text-danger"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Assign selector */}
|
||||
<div className="flex items-end gap-2">
|
||||
<SmallDropdown
|
||||
aria-label="skill to assign"
|
||||
value={skillToAssign}
|
||||
onChange={setSkillToAssign}
|
||||
disabled={vm.busy}
|
||||
className="min-w-0 flex-1"
|
||||
size="md"
|
||||
options={[
|
||||
{ value: "", label: "— assign a skill —" },
|
||||
...skills
|
||||
.filter(
|
||||
(s) =>
|
||||
!selectedAgent.skills.some((r) => r.skillId === s.id),
|
||||
)
|
||||
.map((s) => ({
|
||||
value: s.id,
|
||||
label: `${s.name} (${s.scope})`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="assign skill"
|
||||
disabled={vm.busy || !skillToAssign}
|
||||
onClick={() => void handleAssignSkill()}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAgent && (
|
||||
{selectedAgent && gateways.plugin && (
|
||||
<div className="border-t border-border p-4">
|
||||
<AgentPluginCapabilities
|
||||
projectId={projectId}
|
||||
agentId={selectedAgent.id}
|
||||
agentName={selectedAgent.name}
|
||||
agent={selectedAgent}
|
||||
active={
|
||||
activeAgentId === selectedAgent.id ||
|
||||
vm.liveAgents.some((live) => live.agentId === selectedAgent.id)
|
||||
}
|
||||
onAgentChanged={vm.refresh}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user