Skip to content

Commit fe22d1f

Browse files
RaySinnermrubens
authored andcommitted
feat: add retry request control with delay settings
- Add requestDelaySeconds setting for configuring delay between retry attempts - Add alwaysApproveResubmit option for automatic retry approval - Add api_req_retry_delayed message type for delayed retries - Update UI components to support new retry control settings
1 parent 631d9b9 commit fe22d1f

File tree

7 files changed

+337
-208
lines changed

7 files changed

+337
-208
lines changed

src/core/Cline.ts

Lines changed: 103 additions & 87 deletions
Large diffs are not rendered by default.

src/core/webview/ClineProvider.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ type GlobalStateKey =
8383
| "writeDelayMs"
8484
| "terminalOutputLineLimit"
8585
| "mcpEnabled"
86+
| "alwaysApproveResubmit"
87+
| "requestDelaySeconds"
8688
export const GlobalFileNames = {
8789
apiConversationHistory: "api_conversation_history.json",
8890
uiMessages: "ui_messages.json",
@@ -233,7 +235,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
233235
diffEnabled,
234236
fuzzyMatchThreshold
235237
} = await this.getState()
236-
238+
237239
this.cline = new Cline(
238240
this,
239241
apiConfiguration,
@@ -253,7 +255,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
253255
diffEnabled,
254256
fuzzyMatchThreshold
255257
} = await this.getState()
256-
258+
257259
this.cline = new Cline(
258260
this,
259261
apiConfiguration,
@@ -319,15 +321,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
319321

320322
// Use a nonce to only allow a specific script to be run.
321323
/*
322-
content security policy of your webview to only allow scripts that have a specific nonce
323-
create a content security policy meta tag so that only loading scripts with a nonce is allowed
324-
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
325-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
324+
content security policy of your webview to only allow scripts that have a specific nonce
325+
create a content security policy meta tag so that only loading scripts with a nonce is allowed
326+
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
327+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
326328
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
327329
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
328330
329-
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
330-
*/
331+
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
332+
*/
331333
const nonce = getNonce()
332334

333335
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -555,7 +557,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
555557
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
556558
break
557559
case "refreshGlamaModels":
558-
await this.refreshGlamaModels()
560+
await this.refreshGlamaModels()
559561
break
560562
case "refreshOpenRouterModels":
561563
await this.refreshOpenRouterModels()
@@ -564,7 +566,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
564566
if (message?.values?.baseUrl && message?.values?.apiKey) {
565567
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
566568
this.postMessageToWebview({ type: "openAiModels", openAiModels })
567-
}
569+
}
568570
break
569571
case "openImage":
570572
openImage(message.text!)
@@ -675,6 +677,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
675677
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
676678
await this.postStateToWebview()
677679
break
680+
case "alwaysApproveResubmit":
681+
await this.updateGlobalState("alwaysApproveResubmit", message.bool ?? false)
682+
await this.postStateToWebview()
683+
break
684+
case "requestDelaySeconds":
685+
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
686+
await this.postStateToWebview()
687+
break
678688
case "preferredLanguage":
679689
await this.updateGlobalState("preferredLanguage", message.text)
680690
await this.postStateToWebview()
@@ -1224,9 +1234,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12241234
}
12251235

12261236
async getStateToPostToWebview() {
1227-
const {
1228-
apiConfiguration,
1229-
lastShownAnnouncementId,
1237+
const {
1238+
apiConfiguration,
1239+
lastShownAnnouncementId,
12301240
customInstructions,
12311241
alwaysAllowReadOnly,
12321242
alwaysAllowWrite,
@@ -1244,8 +1254,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12441254
terminalOutputLineLimit,
12451255
fuzzyMatchThreshold,
12461256
mcpEnabled,
1257+
alwaysApproveResubmit,
1258+
requestDelaySeconds,
12471259
} = await this.getState()
1248-
1260+
12491261
const allowedCommands = vscode.workspace
12501262
.getConfiguration('roo-cline')
12511263
.get<string[]>('allowedCommands') || []
@@ -1276,6 +1288,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12761288
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
12771289
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
12781290
mcpEnabled: mcpEnabled ?? true,
1291+
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
1292+
requestDelaySeconds: requestDelaySeconds ?? 5,
12791293
}
12801294
}
12811295

@@ -1381,6 +1395,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13811395
screenshotQuality,
13821396
terminalOutputLineLimit,
13831397
mcpEnabled,
1398+
alwaysApproveResubmit,
1399+
requestDelaySeconds,
13841400
] = await Promise.all([
13851401
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
13861402
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1431,6 +1447,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14311447
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
14321448
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
14331449
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
1450+
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
1451+
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
14341452
])
14351453

14361454
let apiProvider: ApiProvider
@@ -1525,6 +1543,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15251543
return langMap[vscodeLang.split('-')[0]] ?? 'English';
15261544
})(),
15271545
mcpEnabled: mcpEnabled ?? true,
1546+
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
1547+
requestDelaySeconds: requestDelaySeconds ?? 5,
15281548
}
15291549
}
15301550

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

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ jest.mock('../../../integrations/misc/extract-text', () => ({
146146

147147
// Spy on console.error and console.log to suppress expected messages
148148
beforeAll(() => {
149-
jest.spyOn(console, 'error').mockImplementation(() => {})
150-
jest.spyOn(console, 'log').mockImplementation(() => {})
149+
jest.spyOn(console, 'error').mockImplementation(() => { })
150+
jest.spyOn(console, 'log').mockImplementation(() => { })
151151
})
152152

153153
afterAll(() => {
@@ -230,7 +230,7 @@ describe('ClineProvider', () => {
230230

231231
test('resolveWebviewView sets up webview correctly', () => {
232232
provider.resolveWebviewView(mockWebviewView)
233-
233+
234234
expect(mockWebviewView.webview.options).toEqual({
235235
enableScripts: true,
236236
localResourceRoots: [mockContext.extensionUri]
@@ -240,7 +240,7 @@ describe('ClineProvider', () => {
240240

241241
test('postMessageToWebview sends message to webview', async () => {
242242
provider.resolveWebviewView(mockWebviewView)
243-
243+
244244
const mockState: ExtensionState = {
245245
version: '1.0.0',
246246
preferredLanguage: 'English',
@@ -263,14 +263,16 @@ describe('ClineProvider', () => {
263263
browserViewportSize: "900x600",
264264
fuzzyMatchThreshold: 1.0,
265265
mcpEnabled: true,
266+
alwaysApproveResubmit: false,
267+
requestDelaySeconds: 5,
266268
}
267-
268-
const message: ExtensionMessage = {
269-
type: 'state',
269+
270+
const message: ExtensionMessage = {
271+
type: 'state',
270272
state: mockState
271273
}
272274
await provider.postMessageToWebview(message)
273-
275+
274276
expect(mockPostMessage).toHaveBeenCalledWith(message)
275277
})
276278

@@ -301,7 +303,7 @@ describe('ClineProvider', () => {
301303

302304
test('getState returns correct initial state', async () => {
303305
const state = await provider.getState()
304-
306+
305307
expect(state).toHaveProperty('apiConfiguration')
306308
expect(state.apiConfiguration).toHaveProperty('apiProvider')
307309
expect(state).toHaveProperty('customInstructions')
@@ -318,25 +320,25 @@ describe('ClineProvider', () => {
318320
test('preferredLanguage defaults to VSCode language when not set', async () => {
319321
// Mock VSCode language as Spanish
320322
(vscode.env as any).language = 'es-ES';
321-
323+
322324
const state = await provider.getState();
323325
expect(state.preferredLanguage).toBe('Spanish');
324326
})
325327

326328
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
327329
// Mock VSCode language as an unsupported language
328330
(vscode.env as any).language = 'unsupported-LANG';
329-
331+
330332
const state = await provider.getState();
331333
expect(state.preferredLanguage).toBe('English');
332334
})
333335

334336
test('diffEnabled defaults to true when not set', async () => {
335337
// Mock globalState.get to return undefined for diffEnabled
336338
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
337-
339+
338340
const state = await provider.getState()
339-
341+
340342
expect(state.diffEnabled).toBe(true)
341343
})
342344

@@ -348,17 +350,17 @@ describe('ClineProvider', () => {
348350
}
349351
return null
350352
})
351-
353+
352354
const state = await provider.getState()
353355
expect(state.writeDelayMs).toBe(1000)
354356
})
355357

356358
test('handles writeDelayMs message', async () => {
357359
provider.resolveWebviewView(mockWebviewView)
358360
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
359-
361+
360362
await messageHandler({ type: 'writeDelayMs', value: 2000 })
361-
363+
362364
expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000)
363365
expect(mockPostMessage).toHaveBeenCalled()
364366
})
@@ -382,6 +384,42 @@ describe('ClineProvider', () => {
382384
expect(mockPostMessage).toHaveBeenCalled()
383385
})
384386

387+
test('requestDelaySeconds defaults to 5 seconds', async () => {
388+
// Mock globalState.get to return undefined for requestDelaySeconds
389+
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
390+
if (key === 'requestDelaySeconds') {
391+
return undefined
392+
}
393+
return null
394+
})
395+
396+
const state = await provider.getState()
397+
expect(state.requestDelaySeconds).toBe(5)
398+
})
399+
400+
test('alwaysApproveResubmit defaults to false', async () => {
401+
// Mock globalState.get to return undefined for alwaysApproveResubmit
402+
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
403+
404+
const state = await provider.getState()
405+
expect(state.alwaysApproveResubmit).toBe(false)
406+
})
407+
408+
test('handles request delay settings messages', async () => {
409+
provider.resolveWebviewView(mockWebviewView)
410+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
411+
412+
// Test alwaysApproveResubmit
413+
await messageHandler({ type: 'alwaysApproveResubmit', bool: true })
414+
expect(mockContext.globalState.update).toHaveBeenCalledWith('alwaysApproveResubmit', true)
415+
expect(mockPostMessage).toHaveBeenCalled()
416+
417+
// Test requestDelaySeconds
418+
await messageHandler({ type: 'requestDelaySeconds', value: 10 })
419+
expect(mockContext.globalState.update).toHaveBeenCalledWith('requestDelaySeconds', 10)
420+
expect(mockPostMessage).toHaveBeenCalled()
421+
})
422+
385423
test('file content includes line numbers', async () => {
386424
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
387425
const result = await extractTextFromFile('test.js')

src/shared/ExtensionMessage.ts

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,28 @@ import { GitCommit } from "../utils/git"
88
// webview will hold state
99
export interface ExtensionMessage {
1010
type:
11-
| "action"
12-
| "state"
13-
| "selectedImages"
14-
| "ollamaModels"
15-
| "lmStudioModels"
16-
| "theme"
17-
| "workspaceUpdated"
18-
| "invoke"
19-
| "partialMessage"
20-
| "glamaModels"
21-
| "openRouterModels"
22-
| "openAiModels"
23-
| "mcpServers"
24-
| "enhancedPrompt"
25-
| "commitSearchResults"
11+
| "action"
12+
| "state"
13+
| "selectedImages"
14+
| "ollamaModels"
15+
| "lmStudioModels"
16+
| "theme"
17+
| "workspaceUpdated"
18+
| "invoke"
19+
| "partialMessage"
20+
| "glamaModels"
21+
| "openRouterModels"
22+
| "openAiModels"
23+
| "mcpServers"
24+
| "enhancedPrompt"
25+
| "commitSearchResults"
2626
text?: string
2727
action?:
28-
| "chatButtonClicked"
29-
| "mcpButtonClicked"
30-
| "settingsButtonClicked"
31-
| "historyButtonClicked"
32-
| "didBecomeVisible"
28+
| "chatButtonClicked"
29+
| "mcpButtonClicked"
30+
| "settingsButtonClicked"
31+
| "historyButtonClicked"
32+
| "didBecomeVisible"
3333
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
3434
state?: ExtensionState
3535
images?: string[]
@@ -56,6 +56,8 @@ export interface ExtensionState {
5656
alwaysAllowExecute?: boolean
5757
alwaysAllowBrowser?: boolean
5858
alwaysAllowMcp?: boolean
59+
alwaysApproveResubmit?: boolean
60+
requestDelaySeconds: number
5961
uriScheme?: string
6062
allowedCommands?: string[]
6163
soundEnabled?: boolean
@@ -103,6 +105,7 @@ export type ClineSay =
103105
| "user_feedback"
104106
| "user_feedback_diff"
105107
| "api_req_retried"
108+
| "api_req_retry_delayed"
106109
| "command_output"
107110
| "tool"
108111
| "shell_integration_warning"
@@ -114,14 +117,14 @@ export type ClineSay =
114117

115118
export interface ClineSayTool {
116119
tool:
117-
| "editedExistingFile"
118-
| "appliedDiff"
119-
| "newFileCreated"
120-
| "readFile"
121-
| "listFilesTopLevel"
122-
| "listFilesRecursive"
123-
| "listCodeDefinitionNames"
124-
| "searchFiles"
120+
| "editedExistingFile"
121+
| "appliedDiff"
122+
| "newFileCreated"
123+
| "readFile"
124+
| "listFilesTopLevel"
125+
| "listFilesRecursive"
126+
| "listCodeDefinitionNames"
127+
| "searchFiles"
125128
path?: string
126129
diff?: string
127130
content?: string

0 commit comments

Comments
 (0)