import type { StructuredPatchHunk } from 'diff' import * as React from 'react' import { Suspense, use, useState } from 'react' import { useTerminalSize } from '../hooks/useTerminalSize.js' import { Box, Text } from '@anthropic/ink' import type { FileEdit } from '@claude-code-best/builtin-tools/tools/FileEditTool/types.js' import { findActualString, preserveQuoteStyle, } from '@claude-code-best/builtin-tools/tools/FileEditTool/utils.js' import { adjustHunkLineNumbers, CONTEXT_LINES, getPatchForDisplay, } from '../utils/diff.js' import { logError } from '../utils/log.js' import { CHUNK_SIZE, openForScan, readCapped, scanForContext, } from '../utils/readEditContext.js' import { firstLineOf } from '../utils/stringUtils.js' import { StructuredDiffList } from './StructuredDiffList.js' type Props = { file_path: string edits: FileEdit[] } type DiffData = { patch: StructuredPatchHunk[] firstLine: string | null fileContent: string | undefined } export function FileEditToolDiff(props: Props): React.ReactNode { // Snapshot on mount — the diff must stay consistent even if the file changes // while the dialog is open. useMemo on props.edits would re-read the file on // every render because callers pass fresh array literals. const [dataPromise] = useState(() => loadDiffData(props.file_path, props.edits), ) return ( }> ) } function DiffBody({ promise, file_path, }: { promise: Promise file_path: string }): React.ReactNode { const { patch, firstLine, fileContent } = use(promise) const { columns } = useTerminalSize() return ( ) } function DiffFrame({ children, placeholder, }: { children?: React.ReactNode placeholder?: boolean }): React.ReactNode { return ( {placeholder ? : children} ) } async function loadDiffData( file_path: string, edits: FileEdit[], ): Promise { const valid = edits.filter(e => e.old_string != null && e.new_string != null) const single = valid.length === 1 ? valid[0]! : undefined // SedEditPermissionRequest passes the entire file as old_string. Scanning for // a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the // file read entirely and diff the inputs we already have. if (single && single.old_string.length >= CHUNK_SIZE) { return diffToolInputsOnly(file_path, [single]) } try { const handle = await openForScan(file_path) if (handle === null) return diffToolInputsOnly(file_path, valid) try { // Multi-edit and empty old_string genuinely need full-file for sequential // replacements — structuredPatch needs before/after strings. replace_all // routes through the chunked path below (shows first-occurrence window; // matches within the slice still replace via edit.replace_all). if (!single || single.old_string === '') { const file = await readCapped(handle) if (file === null) return diffToolInputsOnly(file_path, valid) const normalized = valid.map(e => normalizeEdit(file, e)) return { patch: getPatchForDisplay({ filePath: file_path, fileContents: file, edits: normalized, }), firstLine: firstLineOf(file), fileContent: file, } } const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES) if (ctx.truncated || ctx.content === '') { return diffToolInputsOnly(file_path, [single]) } const normalized = normalizeEdit(ctx.content, single) const hunks = getPatchForDisplay({ filePath: file_path, fileContents: ctx.content, edits: [normalized], }) return { patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1), firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null, fileContent: ctx.content, } } finally { await handle.close() } } catch (e) { logError(e as Error) return diffToolInputsOnly(file_path, valid) } } function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData { return { patch: edits.flatMap(e => getPatchForDisplay({ filePath, fileContents: e.old_string, edits: [e], }), ), firstLine: null, fileContent: undefined, } } function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit { const actualOld = findActualString(fileContent, edit.old_string) || edit.old_string const actualNew = preserveQuoteStyle( edit.old_string, actualOld, edit.new_string, ) return { ...edit, old_string: actualOld, new_string: actualNew } }