import { relative } from "node:path"; import * as Diff from "diff"; import { Box, Text } from "ink"; import { colors } from "./colors"; // Helper to format path as relative with ../ function formatRelativePath(filePath: string): string { const cwd = process.cwd(); const relativePath = relative(cwd, filePath); // If file is outside cwd, it will start with .. // If file is in cwd, add ./ prefix if (!relativePath.startsWith("..")) { return `./${relativePath}`; } return relativePath; } // Helper to count lines in a string function countLines(str: string): number { if (!str) return 0; return str.split("\n").length; } // Helper to render a diff line with word-level highlighting interface DiffLineProps { lineNumber: number; type: "add" | "remove"; content: string; compareContent?: string; // The other version to compare against for word diff } function DiffLine({ lineNumber, type, content, compareContent, }: DiffLineProps) { const prefix = type === "add" ? "+" : "-"; const lineBg = type === "add" ? colors.diff.addedLineBg : colors.diff.removedLineBg; const wordBg = type === "add" ? colors.diff.addedWordBg : colors.diff.removedWordBg; // If we have something to compare against, do word-level diff if (compareContent !== undefined && content.trim() && compareContent.trim()) { const wordDiffs = type === "add" ? Diff.diffWords(compareContent, content) : Diff.diffWords(content, compareContent); return ( {`${lineNumber} ${prefix} `} {wordDiffs.map((part, i) => { if (part.added && type === "add") { // This part was added (show with brighter background, black text) return ( {part.value} ); } else if (part.removed && type === "remove") { // This part was removed (show with brighter background, black text) return ( {part.value} ); } else if (!part.added && !part.removed) { // Unchanged part (show with line background, white text) return ( {part.value} ); } // Skip parts that don't belong in this line return null; })} ); } // No comparison, just show the whole line with one background return ( {`${lineNumber} ${prefix} ${content}`} ); } interface WriteRendererProps { filePath: string; content: string; } export function WriteRenderer({ filePath, content }: WriteRendererProps) { const relativePath = formatRelativePath(filePath); const lines = content.split("\n"); const lineCount = lines.length; return ( {" "} ⎿ Wrote {lineCount} line{lineCount !== 1 ? "s" : ""} to {relativePath} {lines.map((line, i) => ( {line} ))} ); } interface EditRendererProps { filePath: string; oldString: string; newString: string; } export function EditRenderer({ filePath, oldString, newString, }: EditRendererProps) { const relativePath = formatRelativePath(filePath); const oldLines = oldString.split("\n"); const newLines = newString.split("\n"); // For the summary const additions = newLines.length; const removals = oldLines.length; // Try to match up lines for word-level diff // This is a simple approach - for single-line changes, compare directly // For multi-line, we could do more sophisticated matching const singleLineEdit = oldLines.length === 1 && newLines.length === 1; return ( {" "} ⎿ Updated {relativePath} with {additions} addition {additions !== 1 ? "s" : ""} and {removals} removal {removals !== 1 ? "s" : ""} {/* Show removals */} {oldLines.map((line, i) => ( ))} {/* Show additions */} {newLines.map((line, i) => ( ))} ); } interface MultiEditRendererProps { filePath: string; edits: Array<{ old_string: string; new_string: string; }>; } export function MultiEditRenderer({ filePath, edits }: MultiEditRendererProps) { const relativePath = formatRelativePath(filePath); // Count total additions and removals let totalAdditions = 0; let totalRemovals = 0; edits.forEach((edit) => { totalAdditions += countLines(edit.new_string); totalRemovals += countLines(edit.old_string); }); return ( {" "} ⎿ Updated {relativePath} with {totalAdditions} addition {totalAdditions !== 1 ? "s" : ""} and {totalRemovals} removal {totalRemovals !== 1 ? "s" : ""} {/* For multi-edit, show each edit sequentially */} {edits.map((edit, index) => { const oldLines = edit.old_string.split("\n"); const newLines = edit.new_string.split("\n"); const singleLineEdit = oldLines.length === 1 && newLines.length === 1; return ( {oldLines.map((line, i) => ( ))} {newLines.map((line, i) => ( ))} ); })} ); }