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

@@ -390,8 +390,7 @@ export class Cline {
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) { async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
await this.say( await this.say(
"error", "error",
`Cline tried to use ${toolName}${ `Cline tried to use ${toolName}${relPath ? ` for '${relPath.toPosix()}'` : ""
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`, } without value for required parameter '${paramName}'. Retrying...`,
) )
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName)) return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
@@ -626,8 +625,7 @@ export class Cline {
newUserContent.push({ newUserContent.push({
type: "text", type: "text",
text: text:
`[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.${ `[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.${wasRecent
wasRecent
? "\n\nIMPORTANT: If the last tool use was a write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents." ? "\n\nIMPORTANT: If the last tool use was a write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
: "" : ""
}` + }` +
@@ -743,8 +741,7 @@ export class Cline {
return [ return [
true, true,
formatResponse.toolResult( formatResponse.toolResult(
`Command is still running in the user's terminal.${ `Command is still running in the user's terminal.${result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
}\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`, }\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
userFeedback.images, userFeedback.images,
), ),
@@ -756,8 +753,7 @@ export class Cline {
} else { } else {
return [ return [
false, false,
`Command is still running in the user's terminal.${ `Command is still running in the user's terminal.${result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
}\n\nYou will be updated on the terminal status and new output in the future.`, }\n\nYou will be updated on the terminal status and new output in the future.`,
] ]
} }
@@ -766,7 +762,7 @@ export class Cline {
async *attemptApiRequest(previousApiReqIndex: number): ApiStream { async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
let mcpHub: McpHub | undefined let mcpHub: McpHub | undefined
const { mcpEnabled } = await this.providerRef.deref()?.getState() ?? {} const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } = await this.providerRef.deref()?.getState() ?? {}
if (mcpEnabled ?? true) { if (mcpEnabled ?? true) {
mcpHub = this.providerRef.deref()?.mcpHub mcpHub = this.providerRef.deref()?.mcpHub
@@ -810,6 +806,28 @@ export class Cline {
yield firstChunk.value yield firstChunk.value
} catch (error) { } catch (error) {
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (alwaysApproveResubmit) {
// Automatically retry with delay
await this.say(
"error",
`Error (${
error.message?.toLowerCase().includes("429") ||
error.message?.toLowerCase().includes("rate limit") ||
error.message?.toLowerCase().includes("too many requests") ||
error.message?.toLowerCase().includes("throttled")
? "rate limit"
: error.message?.includes("500") || error.message?.includes("503")
? "internal server error"
: "unknown"
}). ↺ Retrying...`,
)
await this.say("api_req_retry_delayed")
await delay((requestDelaySeconds || 5) * 1000)
await this.say("api_req_retried")
// delegate generator output from the recursive call
yield* this.attemptApiRequest(previousApiReqIndex)
return
} else {
const { response } = await this.ask( const { response } = await this.ask(
"api_req_failed", "api_req_failed",
error.message ?? JSON.stringify(serializeError(error), null, 2), error.message ?? JSON.stringify(serializeError(error), null, 2),
@@ -823,6 +841,7 @@ export class Cline {
yield* this.attemptApiRequest(previousApiReqIndex) yield* this.attemptApiRequest(previousApiReqIndex)
return return
} }
}
// no error, so we can continue to yield all remaining chunks // no error, so we can continue to yield all remaining chunks
// (needs to be placed outside of try/catch since it we want caller to handle errors not with api_req_failed as that is reserved for first chunk failures only) // (needs to be placed outside of try/catch since it we want caller to handle errors not with api_req_failed as that is reserved for first chunk failures only)
@@ -912,8 +931,7 @@ export class Cline {
case "apply_diff": case "apply_diff":
return `[${block.name} for '${block.params.path}']` return `[${block.name} for '${block.params.path}']`
case "search_files": case "search_files":
return `[${block.name} for '${block.params.regex}'${ return `[${block.name} for '${block.params.regex}'${block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
}]` }]`
case "list_files": case "list_files":
return `[${block.name} for '${block.params.path}']` return `[${block.name} for '${block.params.path}']`
@@ -1100,7 +1118,7 @@ export class Cline {
if (block.partial) { if (block.partial) {
// update gui message // update gui message
const partialMessage = JSON.stringify(sharedMessageProps) const partialMessage = JSON.stringify(sharedMessageProps)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
// update editor // update editor
if (!this.diffViewProvider.isEditing) { if (!this.diffViewProvider.isEditing) {
// open the editor and prepare to stream content in // open the editor and prepare to stream content in
@@ -1136,7 +1154,7 @@ export class Cline {
if (!this.diffViewProvider.isEditing) { if (!this.diffViewProvider.isEditing) {
// show gui message before showing edit animation // show gui message before showing edit animation
const partialMessage = JSON.stringify(sharedMessageProps) const partialMessage = JSON.stringify(sharedMessageProps)
await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor await this.ask("tool", partialMessage, true).catch(() => { }) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
await this.diffViewProvider.open(relPath) await this.diffViewProvider.open(relPath)
} }
await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, true) await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, true)
@@ -1234,7 +1252,7 @@ export class Cline {
if (block.partial) { if (block.partial) {
// update gui message // update gui message
const partialMessage = JSON.stringify(sharedMessageProps) const partialMessage = JSON.stringify(sharedMessageProps)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!relPath) { if (!relPath) {
@@ -1349,7 +1367,7 @@ export class Cline {
...sharedMessageProps, ...sharedMessageProps,
content: undefined, content: undefined,
} satisfies ClineSayTool) } satisfies ClineSayTool)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!relPath) { if (!relPath) {
@@ -1391,7 +1409,7 @@ export class Cline {
...sharedMessageProps, ...sharedMessageProps,
content: "", content: "",
} satisfies ClineSayTool) } satisfies ClineSayTool)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!relDirPath) { if (!relDirPath) {
@@ -1431,7 +1449,7 @@ export class Cline {
...sharedMessageProps, ...sharedMessageProps,
content: "", content: "",
} satisfies ClineSayTool) } satisfies ClineSayTool)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!relDirPath) { if (!relDirPath) {
@@ -1476,7 +1494,7 @@ export class Cline {
...sharedMessageProps, ...sharedMessageProps,
content: "", content: "",
} satisfies ClineSayTool) } satisfies ClineSayTool)
await this.ask("tool", partialMessage, block.partial).catch(() => {}) await this.ask("tool", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!relDirPath) { if (!relDirPath) {
@@ -1531,7 +1549,7 @@ export class Cline {
"browser_action_launch", "browser_action_launch",
removeClosingTag("url", url), removeClosingTag("url", url),
block.partial block.partial
).catch(() => {}) ).catch(() => { })
} else { } else {
await this.say( await this.say(
"browser_action", "browser_action",
@@ -1631,8 +1649,7 @@ export class Cline {
await this.say("browser_action_result", JSON.stringify(browserActionResult)) await this.say("browser_action_result", JSON.stringify(browserActionResult))
pushToolResult( pushToolResult(
formatResponse.toolResult( formatResponse.toolResult(
`The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${ `The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${browserActionResult.logs || "(No new logs)"
browserActionResult.logs || "(No new logs)"
}\n\n(REMEMBER: if you need to proceed to using non-\`browser_action\` tools or launch a new browser, you MUST first close this browser. For example, if after analyzing the logs and screenshot you need to edit a file, you must first close the browser before you can use the write_to_file tool.)`, }\n\n(REMEMBER: if you need to proceed to using non-\`browser_action\` tools or launch a new browser, you MUST first close this browser. For example, if after analyzing the logs and screenshot you need to edit a file, you must first close the browser before you can use the write_to_file tool.)`,
browserActionResult.screenshot ? [browserActionResult.screenshot] : [], browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
), ),
@@ -1659,7 +1676,7 @@ export class Cline {
try { try {
if (block.partial) { if (block.partial) {
await this.ask("command", removeClosingTag("command", command), block.partial).catch( await this.ask("command", removeClosingTag("command", command), block.partial).catch(
() => {} () => { }
) )
break break
} else { } else {
@@ -1700,7 +1717,7 @@ export class Cline {
toolName: removeClosingTag("tool_name", tool_name), toolName: removeClosingTag("tool_name", tool_name),
arguments: removeClosingTag("arguments", mcp_arguments), arguments: removeClosingTag("arguments", mcp_arguments),
} satisfies ClineAskUseMcpServer) } satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!server_name) { if (!server_name) {
@@ -1793,7 +1810,7 @@ export class Cline {
serverName: removeClosingTag("server_name", server_name), serverName: removeClosingTag("server_name", server_name),
uri: removeClosingTag("uri", uri), uri: removeClosingTag("uri", uri),
} satisfies ClineAskUseMcpServer) } satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => { })
break break
} else { } else {
if (!server_name) { if (!server_name) {
@@ -1849,7 +1866,7 @@ export class Cline {
try { try {
if (block.partial) { if (block.partial) {
await this.ask("followup", removeClosingTag("question", question), block.partial).catch( await this.ask("followup", removeClosingTag("question", question), block.partial).catch(
() => {}, () => { },
) )
break break
} else { } else {
@@ -1908,7 +1925,7 @@ export class Cline {
"command", "command",
removeClosingTag("command", command), removeClosingTag("command", command),
block.partial, block.partial,
).catch(() => {}) ).catch(() => { })
} else { } else {
// last message is completion_result // last message is completion_result
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet) // we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
@@ -1922,7 +1939,7 @@ export class Cline {
"command", "command",
removeClosingTag("command", command), removeClosingTag("command", command),
block.partial, block.partial,
).catch(() => {}) ).catch(() => { })
} }
} else { } else {
// no command, still outputting partial result // no command, still outputting partial result
@@ -2148,8 +2165,7 @@ export class Cline {
type: "text", type: "text",
text: text:
assistantMessage + assistantMessage +
`\n\n[${ `\n\n[${cancelReason === "streaming_failed"
cancelReason === "streaming_failed"
? "Response interrupted by API Error" ? "Response interrupted by API Error"
: "Response interrupted by user" : "Response interrupted by user"
}]`, }]`,
@@ -2405,7 +2421,7 @@ export class Cline {
await pWaitFor(() => busyTerminals.every((t) => !this.terminalManager.isProcessHot(t.id)), { await pWaitFor(() => busyTerminals.every((t) => !this.terminalManager.isProcessHot(t.id)), {
interval: 100, interval: 100,
timeout: 15_000, timeout: 15_000,
}).catch(() => {}) }).catch(() => { })
} }
// we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc // we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc

View File

@@ -83,6 +83,8 @@ type GlobalStateKey =
| "writeDelayMs" | "writeDelayMs"
| "terminalOutputLineLimit" | "terminalOutputLineLimit"
| "mcpEnabled" | "mcpEnabled"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
export const GlobalFileNames = { export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json", apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json", uiMessages: "ui_messages.json",
@@ -675,6 +677,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("fuzzyMatchThreshold", message.value) await this.updateGlobalState("fuzzyMatchThreshold", message.value)
await this.postStateToWebview() await this.postStateToWebview()
break 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": case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text) await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview() await this.postStateToWebview()
@@ -1244,6 +1254,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit, terminalOutputLineLimit,
fuzzyMatchThreshold, fuzzyMatchThreshold,
mcpEnabled, mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
} = await this.getState() } = await this.getState()
const allowedCommands = vscode.workspace const allowedCommands = vscode.workspace
@@ -1276,6 +1288,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit: terminalOutputLineLimit ?? 500, terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
mcpEnabled: mcpEnabled ?? true, mcpEnabled: mcpEnabled ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 5,
} }
} }
@@ -1381,6 +1395,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality, screenshotQuality,
terminalOutputLineLimit, terminalOutputLineLimit,
mcpEnabled, mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
] = await Promise.all([ ] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>, this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | 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("screenshotQuality") as Promise<number | undefined>,
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>, this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
this.getGlobalState("mcpEnabled") as Promise<boolean | 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 let apiProvider: ApiProvider
@@ -1525,6 +1543,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return langMap[vscodeLang.split('-')[0]] ?? 'English'; return langMap[vscodeLang.split('-')[0]] ?? 'English';
})(), })(),
mcpEnabled: mcpEnabled ?? true, 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 // Spy on console.error and console.log to suppress expected messages
beforeAll(() => { beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {}) jest.spyOn(console, 'error').mockImplementation(() => { })
jest.spyOn(console, 'log').mockImplementation(() => {}) jest.spyOn(console, 'log').mockImplementation(() => { })
}) })
afterAll(() => { afterAll(() => {
@@ -263,6 +263,8 @@ describe('ClineProvider', () => {
browserViewportSize: "900x600", browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0, fuzzyMatchThreshold: 1.0,
mcpEnabled: true, mcpEnabled: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
} }
const message: ExtensionMessage = { const message: ExtensionMessage = {
@@ -382,6 +384,42 @@ describe('ClineProvider', () => {
expect(mockPostMessage).toHaveBeenCalled() 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 () => { test('file content includes line numbers', async () => {
const { extractTextFromFile } = require('../../../integrations/misc/extract-text') const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
const result = await extractTextFromFile('test.js') const result = await extractTextFromFile('test.js')

View File

@@ -56,6 +56,8 @@ export interface ExtensionState {
alwaysAllowExecute?: boolean alwaysAllowExecute?: boolean
alwaysAllowBrowser?: boolean alwaysAllowBrowser?: boolean
alwaysAllowMcp?: boolean alwaysAllowMcp?: boolean
alwaysApproveResubmit?: boolean
requestDelaySeconds: number
uriScheme?: string uriScheme?: string
allowedCommands?: string[] allowedCommands?: string[]
soundEnabled?: boolean soundEnabled?: boolean
@@ -103,6 +105,7 @@ export type ClineSay =
| "user_feedback" | "user_feedback"
| "user_feedback_diff" | "user_feedback_diff"
| "api_req_retried" | "api_req_retried"
| "api_req_retry_delayed"
| "command_output" | "command_output"
| "tool" | "tool"
| "shell_integration_warning" | "shell_integration_warning"

View File

@@ -1,4 +1,4 @@
import { ApiConfiguration, ApiProvider } from "./api" import { ApiConfiguration } from "./api"
export type AudioType = "notification" | "celebration" | "progress_loop" export type AudioType = "notification" | "celebration" | "progress_loop"
@@ -52,6 +52,8 @@ export interface WebviewMessage {
| "terminalOutputLineLimit" | "terminalOutputLineLimit"
| "mcpEnabled" | "mcpEnabled"
| "searchCommits" | "searchCommits"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
text?: string text?: string
disabled?: boolean disabled?: boolean
askResponse?: ClineAskResponse askResponse?: ClineAskResponse

View File

@@ -51,6 +51,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
terminalOutputLineLimit, terminalOutputLineLimit,
setTerminalOutputLineLimit, setTerminalOutputLineLimit,
mcpEnabled, mcpEnabled,
alwaysApproveResubmit,
setAlwaysApproveResubmit,
requestDelaySeconds,
setRequestDelaySeconds,
} = useExtensionState() } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined) const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -83,6 +87,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 }) vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 }) vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 })
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled }) vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
onDone() onDone()
} }
} }
@@ -355,11 +361,47 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<span style={{ fontWeight: "500" }}>Always approve browser actions</span> <span style={{ fontWeight: "500" }}>Always approve browser actions</span>
</VSCodeCheckbox> </VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}> <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically perform browser actions without requiring approval<br/> Automatically perform browser actions without requiring approval<br />
Note: Only applies when the model supports computer use Note: Only applies when the model supports computer use
</p> </p>
</div> </div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysApproveResubmit}
onChange={(e: any) => setAlwaysApproveResubmit(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve resubmit request</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically retry request when server returns an error response, with a configurable delay
</p>
{alwaysApproveResubmit && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="range"
min="0"
max="100"
step="1"
value={requestDelaySeconds}
onChange={(e) => setRequestDelaySeconds(parseInt(e.target.value))}
style={{
flex: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
}}
/>
<span style={{ minWidth: '45px', textAlign: 'left' }}>
{requestDelaySeconds}s
</span>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Delay before retrying the request
</p>
</div>
)}
</div>
<div style={{ marginBottom: 5 }}> <div style={{ marginBottom: 5 }}>
<VSCodeCheckbox <VSCodeCheckbox
checked={alwaysAllowMcp} checked={alwaysAllowMcp}

View File

@@ -46,6 +46,10 @@ export interface ExtensionStateContextType extends ExtensionState {
setTerminalOutputLineLimit: (value: number) => void setTerminalOutputLineLimit: (value: number) => void
mcpEnabled: boolean mcpEnabled: boolean
setMcpEnabled: (value: boolean) => void setMcpEnabled: (value: boolean) => void
alwaysApproveResubmit?: boolean
setAlwaysApproveResubmit: (value: boolean) => void
requestDelaySeconds: number
setRequestDelaySeconds: (value: number) => void
} }
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined) export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -67,6 +71,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: 75, screenshotQuality: 75,
terminalOutputLineLimit: 500, terminalOutputLineLimit: 500,
mcpEnabled: true, mcpEnabled: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5
}) })
const [didHydrateState, setDidHydrateState] = useState(false) const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false) const [showWelcome, setShowWelcome] = useState(false)
@@ -201,6 +207,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })), setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })), setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })), setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value }))
} }
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider> return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>