feat(frontend): surface MCP tool permissions per agent — Permissions panel (#82 lot UX/F)
Adds the "Tools MCP IdeA" tab to the project Permissions panel, alongside the existing "Système" (file/command) tab. Lets the user grant/revoke MCP tool capabilities per agent or project-wide default, grouped by domain (Lecture projet, Lecture tickets, Délégation agents, Contexte et mémoire, Tickets, Travail et exécution, Skills) instead of a flat 25-checkbox list, per the UX conception in carnet #82. - domain/ports/adapters (Tauri, HTTP, mock): wire get_mcp_tool_permissions, update_project_mcp_tool_permissions, update_agent_mcp_tool_permissions (already merged backend API, #82 lots B1-B4) onto PermissionGateway. - useMcpToolPermissions: view-model owning the durable MCP tool policy document, distinct from the file/command permissions in usePermissions. - McpToolPermissionsPanel: target selector (Défaut projet + agents with Hérité/Override badges) and grouped editor — inherited agents are read-only until "Créer un override" (prefilled with the effective allowlist), per-row Ajouté/Retiré diffing against the project default, inline confirmation before granting a write tool at project-default level, and unsaved-draft protection on target change. - mcpToolGroups.ts: presentational-only domain grouping and short French labels — the read/write classification itself always comes from the backend-provided catalogue, never hardcoded here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { Button, Panel, Spinner, cn } from "@/shared";
|
||||
import type { PermissionSet, PermissionPosture } from "@/domain";
|
||||
import { McpToolPermissionsPanel } from "./McpToolPermissionsPanel";
|
||||
import {
|
||||
type CapabilityChoice,
|
||||
type PolicyDraft,
|
||||
@ -14,6 +15,13 @@ export interface PermissionsPanelProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
type PermissionsTab = "system" | "mcpTools";
|
||||
|
||||
const TABS: { id: PermissionsTab; label: string }[] = [
|
||||
{ id: "system", label: "Système" },
|
||||
{ id: "mcpTools", label: "Tools MCP IdeA" },
|
||||
];
|
||||
|
||||
type EditorTarget =
|
||||
| { type: "project" }
|
||||
| { type: "agent"; agentId: string; agentName: string };
|
||||
@ -37,6 +45,7 @@ const POSTURE_LABELS: Record<PermissionPosture, string> = {
|
||||
export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
const vm = usePermissions(projectId);
|
||||
const [target, setTarget] = useState<EditorTarget>({ type: "project" });
|
||||
const [tab, setTab] = useState<PermissionsTab>("system");
|
||||
|
||||
const selectedAgent = target.type === "agent"
|
||||
? vm.rows.find((row) => row.agent.id === target.agentId) ?? null
|
||||
@ -79,9 +88,39 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
className="flex flex-col"
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
flush
|
||||
>
|
||||
<div role="tablist" aria-label="Permissions" className="flex gap-1 border-b border-border px-4 pt-2">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`permissions-tab-${t.id}`}
|
||||
aria-selected={tab === t.id}
|
||||
aria-controls={`permissions-tabpanel-${t.id}`}
|
||||
tabIndex={tab === t.id ? 0 : -1}
|
||||
onClick={() => setTab(t.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
|
||||
e.preventDefault();
|
||||
const i = TABS.findIndex((x) => x.id === tab);
|
||||
const next = e.key === "ArrowRight" ? (i + 1) % TABS.length : (i - 1 + TABS.length) % TABS.length;
|
||||
setTab(TABS[next].id);
|
||||
}}
|
||||
className={cn(
|
||||
"min-h-[32px] rounded-t-md border-b-2 px-3 py-1.5 text-sm font-medium transition-colors",
|
||||
tab === t.id
|
||||
? "border-primary text-content"
|
||||
: "border-transparent text-muted hover:text-content",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{vm.error && (
|
||||
<p
|
||||
role="alert"
|
||||
@ -91,7 +130,13 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="permissions-tabpanel-system"
|
||||
aria-labelledby="permissions-tab-system"
|
||||
hidden={tab !== "system"}
|
||||
className="flex flex-col gap-4 p-4"
|
||||
>
|
||||
<PolicyCard
|
||||
title="Project defaults"
|
||||
subtitle={hasProjectDefaults ? "Configured" : "Native CLI behavior"}
|
||||
@ -158,6 +203,18 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
onClear={() => void handleClear()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="permissions-tabpanel-mcpTools"
|
||||
aria-labelledby="permissions-tab-mcpTools"
|
||||
hidden={tab !== "mcpTools"}
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
{tab === "mcpTools" && (
|
||||
<McpToolPermissionsPanel projectId={projectId} agents={vm.agents} />
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user