export type NativeChatDiffLineKind = 'add' | 'del' | 'context' | 'meta' export type NativeChatDiffLine = { kind: NativeChatDiffLineKind text: string } /** Tools whose call carries the edit itself, so its row is a file change. */ export const EDIT_TOOL_NAMES = new Set(['Edit', 'MultiEdit', 'Write', 'str_replace', 'apply_patch']) const MAX_DIFF_CHARS = 32_000 const DEFAULT_MAX_DIFF_LINES = 120 const DIFF_TRUNCATED_LINE: NativeChatDiffLine = { kind: 'meta', text: '… diff truncated …' } const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/ /** Lines that open a new file section, so any hunk before them has ended. * `--- `/`+++ ` are deliberately absent: inside a hunk they are content — a * removed `-- comment` is emitted as `--- comment` — so they go through * `isFileHeaderPair` instead. */ export const FILE_SECTION_START = /^(?:diff |index |old mode |new mode |new file mode |deleted file mode |similarity index |dissimilarity index |rename |copy |Binary files )/ // Markdown thematic break or YAML document separator, not a marker. const BARE_RULE = /^(?:-{3,}|\+{3,})$/ type DiffStructure = { /** Lines that are structure rather than content, so they never count as add/del. */ metaIndices: Set /** The text carries a hunk header or `diff --git`, so it is provably a diff. */ isStructuredDiff: boolean } /** * True when the row at `index` opens a `--- ` / `+++ ` file header. A * bare `---`/`+++` prefix is not enough to spot one: a removed line whose * content began with `--` (SQL/Lua `-- comment`, C `--i`) is emitted as * `---`. Real headers always come as an adjacent pair and never appear * inside a hunk, so callers must check this only outside one. */ export function isFileHeaderPair(lines: readonly string[], index: number): boolean { return (lines[index] ?? '').startsWith('--- ') && (lines[index + 1] ?? '').startsWith('+++ ') } /** Locates the file headers and rules that are structure rather than content. */ function scanDiffStructure(lines: string[]): DiffStructure { const metaIndices = new Set() let isStructuredDiff = false let inHunk = false for (let index = 0; index < lines.length; index += 1) { const line = lines[index] ?? '' if (line.startsWith('@@')) { inHunk = true isStructuredDiff ||= HUNK_HEADER.test(line) continue } if (FILE_SECTION_START.test(line)) { inHunk = false isStructuredDiff ||= line.startsWith('diff --git ') continue } // Only marker lines can be a header or a rule; skip context and prose early. if (inHunk || !(line.startsWith('-') || line.startsWith('+'))) { continue } if (BARE_RULE.test(line)) { metaIndices.add(index) continue } if (isFileHeaderPair(lines, index)) { metaIndices.add(index) metaIndices.add(index + 1) index += 1 } } return { metaIndices, isStructuredDiff } } function toLines(value: unknown, maxLines: number): { lines: string[]; truncated: boolean } { if (typeof value !== 'string') { return { lines: [], truncated: false } } const clipped = value.slice(0, MAX_DIFF_CHARS) const lines = clipped.split('\n', maxLines + 1) const truncated = value.length > MAX_DIFF_CHARS || lines.length > maxLines const bounded = lines.slice(0, maxLines) if (!truncated && bounded.at(-1) === '') { bounded.pop() } return { lines: bounded, truncated } } function patchTextFromToolInput(value: Record): string | null { if (typeof value.patch === 'string') { return value.patch } if (typeof value.diff === 'string') { return value.diff } if (!Array.isArray(value.changes)) { return null } const sections: string[] = [] let length = 0 for (const entry of value.changes) { if (typeof entry !== 'object' || entry === null) { continue } const change = entry as Record if (typeof change.diff !== 'string') { continue } const path = typeof change.path === 'string' ? change.path : 'file' const kind = typeof change.kind === 'object' && change.kind !== null ? (change.kind as Record) : null const nextPath = kind && typeof kind.move_path === 'string' ? kind.move_path : path const section = `--- ${path}\n+++ ${nextPath}\n${change.diff}` length += section.length + (sections.length > 0 ? 1 : 0) sections.push(section) // Keep the extra character that tells toLines the diff was truncated. if (length > MAX_DIFF_CHARS) { break } } return sections.length > 0 ? sections.join('\n') : null } export function diffFromToolCall( name: string, input: unknown, maxLines = DEFAULT_MAX_DIFF_LINES ): NativeChatDiffLine[] | null { if (!EDIT_TOOL_NAMES.has(name) || typeof input !== 'object' || input === null) { return null } const value = input as Record const patchText = patchTextFromToolInput(value) if (patchText !== null) { return diffFromText(patchText, maxLines) } const oldLines = toLines(value.old_string ?? value.oldString ?? value.old, maxLines) const newLines = toLines( value.new_string ?? value.newString ?? value.new ?? value.content ?? value.file_text, maxLines ) const deleted = oldLines.lines.map((text): NativeChatDiffLine => ({ kind: 'del', text })) const added = newLines.lines.map((text): NativeChatDiffLine => ({ kind: 'add', text })) if (deleted.length === 0 && added.length === 0) { return null } const path = value.file_path ?? value.path const prefix: NativeChatDiffLine[] = typeof path === 'string' ? [{ kind: 'meta', text: path }] : [] const combined = [...prefix, ...deleted, ...added] const truncated = oldLines.truncated || newLines.truncated || combined.length > maxLines return truncated ? [...combined.slice(0, maxLines - 1), DIFF_TRUNCATED_LINE] : combined } export function diffFromText( text: string, maxLines = DEFAULT_MAX_DIFF_LINES ): NativeChatDiffLine[] | null { if (text.length === 0) { return null } const bounded = toLines(text, maxLines) const { metaIndices, isStructuredDiff } = scanDiffStructure(bounded.lines) let added = 0 let removed = 0 const lines = bounded.lines.map((line, index): NativeChatDiffLine => { if ( metaIndices.has(index) || line.startsWith('@@') || line.startsWith('diff ') || line.startsWith('index ') ) { return { kind: 'meta', text: line } } if (line.startsWith('+')) { added += 1 return { kind: 'add', text: line.slice(1) } } if (line.startsWith('-')) { removed += 1 return { kind: 'del', text: line.slice(1) } } return { kind: 'context', text: line } }) // Proven diff text renders a single-line change; without that proof, two // markers guard against colouring prose that merely opens a line with `-`. if (added + removed < (isStructuredDiff ? 1 : 2)) { return null } return bounded.truncated ? [...lines.slice(0, maxLines - 1), DIFF_TRUNCATED_LINE] : lines }