mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge main
This commit is contained in:
5
.changeset/tame-walls-kiss.md
Normal file
5
.changeset/tame-walls-kiss.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Use an exponential backoff for API retries
|
||||||
@@ -1,56 +1,24 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2021,
|
"ecmaVersion": 6,
|
||||||
"sourceType": "module",
|
"sourceType": "module"
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/naming-convention": ["warn"],
|
"@typescript-eslint/naming-convention": [
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
"argsIgnorePattern": "^_",
|
"selector": "import",
|
||||||
"varsIgnorePattern": "^_",
|
"format": ["camelCase", "PascalCase"]
|
||||||
"caughtErrorsIgnorePattern": "^_"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/explicit-function-return-type": [
|
"@typescript-eslint/semi": "off",
|
||||||
"warn",
|
"eqeqeq": "warn",
|
||||||
{
|
|
||||||
"allowExpressions": true,
|
|
||||||
"allowTypedFunctionExpressions": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"accessibility": "explicit"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"no-throw-literal": "warn",
|
"no-throw-literal": "warn",
|
||||||
"semi": ["off", "always"],
|
"semi": "off",
|
||||||
"quotes": ["warn", "double", { "avoidEscape": true }],
|
"react-hooks/exhaustive-deps": "off"
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "warn",
|
|
||||||
"no-extra-semi": "warn",
|
|
||||||
"prefer-const": "warn",
|
|
||||||
"no-mixed-spaces-and-tabs": "warn",
|
|
||||||
"no-case-declarations": "warn",
|
|
||||||
"no-useless-escape": "warn",
|
|
||||||
"require-yield": "warn",
|
|
||||||
"no-empty": "warn",
|
|
||||||
"no-control-regex": "warn",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "warn"
|
|
||||||
},
|
},
|
||||||
"env": {
|
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||||
"node": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||||
"compile-tests": "tsc -p . --outDir out",
|
"compile-tests": "tsc -p . --outDir out",
|
||||||
"install:all": "npm install && cd webview-ui && npm install",
|
"install:all": "npm install && cd webview-ui && npm install",
|
||||||
"lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui",
|
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
|
||||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||||
"start:webview": "cd webview-ui && npm run start",
|
"start:webview": "cd webview-ui && npm run start",
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
|
|
||||||
// Handle models based on deepseek-r1
|
// Handle models based on deepseek-r1
|
||||||
if (
|
if (
|
||||||
this.getModel().id === "deepseek/deepseek-r1" ||
|
this.getModel().id.startsWith("deepseek/deepseek-r1") ||
|
||||||
this.getModel().id.startsWith("deepseek/deepseek-r1:") ||
|
|
||||||
this.getModel().id === "perplexity/sonar-reasoning"
|
this.getModel().id === "perplexity/sonar-reasoning"
|
||||||
) {
|
) {
|
||||||
// Recommended temperature for DeepSeek reasoning models
|
// Recommended temperature for DeepSeek reasoning models
|
||||||
|
|||||||
@@ -793,7 +793,7 @@ export class Cline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
|
||||||
let mcpHub: McpHub | undefined
|
let mcpHub: McpHub | undefined
|
||||||
|
|
||||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
||||||
@@ -887,21 +887,29 @@ export class Cline {
|
|||||||
// 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) {
|
if (alwaysApproveResubmit) {
|
||||||
const errorMsg = error.message ?? "Unknown error"
|
const errorMsg = error.message ?? "Unknown error"
|
||||||
const requestDelay = requestDelaySeconds || 5
|
const baseDelay = requestDelaySeconds || 5
|
||||||
// Automatically retry with delay
|
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
|
||||||
// Show countdown timer in error color
|
|
||||||
for (let i = requestDelay; i > 0; i--) {
|
// Show countdown timer with exponential backoff
|
||||||
|
for (let i = exponentialDelay; i > 0; i--) {
|
||||||
await this.say(
|
await this.say(
|
||||||
"api_req_retry_delayed",
|
"api_req_retry_delayed",
|
||||||
`${errorMsg}\n\nRetrying in ${i} seconds...`,
|
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
await delay(1000)
|
await delay(1000)
|
||||||
}
|
}
|
||||||
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false)
|
|
||||||
// delegate generator output from the recursive call
|
await this.say(
|
||||||
yield* this.attemptApiRequest(previousApiReqIndex)
|
"api_req_retry_delayed",
|
||||||
|
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// delegate generator output from the recursive call with incremented retry count
|
||||||
|
yield* this.attemptApiRequest(previousApiReqIndex, retryAttempt + 1)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
const { response } = await this.ask(
|
const { response } = await this.ask(
|
||||||
@@ -1085,35 +1093,23 @@ export class Cline {
|
|||||||
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
|
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
|
||||||
const { response, text, images } = await this.ask(type, partialMessage, false)
|
const { response, text, images } = await this.ask(type, partialMessage, false)
|
||||||
if (response !== "yesButtonClicked") {
|
if (response !== "yesButtonClicked") {
|
||||||
if (response === "messageResponse") {
|
// Handle both messageResponse and noButtonClicked with text
|
||||||
|
if (text) {
|
||||||
await this.say("user_feedback", text, images)
|
await this.say("user_feedback", text, images)
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
|
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
|
||||||
)
|
)
|
||||||
// this.userMessageContent.push({
|
} else {
|
||||||
// type: "text",
|
pushToolResult(formatResponse.toolDenied())
|
||||||
// text: `${toolDescription()}`,
|
|
||||||
// })
|
|
||||||
// this.toolResults.push({
|
|
||||||
// type: "tool_result",
|
|
||||||
// tool_use_id: toolUseId,
|
|
||||||
// content: this.formatToolResponseWithImages(
|
|
||||||
// await this.formatToolDeniedFeedback(text),
|
|
||||||
// images
|
|
||||||
// ),
|
|
||||||
// })
|
|
||||||
this.didRejectTool = true
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
pushToolResult(formatResponse.toolDenied())
|
|
||||||
// this.toolResults.push({
|
|
||||||
// type: "tool_result",
|
|
||||||
// tool_use_id: toolUseId,
|
|
||||||
// content: await this.formatToolDenied(),
|
|
||||||
// })
|
|
||||||
this.didRejectTool = true
|
this.didRejectTool = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// Handle yesButtonClicked with text
|
||||||
|
if (text) {
|
||||||
|
await this.say("user_feedback", text, images)
|
||||||
|
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -730,25 +730,19 @@ describe("Cline", () => {
|
|||||||
const iterator = cline.attemptApiRequest(0)
|
const iterator = cline.attemptApiRequest(0)
|
||||||
await iterator.next()
|
await iterator.next()
|
||||||
|
|
||||||
|
// Calculate expected delay for first retry
|
||||||
|
const baseDelay = 3 // from requestDelaySeconds
|
||||||
|
|
||||||
// Verify countdown messages
|
// Verify countdown messages
|
||||||
expect(saySpy).toHaveBeenCalledWith(
|
for (let i = baseDelay; i > 0; i--) {
|
||||||
"api_req_retry_delayed",
|
expect(saySpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Retrying in 3 seconds"),
|
"api_req_retry_delayed",
|
||||||
undefined,
|
expect.stringContaining(`Retrying in ${i} seconds`),
|
||||||
true,
|
undefined,
|
||||||
)
|
true,
|
||||||
expect(saySpy).toHaveBeenCalledWith(
|
)
|
||||||
"api_req_retry_delayed",
|
}
|
||||||
expect.stringContaining("Retrying in 2 seconds"),
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
expect(saySpy).toHaveBeenCalledWith(
|
|
||||||
"api_req_retry_delayed",
|
|
||||||
expect.stringContaining("Retrying in 1 seconds"),
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
expect(saySpy).toHaveBeenCalledWith(
|
expect(saySpy).toHaveBeenCalledWith(
|
||||||
"api_req_retry_delayed",
|
"api_req_retry_delayed",
|
||||||
expect.stringContaining("Retrying now"),
|
expect.stringContaining("Retrying now"),
|
||||||
@@ -757,12 +751,14 @@ describe("Cline", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Verify delay was called correctly
|
// Verify delay was called correctly
|
||||||
expect(mockDelay).toHaveBeenCalledTimes(3)
|
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
|
||||||
expect(mockDelay).toHaveBeenCalledWith(1000)
|
expect(mockDelay).toHaveBeenCalledWith(1000)
|
||||||
|
|
||||||
// Verify error message content
|
// Verify error message content
|
||||||
const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1]
|
const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1]
|
||||||
expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`)
|
expect(errorMessage).toBe(
|
||||||
|
`${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("loadContext", () => {
|
describe("loadContext", () => {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export const formatResponse = {
|
|||||||
toolDeniedWithFeedback: (feedback?: string) =>
|
toolDeniedWithFeedback: (feedback?: string) =>
|
||||||
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||||
|
|
||||||
|
toolApprovedWithFeedback: (feedback?: string) =>
|
||||||
|
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
|
||||||
|
|
||||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||||
|
|
||||||
noToolsUsed: () =>
|
noToolsUsed: () =>
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ MODES
|
|||||||
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
||||||
Custom modes will be referred to by their configured name property.
|
Custom modes will be referred to by their configured name property.
|
||||||
|
|
||||||
- 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:
|
- 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.
|
||||||
|
|
||||||
|
- The following fields are required and must not be empty:
|
||||||
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
|
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
|
||||||
* name: The display name for the mode
|
* name: The display name for the mode
|
||||||
* roleDefinition: A detailed description of the mode's role and capabilities
|
* roleDefinition: A detailed description of the mode's role and capabilities
|
||||||
* 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)
|
* 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)
|
||||||
|
|
||||||
The customInstructions field is optional.
|
- The customInstructions field is optional.
|
||||||
|
|
||||||
|
- 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."
|
||||||
|
|
||||||
The file should follow this structure:
|
The file should follow this structure:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react-hooks/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["react", "@typescript-eslint", "react-hooks"],
|
|
||||||
"rules": {
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"react/display-name": "warn",
|
|
||||||
"no-case-declarations": "warn",
|
|
||||||
"react/no-unescaped-entities": "warn",
|
|
||||||
"react/jsx-key": "warn",
|
|
||||||
"no-extra-semi": "warn",
|
|
||||||
"@typescript-eslint/no-var-requires": "warn",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_",
|
|
||||||
"varsIgnorePattern": "^_",
|
|
||||||
"caughtErrorsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -89,7 +89,7 @@ export const ChatRowContent = ({
|
|||||||
}
|
}
|
||||||
}, [isLast, message.say])
|
}, [isLast, message.say])
|
||||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||||
if (message.text != null && message.say === "api_req_started") {
|
if (message.text && message.say === "api_req_started") {
|
||||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||||
return [info.cost, info.cancelReason, info.streamingFailedMessage]
|
return [info.cost, info.cancelReason, info.streamingFailedMessage]
|
||||||
}
|
}
|
||||||
@@ -183,26 +183,26 @@ export const ChatRowContent = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
apiReqCancelReason != null ? (
|
apiReqCancelReason !== null ? (
|
||||||
apiReqCancelReason === "user_cancelled" ? (
|
apiReqCancelReason === "user_cancelled" ? (
|
||||||
getIconSpan("error", cancelledColor)
|
getIconSpan("error", cancelledColor)
|
||||||
) : (
|
) : (
|
||||||
getIconSpan("error", errorColor)
|
getIconSpan("error", errorColor)
|
||||||
)
|
)
|
||||||
) : cost != null ? (
|
) : cost !== null ? (
|
||||||
getIconSpan("check", successColor)
|
getIconSpan("check", successColor)
|
||||||
) : apiRequestFailedMessage ? (
|
) : apiRequestFailedMessage ? (
|
||||||
getIconSpan("error", errorColor)
|
getIconSpan("error", errorColor)
|
||||||
) : (
|
) : (
|
||||||
<ProgressIndicator />
|
<ProgressIndicator />
|
||||||
),
|
),
|
||||||
apiReqCancelReason != null ? (
|
apiReqCancelReason !== null ? (
|
||||||
apiReqCancelReason === "user_cancelled" ? (
|
apiReqCancelReason === "user_cancelled" ? (
|
||||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
|
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
|
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
|
||||||
)
|
)
|
||||||
) : cost != null ? (
|
) : cost !== null ? (
|
||||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
|
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
|
||||||
) : apiRequestFailedMessage ? (
|
) : apiRequestFailedMessage ? (
|
||||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||||
@@ -510,7 +510,7 @@ export const ChatRowContent = ({
|
|||||||
style={{
|
style={{
|
||||||
...headerStyle,
|
...headerStyle,
|
||||||
marginBottom:
|
marginBottom:
|
||||||
(cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
|
(cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
|
||||||
? 10
|
? 10
|
||||||
: 0,
|
: 0,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -524,13 +524,13 @@ export const ChatRowContent = ({
|
|||||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
||||||
{icon}
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
|
<VSCodeBadge style={{ opacity: cost ? 1 : 0 }}>
|
||||||
${Number(cost || 0)?.toFixed(4)}
|
${Number(cost || 0)?.toFixed(4)}
|
||||||
</VSCodeBadge>
|
</VSCodeBadge>
|
||||||
</div>
|
</div>
|
||||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||||
</div>
|
</div>
|
||||||
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
|
{((cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
|
||||||
<>
|
<>
|
||||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
||||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
|
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
|
||||||
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {
|
if (lastApiReqStarted && lastApiReqStarted.text && lastApiReqStarted.say === "api_req_started") {
|
||||||
const cost = JSON.parse(lastApiReqStarted.text).cost
|
const cost = JSON.parse(lastApiReqStarted.text).cost
|
||||||
if (cost === undefined) {
|
if (cost === undefined) {
|
||||||
// api request has not finished yet
|
// api request has not finished yet
|
||||||
@@ -337,56 +337,96 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
/*
|
/*
|
||||||
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
|
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
|
||||||
*/
|
*/
|
||||||
const handlePrimaryButtonClick = useCallback(() => {
|
const handlePrimaryButtonClick = useCallback(
|
||||||
switch (clineAsk) {
|
(text?: string, images?: string[]) => {
|
||||||
case "api_req_failed":
|
const trimmedInput = text?.trim()
|
||||||
case "command":
|
switch (clineAsk) {
|
||||||
case "command_output":
|
case "api_req_failed":
|
||||||
case "tool":
|
case "command":
|
||||||
case "browser_action_launch":
|
case "command_output":
|
||||||
case "use_mcp_server":
|
case "tool":
|
||||||
case "resume_task":
|
case "browser_action_launch":
|
||||||
case "mistake_limit_reached":
|
case "use_mcp_server":
|
||||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
case "resume_task":
|
||||||
break
|
case "mistake_limit_reached":
|
||||||
case "completion_result":
|
// Only send text/images if they exist
|
||||||
case "resume_completed_task":
|
if (trimmedInput || (images && images.length > 0)) {
|
||||||
// extension waiting for feedback. but we can just present a new task button
|
vscode.postMessage({
|
||||||
startNewTask()
|
type: "askResponse",
|
||||||
break
|
askResponse: "yesButtonClicked",
|
||||||
}
|
text: trimmedInput,
|
||||||
setTextAreaDisabled(true)
|
images: images,
|
||||||
setClineAsk(undefined)
|
})
|
||||||
setEnableButtons(false)
|
} else {
|
||||||
disableAutoScrollRef.current = false
|
vscode.postMessage({
|
||||||
}, [clineAsk, startNewTask])
|
type: "askResponse",
|
||||||
|
askResponse: "yesButtonClicked",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Clear input state after sending
|
||||||
|
setInputValue("")
|
||||||
|
setSelectedImages([])
|
||||||
|
break
|
||||||
|
case "completion_result":
|
||||||
|
case "resume_completed_task":
|
||||||
|
// extension waiting for feedback. but we can just present a new task button
|
||||||
|
startNewTask()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setTextAreaDisabled(true)
|
||||||
|
setClineAsk(undefined)
|
||||||
|
setEnableButtons(false)
|
||||||
|
disableAutoScrollRef.current = false
|
||||||
|
},
|
||||||
|
[clineAsk, startNewTask],
|
||||||
|
)
|
||||||
|
|
||||||
const handleSecondaryButtonClick = useCallback(() => {
|
const handleSecondaryButtonClick = useCallback(
|
||||||
if (isStreaming) {
|
(text?: string, images?: string[]) => {
|
||||||
vscode.postMessage({ type: "cancelTask" })
|
const trimmedInput = text?.trim()
|
||||||
setDidClickCancel(true)
|
if (isStreaming) {
|
||||||
return
|
vscode.postMessage({ type: "cancelTask" })
|
||||||
}
|
setDidClickCancel(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch (clineAsk) {
|
switch (clineAsk) {
|
||||||
case "api_req_failed":
|
case "api_req_failed":
|
||||||
case "mistake_limit_reached":
|
case "mistake_limit_reached":
|
||||||
case "resume_task":
|
case "resume_task":
|
||||||
startNewTask()
|
startNewTask()
|
||||||
break
|
break
|
||||||
case "command":
|
case "command":
|
||||||
case "tool":
|
case "tool":
|
||||||
case "browser_action_launch":
|
case "browser_action_launch":
|
||||||
case "use_mcp_server":
|
case "use_mcp_server":
|
||||||
// responds to the API with a "This operation failed" and lets it try again
|
// Only send text/images if they exist
|
||||||
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
if (trimmedInput || (images && images.length > 0)) {
|
||||||
break
|
vscode.postMessage({
|
||||||
}
|
type: "askResponse",
|
||||||
setTextAreaDisabled(true)
|
askResponse: "noButtonClicked",
|
||||||
setClineAsk(undefined)
|
text: trimmedInput,
|
||||||
setEnableButtons(false)
|
images: images,
|
||||||
disableAutoScrollRef.current = false
|
})
|
||||||
}, [clineAsk, startNewTask, isStreaming])
|
} else {
|
||||||
|
// responds to the API with a "This operation failed" and lets it try again
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "askResponse",
|
||||||
|
askResponse: "noButtonClicked",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Clear input state after sending
|
||||||
|
setInputValue("")
|
||||||
|
setSelectedImages([])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setTextAreaDisabled(true)
|
||||||
|
setClineAsk(undefined)
|
||||||
|
setEnableButtons(false)
|
||||||
|
disableAutoScrollRef.current = false
|
||||||
|
},
|
||||||
|
[clineAsk, startNewTask, isStreaming],
|
||||||
|
)
|
||||||
|
|
||||||
const handleTaskCloseButtonClick = useCallback(() => {
|
const handleTaskCloseButtonClick = useCallback(() => {
|
||||||
startNewTask()
|
startNewTask()
|
||||||
@@ -430,10 +470,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
handleSendMessage(message.text ?? "", message.images ?? [])
|
handleSendMessage(message.text ?? "", message.images ?? [])
|
||||||
break
|
break
|
||||||
case "primaryButtonClick":
|
case "primaryButtonClick":
|
||||||
handlePrimaryButtonClick()
|
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
||||||
break
|
break
|
||||||
case "secondaryButtonClick":
|
case "secondaryButtonClick":
|
||||||
handleSecondaryButtonClick()
|
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,9 +700,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
if (message.say === "api_req_started") {
|
if (message.say === "api_req_started") {
|
||||||
// get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
|
// get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
|
||||||
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
|
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
|
||||||
if (lastApiReqStarted?.text != null) {
|
if (lastApiReqStarted?.text) {
|
||||||
const info = JSON.parse(lastApiReqStarted.text)
|
const info = JSON.parse(lastApiReqStarted.text)
|
||||||
const isCancelled = info.cancelReason != null
|
const isCancelled = info.cancelReason !== null
|
||||||
if (isCancelled) {
|
if (isCancelled) {
|
||||||
endBrowserSession()
|
endBrowserSession()
|
||||||
result.push(message)
|
result.push(message)
|
||||||
@@ -1038,7 +1078,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
flex: secondaryButtonText ? 1 : 2,
|
flex: secondaryButtonText ? 1 : 2,
|
||||||
marginRight: secondaryButtonText ? "6px" : "0",
|
marginRight: secondaryButtonText ? "6px" : "0",
|
||||||
}}
|
}}
|
||||||
onClick={handlePrimaryButtonClick}>
|
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||||
{primaryButtonText}
|
{primaryButtonText}
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
)}
|
)}
|
||||||
@@ -1050,7 +1090,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
flex: isStreaming ? 2 : 1,
|
flex: isStreaming ? 2 : 1,
|
||||||
marginLeft: isStreaming ? 0 : "6px",
|
marginLeft: isStreaming ? 0 : "6px",
|
||||||
}}
|
}}
|
||||||
onClick={handleSecondaryButtonClick}>
|
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||||
{isStreaming ? "Cancel" : secondaryButtonText}
|
{isStreaming ? "Cancel" : secondaryButtonText}
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const WelcomeView = () => {
|
|||||||
|
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const disableLetsGoButton = apiErrorMessage != null
|
const disableLetsGoButton = !!apiErrorMessage
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||||
|
|||||||
Reference in New Issue
Block a user