Skip to content

Commit c0b070e

Browse files
authored
Improvements to apply_diff (#52)
1 parent 23486f1 commit c0b070e

File tree

10 files changed

+159
-48
lines changed

10 files changed

+159
-48
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Roo Cline Changelog
22

3+
## [2.1.14]
4+
5+
- Fix bug where diffs were not being applied correctly and try Aider's [unified diff prompt](https://github.com/Aider-AI/aider/blob/3995accd0ca71cea90ef76d516837f8c2731b9fe/aider/coders/udiff_prompts.py#L75-L105)
6+
- If diffs are enabled, automatically reject write_to_file commands that lead to truncated output
7+
38
## [2.1.13]
49

510
- Fix https://github.com/RooVetGit/Roo-Cline/issues/50 where sound effects were not respecting settings
@@ -47,4 +52,4 @@
4752
## [2.1.2]
4853

4954
- Support for auto-approval of write operations and command execution
50-
- Support for .clinerules custom instructions
55+
- Support for .clinerules custom instructions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Cline",
44
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "2.1.13",
6+
"version": "2.1.14",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",

src/core/Cline.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ApiHandler, buildApiHandler } from "../api"
1212
import { ApiStream } from "../api/transform/stream"
1313
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
1414
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
15-
import { extractTextFromFile } from "../integrations/misc/extract-text"
15+
import { extractTextFromFile, addLineNumbers } from "../integrations/misc/extract-text"
1616
import { TerminalManager } from "../integrations/terminal/TerminalManager"
1717
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
1818
import { listFiles } from "../services/glob/list-files"
@@ -46,7 +46,7 @@ import { formatResponse } from "./prompts/responses"
4646
import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system"
4747
import { truncateHalfConversation } from "./sliding-window"
4848
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
49-
import { showOmissionWarning } from "../integrations/editor/detect-omission"
49+
import { detectCodeOmission } from "../integrations/editor/detect-omission"
5050
import { BrowserSession } from "../services/browser/BrowserSession"
5151

5252
const cwd =
@@ -1101,7 +1101,32 @@ export class Cline {
11011101
await this.diffViewProvider.update(newContent, true)
11021102
await delay(300) // wait for diff view to update
11031103
this.diffViewProvider.scrollToFirstDiff()
1104-
showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
1104+
1105+
// Check for code omissions before proceeding
1106+
if (detectCodeOmission(this.diffViewProvider.originalContent || "", newContent)) {
1107+
if (this.diffEnabled) {
1108+
await this.diffViewProvider.revertChanges()
1109+
pushToolResult(formatResponse.toolError(
1110+
"Content appears to be truncated. Found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file."
1111+
))
1112+
break
1113+
} else {
1114+
vscode.window
1115+
.showWarningMessage(
1116+
"Potential code truncation detected. This happens when the AI reaches its max output limit.",
1117+
"Follow this guide to fix the issue",
1118+
)
1119+
.then((selection) => {
1120+
if (selection === "Follow this guide to fix the issue") {
1121+
vscode.env.openExternal(
1122+
vscode.Uri.parse(
1123+
"https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments",
1124+
),
1125+
)
1126+
}
1127+
})
1128+
}
1129+
}
11051130

11061131
const completeMessage = JSON.stringify({
11071132
...sharedMessageProps,
@@ -1133,8 +1158,8 @@ export class Cline {
11331158
)
11341159
pushToolResult(
11351160
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1136-
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
1137-
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
1161+
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
1162+
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
11381163
`Please note:\n` +
11391164
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
11401165
`2. Proceed with the task using this updated file content as the new baseline.\n` +
@@ -1231,11 +1256,32 @@ export class Cline {
12311256
break
12321257
}
12331258

1234-
await fs.writeFile(absolutePath, newContent)
1235-
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
1259+
const { newProblemsMessage, userEdits, finalContent } =
1260+
await this.diffViewProvider.saveChanges()
1261+
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1262+
if (userEdits) {
1263+
await this.say(
1264+
"user_feedback_diff",
1265+
JSON.stringify({
1266+
tool: fileExists ? "editedExistingFile" : "newFileCreated",
1267+
path: getReadablePath(cwd, relPath),
1268+
diff: userEdits,
1269+
} satisfies ClineSayTool),
1270+
)
1271+
pushToolResult(
1272+
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1273+
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
1274+
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
1275+
`Please note:\n` +
1276+
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
1277+
`2. Proceed with the task using this updated file content as the new baseline.\n` +
1278+
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
1279+
`${newProblemsMessage}`,
1280+
)
1281+
} else {
1282+
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`)
1283+
}
12361284
await this.diffViewProvider.reset()
1237-
1238-
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${diffRepresentation}`)
12391285
break
12401286
}
12411287
} catch (error) {
@@ -2242,3 +2288,4 @@ export class Cline {
22422288
return `<environment_details>\n${details.trim()}\n</environment_details>`
22432289
}
22442290
}
2291+

src/core/prompts/system.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Usage:
4646
</execute_command>
4747
4848
## read_file
49-
Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.
49+
Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.
5050
Parameters:
5151
- path: (required) The path of the file to read (relative to the current working directory ${cwd.toPosix()})
5252
Usage:
@@ -69,10 +69,66 @@ Your file content here
6969
7070
${diffEnabled ? `
7171
## apply_diff
72-
Description: Apply a diff to a file at the specified path. The diff should be in unified format (diff -u) and can be used to apply changes to a file. This tool is useful when you need to make specific modifications to a file based on a set of changes provided in a diff.
72+
Description: Apply a diff to a file at the specified path. The diff should be in unified format ('diff -U0') and can be used to apply changes to a file. This tool is useful when you need to make specific modifications to a file based on a set of changes provided in a diff.
73+
74+
Diff Format Requirements:
75+
76+
1. Header (REQUIRED):
77+
\`\`\`
78+
--- path/to/original/file
79+
+++ path/to/modified/file
80+
\`\`\`
81+
- Must include both lines exactly as shown
82+
- Use actual file paths
83+
- NO timestamps after paths
84+
85+
2. Hunks:
86+
\`\`\`
87+
@@ -lineStart,lineCount +lineStart,lineCount @@
88+
-removed line
89+
+added line
90+
\`\`\`
91+
- Each hunk starts with @@ showing line numbers for changes
92+
- Format: @@ -originalStart,originalCount +newStart,newCount @@
93+
- Use - for removed/changed lines
94+
- Use + for new/modified lines
95+
- Indentation must match exactly
96+
97+
Complete Example:
98+
\`\`\`
99+
--- src/utils/helper.ts
100+
+++ src/utils/helper.ts
101+
@@ -10,3 +10,4 @@
102+
-function oldFunction(x: number): number {
103+
- return x + 1;
104+
-}
105+
+function newFunction(x: number): number {
106+
+ const result = x + 2;
107+
+ return result;
108+
+}
109+
\`\`\`
110+
111+
Common Pitfalls:
112+
1. Missing or incorrect header lines
113+
2. Incorrect line numbers in @@ lines
114+
3. Wrong indentation in changed lines
115+
4. Incomplete context (missing lines that need changing)
116+
5. Not marking all modified lines with - and +
117+
118+
Best Practices:
119+
1. Replace entire code blocks:
120+
- Remove complete old version with - lines
121+
- Add complete new version with + lines
122+
- Include correct line numbers
123+
2. Moving code requires two hunks:
124+
- First hunk: Remove from old location
125+
- Second hunk: Add to new location
126+
3. One hunk per logical change
127+
4. Verify line numbers match the file
128+
73129
Parameters:
74130
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd.toPosix()})
75-
- diff: (required) The diff in unified format (diff -u) to apply to the file.
131+
- diff: (required) The diff in unified format (diff -U0) to apply to the file.
76132
Usage:
77133
<apply_diff>
78134
<path>File path here</path>
@@ -342,3 +398,4 @@ The following additional instructions are provided by the user, and should be fo
342398
${joinedInstructions}`
343399
: ""
344400
}
401+

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ jest.mock('../../Cline', () => {
7676
}
7777
})
7878

79+
// Mock extract-text
80+
jest.mock('../../../integrations/misc/extract-text', () => ({
81+
extractTextFromFile: jest.fn().mockImplementation(async (filePath: string) => {
82+
const content = 'const x = 1;\nconst y = 2;\nconst z = 3;'
83+
const lines = content.split('\n')
84+
return lines.map((line, index) => `${index + 1} | ${line}`).join('\n')
85+
})
86+
}))
87+
7988
// Spy on console.error and console.log to suppress expected messages
8089
beforeAll(() => {
8190
jest.spyOn(console, 'error').mockImplementation(() => {})
@@ -258,4 +267,10 @@ describe('ClineProvider', () => {
258267
expect(mockContext.globalState.update).toHaveBeenCalledWith('soundEnabled', false)
259268
expect(mockPostMessage).toHaveBeenCalled()
260269
})
270+
271+
test('file content includes line numbers', async () => {
272+
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
273+
const result = await extractTextFromFile('test.js')
274+
expect(result).toBe('1 | const x = 1;\n2 | const y = 2;\n3 | const z = 3;')
275+
})
261276
})
Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import * as vscode from "vscode"
2-
31
/**
42
* Detects potential AI-generated code omissions in the given file content.
53
* @param originalFileContent The original content of the file.
64
* @param newFileContent The new content of the file to check.
75
* @returns True if a potential omission is detected, false otherwise.
86
*/
9-
function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean {
7+
export function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean {
108
const originalLines = originalFileContent.split("\n")
119
const newLines = newFileContent.split("\n")
1210
const omissionKeywords = ["remain", "remains", "unchanged", "rest", "previous", "existing", "..."]
@@ -33,26 +31,3 @@ function detectCodeOmission(originalFileContent: string, newFileContent: string)
3331
return false
3432
}
3533

36-
/**
37-
* Shows a warning in VSCode if a potential code omission is detected.
38-
* @param originalFileContent The original content of the file.
39-
* @param newFileContent The new content of the file to check.
40-
*/
41-
export function showOmissionWarning(originalFileContent: string, newFileContent: string): void {
42-
if (detectCodeOmission(originalFileContent, newFileContent)) {
43-
vscode.window
44-
.showWarningMessage(
45-
"Potential code truncation detected. This happens when the AI reaches its max output limit.",
46-
"Follow this guide to fix the issue",
47-
)
48-
.then((selection) => {
49-
if (selection === "Follow this guide to fix the issue") {
50-
vscode.env.openExternal(
51-
vscode.Uri.parse(
52-
"https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments",
53-
),
54-
)
55-
}
56-
})
57-
}
58-
}

src/integrations/misc/extract-text.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
2222
default:
2323
const isBinary = await isBinaryFile(filePath).catch(() => false)
2424
if (!isBinary) {
25-
return await fs.readFile(filePath, "utf8")
25+
return addLineNumbers(await fs.readFile(filePath, "utf8"))
2626
} else {
2727
throw new Error(`Cannot read text for file type: ${fileExtension}`)
2828
}
@@ -32,12 +32,12 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
3232
async function extractTextFromPDF(filePath: string): Promise<string> {
3333
const dataBuffer = await fs.readFile(filePath)
3434
const data = await pdf(dataBuffer)
35-
return data.text
35+
return addLineNumbers(data.text)
3636
}
3737

3838
async function extractTextFromDOCX(filePath: string): Promise<string> {
3939
const result = await mammoth.extractRawText({ path: filePath })
40-
return result.value
40+
return addLineNumbers(result.value)
4141
}
4242

4343
async function extractTextFromIPYNB(filePath: string): Promise<string> {
@@ -51,5 +51,17 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
5151
}
5252
}
5353

54-
return extractedText
54+
return addLineNumbers(extractedText)
5555
}
56+
57+
export function addLineNumbers(content: string): string {
58+
const lines = content.split('\n')
59+
const maxLineNumberWidth = String(lines.length).length
60+
return lines
61+
.map((line, index) => {
62+
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
63+
return `${lineNumber} | ${line}`
64+
}).join('\n')
65+
}
66+
67+

src/utils/sound.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const isWAV = (filepath: string): boolean => {
2020
return path.extname(filepath).toLowerCase() === ".wav"
2121
}
2222

23-
let isSoundEnabled = true
23+
let isSoundEnabled = false
2424

2525
/**
2626
* Set sound configuration

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
301301
marginTop: "5px",
302302
color: "var(--vscode-descriptionForeground)",
303303
}}>
304-
When enabled, Cline will be able to apply diffs to make changes to files.
304+
When enabled, Cline will be able to apply diffs to make changes to files and will automatically reject truncated full-file edits.
305305
</p>
306306
</div>
307307

0 commit comments

Comments
 (0)