'use client' import { useState, useMemo, useEffect, useCallback } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { useMissionControl, type ExecApprovalRequest } from '@/store' import { useWebSocket } from '@/lib/websocket' import { matchesGlobPattern } from '@/lib/exec-approval-utils' type FilterTab = 'all' | 'pending' | 'resolved' type PanelView = 'approvals' | 'allowlist' const RISK_BORDER: Record = { low: 'border-l-green-500', medium: 'border-l-yellow-500', high: 'border-l-orange-500', critical: 'border-l-red-500', } const RISK_BADGE: Record = { low: { bg: 'bg-green-500/20', text: 'text-green-400' }, medium: { bg: 'bg-yellow-500/20', text: 'text-yellow-400' }, high: { bg: 'bg-orange-500/20', text: 'text-orange-400' }, critical: { bg: 'bg-red-500/20', text: 'text-red-400' }, } function timeAgo(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000) if (seconds < 5) return 'just now' if (seconds < 60) return `${seconds}s ago` const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` } export function ExecApprovalPanel() { const t = useTranslations('execApproval') const { execApprovals, updateExecApproval } = useMissionControl() const { sendMessage } = useWebSocket() const [filter, setFilter] = useState('pending') const [view, setView] = useState('approvals') const pendingCount = execApprovals.filter(a => a.status === 'pending').length // Mark expired approvals client-side const now = Date.now() const displayApprovals = useMemo(() => { const withExpiry = execApprovals.map(a => { if (a.status === 'pending' && a.expiresAt && a.expiresAt < now) { return { ...a, status: 'expired' as const } } return a }) return withExpiry.filter(a => { if (filter === 'pending') return a.status === 'pending' if (filter === 'resolved') return a.status !== 'pending' return true }) }, [execApprovals, filter, now]) const handleAction = (id: string, decision: 'allow-once' | 'allow-always' | 'deny') => { const sent = sendMessage({ type: 'req', method: 'exec.approval.resolve', id: `ea-${Date.now()}`, params: { id, decision }, }) if (!sent) { const action = decision === 'deny' ? 'deny' : decision === 'allow-always' ? 'always_allow' : 'approve' fetch('/api/exec-approvals', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, action }), }).catch(() => {}) } const newStatus = decision === 'deny' ? 'denied' : 'approved' updateExecApproval(id, { status: newStatus as ExecApprovalRequest['status'] }) } return (
{/* Header */}

{t('title')}

{pendingCount > 0 && ( {t('pendingBadge', { count: pendingCount })} )}
{t('realtimeLabel')}
{/* View toggle */}
{view === 'approvals' ? ( <> {/* Filter tabs */}
{(['all', 'pending', 'resolved'] as const).map((tab) => ( ))}
{/* Approval list */} {displayApprovals.length === 0 ? (
{filter === 'pending' ? t('noPendingApprovals') : t('noApprovals')}
) : (
{displayApprovals.map((approval) => ( ))}
)} ) : ( )}
) } type AllowlistState = Record function AllowlistEditor({ execApprovals }: { execApprovals: ExecApprovalRequest[] }) { const t = useTranslations('execApproval') const [agents, setAgents] = useState({}) const [hash, setHash] = useState('') const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [dirty, setDirty] = useState(false) const [newAgentId, setNewAgentId] = useState('') const loadAllowlist = useCallback(async () => { setLoading(true) setError(null) try { const res = await fetch('/api/exec-approvals?action=allowlist') if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || `HTTP ${res.status}`) } const data = await res.json() setAgents(data.agents ?? {}) setHash(data.hash ?? '') setDirty(false) } catch (err: any) { setError(err.message || 'Failed to load allowlist') } finally { setLoading(false) } }, []) useEffect(() => { loadAllowlist() }, [loadAllowlist]) const saveAllowlist = async () => { setSaving(true) setError(null) try { const res = await fetch('/api/exec-approvals', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agents, hash }), }) const data = await res.json() if (!res.ok) { throw new Error(data.error || `HTTP ${res.status}`) } setHash(data.hash ?? '') setDirty(false) } catch (err: any) { setError(err.message || 'Failed to save allowlist') } finally { setSaving(false) } } const addAgent = () => { const id = newAgentId.trim() if (!id || agents[id]) return setAgents(prev => ({ ...prev, [id]: [] })) setNewAgentId('') setDirty(true) } const addPattern = (agentId: string) => { setAgents(prev => ({ ...prev, [agentId]: [...(prev[agentId] || []), { pattern: '' }], })) setDirty(true) } const updatePattern = (agentId: string, index: number, value: string) => { setAgents(prev => ({ ...prev, [agentId]: prev[agentId].map((p, i) => i === index ? { pattern: value } : p), })) setDirty(true) } const removePattern = (agentId: string, index: number) => { setAgents(prev => ({ ...prev, [agentId]: prev[agentId].filter((_, i) => i !== index), })) setDirty(true) } const removeAgent = (agentId: string) => { setAgents(prev => { const next = { ...prev } delete next[agentId] return next }) setDirty(true) } const recentCommands = useMemo(() => { return execApprovals .filter(a => a.command) .slice(0, 50) .map(a => ({ command: a.command!, agentName: a.agentName || a.sessionId })) }, [execApprovals]) if (loading) { return
{t('loadingAllowlist')}
} const agentIds = Object.keys(agents) return (
{error && (
{error}
)} {/* Action bar */}
setNewAgentId(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addAgent()} placeholder="Agent ID (e.g. claude, assistant)" className="flex-1 bg-secondary border border-border rounded px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50" />
{agentIds.length === 0 ? (
{t('noAgentsConfigured')}
) : ( agentIds.map(agentId => ( addPattern(agentId)} onUpdatePattern={(i, v) => updatePattern(agentId, i, v)} onRemovePattern={(i) => removePattern(agentId, i)} onRemoveAgent={() => removeAgent(agentId)} /> )) )}
) } function AgentAllowlistCard({ agentId, patterns, recentCommands, onAddPattern, onUpdatePattern, onRemovePattern, onRemoveAgent, }: { agentId: string patterns: { pattern: string }[] recentCommands: { command: string; agentName: string }[] onAddPattern: () => void onUpdatePattern: (index: number, value: string) => void onRemovePattern: (index: number) => void onRemoveAgent: () => void }) { const t = useTranslations('execApproval') const [previewIndex, setPreviewIndex] = useState(null) const previewMatches = useMemo(() => { if (previewIndex === null) return [] const pat = patterns[previewIndex]?.pattern if (!pat) return [] return recentCommands.filter(c => matchesGlobPattern(pat, c.command)) }, [previewIndex, patterns, recentCommands]) return (
{agentId} {patterns.length} pattern{patterns.length !== 1 ? 's' : ''}
{patterns.length === 0 ? (
{t('noAllowlistPatterns')}
) : (
{patterns.map((entry, index) => (
onUpdatePattern(index, e.target.value)} onFocus={() => setPreviewIndex(index)} onBlur={() => setPreviewIndex(null)} placeholder="e.g. git *, npm install *, ls" className="flex-1 font-mono bg-secondary border border-border rounded px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/50" />
))}
)} {/* Pattern preview */} {previewIndex !== null && patterns[previewIndex]?.pattern && (
{t('previewMatches', { count: previewMatches.length })}
{previewMatches.length > 0 && (
{previewMatches.slice(0, 5).map((m, i) => (
$ {m.command}
))} {previewMatches.length > 5 && (
{t('andMore', { count: previewMatches.length - 5 })}
)}
)}
)}
) } function ApprovalCard({ approval, onAction, }: { approval: ExecApprovalRequest onAction: (id: string, decision: 'allow-once' | 'allow-always' | 'deny') => void }) { const t = useTranslations('execApproval') const riskBorder = RISK_BORDER[approval.risk] const riskBadge = RISK_BADGE[approval.risk] const isPending = approval.status === 'pending' const isExpired = approval.status === 'expired' return (
{/* Header row */}
{approval.agentName || approval.sessionId} {approval.toolName}
{approval.risk} {timeAgo(approval.createdAt)}
{/* Command block */} {approval.command && (
          $ {approval.command}
        
)} {/* Tool args */} {!approval.command && approval.toolArgs && Object.keys(approval.toolArgs).length > 0 && (
          {JSON.stringify(approval.toolArgs, null, 2)}
        
)} {/* Metadata */} {(approval.cwd || approval.host || approval.resolvedPath) && (
{approval.host &&
Host: {approval.host}
} {approval.cwd &&
CWD: {approval.cwd}
} {approval.resolvedPath &&
Resolved: {approval.resolvedPath}
}
)} {/* Action row */}
{isPending ? ( <> ) : isExpired ? ( {t('statusExpired')} ) : ( {approval.status === 'approved' ? t('statusApproved') : t('statusDenied')} )}
) }