import { useEffect, useMemo, useState } from "react"; import { Button, Panel, Spinner, cn } from "@/shared"; import type { PermissionSet, PermissionPosture } from "@/domain"; import { type CapabilityChoice, type PolicyDraft, draftFromSet, isCustomPolicy, usePermissions, } from "./usePermissions"; export interface PermissionsPanelProps { projectId: string; } type EditorTarget = | { type: "project" } | { type: "agent"; agentId: string; agentName: string }; const CAPABILITY_ROWS: { key: keyof Omit; label: string; }[] = [ { key: "read", label: "Read" }, { key: "write", label: "Write" }, { key: "delete", label: "Delete" }, { key: "executeBash", label: "Bash" }, ]; const POSTURE_LABELS: Record = { ask: "Ask", allow: "Allow", deny: "Deny", }; export function PermissionsPanel({ projectId }: PermissionsPanelProps) { const vm = usePermissions(projectId); const [target, setTarget] = useState({ type: "project" }); const selectedAgent = target.type === "agent" ? vm.rows.find((row) => row.agent.id === target.agentId) ?? null : null; const activePolicy = selectedAgent?.override ?? vm.document?.projectDefaults ?? null; const activeDraft = target.type === "project" ? vm.projectDraft : draftFromSet(selectedAgent?.override ?? vm.document?.projectDefaults ?? null); const activeCustom = target.type === "project" ? isCustomPolicy(vm.document?.projectDefaults) : isCustomPolicy(selectedAgent?.override); const hasProjectDefaults = vm.document?.projectDefaults != null; async function handleSave(draft: PolicyDraft) { if (target.type === "project") { await vm.saveProjectDefaults(draft); } else { await vm.saveAgentOverride(target.agentId, draft); } } async function handleClear() { if (target.type === "project") { await vm.clearProjectDefaults(); } else { await vm.clearAgentOverride(target.agentId); } } return ( void vm.refresh()} > Refresh } className="flex flex-col" flush > {vm.error && (

{vm.error}

)}
setTarget({ type: "project" })} policy={vm.document?.projectDefaults ?? null} />

Agents

{vm.busy && }
{vm.rows.length === 0 ? (

No agents yet.

) : (
    {vm.rows.map((row) => { const active = target.type === "agent" && target.agentId === row.agent.id; return (
  • setTarget({ type: "agent", agentId: row.agent.id, agentName: row.agent.name, }) } policy={row.override} />
  • ); })}
)}
void handleSave(draft)} onClear={() => void handleClear()} />
); } interface PolicyCardProps { title: string; subtitle: string; active: boolean; custom: boolean; policy: PermissionSet | null; onSelect: () => void; } function PolicyCard({ title, subtitle, active, custom, policy, onSelect, }: PolicyCardProps) { return ( ); } interface PermissionEditorProps { title: string; draft: PolicyDraft; sourcePolicy: PermissionSet | null; custom: boolean; canClear: boolean; busy: boolean; onSave: (draft: PolicyDraft) => void; onClear: () => void; } function PermissionEditor({ title, draft, sourcePolicy, custom, canClear, busy, onSave, onClear, }: PermissionEditorProps) { const [local, setLocal] = useState(draft); useEffect(() => { setLocal(draft); }, [draft]); const changed = useMemo( () => JSON.stringify(local) !== JSON.stringify(draft), [draft, local], ); function setCapability( capability: keyof Omit, choice: CapabilityChoice, ) { setLocal((prev) => ({ ...prev, [capability]: choice })); } return (

{title}

{sourcePolicy ? `${sourcePolicy.rules.length} rules` : "Not configured"}

{custom && ( Advanced )}
{CAPABILITY_ROWS.map((row) => (
{row.label} setCapability(row.key, choice)} />
))}
); } interface SegmentedChoiceProps { value: CapabilityChoice; disabled: boolean; label: string; onChange: (choice: CapabilityChoice) => void; } function SegmentedChoice({ value, disabled, label, onChange, }: SegmentedChoiceProps) { const choices: { value: CapabilityChoice; label: string }[] = [ { value: "none", label: "None" }, { value: "allow", label: "Allow" }, { value: "deny", label: "Deny" }, ]; return (
{choices.map((choice) => ( ))}
); }