Skip to content

Commit ce3a492

Browse files
committed
feat: implement retry delay range selection with min/max bounds
Description: - Add requestDelaySeconds and maxRequestDelaySeconds settings (defaults: 1-100) - Convert single retry delay to configurable range with exponential backoff clamping - Update UI to use dual-thumb range slider for min/max selection - Add comprehensive test coverage for new range settings - Update all locale translations to reflect range concept
1 parent 9bf31d3 commit ce3a492

30 files changed

+99
-34
lines changed

packages/types/src/global-settings.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const globalSettingsSchema = z.object({
4141
alwaysAllowBrowser: z.boolean().optional(),
4242
alwaysApproveResubmit: z.boolean().optional(),
4343
requestDelaySeconds: z.number().optional(),
44+
maxRequestDelaySeconds: z.number().optional(),
4445
alwaysAllowMcp: z.boolean().optional(),
4546
alwaysAllowModeSwitch: z.boolean().optional(),
4647
alwaysAllowSubtasks: z.boolean().optional(),
@@ -184,7 +185,8 @@ export const EVALS_SETTINGS: RooCodeSettings = {
184185
writeDelayMs: 1000,
185186
alwaysAllowBrowser: true,
186187
alwaysApproveResubmit: true,
187-
requestDelaySeconds: 10,
188+
requestDelaySeconds: 5,
189+
maxRequestDelaySeconds: 100,
188190
alwaysAllowMcp: true,
189191
alwaysAllowModeSwitch: true,
190192
alwaysAllowSubtasks: true,

src/core/task/Task.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,7 @@ export class Task extends EventEmitter<ClineEvents> {
16401640
autoApprovalEnabled,
16411641
alwaysApproveResubmit,
16421642
requestDelaySeconds,
1643+
maxRequestDelaySeconds,
16431644
mode,
16441645
autoCondenseContext = true,
16451646
autoCondenseContextPercent = 100,
@@ -1791,8 +1792,14 @@ export class Task extends EventEmitter<ClineEvents> {
17911792
errorMsg = "Unknown error"
17921793
}
17931794

1794-
const baseDelay = requestDelaySeconds || 5
1795-
let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
1795+
const minDelay = requestDelaySeconds ?? 5
1796+
const maxDelay = maxRequestDelaySeconds ?? 100
1797+
1798+
// Use the minimum delay as the base for exponential backoff
1799+
let exponentialDelay = Math.ceil(minDelay * Math.pow(2, retryAttempt))
1800+
1801+
// Clamp exponential delay to the configured range
1802+
exponentialDelay = Math.max(minDelay, Math.min(exponentialDelay, maxDelay))
17961803

17971804
// If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff
17981805
if (error.status === 429) {

src/core/webview/ClineProvider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,7 @@ export class ClineProvider
13821382
enableMcpServerCreation,
13831383
alwaysApproveResubmit,
13841384
requestDelaySeconds,
1385+
maxRequestDelaySeconds,
13851386
currentApiConfigName,
13861387
listApiConfigMeta,
13871388
pinnedApiConfigs,
@@ -1475,7 +1476,8 @@ export class ClineProvider
14751476
mcpEnabled: mcpEnabled ?? true,
14761477
enableMcpServerCreation: enableMcpServerCreation ?? true,
14771478
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
1478-
requestDelaySeconds: requestDelaySeconds ?? 10,
1479+
requestDelaySeconds: requestDelaySeconds ?? 5,
1480+
maxRequestDelaySeconds: maxRequestDelaySeconds ?? 100,
14791481
currentApiConfigName: currentApiConfigName ?? "default",
14801482
listApiConfigMeta: listApiConfigMeta ?? [],
14811483
pinnedApiConfigs: pinnedApiConfigs ?? {},
@@ -1635,7 +1637,8 @@ export class ClineProvider
16351637
mcpEnabled: stateValues.mcpEnabled ?? true,
16361638
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
16371639
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
1638-
requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
1640+
requestDelaySeconds: Math.max(1, stateValues.requestDelaySeconds ?? 5),
1641+
maxRequestDelaySeconds: Math.min(100, stateValues.maxRequestDelaySeconds ?? 100),
16391642
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
16401643
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
16411644
pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ describe("ClineProvider", () => {
521521
mcpEnabled: true,
522522
enableMcpServerCreation: false,
523523
requestDelaySeconds: 5,
524+
maxRequestDelaySeconds: 100,
524525
mode: defaultModeSlug,
525526
customModes: [],
526527
experiments: experimentDefault,
@@ -800,7 +801,7 @@ describe("ClineProvider", () => {
800801
expect(mockPostMessage).toHaveBeenCalled()
801802
})
802803

803-
test("requestDelaySeconds defaults to 10 seconds", async () => {
804+
test("requestDelaySeconds defaults to 5 seconds", async () => {
804805
// Mock globalState.get to return undefined for requestDelaySeconds
805806
;(mockContext.globalState.get as any).mockImplementation((key: string) => {
806807
if (key === "requestDelaySeconds") {
@@ -810,7 +811,20 @@ describe("ClineProvider", () => {
810811
})
811812

812813
const state = await provider.getState()
813-
expect(state.requestDelaySeconds).toBe(10)
814+
expect(state.requestDelaySeconds).toBe(5)
815+
})
816+
817+
test("maxRequestDelaySeconds defaults to 100 seconds", async () => {
818+
// Mock globalState.get to return undefined for requestDelaySeconds
819+
;(mockContext.globalState.get as any).mockImplementation((key: string) => {
820+
if (key === "maxRequestDelaySeconds") {
821+
return undefined
822+
}
823+
return null
824+
})
825+
826+
const state = await provider.getState()
827+
expect(state.maxRequestDelaySeconds).toBe(100)
814828
})
815829

816830
test("alwaysApproveResubmit defaults to false", async () => {
@@ -1002,8 +1016,13 @@ describe("ClineProvider", () => {
10021016
expect(mockPostMessage).toHaveBeenCalled()
10031017

10041018
// Test requestDelaySeconds
1005-
await messageHandler({ type: "requestDelaySeconds", value: 10 })
1006-
expect(mockContext.globalState.update).toHaveBeenCalledWith("requestDelaySeconds", 10)
1019+
await messageHandler({ type: "requestDelaySeconds", value: 5 })
1020+
expect(mockContext.globalState.update).toHaveBeenCalledWith("requestDelaySeconds", 5)
1021+
expect(mockPostMessage).toHaveBeenCalled()
1022+
1023+
// Test maxRequestDelaySeconds
1024+
await messageHandler({ type: "maxRequestDelaySeconds", value: 100 })
1025+
expect(mockContext.globalState.update).toHaveBeenCalledWith("maxRequestDelaySeconds", 100)
10071026
expect(mockPostMessage).toHaveBeenCalled()
10081027
})
10091028

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,10 @@ export const webviewMessageHandler = async (
861861
await updateGlobalState("requestDelaySeconds", message.value ?? 5)
862862
await provider.postStateToWebview()
863863
break
864+
case "maxRequestDelaySeconds":
865+
await updateGlobalState("maxRequestDelaySeconds", Math.max(1, message.value ?? 100))
866+
await provider.postStateToWebview()
867+
break
864868
case "writeDelayMs":
865869
await updateGlobalState("writeDelayMs", message.value)
866870
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export type ExtensionState = Pick<
229229

230230
writeDelayMs: number
231231
requestDelaySeconds: number
232+
maxRequestDelaySeconds: number
232233

233234
enableCheckpoints: boolean
234235
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export interface WebviewMessage {
116116
| "searchCommits"
117117
| "alwaysApproveResubmit"
118118
| "requestDelaySeconds"
119+
| "maxRequestDelaySeconds"
119120
| "setApiConfigPassword"
120121
| "mode"
121122
| "updatePrompt"

webview-ui/src/components/settings/AutoApproveSettings.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
2121
alwaysAllowBrowser?: boolean
2222
alwaysApproveResubmit?: boolean
2323
requestDelaySeconds: number
24+
maxRequestDelaySeconds: number
2425
alwaysAllowMcp?: boolean
2526
alwaysAllowModeSwitch?: boolean
2627
alwaysAllowSubtasks?: boolean
@@ -36,6 +37,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
3637
| "alwaysAllowBrowser"
3738
| "alwaysApproveResubmit"
3839
| "requestDelaySeconds"
40+
| "maxRequestDelaySeconds"
3941
| "alwaysAllowMcp"
4042
| "alwaysAllowModeSwitch"
4143
| "alwaysAllowSubtasks"
@@ -54,6 +56,7 @@ export const AutoApproveSettings = ({
5456
alwaysAllowBrowser,
5557
alwaysApproveResubmit,
5658
requestDelaySeconds,
59+
maxRequestDelaySeconds,
5760
alwaysAllowMcp,
5861
alwaysAllowModeSwitch,
5962
alwaysAllowSubtasks,
@@ -186,17 +189,25 @@ export const AutoApproveSettings = ({
186189
<div>
187190
<div className="flex items-center gap-2">
188191
<Slider
189-
min={5}
192+
min={1}
190193
max={100}
191194
step={1}
192-
value={[requestDelaySeconds]}
193-
onValueChange={([value]) => setCachedStateField("requestDelaySeconds", value)}
194-
data-testid="request-delay-slider"
195+
value={[requestDelaySeconds, maxRequestDelaySeconds]}
196+
onValueChange={([min, max]) => {
197+
// Ensure min <= max
198+
const actualMin = Math.min(min, max)
199+
const actualMax = Math.max(min, max)
200+
setCachedStateField("requestDelaySeconds", actualMin)
201+
setCachedStateField("maxRequestDelaySeconds", actualMax)
202+
}}
203+
data-testid="retry-delay-range-slider"
195204
/>
196-
<span className="w-20">{requestDelaySeconds}s</span>
205+
<span className="w-20">
206+
{requestDelaySeconds}s - {maxRequestDelaySeconds}s
207+
</span>
197208
</div>
198209
<div className="text-vscode-descriptionForeground text-sm mt-1">
199-
{t("settings:autoApprove.retry.delayLabel")}
210+
{t("settings:autoApprove.retry.rangeLabel")}
200211
</div>
201212
</div>
202213
</div>

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
146146
maxWorkspaceFiles,
147147
mcpEnabled,
148148
requestDelaySeconds,
149+
maxRequestDelaySeconds,
149150
remoteBrowserHost,
150151
screenshotQuality,
151152
soundEnabled,
@@ -302,6 +303,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
302303
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
303304
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
304305
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
306+
vscode.postMessage({ type: "maxRequestDelaySeconds", value: maxRequestDelaySeconds })
305307
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
306308
vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
307309
vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
@@ -595,6 +597,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
595597
alwaysAllowBrowser={alwaysAllowBrowser}
596598
alwaysApproveResubmit={alwaysApproveResubmit}
597599
requestDelaySeconds={requestDelaySeconds}
600+
maxRequestDelaySeconds={maxRequestDelaySeconds}
598601
alwaysAllowMcp={alwaysAllowMcp}
599602
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
600603
alwaysAllowSubtasks={alwaysAllowSubtasks}

webview-ui/src/components/ui/slider.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ const Slider = React.forwardRef<
1414
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-accent rounded-sm border">
1515
<SliderPrimitive.Range className="absolute h-full bg-vscode-button-background" />
1616
</SliderPrimitive.Track>
17-
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-vscode-button-background transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
17+
{Array.isArray(props.value) ? (
18+
props.value.map((_, i) => (
19+
<SliderPrimitive.Thumb
20+
key={i}
21+
className="block h-3 w-3 rounded-full border border-primary/50 bg-vscode-button-background transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
22+
/>
23+
))
24+
) : (
25+
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-vscode-button-background transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
26+
)}
1827
</SliderPrimitive.Root>
1928
))
2029
Slider.displayName = SliderPrimitive.Root.displayName

0 commit comments

Comments
 (0)