Skip to content

Commit 7282e02

Browse files
committed
Merge main
1 parent 4aff012 commit 7282e02

File tree

12 files changed

+173
-202
lines changed

12 files changed

+173
-202
lines changed

.changeset/tame-walls-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Use an exponential backoff for API retries

.eslintrc.json

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,24 @@
11
{
22
"root": true,
3-
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
43
"parser": "@typescript-eslint/parser",
54
"parserOptions": {
6-
"ecmaVersion": 2021,
7-
"sourceType": "module",
8-
"project": "./tsconfig.json"
5+
"ecmaVersion": 6,
6+
"sourceType": "module"
97
},
108
"plugins": ["@typescript-eslint"],
119
"rules": {
12-
"@typescript-eslint/naming-convention": ["warn"],
13-
"@typescript-eslint/no-explicit-any": "off",
14-
"@typescript-eslint/no-unused-vars": [
10+
"@typescript-eslint/naming-convention": [
1511
"warn",
1612
{
17-
"argsIgnorePattern": "^_",
18-
"varsIgnorePattern": "^_",
19-
"caughtErrorsIgnorePattern": "^_"
13+
"selector": "import",
14+
"format": ["camelCase", "PascalCase"]
2015
}
2116
],
22-
"@typescript-eslint/explicit-function-return-type": [
23-
"warn",
24-
{
25-
"allowExpressions": true,
26-
"allowTypedFunctionExpressions": true
27-
}
28-
],
29-
"@typescript-eslint/explicit-member-accessibility": [
30-
"warn",
31-
{
32-
"accessibility": "explicit"
33-
}
34-
],
35-
"@typescript-eslint/no-non-null-assertion": "warn",
17+
"@typescript-eslint/semi": "off",
18+
"eqeqeq": "warn",
3619
"no-throw-literal": "warn",
37-
"semi": ["off", "always"],
38-
"quotes": ["warn", "double", { "avoidEscape": true }],
39-
"@typescript-eslint/ban-types": "off",
40-
"@typescript-eslint/no-var-requires": "warn",
41-
"no-extra-semi": "warn",
42-
"prefer-const": "warn",
43-
"no-mixed-spaces-and-tabs": "warn",
44-
"no-case-declarations": "warn",
45-
"no-useless-escape": "warn",
46-
"require-yield": "warn",
47-
"no-empty": "warn",
48-
"no-control-regex": "warn",
49-
"@typescript-eslint/ban-ts-comment": "warn"
50-
},
51-
"env": {
52-
"node": true,
53-
"es2021": true
20+
"semi": "off",
21+
"react-hooks/exhaustive-deps": "off"
5422
},
55-
"ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"]
23+
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
5624
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@
214214
"compile": "npm run check-types && npm run lint && node esbuild.js",
215215
"compile-tests": "tsc -p . --outDir out",
216216
"install:all": "npm install && cd webview-ui && npm install",
217-
"lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui",
217+
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
218218
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
219219
"pretest": "npm run compile-tests && npm run compile && npm run lint",
220220
"start:webview": "cd webview-ui && npm run start",

src/api/providers/openrouter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
118118

119119
// Handle models based on deepseek-r1
120120
if (
121-
this.getModel().id === "deepseek/deepseek-r1" ||
122-
this.getModel().id.startsWith("deepseek/deepseek-r1:") ||
121+
this.getModel().id.startsWith("deepseek/deepseek-r1") ||
123122
this.getModel().id === "perplexity/sonar-reasoning"
124123
) {
125124
// Recommended temperature for DeepSeek reasoning models

src/core/Cline.ts

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ export class Cline {
793793
}
794794
}
795795

796-
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
796+
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
797797
let mcpHub: McpHub | undefined
798798

799799
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
@@ -887,21 +887,29 @@ export class Cline {
887887
// 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.
888888
if (alwaysApproveResubmit) {
889889
const errorMsg = error.message ?? "Unknown error"
890-
const requestDelay = requestDelaySeconds || 5
891-
// Automatically retry with delay
892-
// Show countdown timer in error color
893-
for (let i = requestDelay; i > 0; i--) {
890+
const baseDelay = requestDelaySeconds || 5
891+
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
892+
893+
// Show countdown timer with exponential backoff
894+
for (let i = exponentialDelay; i > 0; i--) {
894895
await this.say(
895896
"api_req_retry_delayed",
896-
`${errorMsg}\n\nRetrying in ${i} seconds...`,
897+
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
897898
undefined,
898899
true,
899900
)
900901
await delay(1000)
901902
}
902-
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false)
903-
// delegate generator output from the recursive call
904-
yield* this.attemptApiRequest(previousApiReqIndex)
903+
904+
await this.say(
905+
"api_req_retry_delayed",
906+
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`,
907+
undefined,
908+
false,
909+
)
910+
911+
// delegate generator output from the recursive call with incremented retry count
912+
yield* this.attemptApiRequest(previousApiReqIndex, retryAttempt + 1)
905913
return
906914
} else {
907915
const { response } = await this.ask(
@@ -1085,35 +1093,23 @@ export class Cline {
10851093
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
10861094
const { response, text, images } = await this.ask(type, partialMessage, false)
10871095
if (response !== "yesButtonClicked") {
1088-
if (response === "messageResponse") {
1096+
// Handle both messageResponse and noButtonClicked with text
1097+
if (text) {
10891098
await this.say("user_feedback", text, images)
10901099
pushToolResult(
10911100
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
10921101
)
1093-
// this.userMessageContent.push({
1094-
// type: "text",
1095-
// text: `${toolDescription()}`,
1096-
// })
1097-
// this.toolResults.push({
1098-
// type: "tool_result",
1099-
// tool_use_id: toolUseId,
1100-
// content: this.formatToolResponseWithImages(
1101-
// await this.formatToolDeniedFeedback(text),
1102-
// images
1103-
// ),
1104-
// })
1105-
this.didRejectTool = true
1106-
return false
1102+
} else {
1103+
pushToolResult(formatResponse.toolDenied())
11071104
}
1108-
pushToolResult(formatResponse.toolDenied())
1109-
// this.toolResults.push({
1110-
// type: "tool_result",
1111-
// tool_use_id: toolUseId,
1112-
// content: await this.formatToolDenied(),
1113-
// })
11141105
this.didRejectTool = true
11151106
return false
11161107
}
1108+
// Handle yesButtonClicked with text
1109+
if (text) {
1110+
await this.say("user_feedback", text, images)
1111+
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
1112+
}
11171113
return true
11181114
}
11191115

src/core/__tests__/Cline.test.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -730,25 +730,19 @@ describe("Cline", () => {
730730
const iterator = cline.attemptApiRequest(0)
731731
await iterator.next()
732732

733+
// Calculate expected delay for first retry
734+
const baseDelay = 3 // from requestDelaySeconds
735+
733736
// Verify countdown messages
734-
expect(saySpy).toHaveBeenCalledWith(
735-
"api_req_retry_delayed",
736-
expect.stringContaining("Retrying in 3 seconds"),
737-
undefined,
738-
true,
739-
)
740-
expect(saySpy).toHaveBeenCalledWith(
741-
"api_req_retry_delayed",
742-
expect.stringContaining("Retrying in 2 seconds"),
743-
undefined,
744-
true,
745-
)
746-
expect(saySpy).toHaveBeenCalledWith(
747-
"api_req_retry_delayed",
748-
expect.stringContaining("Retrying in 1 seconds"),
749-
undefined,
750-
true,
751-
)
737+
for (let i = baseDelay; i > 0; i--) {
738+
expect(saySpy).toHaveBeenCalledWith(
739+
"api_req_retry_delayed",
740+
expect.stringContaining(`Retrying in ${i} seconds`),
741+
undefined,
742+
true,
743+
)
744+
}
745+
752746
expect(saySpy).toHaveBeenCalledWith(
753747
"api_req_retry_delayed",
754748
expect.stringContaining("Retrying now"),
@@ -757,12 +751,14 @@ describe("Cline", () => {
757751
)
758752

759753
// Verify delay was called correctly
760-
expect(mockDelay).toHaveBeenCalledTimes(3)
754+
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
761755
expect(mockDelay).toHaveBeenCalledWith(1000)
762756

763757
// Verify error message content
764758
const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1]
765-
expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`)
759+
expect(errorMessage).toBe(
760+
`${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`,
761+
)
766762
})
767763

768764
describe("loadContext", () => {

src/core/prompts/responses.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const formatResponse = {
88
toolDeniedWithFeedback: (feedback?: string) =>
99
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
1010

11+
toolApprovedWithFeedback: (feedback?: string) =>
12+
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
13+
1114
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
1215

1316
noToolsUsed: () =>

src/core/prompts/sections/modes.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ MODES
1616
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
1717
Custom modes will be referred to by their configured name property.
1818
19-
- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty:
19+
- Custom modes can be configured by editing the custom modes file at '${customModesPath}'. The file gets created automatically on startup and should always exist. Make sure to read the latest contents before writing to it to avoid overwriting existing modes.
20+
21+
- The following fields are required and must not be empty:
2022
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
2123
* name: The display name for the mode
2224
* roleDefinition: A detailed description of the mode's role and capabilities
2325
* groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
2426
25-
The customInstructions field is optional.
27+
- The customInstructions field is optional.
28+
29+
- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break."
2630
2731
The file should follow this structure:
2832
{

webview-ui/.eslintrc.json

Lines changed: 0 additions & 40 deletions
This file was deleted.

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const ChatRowContent = ({
8989
}
9090
}, [isLast, message.say])
9191
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
92-
if (message.text != null && message.say === "api_req_started") {
92+
if (message.text && message.say === "api_req_started") {
9393
const info: ClineApiReqInfo = JSON.parse(message.text)
9494
return [info.cost, info.cancelReason, info.streamingFailedMessage]
9595
}
@@ -183,26 +183,26 @@ export const ChatRowContent = ({
183183
</div>
184184
)
185185
return [
186-
apiReqCancelReason != null ? (
186+
apiReqCancelReason !== null ? (
187187
apiReqCancelReason === "user_cancelled" ? (
188188
getIconSpan("error", cancelledColor)
189189
) : (
190190
getIconSpan("error", errorColor)
191191
)
192-
) : cost != null ? (
192+
) : cost !== null ? (
193193
getIconSpan("check", successColor)
194194
) : apiRequestFailedMessage ? (
195195
getIconSpan("error", errorColor)
196196
) : (
197197
<ProgressIndicator />
198198
),
199-
apiReqCancelReason != null ? (
199+
apiReqCancelReason !== null ? (
200200
apiReqCancelReason === "user_cancelled" ? (
201201
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
202202
) : (
203203
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
204204
)
205-
) : cost != null ? (
205+
) : cost !== null ? (
206206
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
207207
) : apiRequestFailedMessage ? (
208208
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
@@ -510,7 +510,7 @@ export const ChatRowContent = ({
510510
style={{
511511
...headerStyle,
512512
marginBottom:
513-
(cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
513+
(cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
514514
? 10
515515
: 0,
516516
justifyContent: "space-between",
@@ -524,13 +524,13 @@ export const ChatRowContent = ({
524524
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
525525
{icon}
526526
{title}
527-
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
527+
<VSCodeBadge style={{ opacity: cost ? 1 : 0 }}>
528528
${Number(cost || 0)?.toFixed(4)}
529529
</VSCodeBadge>
530530
</div>
531531
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
532532
</div>
533-
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
533+
{((cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
534534
<>
535535
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
536536
{apiRequestFailedMessage || apiReqStreamingFailedMessage}

0 commit comments

Comments
 (0)