diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 692798d00d..4b2070bbb8 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -18,10 +18,12 @@ import { providerIdentifiers } from "./provider-identifiers.js" /** * Default delay in milliseconds after writes to allow diagnostics to detect potential problems. - * This delay is particularly important for Go and other languages where tools like goimports - * need time to automatically clean up unused imports. + * Defaults to 0: the write path adds no artificial pacing by default, and post-save + * diagnostics are reported after the (zero) delay. Users who rely on auto-formatters that + * settle asynchronously (e.g. goimports for Go) can raise this setting to give formatters + * time to settle before diagnostics are captured. */ -export const DEFAULT_WRITE_DELAY_MS = 1000 +export const DEFAULT_WRITE_DELAY_MS = 0 /** * Default values for the "auto-close files Zoo opened" settings. diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index ae026b4b86..0c5c80abb9 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -1,5 +1,4 @@ import path from "path" -import delay from "delay" import fs from "fs/promises" import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" @@ -146,7 +145,6 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { true, ) - await delay(300) task.diffViewProvider.scrollToFirstDiff() let unified = fileExists diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 792d1ebc23..f9d3ce9b85 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1688,18 +1688,44 @@ describe("ClineProvider", () => { expect(state.language).toBe("pt-BR") }) - test("writeDelayMs defaults to 1000ms", async () => { - // Mock globalState.get to return undefined for writeDelayMs - ;(mockContext.globalState.get as any).mockImplementation((key: string) => { + test("writeDelayMs defaults to DEFAULT_WRITE_DELAY_MS", async () => { + // Mock globalState.get to return undefined for writeDelayMs (typed reassignment, + // same pattern as the customModePrompts test below — Memento.get has no mock type) + mockContext.globalState.get = vi.fn((key: string) => { return key === "writeDelayMs" ? undefined : null }) const state = await provider.getState() - expect(state.writeDelayMs).toBe(1000) + expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS) + }) + + test("getStateToPostToWebview returns the persisted writeDelayMs value", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Simulate the updateSettings handler storing the value. + await provider.contextProxy.setValue("writeDelayMs", 500) + + const state = await provider.getStateToPostToWebview() + + expect(state.writeDelayMs).toBe(500) + }) + + test("getStateToPostToWebview defaults writeDelayMs to DEFAULT_WRITE_DELAY_MS when unset", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Ensure the setting is not persisted. + await provider.contextProxy.setValue("writeDelayMs", undefined) + + const state = await provider.getStateToPostToWebview() + + expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS) }) test("getState applies fallback defaults for write, diff, and terminal settings", async () => { - ;(mockContext.globalState.get as any).mockImplementation((key: string) => { + // Mock globalState.get to return undefined for the fallback settings + // (typed reassignment — Memento.get has no mock type, same pattern as the + // writeDelayMs default test above) + mockContext.globalState.get = vi.fn((key: string) => { if ( [ "writeDelayMs", diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 24db0bf433..df77823fdd 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1036,7 +1036,7 @@ }, "core/webview/__tests__/ClineProvider.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 198 + "count": 196 } }, "core/webview/__tests__/ClineProvider.sticky-mode.spec.ts": { diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index aee88f4061..00b3dcaf7a 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -2,6 +2,7 @@ import { DiffViewProvider, DIFF_VIEW_URI_SCHEME, DIFF_VIEW_LABEL_CHANGES } from import * as vscode from "vscode" import * as path from "path" import delay from "delay" +import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" import { makeRange, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" @@ -918,12 +919,18 @@ describe("DiffViewProvider", () => { const result = await diffViewProvider.saveChanges() - // Verify default behavior (enabled=true, delay=2000ms) - expect(mockDelay).toHaveBeenCalledWith(1000) + // Verify default behavior (enabled=true, delay falls back to DEFAULT_WRITE_DELAY_MS) + expect(mockDelay).toHaveBeenCalledWith(0) expect(vscode.languages.getDiagnostics).toHaveBeenCalled() expect(result.newProblemsMessage).toBe("") }) + it("pins the default write delay to zero", () => { + // The tests above compare against the constant, so they would still pass if the + // default were changed back to 1000. Pin the value the write path actually uses. + expect(DEFAULT_WRITE_DELAY_MS).toBe(0) + }) + it("should handle custom delay values", async () => { const mockDelay = vi.mocked(delay) mockDelay.mockClear() diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 177372f310..c4fa00f479 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -23,6 +23,7 @@ import { ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_WRITE_DELAY_MS, } from "@roo-code/types" import { findLastIndex } from "@roo/array" @@ -197,7 +198,7 @@ export const mergeExtensionState = (prevState: ExtensionState, newState: Partial } } -const createInitialExtensionState = (): ExtensionState => ({ +export const createInitialExtensionState = (): ExtensionState => ({ apiConfiguration: {}, version: "", clineMessages: [], @@ -214,7 +215,10 @@ const createInitialExtensionState = (): ExtensionState => ({ enableCheckpoints: true, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Default to 15 seconds language: "en", // Default language code - writeDelayMs: 1000, + // Placeholder before the extension state hydrates: use the shared default + // (the pre-hydration value must not disagree with the extension's own + // DEFAULT_WRITE_DELAY_MS fallback). + writeDelayMs: DEFAULT_WRITE_DELAY_MS, diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 4c2e2a092c..dc33af38d1 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -12,9 +12,15 @@ import { type RouterModels, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_WRITE_DELAY_MS, } from "@roo-code/types" -import { ExtensionStateContextProvider, useExtensionState, mergeExtensionState } from "../ExtensionStateContext" +import { + ExtensionStateContextProvider, + useExtensionState, + mergeExtensionState, + createInitialExtensionState, +} from "../ExtensionStateContext" const TestComponent = () => { const { allowedCommands, setAllowedCommands, soundEnabled, showRooIgnoredFiles, setShowRooIgnoredFiles } = @@ -215,6 +221,16 @@ describe("ExtensionStateContext", () => { expect(JSON.parse(screen.getByTestId("show-rooignored-files").textContent!)).toBe(true) }) + it("initializes the write delay to the pre-hydration default", () => { + // The initializer itself (not a merge fixture) must carry the shared + // default: a regression that dropped it from createInitialExtensionState + // would otherwise stay hidden because the merge tests supply the key + // manually. + const state = createInitialExtensionState() + + expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS) + }) + it("initializes shadowed context fields from initialState", () => { const routerModels = {} as RouterModels const marketplaceItems: MarketplaceItem[] = [ @@ -410,7 +426,9 @@ describe("mergeExtensionState", () => { taskHistory: [], shouldShowAnnouncement: false, enableCheckpoints: true, - writeDelayMs: 1000, + // Matches the shared DEFAULT_WRITE_DELAY_MS (pre-hydration placeholder + // must not disagree with the extension's own fallback). + writeDelayMs: 0, mode: "default", experiments: {} as Record, customModes: [], @@ -480,7 +498,9 @@ describe("mergeExtensionState", () => { taskHistory: [], shouldShowAnnouncement: false, enableCheckpoints: true, - writeDelayMs: 1000, + // Matches the shared DEFAULT_WRITE_DELAY_MS (pre-hydration placeholder + // must not disagree with the extension's own fallback). + writeDelayMs: 0, mode: "default", experiments: {} as Record, customModes: [],