diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 11b9fe148d1..dff8f24f380 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -101,6 +101,17 @@ export const globalSettingsSchema = z.object({ alwaysAllowWriteOutsideWorkspace: z.boolean().optional(), alwaysAllowWriteProtected: z.boolean().optional(), writeDelayMs: z.number().min(0).optional(), + /** + * Fuzzy match threshold for diff operations. + * Controls how strictly the search content must match the original file content. + * Value between 0 and 1 where: + * - 1.0 (100%) = exact match required (default) + * - 0.9 (90%) = allows minor differences (recommended for some models) + * - 0.8 (80%) = more lenient matching + * Lower values allow more flexibility but may increase false positives. + * @default 1.0 + */ + fuzzyMatchThreshold: z.number().min(0).max(1).optional(), alwaysAllowBrowser: z.boolean().optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d6fafe7060c..f094b14c4ac 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -159,6 +159,13 @@ export interface TaskOptions extends CreateTaskOptions { workspacePath?: string /** Initial status for the task's history item (e.g., "active" for child tasks) */ initialStatus?: "active" | "delegated" | "completed" + /** + * Fuzzy match threshold for diff operations (0-1). + * 1.0 = exact match required (default) + * 0.9 = 90% similarity (recommended for some models) + * @default 1.0 + */ + fuzzyMatchThreshold?: number } export class Task extends EventEmitter implements TaskLike { @@ -565,6 +572,7 @@ export class Task extends EventEmitter implements TaskLike { initialTodos, workspacePath, initialStatus, + fuzzyMatchThreshold, }: TaskOptions) { super() @@ -683,8 +691,8 @@ export class Task extends EventEmitter implements TaskLike { // Listen for provider profile changes to update parser state this.setupProviderProfileChangeListener(provider) - // Set up diff strategy - this.diffStrategy = new MultiSearchReplaceDiffStrategy() + // Set up diff strategy with optional fuzzy match threshold + this.diffStrategy = new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold) this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bc3f6bd6ef1..71bda13b36b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -969,8 +969,15 @@ export class ClineProvider } } - const { apiConfiguration, enableCheckpoints, checkpointTimeout, experiments, cloudUserInfo, taskSyncEnabled } = - await this.getState() + const { + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + experiments, + cloudUserInfo, + taskSyncEnabled, + fuzzyMatchThreshold, + } = await this.getState() const task = new Task({ provider: this, @@ -980,6 +987,7 @@ export class ClineProvider consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, historyItem, experiments, + fuzzyMatchThreshold, rootTask: historyItem.rootTask, parentTask: historyItem.parentTask, taskNumber: historyItem.number, @@ -2877,6 +2885,7 @@ export class ClineProvider experiments, cloudUserInfo, remoteControlEnabled, + fuzzyMatchThreshold, } = await this.getState() // Single-open-task invariant: always enforce for user-initiated top-level tasks @@ -2901,6 +2910,7 @@ export class ClineProvider task: text, images, experiments, + fuzzyMatchThreshold, rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, parentTask, taskNumber: this.clineStack.length + 1, diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 8663ea6e038..a05369e7752 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -39,6 +39,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number writeDelayMs: number + fuzzyMatchThreshold?: number includeCurrentTime?: boolean includeCurrentCost?: boolean maxGitStatusFiles?: number @@ -57,6 +58,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "includeDiagnosticMessages" | "maxDiagnosticMessages" | "writeDelayMs" + | "fuzzyMatchThreshold" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" @@ -78,6 +80,7 @@ export const ContextManagementSettings = ({ includeDiagnosticMessages, maxDiagnosticMessages, writeDelayMs, + fuzzyMatchThreshold, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -406,6 +409,29 @@ export const ContextManagementSettings = ({ + + + {t("settings:contextManagement.fuzzyMatchThreshold.label")} + +
+ setCachedStateField("fuzzyMatchThreshold", value / 100)} + data-testid="fuzzy-match-threshold-slider" + /> + {Math.round((fuzzyMatchThreshold ?? 1.0) * 100)}% +
+
+ {t("settings:contextManagement.fuzzyMatchThreshold.description")} +
+
+ (({ onDone, t terminalZshP10k, terminalZdotdir, writeDelayMs, + fuzzyMatchThreshold, showRooIgnoredFiles, enableSubfolderRules, remoteBrowserEnabled, @@ -857,6 +858,7 @@ const SettingsView = forwardRef(({ onDone, t includeDiagnosticMessages={includeDiagnosticMessages} maxDiagnosticMessages={maxDiagnosticMessages} writeDelayMs={writeDelayMs} + fuzzyMatchThreshold={fuzzyMatchThreshold} includeCurrentTime={includeCurrentTime} includeCurrentCost={includeCurrentCost} maxGitStatusFiles={maxGitStatusFiles} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index de76ba4c679..c0327ff40f9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -738,6 +738,10 @@ "description": "Time to wait after file writes before proceeding, allowing diagnostic tools to process changes and detect issues." } }, + "fuzzyMatchThreshold": { + "label": "Fuzzy match threshold for file edits", + "description": "Controls how strictly the search content must match when editing files. Lower values allow more tolerance for minor formatting differences that some models produce. 100% requires exact match (default), 90% is recommended for models that produce slight variations." + }, "condensingThreshold": { "label": "Condensing Trigger Threshold", "selectProfile": "Configure threshold for profile",