import type { PermissionContext, PermissionRule } from '@agentscope-ai/agentscope/permission'; import { Ban, CircleHelp, FolderOpen, ShieldCheck, ShieldX } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { PanelEmpty } from '@/components/panel/PanelEmpty'; import { Badge } from '@/components/ui/badge'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useTranslation } from '@/i18n/useI18n'; /** The behavior keys we render sections for (excludes ``passthrough``). */ type RuleBehavior = 'allow' | 'deny' | 'ask'; interface PermissionPanelProps { /** * The permission context to render. Pass ``null`` when no data is * available yet (renders an empty state). */ permissionContext: PermissionContext | null; } /** i18n key suffix for each behavior group title. */ const BEHAVIOR_META: Record = { allow: { i18nKey: 'panel.permission.allow', icon: ShieldCheck }, deny: { i18nKey: 'panel.permission.deny', icon: Ban }, ask: { i18nKey: 'panel.permission.ask', icon: CircleHelp }, }; /** * A monospace value (a path or rule pattern) that truncates to fit its * row. When (and only when) the text is actually clipped, hovering * reveals the full value in a tooltip anchored to the left. * * @param value - The text to display. * @returns The truncating value element, with a tooltip when clipped. */ function TruncatedCode({ value }: { value: string }) { const ref = useRef(null); const [truncated, setTruncated] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const check = () => setTruncated(el.scrollWidth > el.clientWidth); check(); const observer = new ResizeObserver(check); observer.observe(el); return () => observer.disconnect(); }, [value]); return ( {value} {truncated ? ( {value} ) : null} ); } /** * A single tool's rule card: a header naming the tool plus one row per * matching rule (pattern on the left, source on the right). * * @param toolName - The tool these rules apply to. * @param rules - The rules configured for this tool under one behavior. * @returns The tool card element. */ function ToolRuleCard({ toolName, rules }: { toolName: string; rules: PermissionRule[] }) { const { t } = useTranslation(); return (
{toolName} {rules.length}
    {rules.map((rule, index) => (
  • {rule.rule_content ? ( ) : ( {t('panel.permission.anyInvocation')} )} {rule.source}
  • ))}
); } /** * One behavior group (allow / deny / ask): a titled section that lists * a {@link ToolRuleCard} per tool that has rules under this behavior. * * @param behavior - The behavior category for this section. * @param ruleMap - Rules keyed by tool name for this behavior. * @returns The section element, or ``null`` when there are no rules. */ function BehaviorGroup({ behavior, ruleMap, }: { behavior: RuleBehavior; ruleMap: Record | undefined; }) { const { t } = useTranslation(); const entries = Object.entries(ruleMap ?? {}).filter(([, rules]) => rules.length > 0); if (entries.length === 0) return null; const meta = BEHAVIOR_META[behavior]; const Icon = meta.icon; return (
{t(meta.i18nKey)}
{entries.map(([toolName, rules]) => ( ))}
); } /** * Pure content body for the Permission dock panel. Shows the active * permission mode, the working directories in scope, and the configured * rules grouped by behavior (deny / ask / allow) and then by tool. Data * arrives via props so it owns no data fetching. * * Renders without its own header/border — the surrounding `Panel` * chrome (from `PanelDock`) provides those. * * @param permissionContext - The permission context, or ``null``. * @returns The permission panel body. */ export function PermissionPanel({ permissionContext }: PermissionPanelProps) { const { t } = useTranslation(); const workingDirs = Object.values(permissionContext?.working_directories ?? {}); const hasRules = Object.keys(permissionContext?.allow_rules ?? {}).length > 0 || Object.keys(permissionContext?.deny_rules ?? {}).length > 0 || Object.keys(permissionContext?.ask_rules ?? {}).length > 0; return (
{t('panel.permission.description')}
{/* Working directories — always shown, with an empty state. */}
{t('panel.permission.workingDirectories')}
{workingDirs.length === 0 ? (

{t('panel.permission.noWorkingDirectories')}

) : (
    {workingDirs.map((dir) => (
  • {dir.source}
  • ))}
)}
{/* Rules grouped by behavior, then by tool. */} {hasRules ? ( <> ) : ( )}
); }