Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3903,7 +3903,7 @@ 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}\n↻ ${i}s...`, undefined, true)
await this.say("api_req_retry_delayed", `${headerText}<retry_timer>${i}</retry_timer>`, undefined, true)
await delay(1000)
}

Expand Down
37 changes: 27 additions & 10 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
FolderTree,
TerminalSquare,
MessageCircle,
Repeat2,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
Expand Down Expand Up @@ -1110,11 +1111,11 @@ export const ChatRowContent = ({
)
case "api_req_retry_delayed":
let body = t(`chat:apiRequest.failed`)
let retryInfo, code, docsURL
let retryInfo, rawError, code, docsURL
if (message.text !== undefined) {
// Try to show richer error message for that code, if available
const potentialCode = parseInt(message.text.substring(0, 3))
if (potentialCode >= 400) {
if (!isNaN(potentialCode) && potentialCode >= 400) {
code = potentialCode
const stringForError = `chat:apiRequest.errorMessage.${code}`
if (i18n.exists(stringForError)) {
Expand All @@ -1130,15 +1131,30 @@ export const ChatRowContent = ({
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>
)
} else if (message.text.indexOf("Connection error") === 0) {
body = t("chat:apiRequest.errorMessage.connection")
} else {
// Non-HTTP-status-code error message - display the actual error text
body = message.text
// Non-HTTP-status-code error message - store full text as errorDetails
body = t("chat:apiRequest.errorMessage.unknown")
docsURL = "mailto:support@roocode.com?subject=Unknown API Error"
}

// This isn't pretty, but since the retry logic happens at a lower level
// and the message object is just a flat string, we need to extract the
// retry information using this "tag" as a convention
const retryTimerMatch = message.text.match(/<retry_timer>(.*?)<\/retry_timer>/)
const retryTimer = retryTimerMatch && retryTimerMatch[1] ? parseInt(retryTimerMatch[1], 10) : 0
rawError = message.text.replace(/<retry_timer>(.*?)<\/retry_timer>/, "").trim()
retryInfo = retryTimer > 0 && (
<p
className={cn(
"mt-2 font-light text-xs text-vscode-descriptionForeground cursor-default flex items-center gap-1 transition-all duration-1000",
retryTimer === 0 ? "opacity-0 max-h-0" : "max-h-2 opacity-100",
)}>
<Repeat2 className="size-3" strokeWidth={1.5} />
<span>{retryTimer}s</span>
</p>
)
}
return (
<ErrorRow
Expand All @@ -1147,6 +1163,7 @@ export const ChatRowContent = ({
message={body}
docsURL={docsURL}
additionalContent={retryInfo}
errorDetails={rawError}
/>
)
case "api_req_finished":
Expand Down Expand Up @@ -1259,7 +1276,7 @@ export const ChatRowContent = ({
</div>
)
case "error":
return <ErrorRow type="error" message={message.text || ""} />
return <ErrorRow type="error" message={t("chat:error")} errorDetails={message.text || undefined} />
case "completion_result":
return (
<>
Expand Down
130 changes: 100 additions & 30 deletions webview-ui/src/components/chat/ErrorRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useState, useCallback, memo } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { BookOpenText, MessageCircleWarning } from "lucide-react"
import { BookOpenText, MessageCircleWarning, Info, Copy, Check } from "lucide-react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { vscode } from "@src/utils/vscode"
import CodeBlock from "../common/CodeBlock"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@src/components/ui/dialog"
import { Button, Tooltip, TooltipContent, TooltipTrigger } from "../ui"

/**
* Unified error display component for all error types in the chat.
Expand Down Expand Up @@ -61,7 +63,8 @@ export interface ErrorRowProps {
headerClassName?: string
messageClassName?: string
code?: number
docsURL?: string // NEW: Optional documentation link
docsURL?: string // Optional documentation link
errorDetails?: string // Optional detailed error message shown in modal
}

/**
Expand All @@ -80,10 +83,13 @@ export const ErrorRow = memo(
messageClassName,
docsURL,
code,
errorDetails,
}: ErrorRowProps) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false)
const [showDetailsCopySuccess, setShowDetailsCopySuccess] = useState(false)
const { copyWithFeedback } = useCopyToClipboard()

// Default titles for different error types
Expand Down Expand Up @@ -130,6 +136,22 @@ export const ErrorRow = memo(
[message, copyWithFeedback],
)

const handleCopyDetails = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation()
if (errorDetails) {
const success = await copyWithFeedback(errorDetails)
if (success) {
setShowDetailsCopySuccess(true)
setTimeout(() => {
setShowDetailsCopySuccess(false)
}, 1000)
}
}
},
[errorDetails, copyWithFeedback],
)

const errorTitle = getDefaultTitle()

// For diff_error type with expandable content
Expand Down Expand Up @@ -168,36 +190,84 @@ export const ErrorRow = memo(

// Standard error display
return (
<div className="group pr-2">
{errorTitle && (
<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 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 className="group pr-2">
{errorTitle && (
<div className={headerClassName || "flex items-center justify-between gap-2 break-words"}>
<MessageCircleWarning className="w-4 text-vscode-errorForeground" />
<span className="font-bold grow cursor-default">{errorTitle}</span>
<div className="flex items-center gap-2">
{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>
)}
{errorDetails && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setIsDetailsDialogOpen(true)}
className="transition-opacity opacity-0 group-hover:opacity-100 cursor-pointer"
aria-label={t("chat:errorDetails.title")}>
<Info className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent>{t("chat:errorDetails.title")}</TooltipContent>
</Tooltip>
)}
</div>
</div>
)}
<div className="ml-2 pl-4 mt-1 pt-1 border-l border-vscode-errorForeground/50">
<p
className={
messageClassName ||
"my-0 font-light whitespace-pre-wrap break-words text-vscode-descriptionForeground"
}>
{message}
</p>
{additionalContent}
</div>
)}
<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>

{/* Error Details Dialog */}
{errorDetails && (
<Dialog open={isDetailsDialogOpen} onOpenChange={setIsDetailsDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{t("chat:errorDetails.title")}</DialogTitle>
</DialogHeader>
<div className="max-h-96 overflow-auto px-3 bg-vscode-editor-background rounded-xl border border-vscode-editorGroup-border">
<pre className="font-mono text-sm whitespace-pre-wrap break-words bg-transparent">
{errorDetails}
</pre>
</div>
<DialogFooter>
<Button variant="secondary" onClick={handleCopyDetails}>
{showDetailsCopySuccess ? (
<>
<Check className="size-3" />
{t("chat:errorDetails.copied")}
</>
) : (
<>
<Copy className="size-3" />
{t("chat:errorDetails.copyToClipboard")}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
)
},
)
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold my-0", className)}
className={cn("text-lg leading-none font-semibold my-0 cursor-default", className)}
{...props}
/>
)
Expand Down
8 changes: 7 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.

8 changes: 7 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.

6 changes: 6 additions & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"403": "Unauthorized. Your API key is valid, but the provider refused to complete this request.",
"429": "Too many requests. You're being rate-limited by the provider. Please wait a bit before your next API call.",
"500": "Provider server error. Something is wrong on the provider side, there's nothing wrong with your request.",
"connection": "Connection error. Make sure you have a working internet connection.",
"unknown": "Unknown API error. Please contact Roo Code support."
}
},
Expand Down Expand Up @@ -285,6 +286,11 @@
},
"taskCompleted": "Task Completed",
"error": "Error",
"errorDetails": {
"title": "Error Details",
"copyToClipboard": "Copy to Clipboard",
"copied": "Copied!"
},
"diffError": {
"title": "Edit Unsuccessful"
},
Expand Down
8 changes: 7 additions & 1 deletion webview-ui/src/i18n/locales/es/chat.json

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

8 changes: 7 additions & 1 deletion webview-ui/src/i18n/locales/fr/chat.json

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

8 changes: 7 additions & 1 deletion webview-ui/src/i18n/locales/hi/chat.json

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

Loading
Loading