mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
fix: 恢复消息流中 diff 高亮渲染功能
还原 commit 51b8ad46 删除的 diff highlight 显示:FileEdit/FileWrite 工具
执行成功后重新展示 StructuredDiffList,拒绝时重新展示高亮代码预览或
带上下文的 diff 视图。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
@@ -10,10 +12,15 @@ 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, preserveQuoteStyle } from './utils.js';
|
||||
|
||||
export function userFacingName(
|
||||
input:
|
||||
@@ -84,6 +91,8 @@ export function renderToolResultMessage(
|
||||
<FileEditToolUpdatedMessage
|
||||
filePath={filePath}
|
||||
structuredPatch={structuredPatch}
|
||||
firstLine={originalFile.split('\n')[0] ?? null}
|
||||
fileContent={originalFile}
|
||||
style={style}
|
||||
verbose={verbose}
|
||||
previewHint={isPlanFile ? '/plan to preview' : undefined}
|
||||
@@ -99,7 +108,7 @@ export function renderToolUseRejectedMessage(
|
||||
replace_all?: boolean;
|
||||
edits?: unknown[];
|
||||
},
|
||||
_options: {
|
||||
options: {
|
||||
columns: number;
|
||||
messages: Message[];
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
@@ -109,14 +118,40 @@ export function renderToolUseRejectedMessage(
|
||||
verbose: boolean;
|
||||
},
|
||||
): React.ReactElement {
|
||||
const { style, verbose } = _options;
|
||||
const { style, verbose } = options;
|
||||
const filePath = input.file_path;
|
||||
const isNewFile = input.old_string === '';
|
||||
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 (
|
||||
<FileEditToolUseRejectedMessage
|
||||
file_path={filePath}
|
||||
operation={isNewFile ? 'write' : 'update'}
|
||||
<EditRejectionDiff
|
||||
filePath={filePath}
|
||||
oldString={oldString}
|
||||
newString={newString}
|
||||
replaceAll={replaceAll}
|
||||
style={style}
|
||||
verbose={verbose}
|
||||
/>
|
||||
@@ -149,3 +184,103 @@ export function renderToolUseErrorMessage(
|
||||
}
|
||||
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<Suspense
|
||||
fallback={
|
||||
<FileEditToolUseRejectedMessage file_path={filePath} operation="update" firstLine={null} verbose={verbose} />
|
||||
}
|
||||
>
|
||||
<EditRejectionBody promise={dataPromise} filePath={filePath} style={style} verbose={verbose} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function EditRejectionBody({
|
||||
promise,
|
||||
filePath,
|
||||
style,
|
||||
verbose,
|
||||
}: {
|
||||
promise: Promise<RejectionDiffData>;
|
||||
filePath: string;
|
||||
style?: 'condensed';
|
||||
verbose: boolean;
|
||||
}): React.ReactNode {
|
||||
const { patch, firstLine, fileContent } = use(promise);
|
||||
return (
|
||||
<FileEditToolUseRejectedMessage
|
||||
file_path={filePath}
|
||||
operation="update"
|
||||
patch={patch}
|
||||
firstLine={firstLine}
|
||||
fileContent={fileContent}
|
||||
style={style}
|
||||
verbose={verbose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user