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
This commit is contained in:
RaySinner
2025-01-07 16:26:34 +03:00
committed by Matt Rubens
parent 631d9b9e87
commit fe22d1ff2d
7 changed files with 337 additions and 208 deletions

View File

@@ -83,6 +83,8 @@ type GlobalStateKey =
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
@@ -233,7 +235,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@@ -253,7 +255,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@@ -319,15 +321,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
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.
<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}';">
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
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.
<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}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
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.
*/
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.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -555,7 +557,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
break
case "refreshGlamaModels":
await this.refreshGlamaModels()
await this.refreshGlamaModels()
break
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
@@ -564,7 +566,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (message?.values?.baseUrl && message?.values?.apiKey) {
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
}
}
break
case "openImage":
openImage(message.text!)
@@ -675,6 +677,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
await this.postStateToWebview()
break
case "alwaysApproveResubmit":
await this.updateGlobalState("alwaysApproveResubmit", message.bool ?? false)
await this.postStateToWebview()
break
case "requestDelaySeconds":
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
await this.postStateToWebview()
break
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
@@ -1224,9 +1234,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async getStateToPostToWebview() {
const {
apiConfiguration,
lastShownAnnouncementId,
const {
apiConfiguration,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowWrite,
@@ -1244,8 +1254,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit,
fuzzyMatchThreshold,
mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
} = await this.getState()
const allowedCommands = vscode.workspace
.getConfiguration('roo-cline')
.get<string[]>('allowedCommands') || []
@@ -1276,6 +1288,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
mcpEnabled: mcpEnabled ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 5,
}
}
@@ -1381,6 +1395,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
terminalOutputLineLimit,
mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1431,6 +1447,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
])
let apiProvider: ApiProvider
@@ -1525,6 +1543,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return langMap[vscodeLang.split('-')[0]] ?? 'English';
})(),
mcpEnabled: mcpEnabled ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 5,
}
}

View File

@@ -146,8 +146,8 @@ jest.mock('../../../integrations/misc/extract-text', () => ({
// Spy on console.error and console.log to suppress expected messages
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(console, 'error').mockImplementation(() => { })
jest.spyOn(console, 'log').mockImplementation(() => { })
})
afterAll(() => {
@@ -230,7 +230,7 @@ describe('ClineProvider', () => {
test('resolveWebviewView sets up webview correctly', () => {
provider.resolveWebviewView(mockWebviewView)
expect(mockWebviewView.webview.options).toEqual({
enableScripts: true,
localResourceRoots: [mockContext.extensionUri]
@@ -240,7 +240,7 @@ describe('ClineProvider', () => {
test('postMessageToWebview sends message to webview', async () => {
provider.resolveWebviewView(mockWebviewView)
const mockState: ExtensionState = {
version: '1.0.0',
preferredLanguage: 'English',
@@ -263,14 +263,16 @@ describe('ClineProvider', () => {
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
}
const message: ExtensionMessage = {
type: 'state',
const message: ExtensionMessage = {
type: 'state',
state: mockState
}
await provider.postMessageToWebview(message)
expect(mockPostMessage).toHaveBeenCalledWith(message)
})
@@ -301,7 +303,7 @@ describe('ClineProvider', () => {
test('getState returns correct initial state', async () => {
const state = await provider.getState()
expect(state).toHaveProperty('apiConfiguration')
expect(state.apiConfiguration).toHaveProperty('apiProvider')
expect(state).toHaveProperty('customInstructions')
@@ -318,7 +320,7 @@ describe('ClineProvider', () => {
test('preferredLanguage defaults to VSCode language when not set', async () => {
// Mock VSCode language as Spanish
(vscode.env as any).language = 'es-ES';
const state = await provider.getState();
expect(state.preferredLanguage).toBe('Spanish');
})
@@ -326,7 +328,7 @@ describe('ClineProvider', () => {
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
// Mock VSCode language as an unsupported language
(vscode.env as any).language = 'unsupported-LANG';
const state = await provider.getState();
expect(state.preferredLanguage).toBe('English');
})
@@ -334,9 +336,9 @@ describe('ClineProvider', () => {
test('diffEnabled defaults to true when not set', async () => {
// Mock globalState.get to return undefined for diffEnabled
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
const state = await provider.getState()
expect(state.diffEnabled).toBe(true)
})
@@ -348,7 +350,7 @@ describe('ClineProvider', () => {
}
return null
})
const state = await provider.getState()
expect(state.writeDelayMs).toBe(1000)
})
@@ -356,9 +358,9 @@ describe('ClineProvider', () => {
test('handles writeDelayMs message', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
await messageHandler({ type: 'writeDelayMs', value: 2000 })
expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000)
expect(mockPostMessage).toHaveBeenCalled()
})
@@ -382,6 +384,42 @@ describe('ClineProvider', () => {
expect(mockPostMessage).toHaveBeenCalled()
})
test('requestDelaySeconds defaults to 5 seconds', async () => {
// Mock globalState.get to return undefined for requestDelaySeconds
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
if (key === 'requestDelaySeconds') {
return undefined
}
return null
})
const state = await provider.getState()
expect(state.requestDelaySeconds).toBe(5)
})
test('alwaysApproveResubmit defaults to false', async () => {
// Mock globalState.get to return undefined for alwaysApproveResubmit
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
const state = await provider.getState()
expect(state.alwaysApproveResubmit).toBe(false)
})
test('handles request delay settings messages', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Test alwaysApproveResubmit
await messageHandler({ type: 'alwaysApproveResubmit', bool: true })
expect(mockContext.globalState.update).toHaveBeenCalledWith('alwaysApproveResubmit', true)
expect(mockPostMessage).toHaveBeenCalled()
// Test requestDelaySeconds
await messageHandler({ type: 'requestDelaySeconds', value: 10 })
expect(mockContext.globalState.update).toHaveBeenCalledWith('requestDelaySeconds', 10)
expect(mockPostMessage).toHaveBeenCalled()
})
test('file content includes line numbers', async () => {
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
const result = await extractTextFromFile('test.js')