Skip to content

Commit 51b8ad4

Browse files
refactor: 移除消息流中的 diff 渲染,仅保留权限审批页的 diff
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2bad8df commit 51b8ad4

4 files changed

Lines changed: 14 additions & 372 deletions

File tree

Lines changed: 6 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
2-
import type { StructuredPatchHunk } from 'diff'
32
import * as React from 'react'
4-
import { Suspense, use, useState } from 'react'
53
import { FileEditToolUseRejectedMessage } from 'src/components/FileEditToolUseRejectedMessage.js'
64
import { MessageResponse } from 'src/components/MessageResponse.js'
75
import { extractTag } from 'src/utils/messages.js'
@@ -12,19 +10,10 @@ import { Text } from '@anthropic/ink'
1210
import { FilePathLink } from 'src/components/FilePathLink.js'
1311
import type { Tools } from 'src/Tool.js'
1412
import type { Message, ProgressMessage } from 'src/types/message.js'
15-
import { adjustHunkLineNumbers, CONTEXT_LINES } from 'src/utils/diff.js'
1613
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from 'src/utils/file.js'
17-
import { logError } from 'src/utils/log.js'
1814
import { getPlansDirectory } from 'src/utils/plans.js'
19-
import { readEditContext } from 'src/utils/readEditContext.js'
20-
import { firstLineOf } from 'src/utils/stringUtils.js'
2115
import type { ThemeName } from 'src/utils/theme.js'
2216
import type { FileEditOutput } from './types.js'
23-
import {
24-
findActualString,
25-
getPatchForEdit,
26-
preserveQuoteStyle,
27-
} from './utils.js'
2817

2918
export function userFacingName(
3019
input:
@@ -99,8 +88,6 @@ export function renderToolResultMessage(
9988
<FileEditToolUpdatedMessage
10089
filePath={filePath}
10190
structuredPatch={structuredPatch}
102-
firstLine={originalFile.split('\n')[0] ?? null}
103-
fileContent={originalFile}
10491
style={style}
10592
verbose={verbose}
10693
previewHint={isPlanFile ? '/plan to preview' : undefined}
@@ -116,7 +103,7 @@ export function renderToolUseRejectedMessage(
116103
replace_all?: boolean
117104
edits?: unknown[]
118105
},
119-
options: {
106+
_options: {
120107
columns: number
121108
messages: Message[]
122109
progressMessagesForMessage: ProgressMessage[]
@@ -126,45 +113,14 @@ export function renderToolUseRejectedMessage(
126113
verbose: boolean
127114
},
128115
): React.ReactElement {
129-
const { style, verbose } = options
116+
const { style, verbose } = _options
130117
const filePath = input.file_path
131-
const oldString = input.old_string ?? ''
132-
const newString = input.new_string ?? ''
133-
const replaceAll = input.replace_all ?? false
134-
135-
// Defensive: if input has an unexpected shape, show a simple rejection message
136-
if ('edits' in input && input.edits != null) {
137-
return (
138-
<FileEditToolUseRejectedMessage
139-
file_path={filePath}
140-
operation="update"
141-
firstLine={null}
142-
verbose={verbose}
143-
/>
144-
)
145-
}
146-
147-
const isNewFile = oldString === ''
148-
149-
// For new file creation, show content preview instead of diff
150-
if (isNewFile) {
151-
return (
152-
<FileEditToolUseRejectedMessage
153-
file_path={filePath}
154-
operation="write"
155-
content={newString}
156-
firstLine={firstLineOf(newString)}
157-
verbose={verbose}
158-
/>
159-
)
160-
}
118+
const isNewFile = input.old_string === ''
161119

162120
return (
163-
<EditRejectionDiff
164-
filePath={filePath}
165-
oldString={oldString}
166-
newString={newString}
167-
replaceAll={replaceAll}
121+
<FileEditToolUseRejectedMessage
122+
file_path={filePath}
123+
operation={isNewFile ? 'write' : 'update'}
168124
style={style}
169125
verbose={verbose}
170126
/>
@@ -201,115 +157,3 @@ export function renderToolUseErrorMessage(
201157
}
202158
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
203159
}
204-
205-
type RejectionDiffData = {
206-
patch: StructuredPatchHunk[]
207-
firstLine: string | null
208-
fileContent: string | undefined
209-
}
210-
211-
function EditRejectionDiff({
212-
filePath,
213-
oldString,
214-
newString,
215-
replaceAll,
216-
style,
217-
verbose,
218-
}: {
219-
filePath: string
220-
oldString: string
221-
newString: string
222-
replaceAll: boolean
223-
style?: 'condensed'
224-
verbose: boolean
225-
}): React.ReactNode {
226-
const [dataPromise] = useState(() =>
227-
loadRejectionDiff(filePath, oldString, newString, replaceAll),
228-
)
229-
return (
230-
<Suspense
231-
fallback={
232-
<FileEditToolUseRejectedMessage
233-
file_path={filePath}
234-
operation="update"
235-
firstLine={null}
236-
verbose={verbose}
237-
/>
238-
}
239-
>
240-
<EditRejectionBody
241-
promise={dataPromise}
242-
filePath={filePath}
243-
style={style}
244-
verbose={verbose}
245-
/>
246-
</Suspense>
247-
)
248-
}
249-
250-
function EditRejectionBody({
251-
promise,
252-
filePath,
253-
style,
254-
verbose,
255-
}: {
256-
promise: Promise<RejectionDiffData>
257-
filePath: string
258-
style?: 'condensed'
259-
verbose: boolean
260-
}): React.ReactNode {
261-
const { patch, firstLine, fileContent } = use(promise)
262-
return (
263-
<FileEditToolUseRejectedMessage
264-
file_path={filePath}
265-
operation="update"
266-
patch={patch}
267-
firstLine={firstLine}
268-
fileContent={fileContent}
269-
style={style}
270-
verbose={verbose}
271-
/>
272-
)
273-
}
274-
275-
async function loadRejectionDiff(
276-
filePath: string,
277-
oldString: string,
278-
newString: string,
279-
replaceAll: boolean,
280-
): Promise<RejectionDiffData> {
281-
try {
282-
// Chunked read — context window around the first occurrence. replaceAll
283-
// still shows matches *within* the window via getPatchForEdit; we accept
284-
// losing the all-occurrences view to keep the read bounded.
285-
const ctx = await readEditContext(filePath, oldString, CONTEXT_LINES)
286-
if (ctx === null || ctx.truncated || ctx.content === '') {
287-
// ENOENT / not found / truncated — diff just the tool inputs.
288-
const { patch } = getPatchForEdit({
289-
filePath,
290-
fileContents: oldString,
291-
oldString,
292-
newString,
293-
})
294-
return { patch, firstLine: null, fileContent: undefined }
295-
}
296-
const actualOld = findActualString(ctx.content, oldString) || oldString
297-
const actualNew = preserveQuoteStyle(oldString, actualOld, newString)
298-
const { patch } = getPatchForEdit({
299-
filePath,
300-
fileContents: ctx.content,
301-
oldString: actualOld,
302-
newString: actualNew,
303-
replaceAll,
304-
})
305-
return {
306-
patch: adjustHunkLineNumbers(patch, ctx.lineOffset - 1),
307-
firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null,
308-
fileContent: ctx.content,
309-
}
310-
} catch (e) {
311-
// User may have manually applied the change while the diff was shown.
312-
logError(e as Error)
313-
return { patch: [], firstLine: null, fileContent: undefined }
314-
}
315-
}

packages/builtin-tools/src/tools/FileWriteTool/UI.tsx

Lines changed: 3 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
2-
import type { StructuredPatchHunk } from 'diff'
3-
import { isAbsolute, relative, resolve } from 'path'
2+
import { relative } from 'path'
43
import * as React from 'react'
5-
import { Suspense, use, useState } from 'react'
64
import { MessageResponse } from 'src/components/MessageResponse.js'
75
import { extractTag } from 'src/utils/messages.js'
86
import { CtrlOToExpand } from 'src/components/CtrlOToExpand.js'
@@ -17,11 +15,8 @@ import { FilePathLink } from 'src/components/FilePathLink.js'
1715
import type { ToolProgressData } from 'src/Tool.js'
1816
import type { ProgressMessage } from 'src/types/message.js'
1917
import { getCwd } from 'src/utils/cwd.js'
20-
import { getPatchForDisplay } from 'src/utils/diff.js'
2118
import { getDisplayPath } from 'src/utils/file.js'
22-
import { logError } from 'src/utils/log.js'
2319
import { getPlansDirectory } from 'src/utils/plans.js'
24-
import { openForScan, readCapped } from 'src/utils/readEditContext.js'
2520
import type { Output } from './FileWriteTool.js'
2621

2722
const MAX_LINES_TO_RENDER = 10
@@ -137,131 +132,19 @@ export function renderToolUseMessage(
137132
}
138133

139134
export function renderToolUseRejectedMessage(
140-
{ file_path, content }: { file_path: string; content: string },
135+
{ file_path }: { file_path: string; content: string },
141136
{ style, verbose }: { style?: 'condensed'; verbose: boolean },
142137
): React.ReactNode {
143138
return (
144-
<WriteRejectionDiff
145-
filePath={file_path}
146-
content={content}
147-
style={style}
148-
verbose={verbose}
149-
/>
150-
)
151-
}
152-
153-
type RejectionDiffData =
154-
| { type: 'create' }
155-
| { type: 'update'; patch: StructuredPatchHunk[]; oldContent: string }
156-
| { type: 'error' }
157-
158-
function WriteRejectionDiff({
159-
filePath,
160-
content,
161-
style,
162-
verbose,
163-
}: {
164-
filePath: string
165-
content: string
166-
style?: 'condensed'
167-
verbose: boolean
168-
}): React.ReactNode {
169-
const [dataPromise] = useState(() => loadRejectionDiff(filePath, content))
170-
const firstLine = content.split('\n')[0] ?? null
171-
const createFallback = (
172139
<FileEditToolUseRejectedMessage
173-
file_path={filePath}
140+
file_path={file_path}
174141
operation="write"
175-
content={content}
176-
firstLine={firstLine}
177-
verbose={verbose}
178-
/>
179-
)
180-
return (
181-
<Suspense fallback={createFallback}>
182-
<WriteRejectionBody
183-
promise={dataPromise}
184-
filePath={filePath}
185-
firstLine={firstLine}
186-
createFallback={createFallback}
187-
style={style}
188-
verbose={verbose}
189-
/>
190-
</Suspense>
191-
)
192-
}
193-
194-
function WriteRejectionBody({
195-
promise,
196-
filePath,
197-
firstLine,
198-
createFallback,
199-
style,
200-
verbose,
201-
}: {
202-
promise: Promise<RejectionDiffData>
203-
filePath: string
204-
firstLine: string | null
205-
createFallback: React.ReactNode
206-
style?: 'condensed'
207-
verbose: boolean
208-
}): React.ReactNode {
209-
const data = use(promise)
210-
if (data.type === 'create') return createFallback
211-
if (data.type === 'error') {
212-
return (
213-
<MessageResponse>
214-
<Text>(No changes)</Text>
215-
</MessageResponse>
216-
)
217-
}
218-
return (
219-
<FileEditToolUseRejectedMessage
220-
file_path={filePath}
221-
operation="update"
222-
patch={data.patch}
223-
firstLine={firstLine}
224-
fileContent={data.oldContent}
225142
style={style}
226143
verbose={verbose}
227144
/>
228145
)
229146
}
230147

231-
async function loadRejectionDiff(
232-
filePath: string,
233-
content: string,
234-
): Promise<RejectionDiffData> {
235-
try {
236-
const fullFilePath = isAbsolute(filePath)
237-
? filePath
238-
: resolve(getCwd(), filePath)
239-
const handle = await openForScan(fullFilePath)
240-
if (handle === null) return { type: 'create' }
241-
let oldContent: string | null
242-
try {
243-
oldContent = await readCapped(handle)
244-
} finally {
245-
await handle.close()
246-
}
247-
// File exceeds MAX_SCAN_BYTES — fall back to the create view rather than
248-
// OOMing on a diff of a multi-GB file.
249-
if (oldContent === null) return { type: 'create' }
250-
const patch = getPatchForDisplay({
251-
filePath,
252-
fileContents: oldContent,
253-
edits: [
254-
{ old_string: oldContent, new_string: content, replace_all: false },
255-
],
256-
})
257-
return { type: 'update', patch, oldContent }
258-
} catch (e) {
259-
// User may have manually applied the change while the diff was shown.
260-
logError(e as Error)
261-
return { type: 'error' }
262-
}
263-
}
264-
265148
export function renderToolUseErrorMessage(
266149
result: ToolResultBlockParam['content'],
267150
{ verbose }: { verbose: boolean },
@@ -324,8 +207,6 @@ export function renderToolResultMessage(
324207
<FileEditToolUpdatedMessage
325208
filePath={filePath}
326209
structuredPatch={structuredPatch}
327-
firstLine={content.split('\n')[0] ?? null}
328-
fileContent={originalFile ?? undefined}
329210
style={style}
330211
verbose={verbose}
331212
previewHint={isPlanFile ? '/plan to preview' : undefined}

0 commit comments

Comments
 (0)