'use client' import { useEffect, useState, useCallback } from 'react' import { Button } from '@/components/ui/button' import { useMissionControl, type ExecApprovalRequest } from '@/store' import { useWebSocket } from '@/lib/websocket' 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-green-500/20 text-green-400', medium: 'bg-yellow-500/20 text-yellow-400', high: 'bg-orange-500/20 text-orange-400', critical: 'bg-red-500/20 text-red-400', } function formatRemaining(ms: number): string { const remaining = Math.max(0, ms) const totalSeconds = Math.floor(remaining / 1000) if (totalSeconds < 60) return `${totalSeconds}s` const minutes = Math.floor(totalSeconds / 60) if (minutes < 60) return `${minutes}m` const hours = Math.floor(minutes / 60) return `${hours}h` } function MetaRow({ label, value }: { label: string; value?: string | null }) { if (!value) return null return (
{label} {value}
) } export function ExecApprovalOverlay() { const { execApprovals, updateExecApproval } = useMissionControl() const { sendMessage } = useWebSocket() const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [, setTick] = useState(0) const pending = execApprovals.filter(a => a.status === 'pending') const active = pending[0] // Tick every second to update expiry countdown useEffect(() => { if (!active) return const interval = setInterval(() => setTick(t => t + 1), 1000) return () => clearInterval(interval) }, [active?.id]) // Auto-expire client-side useEffect(() => { if (!active?.expiresAt) return if (active.expiresAt < Date.now()) { updateExecApproval(active.id, { status: 'expired' }) } }, [active, updateExecApproval]) const handleDecision = useCallback(async (decision: 'allow-once' | 'allow-always' | 'deny') => { if (!active || busy) return setBusy(true) setError(null) // Try WebSocket RPC first const sent = sendMessage({ type: 'req', method: 'exec.approval.resolve', id: `ea-${Date.now()}`, params: { id: active.id, decision }, }) if (!sent) { // Fallback to HTTP try { const action = decision === 'deny' ? 'deny' : decision === 'allow-always' ? 'always_allow' : 'approve' const res = await fetch('/api/exec-approvals', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: active.id, action }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) setError(data.error || 'Failed to send decision') setBusy(false) return } } catch { setError('Failed to reach gateway') setBusy(false) return } } // Optimistic update const newStatus = decision === 'deny' ? 'denied' : 'approved' updateExecApproval(active.id, { status: newStatus as ExecApprovalRequest['status'] }) setBusy(false) }, [active, busy, sendMessage, updateExecApproval]) if (!active) return null const remainingMs = active.expiresAt ? active.expiresAt - Date.now() : null const remainingText = remainingMs !== null ? (remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : 'expired') : null return (
{/* Header */}
Exec approval needed
{remainingText && (
{remainingText}
)}
{active.risk} {pending.length > 1 && ( {pending.length} pending )}
{/* Command */} {active.command && (
            $ {active.command}
          
)} {/* Tool args (if no command) */} {!active.command && active.toolArgs && Object.keys(active.toolArgs).length > 0 && (
            {JSON.stringify(active.toolArgs, null, 2)}
          
)} {/* Metadata */}
{/* Error */} {error && (
{error}
)} {/* Actions */}
) }