feat: add user-configurable fuzzy match threshold setting

- Add fuzzyMatchThreshold to GlobalSettings schema (0-1 range, optional)
- Update Task class to accept and pass threshold to MultiSearchReplaceDiffStrategy
- Add UI slider control in ContextManagementSettings (80-100% range)
- Add English translations for the new setting

This allows users to adjust how strictly search content must match when
editing files, addressing issues where models like gemini-2-flash-preview
produce output with 89-96% similarity that fails with 100% exact match.

Fixes #11087
This commit is contained in:
Roo Code 2026-02-02 17:55:53 +00:00
parent b020f6be43
commit 27c47c05b6
6 changed files with 65 additions and 4 deletions

View file

@ -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(),

View file

@ -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<TaskEvents> implements TaskLike {
@ -565,6 +572,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
initialTodos,
workspacePath,
initialStatus,
fuzzyMatchThreshold,
}: TaskOptions) {
super()
@ -683,8 +691,8 @@ export class Task extends EventEmitter<TaskEvents> 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)

View file

@ -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,

View file

@ -39,6 +39,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
includeDiagnosticMessages?: boolean
maxDiagnosticMessages?: number
writeDelayMs: number
fuzzyMatchThreshold?: number
includeCurrentTime?: boolean
includeCurrentCost?: boolean
maxGitStatusFiles?: number
@ -57,6 +58,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "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 = ({
</div>
</SearchableSetting>
<SearchableSetting
settingId="context-fuzzy-match-threshold"
section="contextManagement"
label={t("settings:contextManagement.fuzzyMatchThreshold.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.fuzzyMatchThreshold.label")}
</span>
<div className="flex items-center gap-2">
<Slider
min={80}
max={100}
step={1}
value={[Math.round((fuzzyMatchThreshold ?? 1.0) * 100)]}
onValueChange={([value]) => setCachedStateField("fuzzyMatchThreshold", value / 100)}
data-testid="fuzzy-match-threshold-slider"
/>
<span className="w-12">{Math.round((fuzzyMatchThreshold ?? 1.0) * 100)}%</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.fuzzyMatchThreshold.description")}
</div>
</SearchableSetting>
<SearchableSetting
settingId="context-include-current-time"
section="contextManagement"

View file

@ -192,6 +192,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalZshP10k,
terminalZdotdir,
writeDelayMs,
fuzzyMatchThreshold,
showRooIgnoredFiles,
enableSubfolderRules,
remoteBrowserEnabled,
@ -857,6 +858,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
includeDiagnosticMessages={includeDiagnosticMessages}
maxDiagnosticMessages={maxDiagnosticMessages}
writeDelayMs={writeDelayMs}
fuzzyMatchThreshold={fuzzyMatchThreshold}
includeCurrentTime={includeCurrentTime}
includeCurrentCost={includeCurrentCost}
maxGitStatusFiles={maxGitStatusFiles}

View file

@ -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",