Skip to content

Commit 9a31d53

Browse files
committed
Moved business logic out of webview and cleaned up
1 parent 7906320 commit 9a31d53

File tree

12 files changed

+140
-240
lines changed

12 files changed

+140
-240
lines changed

src/core/diff/stats.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { parsePatch, createTwoFilesPatch } from "diff"
2+
3+
/**
4+
* Diff utilities for backend (extension) use.
5+
* Source of truth for diff normalization and stats.
6+
*/
7+
8+
export interface DiffStats {
9+
added: number
10+
removed: number
11+
}
12+
13+
/**
14+
* Remove non-semantic diff noise like "No newline at end of file"
15+
*/
16+
export function sanitizeUnifiedDiff(diff: string): string {
17+
if (!diff) return diff
18+
return diff.replace(/\r\n/g, "\n").replace(/(^|\n)[ \t]*(?:\\ )?No newline at end of file[ \t]*(?=\n|$)/gi, "$1")
19+
}
20+
21+
/**
22+
* Compute +/− counts from a unified diff (ignores headers/hunk lines)
23+
*/
24+
export function computeUnifiedDiffStats(diff?: string): DiffStats | null {
25+
if (!diff) return null
26+
27+
try {
28+
const patches = parsePatch(diff)
29+
if (!patches || patches.length === 0) return null
30+
31+
let added = 0
32+
let removed = 0
33+
34+
for (const p of patches) {
35+
for (const h of (p as any).hunks ?? []) {
36+
for (const l of h.lines ?? []) {
37+
const ch = (l as string)[0]
38+
if (ch === "+") added++
39+
else if (ch === "-") removed++
40+
}
41+
}
42+
}
43+
44+
if (added > 0 || removed > 0) return { added, removed }
45+
return { added: 0, removed: 0 }
46+
} catch {
47+
// If parsing fails for any reason, signal no stats
48+
return null
49+
}
50+
}
51+
52+
/**
53+
* Compute diff stats from any supported diff format (unified or search-replace)
54+
* Tries unified diff format first, then falls back to search-replace format
55+
*/
56+
export function computeDiffStats(diff?: string): DiffStats | null {
57+
if (!diff) return null
58+
return computeUnifiedDiffStats(diff)
59+
}
60+
61+
/**
62+
* Build a unified diff for a brand new file (all content lines are additions).
63+
* Trailing newline is ignored for line counting and emission.
64+
*/
65+
export function convertNewFileToUnifiedDiff(content: string, filePath?: string): string {
66+
const newFileName = filePath || "file"
67+
// Normalize EOLs; rely on library for unified patch formatting
68+
const normalized = (content || "").replace(/\r\n/g, "\n")
69+
// Old file is empty (/dev/null), new file has content; zero context to show all lines as additions
70+
return createTwoFilesPatch("/dev/null", newFileName, "", normalized, undefined, undefined, { context: 0 })
71+
}

src/core/tools/applyDiffTool.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { fileExistsAtPath } from "../../utils/fs"
1313
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1414
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1515
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
16+
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1617

1718
export async function applyDiffToolLegacy(
1819
cline: Task,
@@ -141,7 +142,9 @@ export async function applyDiffToolLegacy(
141142
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
142143

143144
// Generate backend-unified diff for display in chat/webview
144-
const unifiedPatch = formatResponse.createPrettyPatch(relPath, originalContent, diffResult.content)
145+
const unifiedPatchRaw = formatResponse.createPrettyPatch(relPath, originalContent, diffResult.content)
146+
const unifiedPatch = sanitizeUnifiedDiff(unifiedPatchRaw)
147+
const diffStats = computeDiffStats(unifiedPatch) || undefined
145148

146149
// Check if preventFocusDisruption experiment is enabled
147150
const provider = cline.providerRef.deref()
@@ -162,6 +165,7 @@ export async function applyDiffToolLegacy(
162165
...sharedMessageProps,
163166
diff: diffContent,
164167
content: unifiedPatch,
168+
diffStats,
165169
isProtected: isWriteProtected,
166170
} satisfies ClineSayTool)
167171

@@ -199,6 +203,7 @@ export async function applyDiffToolLegacy(
199203
...sharedMessageProps,
200204
diff: diffContent,
201205
content: unifiedPatch,
206+
diffStats,
202207
isProtected: isWriteProtected,
203208
} satisfies ClineSayTool)
204209

src/core/tools/insertContentTool.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { fileExistsAtPath } from "../../utils/fs"
1212
import { insertGroups } from "../diff/insert-groups"
1313
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
1414
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
15+
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1516

1617
export async function insertContentTool(
1718
cline: Task,
@@ -101,7 +102,7 @@ export async function insertContentTool(
101102
cline.diffViewProvider.originalContent = fileContent
102103
const lines = fileExists ? fileContent.split("\n") : []
103104

104-
const updatedContent = insertGroups(lines, [
105+
let updatedContent = insertGroups(lines, [
105106
{
106107
index: lineNumber - 1,
107108
elements: content.split("\n"),
@@ -118,31 +119,31 @@ export async function insertContentTool(
118119
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
119120
)
120121

121-
// For consistency with writeToFileTool, handle new files differently
122-
let diff: string | undefined
123-
let approvalContent: string | undefined
124-
122+
// Build unified diff for display (normalize EOLs only for diff generation)
123+
let unified: string
125124
if (fileExists) {
126-
// For existing files, generate diff and check for changes
127-
diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent)
128-
if (!diff) {
125+
const oldForDiff = fileContent.replace(/\r\n/g, "\n")
126+
const newForDiff = updatedContent.replace(/\r\n/g, "\n")
127+
unified = formatResponse.createPrettyPatch(relPath, oldForDiff, newForDiff)
128+
if (!unified) {
129129
pushToolResult(`No changes needed for '${relPath}'`)
130130
return
131131
}
132-
approvalContent = undefined
133132
} else {
134-
// For new files, skip diff generation and provide full content
135-
diff = undefined
136-
approvalContent = updatedContent
133+
const newForDiff = updatedContent.replace(/\r\n/g, "\n")
134+
unified = convertNewFileToUnifiedDiff(newForDiff, relPath)
137135
}
136+
unified = sanitizeUnifiedDiff(unified)
137+
const diffStats = computeDiffStats(unified) || undefined
138138

139139
// Prepare the approval message (same for both flows)
140140
const completeMessage = JSON.stringify({
141141
...sharedMessageProps,
142-
diff,
143-
content: approvalContent,
142+
// Send unified diff as content for render-only webview
143+
content: unified,
144144
lineNumber: lineNumber,
145145
isProtected: isWriteProtected,
146+
diffStats,
146147
} satisfies ClineSayTool)
147148

148149
// Show diff view if focus disruption prevention is disabled

src/core/tools/multiApplyDiffTool.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
1515
import { parseXmlForDiff } from "../../utils/xml"
1616
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1717
import { applyDiffToolLegacy } from "./applyDiffTool"
18+
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1819

1920
interface DiffOperation {
2021
path: string
@@ -288,6 +289,7 @@ Original error: ${errorMessage}`
288289
changeCount: number
289290
key: string
290291
content: string
292+
diffStats?: { added: number; removed: number }
291293
diffs?: Array<{ content: string; startLine?: number }>
292294
}> = []
293295

@@ -314,11 +316,14 @@ Original error: ${errorMessage}`
314316
unified = ""
315317
}
316318

319+
const unifiedSanitized = sanitizeUnifiedDiff(unified)
320+
const stats = computeDiffStats(unifiedSanitized) || undefined
317321
batchDiffs.push({
318322
path: readablePath,
319323
changeCount,
320324
key: `${readablePath} (${changeText})`,
321-
content: unified,
325+
content: unifiedSanitized,
326+
diffStats: stats,
322327
diffs: opResult.diffItems?.map((item) => ({
323328
content: item.content,
324329
startLine: item.startLine,
@@ -576,11 +581,13 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
576581
if (operationsToApprove.length === 1) {
577582
// Prepare common data for single file operation
578583
const diffContents = diffItems.map((item) => item.content).join("\n\n")
579-
const unifiedPatch = formatResponse.createPrettyPatch(relPath, beforeContent!, originalContent!)
584+
const unifiedPatchRaw = formatResponse.createPrettyPatch(relPath, beforeContent!, originalContent!)
585+
const unifiedPatch = sanitizeUnifiedDiff(unifiedPatchRaw)
580586
const operationMessage = JSON.stringify({
581587
...sharedMessageProps,
582588
diff: diffContents,
583589
content: unifiedPatch,
590+
diffStats: computeDiffStats(unifiedPatch) || undefined,
584591
} satisfies ClineSayTool)
585592

586593
let toolProgressStatus

src/core/tools/writeToFileTool.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { detectCodeOmission } from "../../integrations/editor/detect-omission"
1616
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1717
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
1818
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
19+
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1920

2021
export async function writeToFileTool(
2122
cline: Task,
@@ -211,12 +212,15 @@ export async function writeToFileTool(
211212
}
212213
}
213214

215+
// Build unified diff for both existing and new files
216+
let unified = fileExists
217+
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
218+
: convertNewFileToUnifiedDiff(newContent, relPath)
219+
unified = sanitizeUnifiedDiff(unified)
214220
const completeMessage = JSON.stringify({
215221
...sharedMessageProps,
216-
content: fileExists ? undefined : newContent,
217-
diff: fileExists
218-
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
219-
: undefined,
222+
content: unified,
223+
diffStats: computeDiffStats(unified) || undefined,
220224
} satisfies ClineSayTool)
221225

222226
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
@@ -278,12 +282,15 @@ export async function writeToFileTool(
278282
}
279283
}
280284

285+
// Build unified diff for both existing and new files
286+
let unified = fileExists
287+
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
288+
: convertNewFileToUnifiedDiff(newContent, relPath)
289+
unified = sanitizeUnifiedDiff(unified)
281290
const completeMessage = JSON.stringify({
282291
...sharedMessageProps,
283-
content: fileExists ? undefined : newContent,
284-
diff: fileExists
285-
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
286-
: undefined,
292+
content: unified,
293+
diffStats: computeDiffStats(unified) || undefined,
287294
} satisfies ClineSayTool)
288295

289296
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)

src/shared/ExtensionMessage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ export interface ClineSayTool {
386386
path?: string
387387
diff?: string
388388
content?: string
389+
// Unified diff statistics computed by the extension
390+
diffStats?: { added: number; removed: number }
389391
regex?: string
390392
filePattern?: string
391393
mode?: string
@@ -407,6 +409,8 @@ export interface ClineSayTool {
407409
changeCount: number
408410
key: string
409411
content: string
412+
// Per-file unified diff statistics computed by the extension
413+
diffStats?: { added: number; removed: number }
410414
diffs?: Array<{
411415
content: string
412416
startLine?: number

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { memo, useState } from "react"
22
import CodeAccordian from "../common/CodeAccordian"
3-
import { computeUnifiedDiffStats } from "../../utils/diffStats"
43

54
interface FileDiff {
65
path: string
76
changeCount: number
87
key: string
98
content: string
9+
diffStats?: { added: number; removed: number }
1010
diffs?: Array<{
1111
content: string
1212
startLine?: number
@@ -36,9 +36,8 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp
3636
<div className="pt-[5px]">
3737
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
3838
{files.map((file) => {
39-
// Use backend-provided unified diff only. No client-side fallback for apply_diff batches.
39+
// Use backend-provided unified diff only. Stats also provided by backend.
4040
const unified = file.content || ""
41-
const stats = computeUnifiedDiffStats(unified)
4241

4342
return (
4443
<div key={`${file.path}-${ts}`}>
@@ -48,7 +47,7 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp
4847
language="diff"
4948
isExpanded={expandedFiles[file.path] || false}
5049
onToggleExpand={() => handleToggleExpand(file.path)}
51-
diffStats={stats ?? undefined}
50+
diffStats={file.diffStats ?? undefined}
5251
/>
5352
</div>
5453
)

0 commit comments

Comments
 (0)