diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index e713cafa4c..8086b89b35 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -41,6 +41,8 @@ export const globalSettingsSchema = z.object({ alwaysAllowBrowser: z.boolean().optional(), alwaysApproveResubmit: z.boolean().optional(), requestDelaySeconds: z.number().optional(), + minRetryDelaySeconds: z.number().optional(), + maxRetryDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), alwaysAllowSubtasks: z.boolean().optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 46da7485ed..59eda533ad 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1640,6 +1640,8 @@ export class Task extends EventEmitter { autoApprovalEnabled, alwaysApproveResubmit, requestDelaySeconds, + minRetryDelaySeconds, + maxRetryDelaySeconds, mode, autoCondenseContext = true, autoCondenseContextPercent = 100, @@ -1794,6 +1796,11 @@ export class Task extends EventEmitter { const baseDelay = requestDelaySeconds || 5 let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) + // Apply min/max bounds to the exponential delay + const minDelay = minRetryDelaySeconds || 5 + const maxDelay = maxRetryDelaySeconds || 100 + exponentialDelay = Math.min(Math.max(exponentialDelay, minDelay), maxDelay) + // If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff if (error.status === 429) { const geminiRetryDetails = error.errorDetails?.find( diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index cac94aa0ce..b4224fdca0 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -861,6 +861,14 @@ export const webviewMessageHandler = async ( await updateGlobalState("requestDelaySeconds", message.value ?? 5) await provider.postStateToWebview() break + case "minRetryDelaySeconds": + await updateGlobalState("minRetryDelaySeconds", message.value ?? 5) + await provider.postStateToWebview() + break + case "maxRetryDelaySeconds": + await updateGlobalState("maxRetryDelaySeconds", message.value ?? 100) + await provider.postStateToWebview() + break case "writeDelayMs": await updateGlobalState("writeDelayMs", message.value) await provider.postStateToWebview() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 73ebf59d4c..4f503b6ef3 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -229,6 +229,8 @@ export type ExtensionState = Pick< writeDelayMs: number requestDelaySeconds: number + minRetryDelaySeconds: number + maxRetryDelaySeconds: number enableCheckpoints: boolean maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7efc97e8c7..acba6e338b 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -116,6 +116,8 @@ export interface WebviewMessage { | "searchCommits" | "alwaysApproveResubmit" | "requestDelaySeconds" + | "minRetryDelaySeconds" + | "maxRetryDelaySeconds" | "setApiConfigPassword" | "mode" | "updatePrompt" diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index e825ab8d7c..33c404fb80 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -21,6 +21,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowBrowser?: boolean alwaysApproveResubmit?: boolean requestDelaySeconds: number + minRetryDelaySeconds: number + maxRetryDelaySeconds: number alwaysAllowMcp?: boolean alwaysAllowModeSwitch?: boolean alwaysAllowSubtasks?: boolean @@ -36,6 +38,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowBrowser" | "alwaysApproveResubmit" | "requestDelaySeconds" + | "minRetryDelaySeconds" + | "maxRetryDelaySeconds" | "alwaysAllowMcp" | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" @@ -54,6 +58,8 @@ export const AutoApproveSettings = ({ alwaysAllowBrowser, alwaysApproveResubmit, requestDelaySeconds, + minRetryDelaySeconds, + maxRetryDelaySeconds, alwaysAllowMcp, alwaysAllowModeSwitch, alwaysAllowSubtasks, @@ -184,19 +190,43 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.retry.label")}
-
- setCachedStateField("requestDelaySeconds", value)} - data-testid="request-delay-slider" - /> - {requestDelaySeconds}s +
+ +
+ setCachedStateField("minRetryDelaySeconds", value)} + data-testid="min-retry-delay-slider" + /> + {minRetryDelaySeconds}s +
+
+ {t("settings:autoApprove.retry.minDelayDescription")} +
-
- {t("settings:autoApprove.retry.delayLabel")} +
+ +
+ setCachedStateField("maxRetryDelaySeconds", value)} + data-testid="max-retry-delay-slider" + /> + {maxRetryDelaySeconds}s +
+
+ {t("settings:autoApprove.retry.maxDelayDescription")} +
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 8712b81cf2..8330420693 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -146,6 +146,8 @@ const SettingsView = forwardRef(({ onDone, t maxWorkspaceFiles, mcpEnabled, requestDelaySeconds, + minRetryDelaySeconds, + maxRetryDelaySeconds, remoteBrowserHost, screenshotQuality, soundEnabled, @@ -302,6 +304,8 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled }) vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit }) vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds }) + vscode.postMessage({ type: "minRetryDelaySeconds", value: minRetryDelaySeconds }) + vscode.postMessage({ type: "maxRetryDelaySeconds", value: maxRetryDelaySeconds }) vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext }) vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 }) vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) @@ -595,6 +599,8 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowBrowser={alwaysAllowBrowser} alwaysApproveResubmit={alwaysApproveResubmit} requestDelaySeconds={requestDelaySeconds} + minRetryDelaySeconds={minRetryDelaySeconds} + maxRetryDelaySeconds={maxRetryDelaySeconds} alwaysAllowMcp={alwaysAllowMcp} alwaysAllowModeSwitch={alwaysAllowModeSwitch} alwaysAllowSubtasks={alwaysAllowSubtasks} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c87ccdb6e9..0114575c08 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -91,6 +91,10 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysApproveResubmit: (value: boolean) => void requestDelaySeconds: number setRequestDelaySeconds: (value: number) => void + minRetryDelaySeconds: number + setMinRetryDelaySeconds: (value: number) => void + maxRetryDelaySeconds: number + setMaxRetryDelaySeconds: (value: number) => void setCurrentApiConfigName: (value: string) => void setListApiConfigMeta: (value: ProviderSettingsEntry[]) => void mode: Mode @@ -173,6 +177,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode enableMcpServerCreation: false, alwaysApproveResubmit: false, requestDelaySeconds: 5, + minRetryDelaySeconds: 5, + maxRetryDelaySeconds: 100, currentApiConfigName: "default", listApiConfigMeta: [], mode: defaultModeSlug, @@ -393,6 +399,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })), setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })), setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })), + setMinRetryDelaySeconds: (value) => setState((prevState) => ({ ...prevState, minRetryDelaySeconds: value })), + setMaxRetryDelaySeconds: (value) => setState((prevState) => ({ ...prevState, maxRetryDelaySeconds: value })), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), setListApiConfigMeta, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 1e5867d3fc..6b3c5a27aa 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -191,6 +191,8 @@ describe("mergeExtensionState", () => { enableCheckpoints: true, writeDelayMs: 1000, requestDelaySeconds: 5, + minRetryDelaySeconds: 5, + maxRetryDelaySeconds: 100, mode: "default", experiments: {} as Record, customModes: [], diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 9083d4a204..db7e30b176 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -96,7 +96,11 @@ "retry": { "label": "Retry", "description": "Automatically retry failed API requests when server returns an error response", - "delayLabel": "Delay before retrying the request" + "delayLabel": "Delay before retrying the request", + "minDelayLabel": "Minimum retry delay", + "maxDelayLabel": "Maximum retry delay", + "minDelayDescription": "Minimum delay before retrying failed requests (prevents excessive rapid retries)", + "maxDelayDescription": "Maximum delay before retrying failed requests (prevents excessively long waits)" }, "mcp": { "label": "MCP",