import React, { useMemo, useState } from 'react'; import { computeDiffLineStats, DiffViewer } from '@renderer/components/chat/viewers/DiffViewer'; import { useToolApprovalDiff } from '@renderer/hooks/useToolApprovalDiff'; import { AlertTriangle, ChevronDown, ChevronRight, FileDiff, Loader2 } from 'lucide-react'; // ============================================================================= // Types // ============================================================================= interface ToolApprovalDiffPreviewProps { toolName: string; toolInput: Record; requestId: string; onExpandedChange?: (expanded: boolean) => void; } const DIFF_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']); const STORAGE_KEY = 'tool-approval:preview-expanded'; function loadExpandedPref(): boolean { try { return localStorage.getItem(STORAGE_KEY) === 'true'; } catch { return false; } } function saveExpandedPref(value: boolean): void { try { localStorage.setItem(STORAGE_KEY, String(value)); } catch { // quota or disabled — ignore } } // ============================================================================= // Quick stats from toolInput (before IPC, for Write tool) // ============================================================================= function computeQuickStats( toolName: string, toolInput: Record ): { added: number; removed: number } | null { if (toolName === 'Write' && typeof toolInput.content === 'string') { const lines = toolInput.content.split(/\r?\n/).length; return { added: lines, removed: 0 }; } return null; } // ============================================================================= // Component // ============================================================================= export const ToolApprovalDiffPreview: React.FC = ({ toolName, toolInput, requestId, onExpandedChange, }) => { const [expanded, setExpanded] = useState(loadExpandedPref); const diff = useToolApprovalDiff(toolName, toolInput, requestId, expanded); // Stats from actual diff data (after IPC), using the same algorithm as DiffViewer const diffStats = useMemo(() => { if (!diff.hasDiff || diff.loading || diff.isBinary || diff.error) return null; if (!diff.oldString && !diff.newString) return null; return computeDiffLineStats(diff.oldString, diff.newString); }, [diff.hasDiff, diff.loading, diff.isBinary, diff.error, diff.oldString, diff.newString]); // Quick stats from toolInput (available immediately, before diff loads) const quickStats = useMemo( () => computeQuickStats(toolName, toolInput), // eslint-disable-next-line react-hooks/exhaustive-deps -- toolInput identity tied to requestId [toolName, requestId] ); // Prefer accurate diffStats when available, fall back to quickStats const stats = diffStats ?? quickStats; if (!DIFF_TOOLS.has(toolName)) return null; const toggleExpanded = (): void => { const next = !expanded; setExpanded(next); saveExpandedPref(next); onExpandedChange?.(next); }; return (
{/* Toggle button */} {/* Collapsible content */} {expanded && (
{diff.loading && (
Reading file...
)} {diff.isBinary && (
Binary file — cannot preview
)} {diff.error && !diff.loading && (
{diff.error}
)} {diff.truncated && !diff.loading && (
File truncated at 2MB — diff may be incomplete
)} {!diff.loading && !diff.isBinary && !diff.error && (diff.oldString || diff.newString) && (
{diff.isNewFile && ( New file )}
)}
)}
); };