/** * CodeDiffViewer — Manus-style code diff viewer with tabs: * Diff / Original / Modified * Used in ActionTree when edit_file or write_file tool calls are shown. */ import { useState, useMemo } from "react"; import { cn } from "@/lib/utils"; type DiffTab = "diff" | "original" | "modified"; interface DiffLine { type: "added" | "removed" | "context"; lineOld?: number; lineNew?: number; content: string; } /** * Simple unified diff parser. * Takes old text and new text, produces a line-by-line diff. */ function computeDiff(oldText: string, newText: string): DiffLine[] { const oldLines = oldText.split("\n"); const newLines = newText.split("\n"); const result: DiffLine[] = []; // Simple LCS-based diff const m = oldLines.length; const n = newLines.length; // For large files, fall back to a simpler approach if (m * n > 1_000_000) { // Just show removed then added for very large files oldLines.forEach((line, i) => { result.push({ type: "removed", lineOld: i + 1, content: line }); }); newLines.forEach((line, i) => { result.push({ type: "added", lineNew: i + 1, content: line }); }); return result; } // Build LCS table const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (oldLines[i - 1] === newLines[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // Backtrack to produce diff const diffLines: DiffLine[] = []; let i = m, j = n; while (i > 0 || j > 0) { if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { diffLines.unshift({ type: "context", lineOld: i, lineNew: j, content: oldLines[i - 1] }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { diffLines.unshift({ type: "added", lineNew: j, content: newLines[j - 1] }); j--; } else { diffLines.unshift({ type: "removed", lineOld: i, content: oldLines[i - 1] }); i--; } } return diffLines; } interface CodeDiffViewerProps { oldContent: string; newContent: string; fileName?: string; className?: string; } export function CodeDiffViewer({ oldContent, newContent, fileName, className }: CodeDiffViewerProps) { const [activeTab, setActiveTab] = useState("diff"); const diffLines = useMemo( () => computeDiff(oldContent, newContent), [oldContent, newContent] ); const stats = useMemo(() => { const added = diffLines.filter(l => l.type === "added").length; const removed = diffLines.filter(l => l.type === "removed").length; return { added, removed }; }, [diffLines]); const tabs: { id: DiffTab; label: string }[] = [ { id: "diff", label: "Diff" }, { id: "original", label: "Original" }, { id: "modified", label: "Modified" }, ]; return (
{/* Header with tabs */}
{tabs.map((tab) => ( ))}
{fileName && ( {fileName} )} +{stats.added} -{stats.removed}
{/* Content */}
{activeTab === "diff" && ( {diffLines.map((line, idx) => ( ))}
{line.lineOld || ""} {line.lineNew || ""} {line.type === "added" ? "+" : line.type === "removed" ? "-" : " "} {line.content}
)} {activeTab === "original" && (
            {oldContent || "(empty)"}
          
)} {activeTab === "modified" && (
            {newContent || "(empty)"}
          
)}
); }