import React, { useCallback, useEffect, useRef, useState } from 'react'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useStore } from '@renderer/store'; import { FileText, Search, Terminal } from 'lucide-react'; import type { ToolApprovalRequest } from '@shared/types'; // --------------------------------------------------------------------------- // Tool icon mapping // --------------------------------------------------------------------------- function getToolIcon(toolName: string): React.ReactNode { const cls = 'size-4 shrink-0'; switch (toolName) { case 'Bash': return ; case 'Read': case 'Edit': case 'Write': case 'NotebookEdit': return ; case 'Grep': case 'Glob': return ; default: return ; } } // --------------------------------------------------------------------------- // Smart input preview // --------------------------------------------------------------------------- function renderToolInput(toolName: string, input: Record): string { switch (toolName) { case 'Bash': return typeof input.command === 'string' ? input.command : JSON.stringify(input, null, 2); case 'Edit': case 'Read': case 'Write': case 'NotebookEdit': return typeof input.file_path === 'string' ? input.file_path : JSON.stringify(input, null, 2); case 'Grep': case 'Glob': return typeof input.pattern === 'string' ? input.pattern : JSON.stringify(input, null, 2); default: return JSON.stringify(input, null, 2); } } // --------------------------------------------------------------------------- // Elapsed timer hook // --------------------------------------------------------------------------- function useElapsed(receivedAt: string): number { const [elapsed, setElapsed] = useState(() => Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000)) ); useEffect(() => { setElapsed(Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000))); const id = setInterval(() => { setElapsed(Math.max(0, Math.floor((Date.now() - new Date(receivedAt).getTime()) / 1000))); }, 1000); return () => clearInterval(id); }, [receivedAt]); return elapsed; } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export const ToolApprovalSheet: React.FC = () => { const pendingApprovals = useStore((s) => s.pendingApprovals); const respondToToolApproval = useStore((s) => s.respondToToolApproval); const teams = useStore((s) => s.teams); const current: ToolApprovalRequest | undefined = pendingApprovals[0]; const containerRef = useRef(null); const [disabled, setDisabled] = useState(false); // Auto-focus when new approval arrives useEffect(() => { if (current && containerRef.current) { containerRef.current.focus(); } }, [current?.requestId]); // eslint-disable-line react-hooks/exhaustive-deps const handleRespond = useCallback( (allow: boolean) => { if (!current || disabled) return; setDisabled(true); void respondToToolApproval(current.teamName, current.runId, current.requestId, allow).finally( () => { setTimeout(() => setDisabled(false), 200); } ); }, [current, disabled, respondToToolApproval] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleRespond(true); } else if (e.key === 'Escape') { e.preventDefault(); handleRespond(false); } }, [handleRespond] ); if (!current) return null; const teamSummary = teams.find((t) => t.teamName === current.teamName); const teamColor = teamSummary?.color ? getTeamColorSet(teamSummary.color) : null; return ( {/* Header */} {getToolIcon(current.toolName)} {current.toolName} {teamColor ? ( {teamSummary?.displayName ?? current.teamName} ) : ( {current.teamName} )} {/* Tool input preview */} {renderToolInput(current.toolName, current.toolInput)} {/* Actions */} handleRespond(true)} className="rounded-md px-3.5 py-1.5 text-xs font-medium text-white transition-colors disabled:opacity-50" style={{ backgroundColor: 'rgb(5, 150, 105)' }} onMouseEnter={(e) => { if (!disabled) e.currentTarget.style.backgroundColor = 'rgb(16, 185, 129)'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'rgb(5, 150, 105)'; }} > Allow handleRespond(false)} className="rounded-md border px-3.5 py-1.5 text-xs font-medium transition-colors disabled:opacity-50" style={{ borderColor: 'rgba(239, 68, 68, 0.5)', color: 'rgb(248, 113, 113)', }} onMouseEnter={(e) => { if (!disabled) e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }} > Deny {pendingApprovals.length > 1 && ( {pendingApprovals.length - 1} pending )} ); }; // --------------------------------------------------------------------------- // Elapsed display sub-component (uses hook) // --------------------------------------------------------------------------- function ElapsedDisplay({ receivedAt }: { receivedAt: string }): React.JSX.Element { const elapsed = useElapsed(receivedAt); return ( {elapsed}s ); }
{renderToolInput(current.toolName, current.toolInput)}