diff --git a/.changeset/early-pigs-carry.md b/.changeset/early-pigs-carry.md
new file mode 100644
index 00000000000..61da7a00fa3
--- /dev/null
+++ b/.changeset/early-pigs-carry.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Make fuzzy diff matching configurable (and default to off)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index cfbd4fd1057..e3e0f8db923 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -67,6 +67,7 @@ export class Cline {
private didEditFile: boolean = false
customInstructions?: string
diffStrategy?: DiffStrategy
+ diffEnabled: boolean = false
apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = []
@@ -97,10 +98,11 @@ export class Cline {
provider: ClineProvider,
apiConfiguration: ApiConfiguration,
customInstructions?: string,
- diffEnabled?: boolean,
- task?: string,
- images?: string[],
- historyItem?: HistoryItem,
+ enableDiff?: boolean,
+ fuzzyMatchThreshold?: number,
+ task?: string | undefined,
+ images?: string[] | undefined,
+ historyItem?: HistoryItem | undefined,
) {
this.providerRef = new WeakRef(provider)
this.api = buildApiHandler(apiConfiguration)
@@ -109,8 +111,9 @@ export class Cline {
this.browserSession = new BrowserSession(provider.context)
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
- if (diffEnabled && this.api.getModel().id) {
- this.diffStrategy = getDiffStrategy(this.api.getModel().id)
+ this.diffEnabled = enableDiff ?? false
+ if (this.diffEnabled && this.api.getModel().id) {
+ this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
}
if (historyItem) {
this.taskId = historyItem.id
diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts
index 8365c61aa41..ed755bd3997 100644
--- a/src/core/__tests__/Cline.test.ts
+++ b/src/core/__tests__/Cline.test.ts
@@ -248,7 +248,7 @@ describe('Cline', () => {
// Setup mock API configuration
mockApiConfig = {
apiProvider: 'anthropic',
- apiModelId: 'claude-3-sonnet'
+ apiModelId: 'claude-3-5-sonnet-20241022'
};
// Mock provider methods
@@ -278,20 +278,77 @@ describe('Cline', () => {
mockProvider,
mockApiConfig,
'custom instructions',
- false, // diffEnabled
- 'test task', // task
- undefined, // images
- undefined // historyItem
+ false,
+ 0.95, // 95% threshold
+ 'test task'
);
expect(cline.customInstructions).toBe('custom instructions');
+ expect(cline.diffEnabled).toBe(false);
+ });
+
+ it('should use default fuzzy match threshold when not provided', () => {
+ const cline = new Cline(
+ mockProvider,
+ mockApiConfig,
+ 'custom instructions',
+ true,
+ undefined,
+ 'test task'
+ );
+
+ expect(cline.diffEnabled).toBe(true);
+ // The diff strategy should be created with default threshold (1.0)
+ expect(cline.diffStrategy).toBeDefined();
+ });
+
+ it('should use provided fuzzy match threshold', () => {
+ const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy');
+
+ const cline = new Cline(
+ mockProvider,
+ mockApiConfig,
+ 'custom instructions',
+ true,
+ 0.9, // 90% threshold
+ 'test task'
+ );
+
+ expect(cline.diffEnabled).toBe(true);
+ expect(cline.diffStrategy).toBeDefined();
+ expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 0.9);
+
+ getDiffStrategySpy.mockRestore();
+ });
+
+ it('should pass default threshold to diff strategy when not provided', () => {
+ const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy');
+
+ const cline = new Cline(
+ mockProvider,
+ mockApiConfig,
+ 'custom instructions',
+ true,
+ undefined,
+ 'test task'
+ );
+
+ expect(cline.diffEnabled).toBe(true);
+ expect(cline.diffStrategy).toBeDefined();
+ expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 1.0);
+
+ getDiffStrategySpy.mockRestore();
});
it('should require either task or historyItem', () => {
expect(() => {
new Cline(
mockProvider,
- mockApiConfig
+ mockApiConfig,
+ undefined, // customInstructions
+ false, // diffEnabled
+ undefined, // fuzzyMatchThreshold
+ undefined // task
);
}).toThrow('Either historyItem or task/images must be provided');
});
diff --git a/src/core/diff/DiffStrategy.ts b/src/core/diff/DiffStrategy.ts
index 355424e48d8..c6118564e98 100644
--- a/src/core/diff/DiffStrategy.ts
+++ b/src/core/diff/DiffStrategy.ts
@@ -6,10 +6,10 @@ import { SearchReplaceDiffStrategy } from './strategies/search-replace'
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
* @returns The appropriate diff strategy for the model
*/
-export function getDiffStrategy(model: string): DiffStrategy {
- // For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
+export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number): DiffStrategy {
+ // For now, return SearchReplaceDiffStrategy for all models
// This architecture allows for future optimizations based on model capabilities
- return new SearchReplaceDiffStrategy(0.9)
+ return new SearchReplaceDiffStrategy(fuzzyMatchThreshold ?? 1.0)
}
export type { DiffStrategy }
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index 4681351b4e6..3990848e00d 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -58,7 +58,9 @@ export class SearchReplaceDiffStrategy implements DiffStrategy {
private bufferLines: number;
constructor(fuzzyThreshold?: number, bufferLines?: number) {
- // Default to exact matching (1.0) unless fuzzy threshold specified
+ // Use provided threshold or default to exact matching (1.0)
+ // Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
+ // so we use it directly here
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
this.bufferLines = bufferLines ?? BUFFER_LINES;
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index f42cd193258..2245d8036fc 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -70,6 +70,7 @@ type GlobalStateKey =
| "diffEnabled"
| "alwaysAllowMcp"
| "browserLargeViewport"
+ | "fuzzyMatchThreshold"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -217,7 +218,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const {
apiConfiguration,
customInstructions,
- diffEnabled
+ diffEnabled,
+ fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
@@ -225,6 +227,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions,
diffEnabled,
+ fuzzyMatchThreshold,
task,
images
)
@@ -235,7 +238,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const {
apiConfiguration,
customInstructions,
- diffEnabled
+ diffEnabled,
+ fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
@@ -243,6 +247,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions,
diffEnabled,
+ fuzzyMatchThreshold,
undefined,
undefined,
historyItem
@@ -613,6 +618,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("browserLargeViewport", browserLargeViewport)
await this.postStateToWebview()
break
+ case "fuzzyMatchThreshold":
+ await this.updateGlobalState("fuzzyMatchThreshold", message.value)
+ await this.postStateToWebview()
+ break
}
},
null,
@@ -1062,6 +1071,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
soundVolume,
browserLargeViewport,
+ fuzzyMatchThreshold,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise
+ This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution. +
+