diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bac3548ccb..1f43d30935 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" /** @@ -23,6 +22,16 @@ 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 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 = 1.0 + /** * 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.5 (50% minimum similarity) to 1.0 (exact match only). + * `@default` 1.0 + */ + diffFuzzyThreshold: z.number().min(0.5).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..494dc20e11 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -332,6 +332,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/__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/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index b58e331f8f..297e64fa83 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,119 @@ 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 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 @@ -1374,4 +1487,109 @@ 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 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) + 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 () => { + 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) + 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:") + }) + 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/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index c74faedbfe..a8ba0215d3 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" @@ -82,9 +82,12 @@ 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 + // 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)) this.bufferLines = bufferLines ?? BUFFER_LINES } @@ -537,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( @@ -547,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)}` @@ -555,9 +571,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/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 158238ce7e..cb4218482f 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,6 +13,8 @@ import { type ExtensionState, 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" @@ -673,6 +675,7 @@ describe("ClineProvider", () => { openRouterImageGenerationSelectedModel: undefined, taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, } const message: ExtensionMessage = { @@ -930,6 +933,48 @@ 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("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] @@ -1060,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] diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 8663ea6e03..eaa566f36a 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,31 @@ export const ContextManagementSettings = ({ + + + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")} + +
+ setCachedStateField("diffFuzzyThreshold", value)} + data-testid="diff-fuzzy-threshold-slider" + /> + + {((diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD) * 100).toFixed(0)}% + +
+
+ {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.description")} +
+
+ (({ 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/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) + }) + }) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 2457c554b0..6434eadb23 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,34 @@ describe("SettingsView - Sound Settings", () => { ) }) + it("saves fallback defaults for checkpoint and terminal settings", async () => { + const { activateTab, getSettingsContent } = renderSettingsView({ + enableCheckpoints: undefined, + checkpointTimeout: undefined, + terminalShellIntegrationTimeout: undefined, + settingsImportedAt: new Date().toISOString(), + }) + + activateTab("notifications") + const content = getSettingsContent() + fireEvent.click(await within(content).findByTestId("sound-enabled-checkbox")) + fireEvent.click(screen.getByTestId("save-button")) + + await waitFor(() => + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ + soundEnabled: true, + 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() diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index b4d8361144..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,6 +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: DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, taskSyncEnabled: false, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index a795c08897..efd7b48486 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: DEFAULT_DIFF_FUZZY_THRESHOLD, } const makeMessage = (ts: number, text: string): ClineMessage => 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/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 739ecbde04..4fe5b7a597 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -761,6 +761,12 @@ "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." + } + }, "condensingThreshold": { "label": "Condensing Trigger Threshold", "selectProfile": "Configure threshold for profile", 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": "設定檔的閾值",