import { type CSSProperties } from 'react'; export interface CodeDiffProps { /** Original code. */ before: string; /** Modified code. */ after: string; /** Display mode. Default `'split'` (two columns). */ view?: 'split' | 'unified'; /** Optional language hint (no syntax highlighting yet, kept for parity). */ language?: string; /** Filename or label rendered in the header (e.g. `useChat.ts`). */ filename?: string; className?: string; style?: CSSProperties; } type DiffOp = { kind: 'eq' | 'add' | 'del'; text: string }; /** * `` — pure-React diff renderer with `split` and `unified` * views. Internal diff is a small line-LCS implementation (no runtime * dependency), good enough for short hunks shown in chat surfaces and * tracker PR cards. * * Wave 9.E.2. Hosts that need byte-perfect Myers diff or syntax * highlighting should feed `` a pre-computed pair where the * server has already done the heavy lift. */ export function CodeDiff({ before, after, view = 'split', language, filename, className, style, }: CodeDiffProps) { const ops = diffLines(before, after); return (
{filename ?? 'diff'} {language ? `${language} · ` : ''} {view}
{view === 'split' ? ( ) : ( )}
); } function SplitView({ ops }: { ops: DiffOp[] }) { return (
{ops.map((op, i) => { if (op.kind === 'add') return null; return ( ); })}
{ops.map((op, i) => { if (op.kind === 'del') return null; return ( ); })}
); } function UnifiedView({ ops }: { ops: DiffOp[] }) { return (
{ops.map((op, i) => ( ))}
); } function Row({ kind, text, side, }: { kind: 'eq' | 'add' | 'del'; side: 'left' | 'right' | 'unified'; text: string; }) { const bg = kind === 'add' ? 'color-mix(in srgb, var(--bl-success, #10b981) 14%, transparent)' : kind === 'del' ? 'color-mix(in srgb, var(--bl-danger, #ef4444) 14%, transparent)' : 'transparent'; const gutter = kind === 'add' ? '+' : kind === 'del' ? '−' : ' '; return (
{text}
); } function paneStyle(side: 'left' | 'right'): CSSProperties { return { background: 'var(--bl-surface, #fff)', borderRight: side === 'left' ? '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))' : undefined, overflowX: 'auto', }; } /** * Tiny line-level LCS — O(n·m) memory, fine for hunks <2,000 lines. * Hosts that diff full files should pre-compute with a real Myers * implementation and split into per hunk. */ function diffLines(before: string, after: string): DiffOp[] { const a = before.split('\n'); const b = after.split('\n'); const n = a.length; const m = b.length; // Build LCS length table. const dp: 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--) { if (a[i] === b[j]) { dp[i]![j] = (dp[i + 1]?.[j + 1] ?? 0) + 1; } else { dp[i]![j] = Math.max(dp[i + 1]?.[j] ?? 0, dp[i]?.[j + 1] ?? 0); } } } // Walk back to recover ops. const ops: DiffOp[] = []; let i = 0; let j = 0; while (i < n && j < m) { if (a[i] === b[j]) { ops.push({ kind: 'eq', text: a[i] ?? '' }); i++; j++; } else if ((dp[i + 1]?.[j] ?? 0) >= (dp[i]?.[j + 1] ?? 0)) { ops.push({ kind: 'del', text: a[i] ?? '' }); i++; } else { ops.push({ kind: 'add', text: b[j] ?? '' }); j++; } } while (i < n) ops.push({ kind: 'del', text: a[i++] ?? '' }); while (j < m) ops.push({ kind: 'add', text: b[j++] ?? '' }); return ops; }