Skip to content

Commit 8a51a6c

Browse files
committed
Supports updating multiple locations of a file in one call of the apply_diff tool
1 parent 3168b1e commit 8a51a6c

File tree

9 files changed

+2007
-19
lines changed

9 files changed

+2007
-19
lines changed

src/core/Cline.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,10 @@ export class Cline {
163163
this.enableCheckpoints = enableCheckpoints ?? false
164164

165165
// Initialize diffStrategy based on current state
166-
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
166+
this.updateDiffStrategy(
167+
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY),
168+
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
169+
)
167170

168171
if (startTask) {
169172
if (task || images) {
@@ -193,13 +196,23 @@ export class Cline {
193196
}
194197

195198
// Add method to update diffStrategy
196-
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
199+
async updateDiffStrategy(experimentalDiffStrategy?: boolean, multiSearchReplaceDiffStrategy?: boolean) {
197200
// If not provided, get from current state
198-
if (experimentalDiffStrategy === undefined) {
201+
if (experimentalDiffStrategy === undefined || multiSearchReplaceDiffStrategy === undefined) {
199202
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
200-
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
203+
if (experimentalDiffStrategy === undefined) {
204+
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
205+
}
206+
if (multiSearchReplaceDiffStrategy === undefined) {
207+
multiSearchReplaceDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] ?? false
208+
}
201209
}
202-
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
210+
this.diffStrategy = getDiffStrategy(
211+
this.api.getModel().id,
212+
this.fuzzyMatchThreshold,
213+
experimentalDiffStrategy,
214+
multiSearchReplaceDiffStrategy,
215+
)
203216
}
204217

205218
// Storing task to disk for history
@@ -1578,17 +1591,36 @@ export class Cline {
15781591
success: false,
15791592
error: "No diff strategy available",
15801593
}
1594+
let partResults = ""
1595+
15811596
if (!diffResult.success) {
15821597
this.consecutiveMistakeCount++
15831598
const currentCount =
15841599
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
15851600
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
1586-
const errorDetails = diffResult.details
1587-
? JSON.stringify(diffResult.details, null, 2)
1588-
: ""
1589-
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
1590-
diffResult.error
1591-
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1601+
let formattedError = ""
1602+
if (diffResult.failParts && diffResult.failParts.length > 0) {
1603+
for (const failPart of diffResult.failParts) {
1604+
if (failPart.success) {
1605+
continue
1606+
}
1607+
const errorDetails = failPart.details
1608+
? JSON.stringify(failPart.details, null, 2)
1609+
: ""
1610+
formattedError = `<error_details>\n${
1611+
failPart.error
1612+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1613+
partResults += formattedError
1614+
}
1615+
} else {
1616+
const errorDetails = diffResult.details
1617+
? JSON.stringify(diffResult.details, null, 2)
1618+
: ""
1619+
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
1620+
diffResult.error
1621+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1622+
}
1623+
15921624
if (currentCount >= 2) {
15931625
await this.say("error", formattedError)
15941626
}
@@ -1618,6 +1650,10 @@ export class Cline {
16181650
const { newProblemsMessage, userEdits, finalContent } =
16191651
await this.diffViewProvider.saveChanges()
16201652
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1653+
let partFailHint = ""
1654+
if (diffResult.failParts && diffResult.failParts.length > 0) {
1655+
partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use <read_file> tool to check newest file version and re-apply diffs\n`
1656+
}
16211657
if (userEdits) {
16221658
await this.say(
16231659
"user_feedback_diff",
@@ -1629,6 +1665,7 @@ export class Cline {
16291665
)
16301666
pushToolResult(
16311667
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1668+
partFailHint +
16321669
`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` +
16331670
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
16341671
finalContent || "",
@@ -1641,7 +1678,8 @@ export class Cline {
16411678
)
16421679
} else {
16431680
pushToolResult(
1644-
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
1681+
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` +
1682+
partFailHint,
16451683
)
16461684
}
16471685
await this.diffViewProvider.reset()

src/core/__tests__/Cline.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ describe("Cline", () => {
374374

375375
expect(cline.diffEnabled).toBe(true)
376376
expect(cline.diffStrategy).toBeDefined()
377-
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false)
377+
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false, false)
378378

379379
getDiffStrategySpy.mockRestore()
380380

@@ -395,7 +395,7 @@ describe("Cline", () => {
395395

396396
expect(cline.diffEnabled).toBe(true)
397397
expect(cline.diffStrategy).toBeDefined()
398-
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false)
398+
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false, false)
399399

400400
getDiffStrategySpy.mockRestore()
401401

src/core/diff/DiffStrategy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DiffStrategy } from "./types"
22
import { UnifiedDiffStrategy } from "./strategies/unified"
33
import { SearchReplaceDiffStrategy } from "./strategies/search-replace"
44
import { NewUnifiedDiffStrategy } from "./strategies/new-unified"
5+
import { MultiSearchReplaceDiffStrategy } from "./strategies/multi-search-replace"
56
/**
67
* Get the appropriate diff strategy for the given model
78
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
@@ -11,11 +12,17 @@ export function getDiffStrategy(
1112
model: string,
1213
fuzzyMatchThreshold?: number,
1314
experimentalDiffStrategy: boolean = false,
15+
multiSearchReplaceDiffStrategy: boolean = false,
1416
): DiffStrategy {
1517
if (experimentalDiffStrategy) {
1618
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
1719
}
18-
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
20+
21+
if (multiSearchReplaceDiffStrategy) {
22+
return new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold)
23+
} else {
24+
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
25+
}
1926
}
2027

2128
export type { DiffStrategy }

0 commit comments

Comments
 (0)