mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
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:
@@ -205,10 +205,10 @@ export class Cline {
|
|||||||
const taskMessage = this.clineMessages[0] // first message is always the task say
|
const taskMessage = this.clineMessages[0] // first message is always the task say
|
||||||
const lastRelevantMessage =
|
const lastRelevantMessage =
|
||||||
this.clineMessages[
|
this.clineMessages[
|
||||||
findLastIndex(
|
findLastIndex(
|
||||||
this.clineMessages,
|
this.clineMessages,
|
||||||
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
|
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
await this.providerRef.deref()?.updateTaskHistory({
|
await this.providerRef.deref()?.updateTaskHistory({
|
||||||
id: this.taskId,
|
id: this.taskId,
|
||||||
@@ -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))
|
||||||
@@ -449,7 +448,7 @@ export class Cline {
|
|||||||
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
|
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
|
||||||
|
|
||||||
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
|
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
|
||||||
await this.getSavedApiConversationHistory()
|
await this.getSavedApiConversationHistory()
|
||||||
|
|
||||||
// Now present the cline messages to the user and ask if they want to resume
|
// Now present the cline messages to the user and ask if they want to resume
|
||||||
|
|
||||||
@@ -560,8 +559,8 @@ export class Cline {
|
|||||||
: [{ type: "text", text: lastMessage.content }]
|
: [{ type: "text", text: lastMessage.content }]
|
||||||
if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
|
if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
|
||||||
const assistantContent = Array.isArray(previousAssistantMessage.content)
|
const assistantContent = Array.isArray(previousAssistantMessage.content)
|
||||||
? previousAssistantMessage.content
|
? previousAssistantMessage.content
|
||||||
: [{ type: "text", text: previousAssistantMessage.content }]
|
: [{ type: "text", text: previousAssistantMessage.content }]
|
||||||
|
|
||||||
const toolUseBlocks = assistantContent.filter(
|
const toolUseBlocks = assistantContent.filter(
|
||||||
(block) => block.type === "tool_use",
|
(block) => block.type === "tool_use",
|
||||||
@@ -626,10 +625,9 @@ 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."
|
: ""
|
||||||
: ""
|
|
||||||
}` +
|
}` +
|
||||||
(responseText
|
(responseText
|
||||||
? `\n\nNew instructions for task continuation:\n<user_message>\n${responseText}\n</user_message>`
|
? `\n\nNew instructions for task continuation:\n<user_message>\n${responseText}\n</user_message>`
|
||||||
@@ -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,18 +806,41 @@ 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.
|
||||||
const { response } = await this.ask(
|
if (alwaysApproveResubmit) {
|
||||||
"api_req_failed",
|
// Automatically retry with delay
|
||||||
error.message ?? JSON.stringify(serializeError(error), null, 2),
|
await this.say(
|
||||||
)
|
"error",
|
||||||
if (response !== "yesButtonClicked") {
|
`Error (${
|
||||||
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
|
error.message?.toLowerCase().includes("429") ||
|
||||||
throw new Error("API request failed")
|
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(
|
||||||
|
"api_req_failed",
|
||||||
|
error.message ?? JSON.stringify(serializeError(error), null, 2),
|
||||||
|
)
|
||||||
|
if (response !== "yesButtonClicked") {
|
||||||
|
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
|
||||||
|
throw new Error("API request failed")
|
||||||
|
}
|
||||||
|
await this.say("api_req_retried")
|
||||||
|
// delegate generator output from the recursive call
|
||||||
|
yield* this.attemptApiRequest(previousApiReqIndex)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await this.say("api_req_retried")
|
|
||||||
// delegate generator output from the recursive call
|
|
||||||
yield* this.attemptApiRequest(previousApiReqIndex)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// no error, so we can continue to yield all remaining chunks
|
// no error, so we can continue to yield all remaining chunks
|
||||||
@@ -912,9 +931,8 @@ 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}']`
|
||||||
case "list_code_definition_names":
|
case "list_code_definition_names":
|
||||||
@@ -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)
|
||||||
@@ -1174,10 +1192,10 @@ export class Cline {
|
|||||||
content: fileExists ? undefined : newContent,
|
content: fileExists ? undefined : newContent,
|
||||||
diff: fileExists
|
diff: fileExists
|
||||||
? formatResponse.createPrettyPatch(
|
? formatResponse.createPrettyPatch(
|
||||||
relPath,
|
relPath,
|
||||||
this.diffViewProvider.originalContent,
|
this.diffViewProvider.originalContent,
|
||||||
newContent,
|
newContent,
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
} satisfies ClineSayTool)
|
} satisfies ClineSayTool)
|
||||||
const didApprove = await askApproval("tool", completeMessage)
|
const didApprove = await askApproval("tool", completeMessage)
|
||||||
@@ -1199,13 +1217,13 @@ export class Cline {
|
|||||||
)
|
)
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
||||||
`Please note:\n` +
|
`Please note:\n` +
|
||||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||||
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
||||||
`${newProblemsMessage}`,
|
`${newProblemsMessage}`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
@@ -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) {
|
||||||
@@ -1263,9 +1281,9 @@ export class Cline {
|
|||||||
|
|
||||||
// Apply the diff to the original content
|
// Apply the diff to the original content
|
||||||
const diffResult = this.diffStrategy?.applyDiff(
|
const diffResult = this.diffStrategy?.applyDiff(
|
||||||
originalContent,
|
originalContent,
|
||||||
diffContent,
|
diffContent,
|
||||||
parseInt(block.params.start_line ?? ''),
|
parseInt(block.params.start_line ?? ''),
|
||||||
parseInt(block.params.end_line ?? '')
|
parseInt(block.params.end_line ?? '')
|
||||||
) ?? {
|
) ?? {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1317,13 +1335,13 @@ export class Cline {
|
|||||||
)
|
)
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
||||||
`Please note:\n` +
|
`Please note:\n` +
|
||||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||||
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
||||||
`${newProblemsMessage}`,
|
`${newProblemsMessage}`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`)
|
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`)
|
||||||
@@ -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) {
|
||||||
@@ -1761,19 +1778,19 @@ export class Cline {
|
|||||||
// TODO: add progress indicator and ability to parse images and non-text responses
|
// TODO: add progress indicator and ability to parse images and non-text responses
|
||||||
const toolResultPretty =
|
const toolResultPretty =
|
||||||
(toolResult?.isError ? "Error:\n" : "") +
|
(toolResult?.isError ? "Error:\n" : "") +
|
||||||
toolResult?.content
|
toolResult?.content
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.type === "text") {
|
if (item.type === "text") {
|
||||||
return item.text
|
return item.text
|
||||||
}
|
}
|
||||||
if (item.type === "resource") {
|
if (item.type === "resource") {
|
||||||
const { blob, ...rest } = item.resource
|
const { blob, ...rest } = item.resource
|
||||||
return JSON.stringify(rest, null, 2)
|
return JSON.stringify(rest, null, 2)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n") || "(No response)"
|
.join("\n\n") || "(No response)"
|
||||||
await this.say("mcp_server_response", toolResultPretty)
|
await this.say("mcp_server_response", toolResultPretty)
|
||||||
pushToolResult(formatResponse.toolResult(toolResultPretty))
|
pushToolResult(formatResponse.toolResult(toolResultPretty))
|
||||||
break
|
break
|
||||||
@@ -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,10 +2165,9 @@ 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
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -233,7 +235,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold
|
fuzzyMatchThreshold
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
@@ -253,7 +255,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold
|
fuzzyMatchThreshold
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
@@ -319,15 +321,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
// Use a nonce to only allow a specific script to be run.
|
// 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
|
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
|
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.
|
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}';">
|
<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
|
- '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:;
|
- 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()
|
const nonce = getNonce()
|
||||||
|
|
||||||
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
|
// 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 })
|
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
|
||||||
break
|
break
|
||||||
case "refreshGlamaModels":
|
case "refreshGlamaModels":
|
||||||
await this.refreshGlamaModels()
|
await this.refreshGlamaModels()
|
||||||
break
|
break
|
||||||
case "refreshOpenRouterModels":
|
case "refreshOpenRouterModels":
|
||||||
await this.refreshOpenRouterModels()
|
await this.refreshOpenRouterModels()
|
||||||
@@ -564,7 +566,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
if (message?.values?.baseUrl && message?.values?.apiKey) {
|
if (message?.values?.baseUrl && message?.values?.apiKey) {
|
||||||
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
|
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
|
||||||
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "openImage":
|
case "openImage":
|
||||||
openImage(message.text!)
|
openImage(message.text!)
|
||||||
@@ -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()
|
||||||
@@ -1224,9 +1234,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStateToPostToWebview() {
|
async getStateToPostToWebview() {
|
||||||
const {
|
const {
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
lastShownAnnouncementId,
|
lastShownAnnouncementId,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
alwaysAllowWrite,
|
alwaysAllowWrite,
|
||||||
@@ -1244,8 +1254,10 @@ 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
|
||||||
.getConfiguration('roo-cline')
|
.getConfiguration('roo-cline')
|
||||||
.get<string[]>('allowedCommands') || []
|
.get<string[]>('allowedCommands') || []
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
@@ -230,7 +230,7 @@ describe('ClineProvider', () => {
|
|||||||
|
|
||||||
test('resolveWebviewView sets up webview correctly', () => {
|
test('resolveWebviewView sets up webview correctly', () => {
|
||||||
provider.resolveWebviewView(mockWebviewView)
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
|
||||||
expect(mockWebviewView.webview.options).toEqual({
|
expect(mockWebviewView.webview.options).toEqual({
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [mockContext.extensionUri]
|
localResourceRoots: [mockContext.extensionUri]
|
||||||
@@ -240,7 +240,7 @@ describe('ClineProvider', () => {
|
|||||||
|
|
||||||
test('postMessageToWebview sends message to webview', async () => {
|
test('postMessageToWebview sends message to webview', async () => {
|
||||||
provider.resolveWebviewView(mockWebviewView)
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
|
||||||
const mockState: ExtensionState = {
|
const mockState: ExtensionState = {
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
preferredLanguage: 'English',
|
preferredLanguage: 'English',
|
||||||
@@ -263,14 +263,16 @@ 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 = {
|
||||||
type: 'state',
|
type: 'state',
|
||||||
state: mockState
|
state: mockState
|
||||||
}
|
}
|
||||||
await provider.postMessageToWebview(message)
|
await provider.postMessageToWebview(message)
|
||||||
|
|
||||||
expect(mockPostMessage).toHaveBeenCalledWith(message)
|
expect(mockPostMessage).toHaveBeenCalledWith(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -301,7 +303,7 @@ describe('ClineProvider', () => {
|
|||||||
|
|
||||||
test('getState returns correct initial state', async () => {
|
test('getState returns correct initial state', async () => {
|
||||||
const state = await provider.getState()
|
const state = await provider.getState()
|
||||||
|
|
||||||
expect(state).toHaveProperty('apiConfiguration')
|
expect(state).toHaveProperty('apiConfiguration')
|
||||||
expect(state.apiConfiguration).toHaveProperty('apiProvider')
|
expect(state.apiConfiguration).toHaveProperty('apiProvider')
|
||||||
expect(state).toHaveProperty('customInstructions')
|
expect(state).toHaveProperty('customInstructions')
|
||||||
@@ -318,7 +320,7 @@ describe('ClineProvider', () => {
|
|||||||
test('preferredLanguage defaults to VSCode language when not set', async () => {
|
test('preferredLanguage defaults to VSCode language when not set', async () => {
|
||||||
// Mock VSCode language as Spanish
|
// Mock VSCode language as Spanish
|
||||||
(vscode.env as any).language = 'es-ES';
|
(vscode.env as any).language = 'es-ES';
|
||||||
|
|
||||||
const state = await provider.getState();
|
const state = await provider.getState();
|
||||||
expect(state.preferredLanguage).toBe('Spanish');
|
expect(state.preferredLanguage).toBe('Spanish');
|
||||||
})
|
})
|
||||||
@@ -326,7 +328,7 @@ describe('ClineProvider', () => {
|
|||||||
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
|
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
|
||||||
// Mock VSCode language as an unsupported language
|
// Mock VSCode language as an unsupported language
|
||||||
(vscode.env as any).language = 'unsupported-LANG';
|
(vscode.env as any).language = 'unsupported-LANG';
|
||||||
|
|
||||||
const state = await provider.getState();
|
const state = await provider.getState();
|
||||||
expect(state.preferredLanguage).toBe('English');
|
expect(state.preferredLanguage).toBe('English');
|
||||||
})
|
})
|
||||||
@@ -334,9 +336,9 @@ describe('ClineProvider', () => {
|
|||||||
test('diffEnabled defaults to true when not set', async () => {
|
test('diffEnabled defaults to true when not set', async () => {
|
||||||
// Mock globalState.get to return undefined for diffEnabled
|
// Mock globalState.get to return undefined for diffEnabled
|
||||||
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
|
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
|
||||||
|
|
||||||
const state = await provider.getState()
|
const state = await provider.getState()
|
||||||
|
|
||||||
expect(state.diffEnabled).toBe(true)
|
expect(state.diffEnabled).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -348,7 +350,7 @@ describe('ClineProvider', () => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const state = await provider.getState()
|
const state = await provider.getState()
|
||||||
expect(state.writeDelayMs).toBe(1000)
|
expect(state.writeDelayMs).toBe(1000)
|
||||||
})
|
})
|
||||||
@@ -356,9 +358,9 @@ describe('ClineProvider', () => {
|
|||||||
test('handles writeDelayMs message', async () => {
|
test('handles writeDelayMs message', async () => {
|
||||||
provider.resolveWebviewView(mockWebviewView)
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||||
|
|
||||||
await messageHandler({ type: 'writeDelayMs', value: 2000 })
|
await messageHandler({ type: 'writeDelayMs', value: 2000 })
|
||||||
|
|
||||||
expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000)
|
expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000)
|
||||||
expect(mockPostMessage).toHaveBeenCalled()
|
expect(mockPostMessage).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ import { GitCommit } from "../utils/git"
|
|||||||
// webview will hold state
|
// webview will hold state
|
||||||
export interface ExtensionMessage {
|
export interface ExtensionMessage {
|
||||||
type:
|
type:
|
||||||
| "action"
|
| "action"
|
||||||
| "state"
|
| "state"
|
||||||
| "selectedImages"
|
| "selectedImages"
|
||||||
| "ollamaModels"
|
| "ollamaModels"
|
||||||
| "lmStudioModels"
|
| "lmStudioModels"
|
||||||
| "theme"
|
| "theme"
|
||||||
| "workspaceUpdated"
|
| "workspaceUpdated"
|
||||||
| "invoke"
|
| "invoke"
|
||||||
| "partialMessage"
|
| "partialMessage"
|
||||||
| "glamaModels"
|
| "glamaModels"
|
||||||
| "openRouterModels"
|
| "openRouterModels"
|
||||||
| "openAiModels"
|
| "openAiModels"
|
||||||
| "mcpServers"
|
| "mcpServers"
|
||||||
| "enhancedPrompt"
|
| "enhancedPrompt"
|
||||||
| "commitSearchResults"
|
| "commitSearchResults"
|
||||||
text?: string
|
text?: string
|
||||||
action?:
|
action?:
|
||||||
| "chatButtonClicked"
|
| "chatButtonClicked"
|
||||||
| "mcpButtonClicked"
|
| "mcpButtonClicked"
|
||||||
| "settingsButtonClicked"
|
| "settingsButtonClicked"
|
||||||
| "historyButtonClicked"
|
| "historyButtonClicked"
|
||||||
| "didBecomeVisible"
|
| "didBecomeVisible"
|
||||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||||
state?: ExtensionState
|
state?: ExtensionState
|
||||||
images?: string[]
|
images?: string[]
|
||||||
@@ -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"
|
||||||
@@ -114,14 +117,14 @@ export type ClineSay =
|
|||||||
|
|
||||||
export interface ClineSayTool {
|
export interface ClineSayTool {
|
||||||
tool:
|
tool:
|
||||||
| "editedExistingFile"
|
| "editedExistingFile"
|
||||||
| "appliedDiff"
|
| "appliedDiff"
|
||||||
| "newFileCreated"
|
| "newFileCreated"
|
||||||
| "readFile"
|
| "readFile"
|
||||||
| "listFilesTopLevel"
|
| "listFilesTopLevel"
|
||||||
| "listFilesRecursive"
|
| "listFilesRecursive"
|
||||||
| "listCodeDefinitionNames"
|
| "listCodeDefinitionNames"
|
||||||
| "searchFiles"
|
| "searchFiles"
|
||||||
path?: string
|
path?: string
|
||||||
diff?: string
|
diff?: string
|
||||||
content?: string
|
content?: string
|
||||||
|
|||||||
@@ -1,57 +1,59 @@
|
|||||||
import { ApiConfiguration, ApiProvider } from "./api"
|
import { ApiConfiguration } from "./api"
|
||||||
|
|
||||||
export type AudioType = "notification" | "celebration" | "progress_loop"
|
export type AudioType = "notification" | "celebration" | "progress_loop"
|
||||||
|
|
||||||
export interface WebviewMessage {
|
export interface WebviewMessage {
|
||||||
type:
|
type:
|
||||||
| "apiConfiguration"
|
| "apiConfiguration"
|
||||||
| "customInstructions"
|
| "customInstructions"
|
||||||
| "allowedCommands"
|
| "allowedCommands"
|
||||||
| "alwaysAllowReadOnly"
|
| "alwaysAllowReadOnly"
|
||||||
| "alwaysAllowWrite"
|
| "alwaysAllowWrite"
|
||||||
| "alwaysAllowExecute"
|
| "alwaysAllowExecute"
|
||||||
| "webviewDidLaunch"
|
| "webviewDidLaunch"
|
||||||
| "newTask"
|
| "newTask"
|
||||||
| "askResponse"
|
| "askResponse"
|
||||||
| "clearTask"
|
| "clearTask"
|
||||||
| "didShowAnnouncement"
|
| "didShowAnnouncement"
|
||||||
| "selectImages"
|
| "selectImages"
|
||||||
| "exportCurrentTask"
|
| "exportCurrentTask"
|
||||||
| "showTaskWithId"
|
| "showTaskWithId"
|
||||||
| "deleteTaskWithId"
|
| "deleteTaskWithId"
|
||||||
| "exportTaskWithId"
|
| "exportTaskWithId"
|
||||||
| "resetState"
|
| "resetState"
|
||||||
| "requestOllamaModels"
|
| "requestOllamaModels"
|
||||||
| "requestLmStudioModels"
|
| "requestLmStudioModels"
|
||||||
| "openImage"
|
| "openImage"
|
||||||
| "openFile"
|
| "openFile"
|
||||||
| "openMention"
|
| "openMention"
|
||||||
| "cancelTask"
|
| "cancelTask"
|
||||||
| "refreshGlamaModels"
|
| "refreshGlamaModels"
|
||||||
| "refreshOpenRouterModels"
|
| "refreshOpenRouterModels"
|
||||||
| "refreshOpenAiModels"
|
| "refreshOpenAiModels"
|
||||||
| "alwaysAllowBrowser"
|
| "alwaysAllowBrowser"
|
||||||
| "alwaysAllowMcp"
|
| "alwaysAllowMcp"
|
||||||
| "playSound"
|
| "playSound"
|
||||||
| "soundEnabled"
|
| "soundEnabled"
|
||||||
| "soundVolume"
|
| "soundVolume"
|
||||||
| "diffEnabled"
|
| "diffEnabled"
|
||||||
| "browserViewportSize"
|
| "browserViewportSize"
|
||||||
| "screenshotQuality"
|
| "screenshotQuality"
|
||||||
| "openMcpSettings"
|
| "openMcpSettings"
|
||||||
| "restartMcpServer"
|
| "restartMcpServer"
|
||||||
| "toggleToolAlwaysAllow"
|
| "toggleToolAlwaysAllow"
|
||||||
| "toggleMcpServer"
|
| "toggleMcpServer"
|
||||||
| "fuzzyMatchThreshold"
|
| "fuzzyMatchThreshold"
|
||||||
| "preferredLanguage"
|
| "preferredLanguage"
|
||||||
| "writeDelayMs"
|
| "writeDelayMs"
|
||||||
| "enhancePrompt"
|
| "enhancePrompt"
|
||||||
| "enhancedPrompt"
|
| "enhancedPrompt"
|
||||||
| "draggedImages"
|
| "draggedImages"
|
||||||
| "deleteMessage"
|
| "deleteMessage"
|
||||||
| "terminalOutputLineLimit"
|
| "terminalOutputLineLimit"
|
||||||
| "mcpEnabled"
|
| "mcpEnabled"
|
||||||
| "searchCommits"
|
| "searchCommits"
|
||||||
|
| "alwaysApproveResubmit"
|
||||||
|
| "requestDelaySeconds"
|
||||||
text?: string
|
text?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
askResponse?: ClineAskResponse
|
askResponse?: ClineAskResponse
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -525,7 +567,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<div style={{ marginBottom: 10 }}>
|
<div style={{ marginBottom: 10 }}>
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
|
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
|
||||||
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
||||||
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
||||||
</VSCodeCheckbox>
|
</VSCodeCheckbox>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -90,18 +96,18 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const config = message.state?.apiConfiguration
|
const config = message.state?.apiConfiguration
|
||||||
const hasKey = config
|
const hasKey = config
|
||||||
? [
|
? [
|
||||||
config.apiKey,
|
config.apiKey,
|
||||||
config.glamaApiKey,
|
config.glamaApiKey,
|
||||||
config.openRouterApiKey,
|
config.openRouterApiKey,
|
||||||
config.awsRegion,
|
config.awsRegion,
|
||||||
config.vertexProjectId,
|
config.vertexProjectId,
|
||||||
config.openAiApiKey,
|
config.openAiApiKey,
|
||||||
config.ollamaModelId,
|
config.ollamaModelId,
|
||||||
config.lmStudioModelId,
|
config.lmStudioModelId,
|
||||||
config.geminiApiKey,
|
config.geminiApiKey,
|
||||||
config.openAiNativeApiKey,
|
config.openAiNativeApiKey,
|
||||||
config.deepSeekApiKey,
|
config.deepSeekApiKey,
|
||||||
].some((key) => key !== undefined)
|
].some((key) => key !== undefined)
|
||||||
: false
|
: false
|
||||||
setShowWelcome(!hasKey)
|
setShowWelcome(!hasKey)
|
||||||
setDidHydrateState(true)
|
setDidHydrateState(true)
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user