Skip to content

Commit 7fe8cb5

Browse files
committed
apply_diff: stream unified diffs to UI during batch preview; include unified patch in single-file approval; remove client-side fallback normalization for appliedDiff; align gap-row styling to editor background
1 parent af8c00a commit 7fe8cb5

File tree

5 files changed

+67
-25
lines changed

5 files changed

+67
-25
lines changed

src/core/tools/applyDiffTool.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ export async function applyDiffToolLegacy(
140140
cline.consecutiveMistakeCount = 0
141141
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
142142

143+
// Generate backend-unified diff for display in chat/webview
144+
const unifiedPatch = formatResponse.createPrettyPatch(relPath, originalContent, diffResult.content)
145+
143146
// Check if preventFocusDisruption experiment is enabled
144147
const provider = cline.providerRef.deref()
145148
const state = await provider?.getState()
@@ -158,6 +161,7 @@ export async function applyDiffToolLegacy(
158161
const completeMessage = JSON.stringify({
159162
...sharedMessageProps,
160163
diff: diffContent,
164+
content: unifiedPatch,
161165
isProtected: isWriteProtected,
162166
} satisfies ClineSayTool)
163167

@@ -194,6 +198,7 @@ export async function applyDiffToolLegacy(
194198
const completeMessage = JSON.stringify({
195199
...sharedMessageProps,
196200
diff: diffContent,
201+
content: unifiedPatch,
197202
isProtected: isWriteProtected,
198203
} satisfies ClineSayTool)
199204

src/core/tools/multiApplyDiffTool.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,31 +282,66 @@ Original error: ${errorMessage}`
282282
(opResult) => cline.rooProtectedController?.isWriteProtected(opResult.path) || false,
283283
)
284284

285-
// Prepare batch diff data
286-
const batchDiffs = operationsToApprove.map((opResult) => {
285+
// Stream batch diffs progressively for better UX
286+
const batchDiffs: Array<{
287+
path: string
288+
changeCount: number
289+
key: string
290+
content: string
291+
diffs?: Array<{ content: string; startLine?: number }>
292+
}> = []
293+
294+
for (const opResult of operationsToApprove) {
287295
const readablePath = getReadablePath(cline.cwd, opResult.path)
288296
const changeCount = opResult.diffItems?.length || 0
289297
const changeText = changeCount === 1 ? "1 change" : `${changeCount} changes`
290298

291-
return {
299+
let unified = ""
300+
try {
301+
const original = await fs.readFile(opResult.absolutePath!, "utf-8")
302+
const processed = !cline.api.getModel().id.includes("claude")
303+
? (opResult.diffItems || []).map((item) => ({
304+
...item,
305+
content: item.content ? unescapeHtmlEntities(item.content) : item.content,
306+
}))
307+
: opResult.diffItems || []
308+
309+
const applyRes =
310+
(await cline.diffStrategy?.applyDiff(original, processed)) ?? ({ success: false } as any)
311+
const newContent = applyRes.success && applyRes.content ? applyRes.content : original
312+
unified = formatResponse.createPrettyPatch(opResult.path, original, newContent)
313+
} catch {
314+
unified = ""
315+
}
316+
317+
batchDiffs.push({
292318
path: readablePath,
293319
changeCount,
294320
key: `${readablePath} (${changeText})`,
295-
content: opResult.path, // Full relative path
321+
content: unified,
296322
diffs: opResult.diffItems?.map((item) => ({
297323
content: item.content,
298324
startLine: item.startLine,
299325
})),
300-
}
301-
})
326+
})
327+
328+
// Send a partial update after each file preview is ready
329+
const partialMessage = JSON.stringify({
330+
tool: "appliedDiff",
331+
batchDiffs,
332+
isProtected: hasProtectedFiles,
333+
} satisfies ClineSayTool)
334+
await cline.ask("tool", partialMessage, true).catch(() => {})
335+
}
302336

337+
// Final approval message (non-partial)
303338
const completeMessage = JSON.stringify({
304339
tool: "appliedDiff",
305340
batchDiffs,
306341
isProtected: hasProtectedFiles,
307342
} satisfies ClineSayTool)
308343

309-
const { response, text, images } = await cline.ask("tool", completeMessage, hasProtectedFiles)
344+
const { response, text, images } = await cline.ask("tool", completeMessage, false)
310345

311346
// Process batch response
312347
if (response === "yesButtonClicked") {
@@ -418,6 +453,7 @@ Original error: ${errorMessage}`
418453

419454
try {
420455
let originalContent: string | null = await fs.readFile(absolutePath, "utf-8")
456+
let beforeContent: string | null = originalContent
421457
let successCount = 0
422458
let formattedError = ""
423459

@@ -540,9 +576,11 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
540576
if (operationsToApprove.length === 1) {
541577
// Prepare common data for single file operation
542578
const diffContents = diffItems.map((item) => item.content).join("\n\n")
579+
const unifiedPatch = formatResponse.createPrettyPatch(relPath, beforeContent!, originalContent!)
543580
const operationMessage = JSON.stringify({
544581
...sharedMessageProps,
545582
diff: diffContents,
583+
content: unifiedPatch,
546584
} satisfies ClineSayTool)
547585

548586
let toolProgressStatus

webview-ui/src/components/chat/BatchDiffApproval.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { memo, useState } from "react"
22
import CodeAccordian from "../common/CodeAccordian"
3-
import { extractUnifiedDiff } from "../../utils/diffUtils"
43

54
interface FileDiff {
65
path: string
@@ -57,14 +56,8 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp
5756
<div className="pt-[5px]">
5857
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
5958
{files.map((file) => {
60-
// Normalize to unified diff and compute stats
61-
const rawCombined = file.diffs?.map((d) => d.content).join("\n\n") || file.content
62-
const unified = extractUnifiedDiff({
63-
toolName: "appliedDiff",
64-
path: file.path,
65-
diff: rawCombined,
66-
content: undefined,
67-
})
59+
// Use backend-provided unified diff only. No client-side fallback for apply_diff batches.
60+
const unified = file.content || ""
6861
const stats = computeUnifiedStats(unified)
6962

7063
return (

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,16 @@ export const ChatRowContent = ({
397397
// Inline diff stats for edit/apply_diff/insert/search-replace/newFile asks
398398
const diffTextForStats = useMemo(() => {
399399
if (!tool) return ""
400-
// Normalize to unified diff using frontend-only capture/surmise helper
400+
// For appliedDiff, backend provides unified diff; do not fallback/normalize
401+
if ((tool as any).tool === "appliedDiff") {
402+
return ((tool as any).content as string) || ""
403+
}
401404
return (
402405
extractUnifiedDiff({
403406
toolName: tool.tool as string,
404407
path: tool.path,
405-
diff: (tool as any).diff,
406-
content: (tool as any).content,
408+
diff: (tool as any).content,
409+
content: (tool as any).diff,
407410
}) || ""
408411
)
409412
}, [tool])
@@ -415,11 +418,15 @@ export const ChatRowContent = ({
415418
// Clean diff content for display (normalize to unified diff)
416419
const cleanDiffContent = useMemo(() => {
417420
if (!tool) return undefined
421+
// For appliedDiff, show backend's unified diff directly
422+
if ((tool as any).tool === "appliedDiff") {
423+
return ((tool as any).content as string) || undefined
424+
}
418425
const unified = extractUnifiedDiff({
419426
toolName: tool.tool as string,
420427
path: tool.path,
421-
diff: (tool as any).diff,
422-
content: (tool as any).content,
428+
diff: (tool as any).content,
429+
content: (tool as any).diff,
423430
})
424431
return unified || undefined
425432
}, [tool])

webview-ui/src/components/common/DiffView.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ const DiffView = memo(({ source, filePath }: DiffViewProps) => {
173173
{diffLines.map((line, idx) => {
174174
// Render compact separator between hunks
175175
if (line.type === "gap") {
176-
const gapBg = "color-mix(in srgb, var(--vscode-editorGroup-border) 100%, transparent)"
176+
// Match the header/container background tone
177+
const gapBg = "var(--vscode-editor-background)"
177178
return (
178179
<tr key={idx}>
179180
<td
@@ -217,15 +218,13 @@ const DiffView = memo(({ source, filePath }: DiffViewProps) => {
217218
/>
218219
<td
219220
style={{
220-
paddingLeft: "4px",
221221
paddingRight: "12px",
222222
whiteSpace: "pre-wrap",
223223
overflowWrap: "anywhere",
224224
wordBreak: "break-word",
225225
fontFamily: "var(--vscode-editor-font-family)",
226-
color: "var(--vscode-descriptionForeground)",
227226
width: "100%",
228-
textAlign: "center",
227+
textAlign: "left",
229228
fontStyle: "italic",
230229
backgroundColor: gapBg,
231230
}}>

0 commit comments

Comments
 (0)