mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:26:58 +10:00
289 lines
34 KiB
TypeScript
289 lines
34 KiB
TypeScript
|
|
import { c as _c } from "react/compiler-runtime";
|
||
|
|
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 '../../components/FallbackToolUseErrorMessage.js';
|
||
|
|
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js';
|
||
|
|
import { FilePathLink } from '../../components/FilePathLink.js';
|
||
|
|
import { Text } from '../../ink.js';
|
||
|
|
import type { Tools } from '../../Tool.js';
|
||
|
|
import type { Message, ProgressMessage } from '../../types/message.js';
|
||
|
|
import { adjustHunkLineNumbers, CONTEXT_LINES } from '../../utils/diff.js';
|
||
|
|
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js';
|
||
|
|
import { logError } from '../../utils/log.js';
|
||
|
|
import { getPlansDirectory } from '../../utils/plans.js';
|
||
|
|
import { readEditContext } from '../../utils/readEditContext.js';
|
||
|
|
import { firstLineOf } from '../../utils/stringUtils.js';
|
||
|
|
import type { ThemeName } from '../../utils/theme.js';
|
||
|
|
import type { FileEditOutput } from './types.js';
|
||
|
|
import { findActualString, getPatchForEdit, preserveQuoteStyle } 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 <FilePathLink filePath={file_path}>
|
||
|
|
{verbose ? file_path : getDisplayPath(file_path)}
|
||
|
|
</FilePathLink>;
|
||
|
|
}
|
||
|
|
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 <FileEditToolUpdatedMessage filePath={filePath} structuredPatch={structuredPatch} firstLine={originalFile.split('\n')[0] ?? null} fileContent={originalFile} style={style} verbose={verbose} previewHint={isPlanFile ? '/plan to preview' : undefined} />;
|
||
|
|
}
|
||
|
|
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 <FileEditToolUseRejectedMessage file_path={filePath} operation="update" firstLine={null} verbose={verbose} />;
|
||
|
|
}
|
||
|
|
const isNewFile = oldString === '';
|
||
|
|
|
||
|
|
// For new file creation, show content preview instead of diff
|
||
|
|
if (isNewFile) {
|
||
|
|
return <FileEditToolUseRejectedMessage file_path={filePath} operation="write" content={newString} firstLine={firstLineOf(newString)} verbose={verbose} />;
|
||
|
|
}
|
||
|
|
return <EditRejectionDiff filePath={filePath} oldString={oldString} newString={newString} replaceAll={replaceAll} style={style} verbose={verbose} />;
|
||
|
|
}
|
||
|
|
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');
|
||
|
|
// Show a less scary message for intended behavior
|
||
|
|
if (errorMessage?.includes('File has not been read yet')) {
|
||
|
|
return <MessageResponse>
|
||
|
|
<Text dimColor>File must be read first</Text>
|
||
|
|
</MessageResponse>;
|
||
|
|
}
|
||
|
|
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
|
||
|
|
return <MessageResponse>
|
||
|
|
<Text color="error">File not found</Text>
|
||
|
|
</MessageResponse>;
|
||
|
|
}
|
||
|
|
return <MessageResponse>
|
||
|
|
<Text color="error">Error editing file</Text>
|
||
|
|
</MessageResponse>;
|
||
|
|
}
|
||
|
|
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
|
||
|
|
}
|
||
|
|
type RejectionDiffData = {
|
||
|
|
patch: StructuredPatchHunk[];
|
||
|
|
firstLine: string | null;
|
||
|
|
fileContent: string | undefined;
|
||
|
|
};
|
||
|
|
function EditRejectionDiff(t0) {
|
||
|
|
const $ = _c(16);
|
||
|
|
const {
|
||
|
|
filePath,
|
||
|
|
oldString,
|
||
|
|
newString,
|
||
|
|
replaceAll,
|
||
|
|
style,
|
||
|
|
verbose
|
||
|
|
} = t0;
|
||
|
|
let t1;
|
||
|
|
if ($[0] !== filePath || $[1] !== newString || $[2] !== oldString || $[3] !== replaceAll) {
|
||
|
|
t1 = () => loadRejectionDiff(filePath, oldString, newString, replaceAll);
|
||
|
|
$[0] = filePath;
|
||
|
|
$[1] = newString;
|
||
|
|
$[2] = oldString;
|
||
|
|
$[3] = replaceAll;
|
||
|
|
$[4] = t1;
|
||
|
|
} else {
|
||
|
|
t1 = $[4];
|
||
|
|
}
|
||
|
|
const [dataPromise] = useState(t1);
|
||
|
|
let t2;
|
||
|
|
if ($[5] !== filePath || $[6] !== verbose) {
|
||
|
|
t2 = <FileEditToolUseRejectedMessage file_path={filePath} operation="update" firstLine={null} verbose={verbose} />;
|
||
|
|
$[5] = filePath;
|
||
|
|
$[6] = verbose;
|
||
|
|
$[7] = t2;
|
||
|
|
} else {
|
||
|
|
t2 = $[7];
|
||
|
|
}
|
||
|
|
let t3;
|
||
|
|
if ($[8] !== dataPromise || $[9] !== filePath || $[10] !== style || $[11] !== verbose) {
|
||
|
|
t3 = <EditRejectionBody promise={dataPromise} filePath={filePath} style={style} verbose={verbose} />;
|
||
|
|
$[8] = dataPromise;
|
||
|
|
$[9] = filePath;
|
||
|
|
$[10] = style;
|
||
|
|
$[11] = verbose;
|
||
|
|
$[12] = t3;
|
||
|
|
} else {
|
||
|
|
t3 = $[12];
|
||
|
|
}
|
||
|
|
let t4;
|
||
|
|
if ($[13] !== t2 || $[14] !== t3) {
|
||
|
|
t4 = <Suspense fallback={t2}>{t3}</Suspense>;
|
||
|
|
$[13] = t2;
|
||
|
|
$[14] = t3;
|
||
|
|
$[15] = t4;
|
||
|
|
} else {
|
||
|
|
t4 = $[15];
|
||
|
|
}
|
||
|
|
return t4;
|
||
|
|
}
|
||
|
|
function EditRejectionBody(t0) {
|
||
|
|
const $ = _c(7);
|
||
|
|
const {
|
||
|
|
promise,
|
||
|
|
filePath,
|
||
|
|
style,
|
||
|
|
verbose
|
||
|
|
} = t0;
|
||
|
|
const {
|
||
|
|
patch,
|
||
|
|
firstLine,
|
||
|
|
fileContent
|
||
|
|
} = use(promise);
|
||
|
|
let t1;
|
||
|
|
if ($[0] !== fileContent || $[1] !== filePath || $[2] !== firstLine || $[3] !== patch || $[4] !== style || $[5] !== verbose) {
|
||
|
|
t1 = <FileEditToolUseRejectedMessage file_path={filePath} operation="update" patch={patch} firstLine={firstLine} fileContent={fileContent} style={style} verbose={verbose} />;
|
||
|
|
$[0] = fileContent;
|
||
|
|
$[1] = filePath;
|
||
|
|
$[2] = firstLine;
|
||
|
|
$[3] = patch;
|
||
|
|
$[4] = style;
|
||
|
|
$[5] = verbose;
|
||
|
|
$[6] = t1;
|
||
|
|
} else {
|
||
|
|
t1 = $[6];
|
||
|
|
}
|
||
|
|
return t1;
|
||
|
|
}
|
||
|
|
async function loadRejectionDiff(filePath: string, oldString: string, newString: string, replaceAll: boolean): Promise<RejectionDiffData> {
|
||
|
|
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 actualNew = preserveQuoteStyle(oldString, actualOld, newString);
|
||
|
|
const {
|
||
|
|
patch
|
||
|
|
} = getPatchForEdit({
|
||
|
|
filePath,
|
||
|
|
fileContents: ctx.content,
|
||
|
|
oldString: actualOld,
|
||
|
|
newString: actualNew,
|
||
|
|
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
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJUb29sUmVzdWx0QmxvY2tQYXJhbSIsIlN0cnVjdHVyZWRQYXRjaEh1bmsiLCJSZWFjdCIsIlN1c3BlbnNlIiwidXNlIiwidXNlU3RhdGUiLCJGaWxlRWRpdFRvb2xVc2VSZWplY3RlZE1lc3NhZ2UiLCJNZXNzYWdlUmVzcG9uc2UiLCJleHRyYWN0VGFnIiwiRmFsbGJhY2tUb29sVXNlRXJyb3JNZXNzYWdlIiwiRmlsZUVkaXRUb29sVXBkYXRlZE1lc3NhZ2UiLCJGaWxlUGF0aExpbmsiLCJUZXh0IiwiVG9vbHMiLCJNZXNzYWdlIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiYWRqdXN0SHVua0xpbmVOdW1iZXJzIiwiQ09OVEVYVF9MSU5FUyIsIkZJTEVfTk9UX0ZPVU5EX0NXRF9OT1RFIiwiZ2V0RGlzcGxheVBhdGgiLCJsb2dFcnJvciIsImdldFBsYW5zRGlyZWN0b3J5IiwicmVhZEVkaXRDb250ZXh0IiwiZmlyc3RMaW5lT2YiLCJUaGVtZU5hbWUiLCJGaWxlRWRpdE91dHB1dCIsImZpbmRBY3R1YWxTdHJpbmciLCJnZXRQYXRjaEZvckVkaXQiLCJwcmVzZXJ2ZVF1b3RlU3R5bGUiLCJ1c2VyRmFjaW5nTmFtZSIsImlucHV0IiwiUGFydGlhbCIsImZpbGVfcGF0aCIsIm9sZF9zdHJpbmciLCJuZXdfc3RyaW5nIiwicmVwbGFjZV9hbGwiLCJlZGl0cyIsInN0YXJ0c1dpdGgiLCJnZXRUb29sVXNlU3VtbWFyeSIsInJlbmRlclRvb2xVc2VNZXNzYWdlIiwidmVyYm9zZSIsIlJlYWN0Tm9kZSIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwiZmlsZVBhdGgiLCJzdHJ1Y3R1cmVkUGF0Y2giLCJvcmlnaW5hbEZpbGUiLCJfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UiLCJzdHlsZSIsImlzUGxhbkZpbGUiLCJzcGxpdCIsInVuZGVmaW5lZCIsInJlbmRlclRvb2xVc2VSZWplY3RlZE1lc3NhZ2UiLCJvcHRpb25zIiwiY29sdW1ucyIsIm1lc3NhZ2VzIiwicHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UiLCJ0aGVtZSIsInRvb2xzIiwiUmVhY3RFbGVtZW50Iiwib2xkU3RyaW5nIiwibmV3U3RyaW5nIiwicmVwbGFjZUFsbCIsImlzTmV3RmlsZSIsInJlbmRlclRvb2xVc2VFcnJvck1lc3NhZ2UiLCJyZXN1bHQiLCJlcnJvck1lc3NhZ2UiLCJpbmNsdWRlcyIsIlJlamVjdGlvbkRpZmZEYXRhIiwicGF0Y2giLCJmaXJzdExpbmUiLCJmaWxlQ29udGVudCIsIkVkaXRSZWplY3Rpb25EaWZmIiwidDAiLCIkIiwiX2MiLCJ0MSIsImxvYWRSZWplY3Rpb25EaWZmIiwiZGF0YVByb21pc2UiLCJ0MiIsInQzIiwidDQiLCJFZGl0UmVqZWN0aW9uQm9keSIsInByb21pc2UiLCJQcm9taXNlIiwiY3R4IiwidHJ1bmNhdGVkIiwiY29udGVudCIsImZpbGVDb250ZW50cyIsImFjdHVhbE9sZCIsImFjdHVhbE5ldyIsImxpbmVPZmZzZXQiLCJlIiwiRXJyb3IiXSwic291cmNlcyI6WyJVSS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHR5cGUgeyBUb29sUmVzdWx0QmxvY2tQYXJhbSB9IGZyb20gJ0BhbnRocm9waWMtYWkvc2RrL3Jlc291cmNlcy9pbmRleC5tanMnXG5pbXBvcnQgdHlwZSB7IFN0cnVjdHVyZWRQYXRjaEh1bmsgfSBmcm9tICdkaWZmJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBTdXNwZW5zZSwgdXNlLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgRmlsZUVkaXRUb29sVXNlUmVqZWN0ZWRNZXNzYWdlIH0gZnJvbSAnc3JjL2NvbXBvbmVudHMvRmlsZUVkaXRUb29sVXNlUmVqZWN0ZWRNZXNzYWdlLmpzJ1xuaW1wb3J0IHsgTWVzc2FnZVJlc3BvbnNlIH0gZnJvbSAnc3JjL2NvbXBvbmVudHMvTWVzc2FnZVJlc3BvbnNlLmpzJ1xuaW1wb3J0IHsgZXh0cmFjdFRhZyB9IGZyb20gJ3NyYy91dGlscy9tZXNzYWdlcy5qcydcbmltcG9ydCB7IEZhbGxiYWNrVG9vbFVzZUVycm9yTWVzc2FnZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvRmFsbGJhY2tUb29sVXNlRXJyb3JNZXNzYWdlLmpzJ1xuaW1wb3J0IHsgRmlsZUVkaXRUb29sVXBkYXRlZE1lc3NhZ2UgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL0ZpbGVFZGl0VG9vbFVwZGF0ZWRNZXNzYWdlLmpzJ1xuaW1wb3J0IHsgRmlsZVBhdGhMaW5rIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9GaWxlUGF0aExpbmsuanMnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUb29scyB9IGZyb20gJy4uLy4uL1Rvb2wuanMnXG5pbXBvcnQgdHlwZSB7IE1lc3NhZ2UsIFByb2dyZXNzTWVzc2FnZSB9IGZyb20gJy4uLy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgeyBhZGp1c3RIdW5rTGluZU51bWJlcnMsIENPTlRFWFRfTElORVMgfSBmcm9tICcuLi8uLi91dGlscy9kaWZmLmpzJ1xuaW1wb3J0IHsgRklMRV9OT1RfRk9VTkRfQ1dEX05PVEUsIGdldERpc3BsYXlQYXRoIH0gZnJvbSAnLi4vLi4vdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJ1xuaW1wb3J0IHsgZ2V0UGxhbnNEaXJlY3RvcnkgfSBmcm9tICcuLi8uLi91dGlscy9wbGFucy5qcydcbmltcG9ydCB7IHJlYWRFZGl0Q29udGV4dCB9IGZyb20gJy4uLy4uL3V0aWxzL3JlYWRFZGl0Q29udGV4dC5qcydcbmltcG9ydCB7IGZpcnN0TGluZU9mIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lTmFtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHR5cGUgeyBGaWxlRWRpdE91dHB1dCB9IGZyb20gJy4vdHlwZXMuanMnXG5pbXBvcnQge1xuICBmaW5kQWN0dWFsU3RyaW5nLFxuICBnZXRQYXRjaEZvckVkaXQsXG4gIHByZXNlcnZlUXVvdGVTdHlsZSxcbn0gZnJvbSAnLi91dGlscy5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIHVzZXJGYWNpbmdOYW1lKFxuICBpbnB1dDpcbiAgICB8IFBhcnRpYWw8e1xuICAgICAgICBmaWxlX3BhdGg6IHN0cmluZ1xuICAgICAgICBvbGRfc3RyaW5nOiBzdHJpbmdcbiAgICAgICAgbmV3X3N0cmluZzogc3RyaW5nXG4gICAgICAgIHJlcGxhY2VfYWxsOiBib29sZWFuXG4gICAgICAgIGVkaXRzOiB1bmtub3duW11cbiAgICAgIH0+XG4gICAgfCB1bmR
|