Skip to content
Merged
36 changes: 14 additions & 22 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3775,17 +3775,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (finalDelay <= 0) return

// Build header text; fall back to error message if none provided
let headerText = header
if (!headerText) {
if (error?.error?.metadata?.raw) {
headerText = JSON.stringify(error.error.metadata.raw, null, 2)
} else if (error?.message) {
headerText = error.message
} else {
headerText = "Unknown error"
}
let headerText
if (error.status) {
// This sets the message as just the error code, for which
// ChatRow knows how to handle and use an i18n'd error string
// In development, hardcode headerText to an HTTP status code to check it
headerText = error.status
} else if (error?.message) {
headerText = error.message
} else {
headerText = "Unknown error"
}
headerText = headerText ? `${headerText}\n\n` : ""

headerText = headerText ? `${headerText}\n` : ""

// Show countdown timer with exponential backoff
for (let i = finalDelay; i > 0; i--) {
Expand All @@ -3794,21 +3796,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`)
}

await this.say(
"api_req_retry_delayed",
`${headerText}Retry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
undefined,
true,
)
await this.say("api_req_retry_delayed", `${headerText}\n↻ ${i}s...`, undefined, true)
await delay(1000)
}

await this.say(
"api_req_retry_delayed",
`${headerText}Retry attempt ${retryAttempt + 1}\nRetrying now...`,
undefined,
false,
)
await this.say("api_req_retry_delayed", headerText, undefined, false)
} catch (err) {
console.error("Exponential backoff failed:", err)
}
Expand Down
67 changes: 50 additions & 17 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { Markdown } from "./Markdown"
import { CommandExecution } from "./CommandExecution"
import { CommandExecutionError } from "./CommandExecutionError"
import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning"
import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
import { CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay"
import { appendImages } from "@src/utils/imageUtils"
import { McpExecution } from "./McpExecution"
Expand Down Expand Up @@ -162,7 +162,7 @@ export const ChatRowContent = ({
onBatchFileResponse,
isFollowUpAnswered,
}: ChatRowContentProps) => {
const { t } = useTranslation()
const { t, i18n } = useTranslation()

const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
Expand Down Expand Up @@ -1099,25 +1099,52 @@ export const ChatRowContent = ({
<ErrorRow
type="api_failure"
message={apiRequestFailedMessage || apiReqStreamingFailedMessage || ""}
additionalContent={
apiRequestFailedMessage?.toLowerCase().includes("powershell") ? (
<>
<br />
<br />
{t("chat:powershell.issues")}{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{ color: "inherit", textDecoration: "underline" }}>
troubleshooting guide
</a>
.
</>
) : undefined
docsURL={
apiRequestFailedMessage?.toLowerCase().includes("powershell")
? "https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
: undefined
}
/>
)}
</>
)
case "api_req_retry_delayed":
let body = t(`chat:apiRequest.failed`)
let retryInfo, code, docsURL
if (message.text !== undefined) {
// Try to show richer error message for that code, if available
if (parseInt(message.text.substring(0, 3)) >= 400) {
code = parseInt(message.text)
const stringForError = `chat:apiRequest.errorMessage.${code}`
if (i18n.exists(stringForError)) {
body = t(stringForError)
// Fill this out in upcoming PRs
// Do not remove this
// switch(code) {
// case ERROR_CODE:
// docsURL = ???
// break;
// }
} else {
body = t("chat:apiRequest.errorMessage.unknown")
docsURL = "mailto:support@roocode.com?subject=Unknown API Error"
}
retryInfo = (
<p className="mt-1 font-light text-xs text-vscode-errorForeground/80 cursor-default">
{message.text.substring(4)}
</p>
)
}
}
return (
<ErrorRow
type="api_req_retry_delayed"
code={code}
message={body}
docsURL={docsURL}
additionalContent={retryInfo}
/>
)
case "api_req_finished":
return null // we should never see this message type
case "text":
Expand Down Expand Up @@ -1258,7 +1285,13 @@ export const ChatRowContent = ({
}
return message.contextCondense ? <ContextCondenseRow {...message.contextCondense} /> : null
case "condense_context_error":
return <CondenseContextErrorRow errorText={message.text} />
return (
<ErrorRow
type="error"
title={t("chat:contextCondense.errorHeader")}
message={message.text || ""}
/>
)
case "codebase_search_result":
let parsed: {
content: {
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/CommandExecutionError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export const CommandExecutionError = () => {
}, [])

return (
<div className="text-sm bg-vscode-editor-background border border-vscode-border rounded-xs p-2">
<div className="text-sm bg-vscode-editor-background border border-vscode-border rounded-lg p-3 ml-6">
<div className="flex flex-col gap-2">
<div className="flex items-center">
<i className="codicon codicon-warning mr-1 text-vscode-editorWarning-foreground" />
<span className="text-vscode-editorWarning-foreground font-medium">
<span className="text-vscode-editorWarning-foreground font-semibold">
{t("chat:shellIntegration.title")}
</span>
</div>
Expand Down
13 changes: 0 additions & 13 deletions webview-ui/src/components/chat/ContextCondenseRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,3 @@ export const CondensingContextRow = () => {
</div>
)
}

export const CondenseContextErrorRow = ({ errorText }: { errorText?: string }) => {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="codicon codicon-warning text-vscode-editorWarning-foreground opacity-80 text-base -mb-0.5"></span>
<span className="font-bold text-vscode-foreground">{t("chat:contextCondense.errorHeader")}</span>
</div>
<span className="text-vscode-descriptionForeground text-sm">{errorText}</span>
</div>
)
}
110 changes: 88 additions & 22 deletions webview-ui/src/components/chat/ErrorRow.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,57 @@
import React, { useState, useCallback, memo } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { MessageCircleWarning } from "lucide-react"
import { BookOpenText, MessageCircleWarning } from "lucide-react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { vscode } from "@src/utils/vscode"
import CodeBlock from "../common/CodeBlock"

/**
* Unified error display component for all error types in the chat.
* Provides consistent styling, icons, and optional documentation links across all errors.
*
* @param type - Error type determines icon and default title
* @param title - Optional custom title (overrides default for error type)
* @param message - Error message text (required)
* @param docsURL - Optional documentation link URL (shown as "Learn more" with book icon)
* @param showCopyButton - Whether to show copy button for error message
* @param expandable - Whether error content can be expanded/collapsed
* @param defaultExpanded - Whether expandable content starts expanded
* @param additionalContent - Optional React nodes to render after message
* @param headerClassName - Custom CSS classes for header section
* @param messageClassName - Custom CSS classes for message section
*
* @example
* // Simple error
* <ErrorRow type="error" message="File not found" />
*
* @example
* // Error with documentation link
* <ErrorRow
* type="api_failure"
* message="API key missing"
* docsURL="https://docs.example.com/api-setup"
* />
*
* @example
* // Expandable error with code
* <ErrorRow
* type="diff_error"
* message="Patch failed to apply"
* expandable={true}
* defaultExpanded={false}
* additionalContent={<pre>{errorDetails}</pre>}
* />
*/
export interface ErrorRowProps {
type: "error" | "mistake_limit" | "api_failure" | "diff_error" | "streaming_failed" | "cancelled"
type:
| "error"
| "mistake_limit"
| "api_failure"
| "diff_error"
| "streaming_failed"
| "cancelled"
| "api_req_retry_delayed"
title?: string
message: string
showCopyButton?: boolean
Expand All @@ -15,6 +60,8 @@ export interface ErrorRowProps {
additionalContent?: React.ReactNode
headerClassName?: string
messageClassName?: string
code?: number
docsURL?: string // NEW: Optional documentation link
}

/**
Expand All @@ -31,6 +78,8 @@ export const ErrorRow = memo(
additionalContent,
headerClassName,
messageClassName,
docsURL,
code,
}: ErrorRowProps) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
Expand All @@ -48,6 +97,8 @@ export const ErrorRow = memo(
return t("chat:troubleMessage")
case "api_failure":
return t("chat:apiRequest.failed")
case "api_req_retry_delayed":
return t("chat:apiRequest.errorTitle", { code: code ? ` · ${code}` : "" })
case "streaming_failed":
return t("chat:apiRequest.streamingFailed")
case "cancelled":
Expand Down Expand Up @@ -84,17 +135,17 @@ export const ErrorRow = memo(
// For diff_error type with expandable content
if (type === "diff_error" && expandable) {
return (
<div className="mt-0 overflow-hidden mb-2">
<div className="mt-0 overflow-hidden mb-2 pr-1 group">
<div
className={`font-normal text-vscode-editor-foreground flex items-center justify-between cursor-pointer ${
isExpanded ? "border-b border-vscode-editorGroup-border" : ""
}`}
className="font-sm text-vscode-editor-foreground flex items-center justify-between cursor-pointer"
onClick={handleToggleExpand}>
<div className="flex items-center gap-2 flex-grow">
<MessageCircleWarning className="w-4 text-vscode-errorForeground" />
<span className="font-bold">{errorTitle}</span>
<div className="flex items-center gap-2 flex-grow text-vscode-errorForeground">
<MessageCircleWarning className="w-4" />
<span className="text-vscode-errorForeground font-bold grow cursor-pointer">
{errorTitle}
</span>
</div>
<div className="flex items-center">
<div className="flex items-center transition-opacity opacity-0 group-hover:opacity-100">
{showCopyButton && (
<VSCodeButton
appearance="icon"
Expand All @@ -107,7 +158,7 @@ export const ErrorRow = memo(
</div>
</div>
{isExpanded && (
<div className="p-2 bg-vscode-editor-background border-t-0">
<div className="px-2 py-1 mt-2 bg-vscode-editor-background ml-6 rounded-lg">
<CodeBlock source={message} language="xml" />
</div>
)}
Expand All @@ -117,21 +168,36 @@ export const ErrorRow = memo(

// Standard error display
return (
<>
<div className="group pr-2">
{errorTitle && (
<div className={headerClassName || "flex items-center gap-2 break-words"}>
<div className={headerClassName || "flex items-center justify-between gap-2 break-words"}>
<MessageCircleWarning className="w-4 text-vscode-errorForeground" />
<span className="text-vscode-errorForeground font-bold">{errorTitle}</span>
<span className="text-vscode-errorForeground font-bold grow cursor-default">{errorTitle}</span>
{docsURL && (
<a
href={docsURL}
className="text-sm flex items-center gap-1 transition-opacity opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.preventDefault()
vscode.postMessage({ type: "openExternal", url: docsURL })
}}>
<BookOpenText className="size-3 mt-[3px]" />
{t("chat:apiRequest.errorMessage.docs")}
</a>
)}
</div>
)}
<p
className={
messageClassName || "ml-6 my-0 whitespace-pre-wrap break-words text-vscode-errorForeground"
}>
{message}
</p>
{additionalContent}
</>
<div className="pl-6 py-1">
<p
className={
messageClassName ||
"my-0 font-light whitespace-pre-wrap break-words text-vscode-errorForeground"
}>
{message}
</p>
{additionalContent}
</div>
</div>
)
},
)
Expand Down
13 changes: 12 additions & 1 deletion webview-ui/src/i18n/locales/ca/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion webview-ui/src/i18n/locales/de/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading