From 339eedf53d38fdb17c3134addf0866a6782d89c5 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 10:53:35 +0700 Subject: [PATCH 01/36] feat(issue-457-edit-unsuccessfull): introduce configurable relaxed diff thresholds and diagnostics --- packages/types/src/global-settings.ts | 15 +++ packages/types/src/vscode-extension-host.ts | 2 + .../__tests__/multi-search-replace.spec.ts | 93 +++++++++++++++++++ .../diff/strategies/multi-search-replace.ts | 17 ++-- src/core/task/Task.ts | 4 +- src/core/webview/ClineProvider.ts | 27 +++++- .../settings/ContextManagementSettings.tsx | 28 ++++++ .../src/components/settings/SettingsView.tsx | 3 + .../src/context/ExtensionStateContext.tsx | 1 + 9 files changed, 179 insertions(+), 11 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bac3548ccb..ccf4fc5f96 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -23,6 +23,15 @@ import { languagesSchema } from "./vscode.js" */ export const DEFAULT_WRITE_DELAY_MS = 1000 +/** + * Default fuzzy matching threshold for the multi-search-replace diff strategy. + * A value of 0.90 means the search content must be at least 90% similar to the + * file content for a match to succeed. Lowered from the previous strict 1.0 (exact + * match) to reduce "Edit Unsuccessful" errors caused by minor whitespace or + * formatting differences. + */ +export const DEFAULT_DIFF_FUZZY_THRESHOLD = 0.9 + /** * Terminal output preview size options for persisted command output. * @@ -102,6 +111,12 @@ export const globalSettingsSchema = z.object({ alwaysAllowWriteOutsideWorkspace: z.boolean().optional(), alwaysAllowWriteProtected: z.boolean().optional(), writeDelayMs: z.number().min(0).optional(), + /** + * Fuzzy matching threshold for the multi-search-replace diff strategy. + * Range: 0.0 (accept anything) to 1.0 (exact match only). + * @default 0.9 + */ + diffFuzzyThreshold: z.number().min(0).max(1).optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 2cf42c342b..d4b50270de 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -318,6 +318,7 @@ export type ExtensionState = Pick< | "requestDelaySeconds" | "showWorktreesInHomeScreen" | "disabledTools" + | "diffFuzzyThreshold" > & { lockApiConfigAcrossModes?: boolean version: string @@ -332,6 +333,7 @@ export type ExtensionState = Pick< taskHistory: HistoryItem[] writeDelayMs: number + diffFuzzyThreshold?: number enableCheckpoints: boolean checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index b58e331f8f..727ce52e93 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -1374,4 +1374,97 @@ function sum(a, b) { } }) }) + + describe("fuzzyThreshold and diagnostics", () => { + const originalContent = + "function calculateTotal(price: number, tax: number) {\n\tconst subtotal = price;\n\treturn subtotal + tax;\n}\n" + + it("should use default 0.90 threshold when instantiated without arguments", () => { + const strategy = new MultiSearchReplaceDiffStrategy() + expect(strategy["fuzzyThreshold"]).toBe(0.9) + }) + + it("should succeed with near-miss match (e.g. minor whitespace diff) when threshold is 0.90", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.9) + // Near-miss search block with slightly different formatting/whitespace (e.g., spaces instead of tab, missing semicolon) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price\n" + + " return subtotal + tax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toContain("(subtotal + tax) * 1.1") + } + }) + + it("should fail with near-miss match when threshold is set to 1.0", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(1.0) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price\n" + + " return subtotal + tax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + if (!result.success && result.failParts) { + const failedPart = result.failParts[0] + if (failedPart && "error" in failedPart && failedPart.error) { + expect(failedPart.error).toContain("No sufficiently similar match found") + } else { + throw new Error("Expected failedPart to have an error property") + } + } + }) + + it("should output enhanced error diagnostics (Levenshtein distance, character counts) when a match fails", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.95) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateGrandTotal(initialPrice: number, standardTax: number) {\n" + + " const totalVal = initialPrice\n" + + " return totalVal + standardTax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + if (!result.success && result.failParts) { + const failedPart = result.failParts[0] + if (failedPart && "error" in failedPart && failedPart.error) { + const errorMsg = failedPart.error + expect(errorMsg).toContain("Debug Info:") + expect(errorMsg).toContain("Similarity Score:") + expect(errorMsg).toContain("Required Threshold: 95%") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Length:") + expect(errorMsg).toContain("Best Match Length:") + } else { + throw new Error("Expected failedPart to have an error property") + } + } + }) + }) }) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index c74faedbfe..db1502cc29 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -1,6 +1,6 @@ import { distance } from "fastest-levenshtein" -import { ToolProgressStatus } from "@roo-code/types" +import { ToolProgressStatus, DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types" import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools" @@ -81,10 +81,11 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { } constructor(fuzzyThreshold?: number, bufferLines?: number) { - // 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 + // Use provided threshold or default to relaxed matching (0.9) + // A value of 0.9 means 90% similarity is required for a match. + // This was previously 1.0 (exact match) which caused frequent + // "Edit Unsuccessful" errors on minor whitespace differences. + this.fuzzyThreshold = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD this.bufferLines = bufferLines ?? BUFFER_LINES } @@ -555,9 +556,13 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { const lineRange = startLine ? ` at line: ${startLine}` : "" + const levenDist = bestMatchContent + ? distance(normalizeString(searchChunk), normalizeString(bestMatchContent)) + : -1 + diffResults.push({ success: false, - error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`, + error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Levenshtein Distance: ${levenDist >= 0 ? `${levenDist} characters` : "N/A"}\n- Search Length: ${searchChunk.length} characters\n- Best Match Length: ${bestMatchContent ? bestMatchContent.length : 0} characters\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`, }) continue } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2f1b370b48..0d14a2e8b7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -160,6 +160,7 @@ export interface TaskOptions extends CreateTaskOptions { /** Initial status for the task's history item (e.g., "active" for child tasks) */ initialStatus?: "active" | "delegated" | "completed" rateLimitClock?: RateLimitClock + diffFuzzyThreshold?: number } export class Task extends EventEmitter implements TaskLike { @@ -432,6 +433,7 @@ export class Task extends EventEmitter implements TaskLike { workspacePath, initialStatus, rateLimitClock, + diffFuzzyThreshold, }: TaskOptions) { super() @@ -529,7 +531,7 @@ export class Task extends EventEmitter implements TaskLike { this.setupProviderProfileChangeListener(provider) // Set up diff strategy - this.diffStrategy = new MultiSearchReplaceDiffStrategy() + this.diffStrategy = new MultiSearchReplaceDiffStrategy(diffFuzzyThreshold) this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b2bb73e982..ee73649636 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -41,6 +41,7 @@ import { requestyDefaultModelId, openRouterDefaultModelId, DEFAULT_WRITE_DELAY_MS, + DEFAULT_DIFF_FUZZY_THRESHOLD, ORGANIZATION_ALLOW_ALL, DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, @@ -1076,8 +1077,15 @@ export class ClineProvider ) } - const { apiConfiguration, enableCheckpoints, checkpointTimeout, experiments, cloudUserInfo, taskSyncEnabled } = - await this.getState() + const { + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + experiments, + cloudUserInfo, + taskSyncEnabled, + diffFuzzyThreshold, + } = await this.getState() const task = new Task({ provider: this, @@ -1096,6 +1104,7 @@ export class ClineProvider // Preserve the status from the history item to avoid overwriting it when the task saves messages initialStatus: historyItem.status, rateLimitClock: this.rateLimitClock, + diffFuzzyThreshold, }) if (isRehydratingCurrentTask) { @@ -2222,6 +2231,7 @@ export class ClineProvider taskHistory, soundVolume, writeDelayMs, + diffFuzzyThreshold, terminalShellIntegrationTimeout, terminalShellIntegrationDisabled, terminalCommandDelay, @@ -2379,6 +2389,7 @@ export class ClineProvider deniedCommands: mergedDeniedCommands, soundVolume: soundVolume ?? 0.5, writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS, + diffFuzzyThreshold: diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true, terminalCommandDelay: terminalCommandDelay ?? 0, @@ -2588,6 +2599,7 @@ export class ClineProvider checkpointTimeout: stateValues.checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, soundVolume: stateValues.soundVolume, writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS, + diffFuzzyThreshold: stateValues.diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? true, @@ -3042,8 +3054,14 @@ export class ClineProvider } } - const { apiConfiguration, organizationAllowList, enableCheckpoints, checkpointTimeout, experiments } = - await this.getState() + const { + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + experiments, + organizationAllowList, + diffFuzzyThreshold, + } = await this.getState() // Single-open-task invariant: always enforce for user-initiated top-level tasks if (!parentTask) { @@ -3075,6 +3093,7 @@ export class ClineProvider // Ensure this task is present in clineStack before startTask() emits // its initial state update, so state.currentTaskId is available ASAP. startTask: false, + diffFuzzyThreshold, ...options, rateLimitClock: this.rateLimitClock, }) diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 8663ea6e03..7c50c0ec89 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -3,6 +3,7 @@ import React from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" import { FoldVertical } from "lucide-react" +import { DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types" import { supportPrompt } from "@roo/support-prompt" @@ -39,6 +40,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number writeDelayMs: number + diffFuzzyThreshold?: number includeCurrentTime?: boolean includeCurrentCost?: boolean maxGitStatusFiles?: number @@ -57,6 +59,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "includeDiagnosticMessages" | "maxDiagnosticMessages" | "writeDelayMs" + | "diffFuzzyThreshold" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" @@ -78,6 +81,7 @@ export const ContextManagementSettings = ({ includeDiagnosticMessages, maxDiagnosticMessages, writeDelayMs, + diffFuzzyThreshold, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -406,6 +410,30 @@ export const ContextManagementSettings = ({ + + Diff Match Threshold +
+ setCachedStateField("diffFuzzyThreshold", value)} + data-testid="diff-fuzzy-threshold-slider" + /> + + {((diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD) * 100).toFixed(0)}% + +
+
+ Lower thresholds make file edits more resilient to formatting and whitespace variations. A + threshold of 100% requires an exact match. +
+
+ (({ onDone, t terminalZdotdir, terminalProfile, writeDelayMs, + diffFuzzyThreshold, showRooIgnoredFiles, enableSubfolderRules, maxImageFileSize, @@ -393,6 +394,7 @@ const SettingsView = forwardRef(({ onDone, t enableCheckpoints: enableCheckpoints ?? false, checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, writeDelayMs, + diffFuzzyThreshold, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 30_000, terminalShellIntegrationDisabled, terminalCommandDelay, @@ -851,6 +853,7 @@ const SettingsView = forwardRef(({ onDone, t includeDiagnosticMessages={includeDiagnosticMessages} maxDiagnosticMessages={maxDiagnosticMessages} writeDelayMs={writeDelayMs} + diffFuzzyThreshold={diffFuzzyThreshold} includeCurrentTime={includeCurrentTime} includeCurrentCost={includeCurrentCost} maxGitStatusFiles={maxGitStatusFiles} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index b4d8361144..6e8e860418 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -209,6 +209,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Default to 15 seconds language: "en", // Default language code writeDelayMs: 1000, + diffFuzzyThreshold: 0.9, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, taskSyncEnabled: false, From f1945d30976560ca8368a2586dd5314bfd803d6f Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 11:12:57 +0700 Subject: [PATCH 02/36] Update packages/types/src/global-settings.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/types/src/global-settings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index ccf4fc5f96..168d291d83 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -114,9 +114,9 @@ export const globalSettingsSchema = z.object({ /** * Fuzzy matching threshold for the multi-search-replace diff strategy. * Range: 0.0 (accept anything) to 1.0 (exact match only). - * @default 0.9 + * `@default` 0.9 */ - diffFuzzyThreshold: z.number().min(0).max(1).optional(), + diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), From 735759b7d2314164ef96b350d0433a9a112fe93b Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 11:19:06 +0700 Subject: [PATCH 03/36] fix(issue-457-edit-unsuccessfull): fix js doc and remove hard-coded english --- packages/types/src/global-settings.ts | 13 ++++++------- .../settings/ContextManagementSettings.tsx | 9 +++++---- webview-ui/src/i18n/locales/en/settings.json | 4 ++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 168d291d83..201f8528a4 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -1,19 +1,18 @@ import { z } from "zod" -import { type Keys } from "./type-fu.js" +import { codebaseIndexConfigSchema, codebaseIndexModelsSchema } from "./codebase-index.js" +import { experimentsSchema } from "./experiment.js" +import { historyItemSchema } from "./history.js" +import { customModePromptsSchema, customSupportPromptsSchema, modeConfigSchema } from "./mode.js" import { type ProviderSettings, PROVIDER_SETTINGS_KEYS, providerSettingsEntrySchema, providerSettingsSchema, } from "./provider-settings.js" -import { historyItemSchema } from "./history.js" -import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js" -import { experimentsSchema } from "./experiment.js" import { telemetrySettingsSchema } from "./telemetry.js" -import { modeConfigSchema } from "./mode.js" -import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js" import { toolNamesSchema } from "./tool.js" +import { type Keys } from "./type-fu.js" import { languagesSchema } from "./vscode.js" /** @@ -113,7 +112,7 @@ export const globalSettingsSchema = z.object({ writeDelayMs: z.number().min(0).optional(), /** * Fuzzy matching threshold for the multi-search-replace diff strategy. - * Range: 0.0 (accept anything) to 1.0 (exact match only). + * Range: 0.5 (accept anything) to 1.0 (exact match only). * `@default` 0.9 */ diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 7c50c0ec89..36792c0585 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -413,8 +413,10 @@ export const ContextManagementSettings = ({ - Diff Match Threshold + label={t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")}> + + {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")} +
- Lower thresholds make file edits more resilient to formatting and whitespace variations. A - threshold of 100% requires an exact match. + {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.description")}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 739ecbde04..c6dcde0265 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -759,6 +759,10 @@ "delayAfterWrite": { "label": "Delay after writes to allow diagnostics to detect potential problems", "description": "Time to wait after file writes before proceeding, allowing diagnostic tools to process changes and detect issues." + }, + "diffFuzzyThreshold": { + "label": "Diff match threshold", + "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." } }, "condensingThreshold": { From 2587469000978f2d1eca9e51a0bacea60c833885 Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 11:15:42 +0700 Subject: [PATCH 04/36] fix(issue-457-edit-unsuccessfull): limit original content preview to a bounded window on math failure --- src/core/diff/strategies/multi-search-replace.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index db1502cc29..d9f20347cf 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -548,7 +548,20 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` + : matchIndex !== -1 + ? `\n\nOriginal Content (around best match):\n${addLineNumbers( + resultLines + .slice( + Math.max(0, matchIndex - this.bufferLines), + Math.min( + resultLines.length, + matchIndex + searchLines.length + this.bufferLines, + ), + ) + .join("\n"), + Math.max(1, matchIndex + 1 - this.bufferLines), + )}` + : "" const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` From 6bdb35cc5840cd4a41bc5ba748b90be774d9da2a Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 11:19:46 +0700 Subject: [PATCH 05/36] fix(issue-457-edit-unsuccessfull): clamp the threshold --- src/core/diff/strategies/multi-search-replace.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index d9f20347cf..16cbbcbeb0 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -85,7 +85,9 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { // A value of 0.9 means 90% similarity is required for a match. // This was previously 1.0 (exact match) which caused frequent // "Edit Unsuccessful" errors on minor whitespace differences. - this.fuzzyThreshold = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD + // Clamp the threshold to [0.5, 1.0] as a defence-in-depth guard. + const thresholdVal = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD + this.fuzzyThreshold = Math.max(0.5, Math.min(1.0, thresholdVal)) this.bufferLines = bufferLines ?? BUFFER_LINES } From 5ffb87e16685ad7344a5ad27fc2e2df41cb3d3c9 Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 13:19:08 +0700 Subject: [PATCH 06/36] fix(issue-457-edit-unsuccessfull): remove access private property --- .../diff/strategies/__tests__/multi-search-replace.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index 727ce52e93..e6efbb5765 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -1379,11 +1379,6 @@ function sum(a, b) { const originalContent = "function calculateTotal(price: number, tax: number) {\n\tconst subtotal = price;\n\treturn subtotal + tax;\n}\n" - it("should use default 0.90 threshold when instantiated without arguments", () => { - const strategy = new MultiSearchReplaceDiffStrategy() - expect(strategy["fuzzyThreshold"]).toBe(0.9) - }) - it("should succeed with near-miss match (e.g. minor whitespace diff) when threshold is 0.90", async () => { const strategy = new MultiSearchReplaceDiffStrategy(0.9) // Near-miss search block with slightly different formatting/whitespace (e.g., spaces instead of tab, missing semicolon) From b0ddd4ca3f39e27d09e946eea95c17cadfb9288b Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 13:36:39 +0700 Subject: [PATCH 07/36] fix(issue-457-edit-unsuccessfull): remove from Pick and import types --- packages/types/src/vscode-extension-host.ts | 3 +-- src/core/webview/__tests__/ClineProvider.spec.ts | 2 ++ webview-ui/src/context/ExtensionStateContext.tsx | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index d4b50270de..494dc20e11 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -318,7 +318,6 @@ export type ExtensionState = Pick< | "requestDelaySeconds" | "showWorktreesInHomeScreen" | "disabledTools" - | "diffFuzzyThreshold" > & { lockApiConfigAcrossModes?: boolean version: string @@ -333,7 +332,7 @@ export type ExtensionState = Pick< taskHistory: HistoryItem[] writeDelayMs: number - diffFuzzyThreshold?: number + diffFuzzyThreshold: number enableCheckpoints: boolean checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 158238ce7e..d3bbea17de 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,6 +13,7 @@ import { type ExtensionState, ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + DEFAULT_DIFF_FUZZY_THRESHOLD, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -673,6 +674,7 @@ describe("ClineProvider", () => { openRouterImageGenerationSelectedModel: undefined, taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, } const message: ExtensionMessage = { diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 6e8e860418..bf58bbe37a 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -19,6 +19,7 @@ import { RouterModels, ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + DEFAULT_DIFF_FUZZY_THRESHOLD, } from "@roo-code/types" import { findLastIndex } from "@roo/array" @@ -209,7 +210,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Default to 15 seconds language: "en", // Default language code writeDelayMs: 1000, - diffFuzzyThreshold: 0.9, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, taskSyncEnabled: false, From c5d258f21b3b78ae7d7846ecb8e5d7aad2fd4ba5 Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 16:42:01 +0700 Subject: [PATCH 08/36] fix(issue-457-edit-unsuccessfull): Move diffFuzzyThreshold to new 'File Edits' section --- .../src/components/settings/ContextManagementSettings.tsx | 6 +++--- .../src/context/__tests__/ExtensionStateContext.spec.tsx | 3 +++ webview-ui/src/i18n/locales/en/settings.json | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 36792c0585..eaa566f36a 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -413,9 +413,9 @@ export const ContextManagementSettings = ({ + label={t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")}> - {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")} + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")}
- {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.description")} + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.description")}
diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index a795c08897..bad6099c79 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -6,6 +6,7 @@ import { type ExtensionState, type ClineMessage, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + DEFAULT_DIFF_FUZZY_THRESHOLD, } from "@roo-code/types" import { ExtensionStateContextProvider, useExtensionState, mergeExtensionState } from "../ExtensionStateContext" @@ -270,6 +271,7 @@ describe("mergeExtensionState", () => { taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Add the checkpoint timeout property maxReadFileLine: -1, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, } const prevState: ExtensionState = { @@ -339,6 +341,7 @@ describe("mergeExtensionState", () => { taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, maxReadFileLine: -1, + diffFuzzyThreshold: 0.9, } const makeMessage = (ts: number, text: string): ClineMessage => diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c6dcde0265..4fe5b7a597 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -759,7 +759,9 @@ "delayAfterWrite": { "label": "Delay after writes to allow diagnostics to detect potential problems", "description": "Time to wait after file writes before proceeding, allowing diagnostic tools to process changes and detect issues." - }, + } + }, + "fileEdits": { "diffFuzzyThreshold": { "label": "Diff match threshold", "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." From 91c5eac4db5213dcea8b7846a8de8b08ce7d3f83 Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 20:03:52 +0700 Subject: [PATCH 09/36] Revert "fix(issue-457-edit-unsuccessfull): limit original content preview to a bounded window on math failure" This reverts commit 4b2af0d8ef59a3b89138c9cba469fccb2ea1b3e3. --- src/core/diff/strategies/multi-search-replace.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 16cbbcbeb0..1239e72792 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -550,20 +550,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : matchIndex !== -1 - ? `\n\nOriginal Content (around best match):\n${addLineNumbers( - resultLines - .slice( - Math.max(0, matchIndex - this.bufferLines), - Math.min( - resultLines.length, - matchIndex + searchLines.length + this.bufferLines, - ), - ) - .join("\n"), - Math.max(1, matchIndex + 1 - this.bufferLines), - )}` - : "" + : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` From 40520b0219540ffd826bebd2e5aa786a16b34b41 Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 20:57:49 +0700 Subject: [PATCH 10/36] fix(issue-457-edit-unsuccessfull): clarify diffuzyThreshold JSDoc comment --- packages/types/src/global-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 201f8528a4..0230c0d8d2 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -112,7 +112,7 @@ export const globalSettingsSchema = z.object({ writeDelayMs: z.number().min(0).optional(), /** * Fuzzy matching threshold for the multi-search-replace diff strategy. - * Range: 0.5 (accept anything) to 1.0 (exact match only). + * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). * `@default` 0.9 */ diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), From ee7a699457bf25aabcd0541c10fe674cf2039516 Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 21:57:40 +0700 Subject: [PATCH 11/36] fix(issue-457-edit-unsuccessfull): Default diffFuzzyThreshold to 1.0 for safety --- packages/types/src/global-settings.ts | 11 ++++++----- .../context/__tests__/ExtensionStateContext.spec.tsx | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 0230c0d8d2..d762c9f9b0 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -24,12 +24,13 @@ export const DEFAULT_WRITE_DELAY_MS = 1000 /** * Default fuzzy matching threshold for the multi-search-replace diff strategy. - * A value of 0.90 means the search content must be at least 90% similar to the - * file content for a match to succeed. Lowered from the previous strict 1.0 (exact - * match) to reduce "Edit Unsuccessful" errors caused by minor whitespace or - * formatting differences. + * A value of 1.0 (exact match) is used by default for safety, especially when + * auto-approval for writes is enabled. This prevents unintended changes from + * being applied due to minor mismatches. Users can lower this threshold manually + * in settings to reduce "Edit Unsuccessful" errors caused by minor whitespace + * or formatting differences, accepting a higher risk of unintended edits. */ -export const DEFAULT_DIFF_FUZZY_THRESHOLD = 0.9 +export const DEFAULT_DIFF_FUZZY_THRESHOLD = 1.0 /** * Terminal output preview size options for persisted command output. diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index bad6099c79..efd7b48486 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -341,7 +341,7 @@ describe("mergeExtensionState", () => { taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, maxReadFileLine: -1, - diffFuzzyThreshold: 0.9, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, } const makeMessage = (ts: number, text: string): ClineMessage => From 447408955f6c7a679c2a56cae8217ee9cd65746a Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 22:29:24 +0700 Subject: [PATCH 12/36] fix(issue-457-edit-unsuccessfull): Correct the documented default for diffFuzzyThreshold and fix failure-diagnostics assertions unconditional --- packages/types/src/global-settings.ts | 2 +- .../__tests__/multi-search-replace.spec.ts | 38 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d762c9f9b0..1f43d30935 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -114,7 +114,7 @@ export const globalSettingsSchema = z.object({ /** * Fuzzy matching threshold for the multi-search-replace diff strategy. * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). - * `@default` 0.9 + * `@default` 1.0 */ diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), requestDelaySeconds: z.number().optional(), diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index e6efbb5765..b6471aef71 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -1419,13 +1419,12 @@ function sum(a, b) { const result = await strategy.applyDiff(originalContent, diff) expect(result.success).toBe(false) - if (!result.success && result.failParts) { - const failedPart = result.failParts[0] - if (failedPart && "error" in failedPart && failedPart.error) { - expect(failedPart.error).toContain("No sufficiently similar match found") - } else { - throw new Error("Expected failedPart to have an error property") - } + if (!result.success) { + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + expect((failedPart as { error: string }).error).toContain("No sufficiently similar match found") } }) @@ -1446,19 +1445,18 @@ function sum(a, b) { const result = await strategy.applyDiff(originalContent, diff) expect(result.success).toBe(false) - if (!result.success && result.failParts) { - const failedPart = result.failParts[0] - if (failedPart && "error" in failedPart && failedPart.error) { - const errorMsg = failedPart.error - expect(errorMsg).toContain("Debug Info:") - expect(errorMsg).toContain("Similarity Score:") - expect(errorMsg).toContain("Required Threshold: 95%") - expect(errorMsg).toContain("Levenshtein Distance:") - expect(errorMsg).toContain("Search Length:") - expect(errorMsg).toContain("Best Match Length:") - } else { - throw new Error("Expected failedPart to have an error property") - } + if (!result.success) { + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + const errorMsg = (failedPart as { error: string }).error + expect(errorMsg).toContain("Debug Info:") + expect(errorMsg).toContain("Similarity Score:") + expect(errorMsg).toContain("Required Threshold: 95%") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Length:") + expect(errorMsg).toContain("Best Match Length:") } }) }) From b619c458354d82859be9b4ec4a9fe241bcadcd0d Mon Sep 17 00:00:00 2001 From: Nigel Date: Tue, 9 Jun 2026 22:51:32 +0700 Subject: [PATCH 13/36] fix(issue-457-edit-unsuccessfull): added missing translations --- webview-ui/src/i18n/locales/ca/settings.json | 6 ++++++ webview-ui/src/i18n/locales/de/settings.json | 6 ++++++ webview-ui/src/i18n/locales/es/settings.json | 6 ++++++ webview-ui/src/i18n/locales/fr/settings.json | 6 ++++++ webview-ui/src/i18n/locales/hi/settings.json | 6 ++++++ webview-ui/src/i18n/locales/id/settings.json | 6 ++++++ webview-ui/src/i18n/locales/it/settings.json | 6 ++++++ webview-ui/src/i18n/locales/ja/settings.json | 6 ++++++ webview-ui/src/i18n/locales/ko/settings.json | 6 ++++++ webview-ui/src/i18n/locales/nl/settings.json | 6 ++++++ webview-ui/src/i18n/locales/pl/settings.json | 6 ++++++ webview-ui/src/i18n/locales/pt-BR/settings.json | 6 ++++++ webview-ui/src/i18n/locales/ru/settings.json | 6 ++++++ webview-ui/src/i18n/locales/tr/settings.json | 6 ++++++ webview-ui/src/i18n/locales/vi/settings.json | 6 ++++++ webview-ui/src/i18n/locales/zh-CN/settings.json | 6 ++++++ webview-ui/src/i18n/locales/zh-TW/settings.json | 6 ++++++ 17 files changed, 102 insertions(+) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 886b23d804..e43da62981 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -671,6 +671,12 @@ "description": "Temps d'espera després d'escriptures de fitxers abans de continuar, permetent que les eines de diagnòstic processin els canvis i detectin problemes." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Llindar de coincidència de diff", + "description": "Els llindars més baixos fan que les edicions de fitxer siguin més resistents a variacions de format i espais en blanc. Un llindar del 100% requereix una coincidència exacta." + } + }, "condensingThreshold": { "label": "Llindar d'activació de condensació", "selectProfile": "Configura el llindar per al perfil", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c874680af4..dce5dce407 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -671,6 +671,12 @@ "description": "Wartezeit nach Dateischreibvorgängen vor dem Fortfahren, damit Diagnosetools Änderungen verarbeiten und Probleme erkennen können." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Diff-Übereinstimmungsschwellwert", + "description": "Niedrigere Schwellenwerte machen Dateibearbeitungen widerstandsfähiger gegen Formatierungs- und Leerzeichenunterschiede. Ein Schwellenwert von 100% erfordert eine exakte Übereinstimmung." + } + }, "condensingThreshold": { "label": "Schwellenwert für Kontextkomprimierung", "selectProfile": "Profil für Schwellenwert konfigurieren", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 30a3df4ef5..625db9fff3 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -681,6 +681,12 @@ "description": "Tiempo de espera después de escrituras de archivos antes de continuar, permitiendo que las herramientas de diagnóstico procesen cambios y detecten problemas." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Umbral de coincidencia de diff", + "description": "Los umbrales más bajos hacen que las ediciones de archivos sean más resistentes a variaciones de formato y espacios en blanco. Un umbral del 100% requiere una coincidencia exacta." + } + }, "condensingThreshold": { "label": "Umbral de condensación de contexto", "selectProfile": "Configurar umbral para perfil", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 13e70776ed..6afcab8083 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -681,6 +681,12 @@ "description": "Temps d'attente après les écritures de fichiers avant de continuer, permettant aux outils de diagnostic de traiter les modifications et de détecter les problèmes." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Seuil de correspondance du diff", + "description": "Des seuils plus bas rendent les modifications de fichiers plus résistantes aux variations de formatage et d'espaces blancs. Un seuil de 100% nécessite une correspondance exacte." + } + }, "condensingThreshold": { "label": "Seuil de condensation du contexte", "selectProfile": "Configurer le seuil pour le profil", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 3f6633d45f..a64f1b7465 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -671,6 +671,12 @@ "description": "फ़ाइल लिखने के बाद आगे बढ़ने से पहले प्रतीक्षा करने का समय, डायग्नोस्टिक टूल को परिवर्तनों को संसाधित करने और समस्याओं का पता लगाने की अनुमति देता है।" } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "डिफ़ मैच थ्रेशोल्ड", + "description": "कम थ्रेशोल्ड फ़ाइल संपादनों को फ़ॉर्मेटिंग और व्हाइटस्पेस भिन्नताओं के प्रति अधिक लचीला बनाते हैं। 100% का थ्रेशोल्ड सटीक मिलान की आवश्यकता रखता है।" + } + }, "condensingThreshold": { "label": "संघनन ट्रिगर सीमा", "selectProfile": "प्रोफ़ाइल के लिए सीमा कॉन्फ़िगर करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 79f81f9507..429639904c 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -649,6 +649,12 @@ "description": "Waktu tunggu setelah penulisan file sebelum melanjutkan, memungkinkan alat diagnostik untuk memproses perubahan dan mendeteksi masalah." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Ambang batas pencocokan diff", + "description": "Ambang batas yang lebih rendah membuat pengeditan file lebih tahan terhadap variasi format dan spasi. Ambang batas 100% memerlukan kecocokan yang tepat." + } + }, "condensingThreshold": { "label": "Ambang Batas Pemicu Kondensasi", "selectProfile": "Konfigurasi ambang batas untuk profil", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index e0fb734844..3fd5ebe5d1 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -671,6 +671,12 @@ "description": "Tempo di attesa dopo la scrittura dei file prima di procedere, consentendo agli strumenti di diagnostica di elaborare le modifiche e rilevare i problemi." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Soglia di corrispondenza diff", + "description": "Soglie più basse rendono le modifiche ai file più resilienti a variazioni di formattazione e spazi bianchi. Una soglia del 100% richiede una corrispondenza esatta." + } + }, "condensingThreshold": { "label": "Soglia di attivazione condensazione", "selectProfile": "Configura soglia per profilo", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index cb44e1c97b..690247dfe8 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -671,6 +671,12 @@ "description": "ファイルの書き込み後に処理を続行するまでの待機時間。これにより、診断ツールが変更を処理して問題を検出できます。" } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Diff一致しきい値", + "description": "しきい値を低くすると、ファイル編集が書式や空白のバリエーションに対してより柔軟になります。100%のしきい値は完全一致が必要です。" + } + }, "condensingThreshold": { "label": "圧縮トリガーしきい値", "selectProfile": "プロファイルのしきい値を設定", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index cd19f47e65..ae0761f6cb 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -671,6 +671,12 @@ "description": "파일 쓰기 후 계속 진행하기 전에 대기하는 시간으로, 진단 도구가 변경 사항을 처리하고 문제를 감지할 수 있도록 합니다." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Diff 일치 임계값", + "description": "낮은 임계값은 파일 편집이 서식 및 공백 변동에 더 탄력적으로 대응하게 합니다. 100% 임계값은 정확히 일치해야 합니다." + } + }, "condensingThreshold": { "label": "압축 트리거 임계값", "selectProfile": "프로필 임계값 구성", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 6c2b2fcfa1..11e18758a7 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -681,6 +681,12 @@ "description": "Wachttijd na het schrijven van bestanden voordat u doorgaat, zodat diagnostische hulpmiddelen wijzigingen kunnen verwerken en problemen kunnen detecteren." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Diff-match-drempelwaarde", + "description": "Lagere drempelwaarden maken bestandbewerkingen beter bestand tegen opmaak- en witruimtevariaties. Een drempelwaarde van 100% vereist een exacte overeenkomst." + } + }, "condensingThreshold": { "label": "Compressie trigger drempelwaarde", "selectProfile": "Drempelwaarde voor profiel configureren", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 2348cf9fe8..918bc6b7f1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -671,6 +671,12 @@ "description": "Czas oczekiwania po zapisie plików przed kontynuowaniem, aby narzędzia diagnostyczne mogły przetworzyć zmiany i wykryć problemy." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Próg dopasowania diff", + "description": "Niższe progi sprawiają, że edycje plików są bardziej odporne na różnice w formatowaniu i białych znakach. Próg 100% wymaga dokładnego dopasowania." + } + }, "condensingThreshold": { "label": "Próg wyzwalania kondensacji", "selectProfile": "Skonfiguruj próg dla profilu", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 253099a4c4..78d9a47bc9 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -671,6 +671,12 @@ "description": "Tempo de espera após a gravação de arquivos antes de prosseguir, permitindo que as ferramentas de diagnóstico processem as alterações e detectem problemas." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Limite de correspondência de diff", + "description": "Limites mais baixos tornam as edições de arquivo mais resistentes a variações de formatação e espaços em branco. Um limite de 100% exige uma correspondência exata." + } + }, "condensingThreshold": { "label": "Limite de Ativação de Condensação", "selectProfile": "Configurar limite para perfil", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 9c3333d7bb..c72b32fbfc 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -671,6 +671,12 @@ "description": "Время ожидания после записи файлов перед продолжением, чтобы средства диагностики могли обработать изменения и выявить проблемы." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Порог совпадения diff", + "description": "Более низкие пороги делают редактирование файлов более устойчивым к изменениям форматирования и пробелов. Порог 100% требует точного совпадения." + } + }, "condensingThreshold": { "label": "Порог запуска сжатия", "selectProfile": "Настроить порог для профиля", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 9c04a0500c..48a104ace2 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -671,6 +671,12 @@ "description": "Devam etmeden önce dosya yazımlarından sonra beklenecek süre, tanılama araçlarının değişiklikleri işlemesine ve sorunları tespit etmesine olanak tanır." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Diff eşleşme eşiği", + "description": "Düşük eşikler, dosya düzenlemelerini biçimlendirme ve boşluk farklılıklarına karşı daha dayanıklı hale getirir. %100 eşik, tam eşleşme gerektirir." + } + }, "condensingThreshold": { "label": "Sıkıştırma Tetikleme Eşiği", "selectProfile": "Profil için eşik yapılandır", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 21daceba3b..0f2e843d3e 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -671,6 +671,12 @@ "description": "Thời gian chờ sau khi ghi tệp trước khi tiếp tục, cho phép các công cụ chẩn đoán xử lý các thay đổi và phát hiện sự cố." } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "Ngưỡng khớp diff", + "description": "Ngưỡng thấp hơn làm cho việc chỉnh sửa tệp linh hoạt hơn với các biến thể định dạng và khoảng trắng. Ngưỡng 100% yêu cầu khớp chính xác." + } + }, "condensingThreshold": { "label": "Ngưỡng kích hoạt nén", "selectProfile": "Cấu hình ngưỡng cho hồ sơ", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 3d9abde161..e17cf5a0df 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -671,6 +671,12 @@ "description": "在继续之前写入文件后等待的时间,允许诊断工具处理更改并检测问题。" } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "差异匹配阈值", + "description": "较低的阈值使文件编辑对格式和空白变化更具弹性。100%的阈值要求完全匹配。" + } + }, "condensingThreshold": { "label": "压缩触发阈值", "selectProfile": "配置配置文件阈值", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index f9971f81c2..e0c5c5833a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -708,6 +708,12 @@ "description": "在繼續之前寫入檔案後等待的時間,允許診斷工具處理變更並偵測問題。" } }, + "fileEdits": { + "diffFuzzyThreshold": { + "label": "差異匹配閾值", + "description": "較低的閾值使檔案編輯對格式和空白變化更具彈性。100%的閾值要求完全匹配。" + } + }, "condensingThreshold": { "label": "壓縮觸發閾值", "selectProfile": "設定檔的閾值", From 0c788e27c5b1c4fbb5663976e749bbbd43ddcaa4 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 10:53:35 +0700 Subject: [PATCH 14/36] feat(issue-457-edit-unsuccessfull): introduce configurable relaxed diff thresholds and diagnostics --- packages/types/src/vscode-extension-host.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 494dc20e11..5cb7fa6490 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -318,6 +318,7 @@ export type ExtensionState = Pick< | "requestDelaySeconds" | "showWorktreesInHomeScreen" | "disabledTools" + | "diffFuzzyThreshold" > & { lockApiConfigAcrossModes?: boolean version: string From 8c9050f02ec6f6f37765bb8bbdadbda410f99dc9 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 11:12:57 +0700 Subject: [PATCH 15/36] Update packages/types/src/global-settings.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/types/src/global-settings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 1f43d30935..bf90bbdbd3 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -113,10 +113,10 @@ export const globalSettingsSchema = z.object({ writeDelayMs: z.number().min(0).optional(), /** * Fuzzy matching threshold for the multi-search-replace diff strategy. - * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). - * `@default` 1.0 + * Range: 0.0 (accept anything) to 1.0 (exact match only). + * `@default` 0.9 */ - diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), + diffFuzzyThreshold: z.number().min(0).max(1).optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), From 8f9a1a88c04a2289e8f2533d224e04ec59e71204 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 4 Jun 2026 11:19:06 +0700 Subject: [PATCH 16/36] fix(issue-457-edit-unsuccessfull): fix js doc and remove hard-coded english --- packages/types/src/global-settings.ts | 2 +- .../src/components/settings/ContextManagementSettings.tsx | 6 +++--- webview-ui/src/i18n/locales/en/settings.json | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bf90bbdbd3..c9c3e9b083 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -113,7 +113,7 @@ export const globalSettingsSchema = z.object({ writeDelayMs: z.number().min(0).optional(), /** * Fuzzy matching threshold for the multi-search-replace diff strategy. - * Range: 0.0 (accept anything) to 1.0 (exact match only). + * Range: 0.5 (accept anything) to 1.0 (exact match only). * `@default` 0.9 */ diffFuzzyThreshold: z.number().min(0).max(1).optional(), diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index eaa566f36a..36792c0585 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -413,9 +413,9 @@ export const ContextManagementSettings = ({ + label={t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")}> - {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")} + {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")}
- {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.description")} + {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.description")}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 4fe5b7a597..b1d02f20b4 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -759,6 +759,10 @@ "delayAfterWrite": { "label": "Delay after writes to allow diagnostics to detect potential problems", "description": "Time to wait after file writes before proceeding, allowing diagnostic tools to process changes and detect issues." + }, + "diffFuzzyThreshold": { + "label": "Diff match threshold", + "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." } }, "fileEdits": { From b6635cfdc88a2eb16017c4bbeb9b3bd4e4467473 Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 11:15:42 +0700 Subject: [PATCH 17/36] fix(issue-457-edit-unsuccessfull): limit original content preview to a bounded window on math failure --- src/core/diff/strategies/multi-search-replace.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 1239e72792..16cbbcbeb0 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -550,7 +550,20 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` + : matchIndex !== -1 + ? `\n\nOriginal Content (around best match):\n${addLineNumbers( + resultLines + .slice( + Math.max(0, matchIndex - this.bufferLines), + Math.min( + resultLines.length, + matchIndex + searchLines.length + this.bufferLines, + ), + ) + .join("\n"), + Math.max(1, matchIndex + 1 - this.bufferLines), + )}` + : "" const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` From 90c07052bee44fd17b04586fdc07cf84e9d6c159 Mon Sep 17 00:00:00 2001 From: Nigel Date: Fri, 5 Jun 2026 13:36:39 +0700 Subject: [PATCH 18/36] fix(issue-457-edit-unsuccessfull): remove from Pick and import types --- packages/types/src/vscode-extension-host.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 5cb7fa6490..494dc20e11 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -318,7 +318,6 @@ export type ExtensionState = Pick< | "requestDelaySeconds" | "showWorktreesInHomeScreen" | "disabledTools" - | "diffFuzzyThreshold" > & { lockApiConfigAcrossModes?: boolean version: string From 1c7e85abb394d1903b821f5461de0f6ab1e1eeeb Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 16:42:01 +0700 Subject: [PATCH 19/36] fix(issue-457-edit-unsuccessfull): Move diffFuzzyThreshold to new 'File Edits' section --- .../src/components/settings/ContextManagementSettings.tsx | 6 +++--- webview-ui/src/i18n/locales/en/settings.json | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 36792c0585..eaa566f36a 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -413,9 +413,9 @@ export const ContextManagementSettings = ({ + label={t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")}> - {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.label")} + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")}
- {t("settings:contextManagement.diagnostics.diffFuzzyThreshold.description")} + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.description")}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index b1d02f20b4..0954ef306e 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -759,7 +759,9 @@ "delayAfterWrite": { "label": "Delay after writes to allow diagnostics to detect potential problems", "description": "Time to wait after file writes before proceeding, allowing diagnostic tools to process changes and detect issues." - }, + } + }, + "fileEdits": { "diffFuzzyThreshold": { "label": "Diff match threshold", "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." From 630ef7538f8320b30afa84638951c0820af89b4b Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 20:03:52 +0700 Subject: [PATCH 20/36] Revert "fix(issue-457-edit-unsuccessfull): limit original content preview to a bounded window on math failure" This reverts commit 4b2af0d8ef59a3b89138c9cba469fccb2ea1b3e3. --- src/core/diff/strategies/multi-search-replace.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 16cbbcbeb0..1239e72792 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -550,20 +550,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : matchIndex !== -1 - ? `\n\nOriginal Content (around best match):\n${addLineNumbers( - resultLines - .slice( - Math.max(0, matchIndex - this.bufferLines), - Math.min( - resultLines.length, - matchIndex + searchLines.length + this.bufferLines, - ), - ) - .join("\n"), - Math.max(1, matchIndex + 1 - this.bufferLines), - )}` - : "" + : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` From fc0a14cb9209eff5524cc267feaf93e7a6b182c3 Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 20:57:49 +0700 Subject: [PATCH 21/36] fix(issue-457-edit-unsuccessfull): clarify diffuzyThreshold JSDoc comment --- packages/types/src/global-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index c9c3e9b083..104889d4e2 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -113,7 +113,7 @@ export const globalSettingsSchema = z.object({ writeDelayMs: z.number().min(0).optional(), /** * Fuzzy matching threshold for the multi-search-replace diff strategy. - * Range: 0.5 (accept anything) to 1.0 (exact match only). + * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). * `@default` 0.9 */ diffFuzzyThreshold: z.number().min(0).max(1).optional(), From 98a31a537396f4025c33496320b58df776c1c67e Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 6 Jun 2026 22:29:24 +0700 Subject: [PATCH 22/36] fix(issue-457-edit-unsuccessfull): Correct the documented default for diffFuzzyThreshold and fix failure-diagnostics assertions unconditional --- packages/types/src/global-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 104889d4e2..55ed989b58 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -114,7 +114,7 @@ export const globalSettingsSchema = z.object({ /** * Fuzzy matching threshold for the multi-search-replace diff strategy. * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). - * `@default` 0.9 + * `@default` 1.0 */ diffFuzzyThreshold: z.number().min(0).max(1).optional(), requestDelaySeconds: z.number().optional(), From fb9a30c1760526b4dc37328e8524db404243d3b9 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 10:47:38 +0700 Subject: [PATCH 23/36] fix(multi-search-replace): cap original content preview to bounded window when startLine is absent Prevents full file content (potentially including secrets) from being sent to the LLM API via pushToolResult on failed match. When startLine is not provided, the error message now slices resultLines around matchIndex using bufferLines, mirroring the existing behavior of the startLine-present branch. --- src/core/diff/strategies/multi-search-replace.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 1239e72792..1d98e0daba 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -550,7 +550,20 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` + : `\n\nOriginal Content:\n${addLineNumbers( + resultLines + .slice( + Math.max(0, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines), + Math.min( + resultLines.length, + (matchIndex >= 0 ? matchIndex : 0) + + searchLines.length + + this.bufferLines, + ), + ) + .join("\n"), + Math.max(1, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines + 1), + )}` const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` From eaca0fa53865b3e71e077225366e20eeaee80424 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 11:05:10 +0700 Subject: [PATCH 24/36] fix(multi-search-replace): address review feedback - Cap original content preview to bounded window when startLine is absent, preventing full file content (potentially including secrets) from being sent to LLM via pushToolError - Move test assertions outside if (!result.success) guards so tests fail explicitly instead of silently passing when failParts is absent --- .../__tests__/multi-search-replace.spec.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index b6471aef71..f8cae80c7d 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -1419,13 +1419,11 @@ function sum(a, b) { const result = await strategy.applyDiff(originalContent, diff) expect(result.success).toBe(false) - if (!result.success) { - expect(result.failParts).toBeDefined() - expect(result.failParts!.length).toBeGreaterThan(0) - const failedPart = result.failParts![0] - expect(failedPart).toHaveProperty("error") - expect((failedPart as { error: string }).error).toContain("No sufficiently similar match found") - } + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + expect((failedPart as { error: string }).error).toContain("No sufficiently similar match found") }) it("should output enhanced error diagnostics (Levenshtein distance, character counts) when a match fails", async () => { @@ -1445,19 +1443,17 @@ function sum(a, b) { const result = await strategy.applyDiff(originalContent, diff) expect(result.success).toBe(false) - if (!result.success) { - expect(result.failParts).toBeDefined() - expect(result.failParts!.length).toBeGreaterThan(0) - const failedPart = result.failParts![0] - expect(failedPart).toHaveProperty("error") - const errorMsg = (failedPart as { error: string }).error - expect(errorMsg).toContain("Debug Info:") - expect(errorMsg).toContain("Similarity Score:") - expect(errorMsg).toContain("Required Threshold: 95%") - expect(errorMsg).toContain("Levenshtein Distance:") - expect(errorMsg).toContain("Search Length:") - expect(errorMsg).toContain("Best Match Length:") - } + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + const errorMsg = (failedPart as { error: string }).error + expect(errorMsg).toContain("Debug Info:") + expect(errorMsg).toContain("Similarity Score:") + expect(errorMsg).toContain("Required Threshold: 95%") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Length:") + expect(errorMsg).toContain("Best Match Length:") }) }) }) From 7c5368b3e0c5d7471c7d6a0759dc76690f8a8241 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 11:28:16 +0700 Subject: [PATCH 25/36] fix(multi-search-replace): fix coverage holes - Fix ternary condition (startLine && endLine instead of startLine !== undefined && endLine !== undefined) so bounded window else arm is reachable when startLine is 0/NaN - Add test for fuzzy match failure without :start_line: to exercise bounded window code path - Add UI test for diffFuzzyThreshold slider interaction --- .../diff/strategies/multi-search-replace.ts | 2 +- .../ContextManagementSettings.spec.tsx | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 1d98e0daba..ffbe191f57 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -540,7 +540,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { } else { // No match found with either method const originalContentSection = - startLine !== undefined && endLine !== undefined + startLine && endLine ? `\n\nOriginal Content:\n${addLineNumbers( resultLines .slice( diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 2de2954c2b..57b3066b4d 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -479,4 +479,25 @@ describe("ContextManagementSettings", () => { expect(screen.getByText("settings:contextManagement.rooignore.label")).toBeInTheDocument() }) }) + + describe("diffFuzzyThreshold", () => { + it("renders diff fuzzy threshold slider with default value", () => { + render() + + const slider = screen.getByTestId("diff-fuzzy-threshold-slider") + expect(slider).toBeInTheDocument() + }) + + it("calls setCachedStateField when slider changes", async () => { + const setCachedStateField = vi.fn() + render() + + const slider = screen.getByTestId("diff-fuzzy-threshold-slider") + fireEvent.change(slider, { target: { value: "0.85" } }) + + await waitFor(() => { + expect(setCachedStateField).toHaveBeenCalledWith("diffFuzzyThreshold", 0.85) + }) + }) + }) }) From 8449c272c7ec5b6e610221e6968a1f6fecd89953 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 15:53:56 +0700 Subject: [PATCH 26/36] fix(test): align no-match diagnostics expectations with current error output --- .../__tests__/multi-search-replace.spec.ts | 23 +++++++++++++++++++ .../settings/__tests__/SettingsView.spec.tsx | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index f8cae80c7d..c3d2ad12de 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -1455,5 +1455,28 @@ function sum(a, b) { expect(errorMsg).toContain("Search Length:") expect(errorMsg).toContain("Best Match Length:") }) + it("should report no-match diagnostics when search content is completely different and no :start_line: is given", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.9) + const diff = + "<<<<<<< SEARCH\n" + + "§§§§§§§§§§§§\n" + + "§§§§§§§§§§§§\n" + + "=======\n" + + "¤¤¤¤¤¤¤¤¤¤¤¤\n" + + "¤¤¤¤¤¤¤¤¤¤¤¤\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + const errorMsg = (failedPart as { error: string }).error + expect(errorMsg).toContain("No sufficiently similar match found") + expect(errorMsg).toContain("Best Match Found:") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Range: start to end") + }) }) }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 2457c554b0..166b8646fe 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -443,6 +443,28 @@ describe("SettingsView - Sound Settings", () => { ) }) + it("defaults checkpoints to false when state omits the field", () => { + const { activateTab, getSettingsContent } = renderSettingsView() + mockPostMessage({ enableCheckpoints: undefined }) + + activateTab("notifications") + const soundCheckbox = within(getSettingsContent()).getByTestId("sound-enabled-checkbox") + fireEvent.click(soundCheckbox) + fireEvent.click(screen.getByTestId("save-button")) + + const postMessageMock = vscode.postMessage as unknown as { + mock: { calls: Array<[any]> } + } + expect( + postMessageMock.mock.calls.some( + ([message]) => + message?.type === "updateSettings" && + message.updatedSettings?.soundEnabled === true && + message.updatedSettings?.enableCheckpoints === false, + ), + ).toBe(true) + }) + it("shows tts slider when sound is enabled", () => { // Render once and get the activateTab helper const { activateTab, getSettingsContent } = renderSettingsView() From fdfff9012bd32fd5c8bfdb92fbe10bc131b76c2f Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 17:41:07 +0700 Subject: [PATCH 27/36] test(settings): remove invalid checkpoints default assertion --- .../settings/__tests__/SettingsView.spec.tsx | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 166b8646fe..2457c554b0 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -443,28 +443,6 @@ describe("SettingsView - Sound Settings", () => { ) }) - it("defaults checkpoints to false when state omits the field", () => { - const { activateTab, getSettingsContent } = renderSettingsView() - mockPostMessage({ enableCheckpoints: undefined }) - - activateTab("notifications") - const soundCheckbox = within(getSettingsContent()).getByTestId("sound-enabled-checkbox") - fireEvent.click(soundCheckbox) - fireEvent.click(screen.getByTestId("save-button")) - - const postMessageMock = vscode.postMessage as unknown as { - mock: { calls: Array<[any]> } - } - expect( - postMessageMock.mock.calls.some( - ([message]) => - message?.type === "updateSettings" && - message.updatedSettings?.soundEnabled === true && - message.updatedSettings?.enableCheckpoints === false, - ), - ).toBe(true) - }) - it("shows tts slider when sound is enabled", () => { // Render once and get the activateTab helper const { activateTab, getSettingsContent } = renderSettingsView() From c78e938660b206b2a49be9269c5ad882b892ac64 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 19:28:35 +0700 Subject: [PATCH 28/36] test(settings): cover settings fallback defaults --- .../settings/__tests__/SettingsView.spec.tsx | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 2457c554b0..bbc1f1f632 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -1,10 +1,12 @@ // pnpm --filter @roo-code/vscode-webview test src/components/settings/__tests__/SettingsView.spec.tsx -import { render, screen, fireEvent, within } from "@/utils/test-utils" +import { render, screen, fireEvent, within, waitFor } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { act } from "@testing-library/react" import { vscode } from "@/utils/vscode" import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" +import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS } from "@roo-code/types" import SettingsView from "../SettingsView" @@ -287,7 +289,7 @@ const mockPostMessage = (state: any) => { ) } -const renderSettingsView = () => { +const renderSettingsView = (initialState: any = {}) => { const onDone = vi.fn() const queryClient = new QueryClient() @@ -300,7 +302,9 @@ const renderSettingsView = () => { ) // Hydrate initial state. - mockPostMessage({}) + act(() => { + mockPostMessage(initialState) + }) // Helper function to activate a tab and ensure its content is visible const activateTab = (tabId: string) => { @@ -443,6 +447,31 @@ describe("SettingsView - Sound Settings", () => { ) }) + it("saves fallback defaults for checkpoint and terminal settings", async () => { + renderSettingsView({ + enableCheckpoints: undefined, + checkpointTimeout: undefined, + terminalShellIntegrationTimeout: undefined, + settingsImportedAt: new Date().toISOString(), + }) + + await waitFor(() => expect(screen.getByTestId("save-button")).toBeInTheDocument()) + fireEvent.click(screen.getByTestId("save-button")) + + await waitFor(() => + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ + enableCheckpoints: false, + checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + terminalShellIntegrationTimeout: 30_000, + }), + }), + ), + ) + }) + it("shows tts slider when sound is enabled", () => { // Render once and get the activateTab helper const { activateTab, getSettingsContent } = renderSettingsView() From dde907c636909d86cd32ea6f57588e01db5bbdf4 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 20:24:00 +0700 Subject: [PATCH 29/36] test: stabilize settings fallback defaults --- .../components/settings/__tests__/SettingsView.spec.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index bbc1f1f632..6434eadb23 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -448,14 +448,16 @@ describe("SettingsView - Sound Settings", () => { }) it("saves fallback defaults for checkpoint and terminal settings", async () => { - renderSettingsView({ + const { activateTab, getSettingsContent } = renderSettingsView({ enableCheckpoints: undefined, checkpointTimeout: undefined, terminalShellIntegrationTimeout: undefined, settingsImportedAt: new Date().toISOString(), }) - await waitFor(() => expect(screen.getByTestId("save-button")).toBeInTheDocument()) + activateTab("notifications") + const content = getSettingsContent() + fireEvent.click(await within(content).findByTestId("sound-enabled-checkbox")) fireEvent.click(screen.getByTestId("save-button")) await waitFor(() => @@ -463,6 +465,7 @@ describe("SettingsView - Sound Settings", () => { expect.objectContaining({ type: "updateSettings", updatedSettings: expect.objectContaining({ + soundEnabled: true, enableCheckpoints: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, terminalShellIntegrationTimeout: 30_000, From 249698e58dfa36734cc2fa991c1906ce9680c427 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 20:58:45 +0700 Subject: [PATCH 30/36] test: cover ClineProvider partial branches --- src/__tests__/single-open-invariant.spec.ts | 40 +++++++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 27 +++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 2dd466a992..af5b429a80 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -73,6 +73,46 @@ describe("Single-open-task invariant", () => { expect(addClineToStack).toHaveBeenCalledTimes(1) }) + it("Subtask create: keeps existing task open when parentTask is provided", async () => { + vi.spyOn(ProfileValidatorMod.ProfileValidator, "isProfileAllowed").mockReturnValue(true) + + const removeClineFromStack = vi.fn().mockResolvedValue(undefined) + const addClineToStack = vi.fn().mockResolvedValue(undefined) + const parentTask = { taskId: "parent-1" } + + const provider = { + clineStack: [parentTask], + setValues: vi.fn(), + getState: vi.fn().mockResolvedValue({ + apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, + organizationAllowList: "*", + enableCheckpoints: true, + checkpointTimeout: 60, + cloudUserInfo: null, + }), + removeClineFromStack, + addClineToStack, + setProviderProfile: vi.fn(), + log: vi.fn(), + getStateToPostToWebview: vi.fn(), + providerSettingsManager: { getModeConfigId: vi.fn(), listConfig: vi.fn() }, + customModesManager: { getCustomModes: vi.fn().mockResolvedValue([]) }, + taskCreationCallback: vi.fn(), + contextProxy: { + extensionUri: {}, + setValue: vi.fn(), + getValue: vi.fn(), + setProviderSettings: vi.fn(), + getProviderSettings: vi.fn(() => ({})), + }, + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).createTask.call(provider, "Subtask", undefined, parentTask as any) + + expect(removeClineFromStack).not.toHaveBeenCalled() + expect(addClineToStack).toHaveBeenCalledTimes(1) + }) + it("History resume path always closes current before rehydration (non-rehydrating case)", async () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index d3bbea17de..aed1355f84 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -14,6 +14,7 @@ import { ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_WRITE_DELAY_MS, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -932,6 +933,32 @@ describe("ClineProvider", () => { expect(state.writeDelayMs).toBe(1000) }) + test("getState applies fallback defaults for write, diff, and terminal settings", async () => { + ;(mockContext.globalState.get as any).mockImplementation((key: string) => { + if ( + [ + "writeDelayMs", + "diffFuzzyThreshold", + "terminalShellIntegrationTimeout", + "terminalShellIntegrationDisabled", + "terminalCommandDelay", + ].includes(key) + ) { + return undefined + } + + return null + }) + + const state = await provider.getState() + + expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS) + expect(state.diffFuzzyThreshold).toBe(DEFAULT_DIFF_FUZZY_THRESHOLD) + expect(state.terminalShellIntegrationTimeout).toBe(Terminal.defaultShellIntegrationTimeout) + expect(state.terminalShellIntegrationDisabled).toBe(true) + expect(state.terminalCommandDelay).toBe(0) + }) + test("handles writeDelayMs message", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] From 9543a5bb10b0f49f93e077f71daaad4384b64e1b Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 22:24:26 +0700 Subject: [PATCH 31/36] test: cover multi-search-replace failure branches --- .../__tests__/multi-search-replace.spec.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index c3d2ad12de..c6cc1d6bd1 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -878,6 +878,92 @@ function processData(data) { expect(result.success).toBe(false) }) + it("should include line range debug info when search fails with start_line marker", async () => { + const originalContent = "line one\nline two" + const diffContent = `test.ts +<<<<<<< SEARCH +:start_line:999 +------- +non-existent content that cannot be found in the file +======= +replacement content here +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("at line: 999") + expect(error).toContain("Search Range: starting at line 999") + expect(error).toContain("Best Match Found:\n(no match)") + expect(error).toContain("Levenshtein Distance: N/A") + expect(error).toContain("Best Match Length: 0 characters") + }) + + it("should include scoped original content when search fails with start_line that has a low-score match", async () => { + const originalContent = "function existing() {\n return 42;\n}\n" + const diffContent = `test.ts +<<<<<<< SEARCH +:start_line:1 +------- +function different() { + return 99; +} +======= +function newVersion() { + return 99; +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("at line: 1") + expect(error).toContain("Search Range: starting at line 1") + expect(error).toContain("Best Match Found:") + expect(error).toContain("Original Content:\n1 | function existing()") + }) + + it("should include best-match debug info when unscoped search is below threshold", async () => { + const strictStrategy = new MultiSearchReplaceDiffStrategy(1, 5) + const originalContent = "function processUsers(data) {\n return data.map(user => user.name);\n}\n" + const diffContent = `test.ts +<<<<<<< SEARCH +function processUsers(data) { + return data.map(user => user.username); +} +======= +function processUsers(data) { + return data.map(user => user.displayName); +} +>>>>>>> REPLACE` + + const result = await strictStrategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("Search Range: start to end") + expect(error).toContain("Levenshtein Distance:") + expect(error).toContain("characters") + expect(error).toContain("Best Match Length:") + expect(error).toContain("Best Match Found:\n1 | function processUsers(data)") + }) + it("should match content with extra whitespace", async () => { const originalContent = "function sum(a, b) {\n return a + b;\n}" const diffContent = `test.ts From ccf8c4549b8e7720ac1be8d4acc80aeedf007300 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 22 Jun 2026 23:29:03 +0700 Subject: [PATCH 32/36] test: cover remaining codecov patch branches on ClineProvider and multi-search-replace --- .../__tests__/multi-search-replace.spec.ts | 27 +++++++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 16 +++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index c6cc1d6bd1..297e64fa83 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -964,6 +964,33 @@ function processUsers(data) { expect(error).toContain("Best Match Found:\n1 | function processUsers(data)") }) + it("should include zero-match info when unscoped search finds no similarity at all", async () => { + const strictStrategy = new MultiSearchReplaceDiffStrategy(1, 0) + const originalContent = "xxxxxx\nyyyyyy\nzzzzzz" + const diffContent = `test.ts +<<<<<<< SEARCH +!!!!!! +======= +aaaaaa +>>>>>>> REPLACE` + + const result = await strictStrategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("Search Range: start to end") + expect(error).toContain("Best Match Found:\n(no match)") + expect(error).toContain("Levenshtein Distance: N/A") + expect(error).toContain("Best Match Length: 0 characters") + expect(error).toContain("Original Content:") + expect(error).toContain("1 | xxxxxx") + }) + it("should match content with extra whitespace", async () => { const originalContent = "function sum(a, b) {\n return a + b;\n}" const diffContent = `test.ts diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index aed1355f84..9609cad372 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -959,6 +959,22 @@ describe("ClineProvider", () => { expect(state.terminalCommandDelay).toBe(0) }) + test("getState passes through defined write/diff/terminal values instead of defaults", async () => { + await provider.contextProxy.setValue("writeDelayMs", 500) + await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5) + await provider.contextProxy.setValue("terminalShellIntegrationTimeout", 99999) + await provider.contextProxy.setValue("terminalShellIntegrationDisabled", false) + await provider.contextProxy.setValue("terminalCommandDelay", 1234) + + const state = await provider.getState() + + expect(state.writeDelayMs).toBe(500) + expect(state.diffFuzzyThreshold).toBe(0.5) + expect(state.terminalShellIntegrationTimeout).toBe(99999) + expect(state.terminalShellIntegrationDisabled).toBe(false) + expect(state.terminalCommandDelay).toBe(1234) + }) + test("handles writeDelayMs message", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] From a875f01d067c061c7c83b84bdfe3bed179c8b0a6 Mon Sep 17 00:00:00 2001 From: Nigel Date: Tue, 23 Jun 2026 08:03:06 +0700 Subject: [PATCH 33/36] docs: correct constructor comment to reflect actual default threshold (1.0) --- src/core/diff/strategies/multi-search-replace.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index ffbe191f57..a8ba0215d3 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -81,10 +81,10 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { } constructor(fuzzyThreshold?: number, bufferLines?: number) { - // Use provided threshold or default to relaxed matching (0.9) - // A value of 0.9 means 90% similarity is required for a match. - // This was previously 1.0 (exact match) which caused frequent - // "Edit Unsuccessful" errors on minor whitespace differences. + // Use provided threshold or default to exact matching (1.0) + // A value of 0.9 means 90% similarity is required for a match, + // but the default remains 1.0 (exact match). Users can opt in + // to relaxed matching via diffFuzzyThreshold in settings. // Clamp the threshold to [0.5, 1.0] as a defence-in-depth guard. const thresholdVal = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD this.fuzzyThreshold = Math.max(0.5, Math.min(1.0, thresholdVal)) From c3b4b6d1d898501110bbbd20a72e6241f8447c3c Mon Sep 17 00:00:00 2001 From: Nigel Date: Tue, 23 Jun 2026 08:08:07 +0700 Subject: [PATCH 34/36] fix: align schema min(0.5) with JSDoc range and constructor clamp --- packages/types/src/global-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 55ed989b58..1f43d30935 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -116,7 +116,7 @@ export const globalSettingsSchema = z.object({ * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). * `@default` 1.0 */ - diffFuzzyThreshold: z.number().min(0).max(1).optional(), + diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), From fd5d031cfff5617a750108682cf5f285f47dc928 Mon Sep 17 00:00:00 2001 From: Nigel Date: Tue, 23 Jun 2026 08:14:04 +0700 Subject: [PATCH 35/36] fix: remove duplicate fileEdits.diffFuzzyThreshold entry in en locale --- webview-ui/src/i18n/locales/en/settings.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 0954ef306e..4fe5b7a597 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -767,12 +767,6 @@ "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." } }, - "fileEdits": { - "diffFuzzyThreshold": { - "label": "Diff match threshold", - "description": "Lower thresholds make file edits more resilient to formatting and whitespace variations. A threshold of 100% requires an exact match." - } - }, "condensingThreshold": { "label": "Condensing Trigger Threshold", "selectProfile": "Configure threshold for profile", From aacf6fc91df38b9f92217a1d23564c037f7a8f6e Mon Sep 17 00:00:00 2001 From: Nigel Date: Tue, 23 Jun 2026 08:18:05 +0700 Subject: [PATCH 36/36] test: cover diffFuzzyThreshold passthrough in getStateToPostToWebview --- src/core/webview/__tests__/ClineProvider.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 9609cad372..cb4218482f 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1105,6 +1105,15 @@ describe("ClineProvider", () => { }) }) + it("getStateToPostToWebview passes through defined diffFuzzyThreshold value", async () => { + await provider.resolveWebviewView(mockWebviewView) + await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5) + + const state = await provider.getStateToPostToWebview() + + expect(state.diffFuzzyThreshold).toBe(0.5) + }) + it("loads saved API config when switching modes", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]