import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; import type { StructuredPatchHunk } from 'diff'; import * as React from 'react'; import { Suspense, use, useState } from 'react'; import { FileEditToolUseRejectedMessage } from 'src/components/FileEditToolUseRejectedMessage.js'; import { MessageResponse } from 'src/components/MessageResponse.js'; import { extractTag } from 'src/utils/messages.js'; import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'; import { FileEditToolUpdatedMessage } from 'src/components/FileEditToolUpdatedMessage.js'; import { Text } from '@anthropic/ink'; import { FilePathLink } from 'src/components/FilePathLink.js'; import type { Tools } from 'src/Tool.js'; import type { Message, ProgressMessage } from 'src/types/message.js'; import { adjustHunkLineNumbers, CONTEXT_LINES } from 'src/utils/diff.js'; import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from 'src/utils/file.js'; import { logError } from 'src/utils/log.js'; import { getPlansDirectory } from 'src/utils/plans.js'; import { readEditContext } from 'src/utils/readEditContext.js'; import { firstLineOf } from 'src/utils/stringUtils.js'; import type { ThemeName } from 'src/utils/theme.js'; import type { FileEditOutput } from './types.js'; import { findActualString, getPatchForEdit } from './utils.js'; export function userFacingName( input: | Partial<{ file_path: string; old_string: string; new_string: string; replace_all: boolean; edits: unknown[]; }> | undefined, ): string { if (!input) { return 'Update'; } if (input.file_path?.startsWith(getPlansDirectory())) { return 'Updated plan'; } // Hashline edits always modify an existing file (line-ref based) if (input.edits != null) { return 'Update'; } if (input.old_string === '') { return 'Create'; } return 'Update'; } export function getToolUseSummary( input: | Partial<{ file_path: string; old_string: string; new_string: string; replace_all: boolean; }> | undefined, ): string | null { if (!input?.file_path) { return null; } return getDisplayPath(input.file_path); } export function renderToolUseMessage( { file_path }: { file_path?: string }, { verbose }: { verbose: boolean }, ): React.ReactNode { if (!file_path) { return null; } // For plan files, path is already in userFacingName if (file_path.startsWith(getPlansDirectory())) { return ''; } return {verbose ? file_path : getDisplayPath(file_path)}; } export function renderToolResultMessage( { filePath, structuredPatch, originalFile }: FileEditOutput, _progressMessagesForMessage: ProgressMessage[], { style, verbose }: { style?: 'condensed'; verbose: boolean }, ): React.ReactNode { // For plan files, show /plan hint above the diff const isPlanFile = filePath.startsWith(getPlansDirectory()); return ( ); } export function renderToolUseRejectedMessage( input: { file_path: string; old_string?: string; new_string?: string; replace_all?: boolean; edits?: unknown[]; }, options: { columns: number; messages: Message[]; progressMessagesForMessage: ProgressMessage[]; style?: 'condensed'; theme: ThemeName; tools: Tools; verbose: boolean; }, ): React.ReactElement { const { style, verbose } = options; const filePath = input.file_path; const oldString = input.old_string ?? ''; const newString = input.new_string ?? ''; const replaceAll = input.replace_all ?? false; // Defensive: if input has an unexpected shape, show a simple rejection message if ('edits' in input && input.edits != null) { return ( ); } const isNewFile = oldString === ''; // For new file creation, show content preview instead of diff if (isNewFile) { return ( ); } return ( ); } export function renderToolUseErrorMessage( result: ToolResultBlockParam['content'], options: { progressMessagesForMessage: ProgressMessage[]; tools: Tools; verbose: boolean; }, ): React.ReactElement { const { verbose } = options; if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) { const errorMessage = extractTag(result, 'tool_use_error'); if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) { return ( File not found ); } return ( Error editing file ); } return ; } type RejectionDiffData = { patch: StructuredPatchHunk[]; firstLine: string | null; fileContent: string | undefined; }; function EditRejectionDiff({ filePath, oldString, newString, replaceAll, style, verbose, }: { filePath: string; oldString: string; newString: string; replaceAll: boolean; style?: 'condensed'; verbose: boolean; }): React.ReactNode { const [dataPromise] = useState(() => loadRejectionDiff(filePath, oldString, newString, replaceAll)); return ( } > ); } function EditRejectionBody({ promise, filePath, style, verbose, }: { promise: Promise; filePath: string; style?: 'condensed'; verbose: boolean; }): React.ReactNode { const { patch, firstLine, fileContent } = use(promise); return ( ); } async function loadRejectionDiff( filePath: string, oldString: string, newString: string, replaceAll: boolean, ): Promise { try { // Chunked read — context window around the first occurrence. replaceAll // still shows matches *within* the window via getPatchForEdit; we accept // losing the all-occurrences view to keep the read bounded. const ctx = await readEditContext(filePath, oldString, CONTEXT_LINES); if (ctx === null || ctx.truncated || ctx.content === '') { // ENOENT / not found / truncated — diff just the tool inputs. const { patch } = getPatchForEdit({ filePath, fileContents: oldString, oldString, newString, }); return { patch, firstLine: null, fileContent: undefined }; } const actualOld = findActualString(ctx.content, oldString) || oldString; const { patch } = getPatchForEdit({ filePath, fileContents: ctx.content, oldString: actualOld, newString: newString, replaceAll, }); return { patch: adjustHunkLineNumbers(patch, ctx.lineOffset - 1), firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null, fileContent: ctx.content, }; } catch (e) { // User may have manually applied the change while the diff was shown. logError(e as Error); return { patch: [], firstLine: null, fileContent: undefined }; } }