import { Box, Text } from 'ink'; import React from 'react'; export type DiffLine = { kind: 'context' | 'add' | 'remove'; text: string }; /** * Line-level diff by longest common subsequence. O(n*m) is fine here because an * edit_file payload is a handful of lines, not a whole file. */ export function diffLines(before: string, after: string): DiffLine[] { const a = before.split('\n'); const b = after.split('\n'); const n = a.length; const m = b.length; const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); for (let i = n - 1; i >= 0; i--) { for (let j = m - 1; j >= 0; j--) { lcs[i]![j] = a[i] === b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!); } } const out: DiffLine[] = []; let i = 0; let j = 0; while (i < n && j < m) { if (a[i] === b[j]) { out.push({ kind: 'context', text: a[i]! }); i++; j++; } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) { out.push({ kind: 'remove', text: a[i]! }); i++; } else { out.push({ kind: 'add', text: b[j]! }); j++; } } while (i < n) out.push({ kind: 'remove', text: a[i++]! }); while (j < m) out.push({ kind: 'add', text: b[j++]! }); return out; } /** Drops runs of unchanged lines longer than `context` on both sides of a change. */ export function collapseContext(lines: DiffLine[], context = 2): (DiffLine | { kind: 'gap'; count: number })[] { const keep = new Set(); lines.forEach((line, i) => { if (line.kind === 'context') return; for (let k = i - context; k <= i + context; k++) if (k >= 0 && k < lines.length) keep.add(k); }); const out: (DiffLine | { kind: 'gap'; count: number })[] = []; let skipped = 0; lines.forEach((line, i) => { if (keep.has(i)) { if (skipped > 0) { out.push({ kind: 'gap', count: skipped }); skipped = 0; } out.push(line); } else { skipped++; } }); if (skipped > 0) out.push({ kind: 'gap', count: skipped }); return out; } const MAX_RENDERED = 40; export function Diff({ before, after, path }: { before: string; after: string; path?: string }) { const all = collapseContext(diffLines(before, after)); const shown = all.slice(0, MAX_RENDERED); const hidden = all.length - shown.length; const added = all.filter((l) => l.kind === 'add').length; const removed = all.filter((l) => l.kind === 'remove').length; return ( {path && ( {path} +{added} -{removed} )} {shown.map((line, i) => line.kind === 'gap' ? ( {` ... ${line.count} unchanged line${line.count === 1 ? '' : 's'}`} ) : ( {`${line.kind === 'add' ? ' + ' : line.kind === 'remove' ? ' - ' : ' '}${line.text}`} ), )} {hidden > 0 && {` ... ${hidden} more diff lines`}} ); }