import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Download, Plus, Trash2, Upload } from 'lucide-react'; import { useI18n } from '../../../../application/i18n/I18nProvider'; import { Button } from '../../../ui/button'; import { SettingCard, SettingsSection } from '../../settings-ui'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'; import type { PermissionGrantRule } from '../../../../infrastructure/ai/harness/permissionGrants'; import { capabilitySupportsCommandPatternGrant, createPermissionGrantId, listGrantableCapabilityIds, } from '../../../../infrastructure/ai/harness/permissionGrants'; const cellInputClass = 'w-full min-w-0 max-w-full h-7 rounded border border-input bg-background px-2 text-xs font-mono focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring overflow-x-auto whitespace-nowrap scrollbar-thin'; const cellSelectClass = `${cellInputClass} font-sans truncate pr-6`; const GrantCellInput: React.FC<{ value: string; placeholder?: string; mono?: boolean; onChange: (value: string) => void; }> = ({ value, placeholder, mono = true, onChange }) => ( onChange(e.target.value)} className={mono ? cellInputClass : `${cellInputClass} font-sans whitespace-normal`} title={value} /> ); const GrantCapabilitySelect: React.FC<{ value: string; options: readonly string[]; onChange: (value: string) => void; }> = ({ value, options, onChange }) => { const selectOptions = useMemo(() => { if (options.includes(value)) return options; return [value, ...options]; }, [options, value]); return ( ); }; export const PermissionGrantsSettings: React.FC<{ grants: PermissionGrantRule[]; addGrant: (rule: PermissionGrantRule) => void; updateGrant: (id: string, updates: Partial>) => void; removeGrant: (id: string) => void; importGrants: (raw: unknown, mode?: 'merge' | 'replace') => void; exportGrants: () => PermissionGrantRule[]; }> = ({ grants, addGrant, updateGrant, removeGrant, importGrants, exportGrants, }) => { const { t } = useI18n(); const fileInputRef = useRef(null); const [importError, setImportError] = useState(null); const grantableCapabilityIds = useMemo(() => listGrantableCapabilityIds(), []); const handleAdd = useCallback(() => { addGrant({ id: createPermissionGrantId(), capabilityId: grantableCapabilityIds[0] ?? 'terminal.execute', sessionPattern: '*', createdAt: Date.now(), }); }, [addGrant, grantableCapabilityIds]); const handleExport = useCallback(() => { const payload = exportGrants(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = 'netcatty-permission-grants.json'; anchor.click(); URL.revokeObjectURL(url); }, [exportGrants]); const handleImportFile = useCallback(async (file: File) => { setImportError(null); try { const text = await file.text(); const parsed = JSON.parse(text) as unknown; importGrants(parsed, 'replace'); } catch (error) { setImportError(error instanceof Error ? error.message : String(error)); } }, [importGrants]); return (

{t('ai.safety.grants.heading')}

{t('ai.safety.grants.description')}

{ const file = event.target.files?.[0]; event.target.value = ''; if (file) void handleImportFile(file); }} />
{importError && (

{importError}

)} {grants.length === 0 ? (

{t('ai.safety.grants.empty')}

) : (
{grants.map((grant) => { const supportsCommandPattern = capabilitySupportsCommandPatternGrant(grant.capabilityId); return ( ); })}
{t('ai.safety.grants.capability')} {t('ai.safety.grants.commandPattern')} {t('ai.safety.grants.note')}
{ const updates: Partial> = { capabilityId, }; if (!capabilitySupportsCommandPatternGrant(capabilityId)) { updates.commandPattern = undefined; } updateGrant(grant.id, updates); }} /> {supportsCommandPattern ? ( updateGrant(grant.id, { commandPattern: commandPattern.trim() || undefined, })} /> ) : ( )} updateGrant(grant.id, { note: note.trim() || undefined, })} /> {t('ai.safety.grants.remove')}
)}
); };