Skip to content

Commit ab18bf3

Browse files
roomote[bot]roomotebrunobergher
authored
feat: add error details modal with on-demand display (#9985)
* feat: add error details modal with on-demand display - Add errorDetails prop to ErrorRow component - Show Info icon on hover in error header when errorDetails is provided - Display detailed error message in modal dialog on Info icon click - Add Copy to Clipboard button in error details modal - Update generic error case to show localized message with details on demand - Add i18n translations for error details UI * UI Tweaks * Properly handles error details * i18n * Lighter visual treatment for errors --------- Co-authored-by: Roo Code <[email protected]> Co-authored-by: Bruno Bergher <[email protected]>
1 parent 36ef603 commit ab18bf3

File tree

22 files changed

+254
-59
lines changed

22 files changed

+254
-59
lines changed

src/core/task/Task.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3903,7 +3903,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39033903
throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`)
39043904
}
39053905

3906-
await this.say("api_req_retry_delayed", `${headerText}\n↻ ${i}s...`, undefined, true)
3906+
await this.say("api_req_retry_delayed", `${headerText}<retry_timer>${i}</retry_timer>`, undefined, true)
39073907
await delay(1000)
39083908
}
39093909

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

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
FolderTree,
6060
TerminalSquare,
6161
MessageCircle,
62+
Repeat2,
6263
} from "lucide-react"
6364
import { cn } from "@/lib/utils"
6465
import { PathTooltip } from "../ui/PathTooltip"
@@ -1110,11 +1111,11 @@ export const ChatRowContent = ({
11101111
)
11111112
case "api_req_retry_delayed":
11121113
let body = t(`chat:apiRequest.failed`)
1113-
let retryInfo, code, docsURL
1114+
let retryInfo, rawError, code, docsURL
11141115
if (message.text !== undefined) {
11151116
// Try to show richer error message for that code, if available
11161117
const potentialCode = parseInt(message.text.substring(0, 3))
1117-
if (potentialCode >= 400) {
1118+
if (!isNaN(potentialCode) && potentialCode >= 400) {
11181119
code = potentialCode
11191120
const stringForError = `chat:apiRequest.errorMessage.${code}`
11201121
if (i18n.exists(stringForError)) {
@@ -1130,15 +1131,30 @@ export const ChatRowContent = ({
11301131
body = t("chat:apiRequest.errorMessage.unknown")
11311132
docsURL = "mailto:[email protected]?subject=Unknown API Error"
11321133
}
1133-
retryInfo = (
1134-
<p className="mt-1 font-light text-xs text-vscode-errorForeground/80 cursor-default">
1135-
{message.text.substring(4)}
1136-
</p>
1137-
)
1134+
} else if (message.text.indexOf("Connection error") === 0) {
1135+
body = t("chat:apiRequest.errorMessage.connection")
11381136
} else {
1139-
// Non-HTTP-status-code error message - display the actual error text
1140-
body = message.text
1137+
// Non-HTTP-status-code error message - store full text as errorDetails
1138+
body = t("chat:apiRequest.errorMessage.unknown")
1139+
docsURL = "mailto:[email protected]?subject=Unknown API Error"
11411140
}
1141+
1142+
// This isn't pretty, but since the retry logic happens at a lower level
1143+
// and the message object is just a flat string, we need to extract the
1144+
// retry information using this "tag" as a convention
1145+
const retryTimerMatch = message.text.match(/<retry_timer>(.*?)<\/retry_timer>/)
1146+
const retryTimer = retryTimerMatch && retryTimerMatch[1] ? parseInt(retryTimerMatch[1], 10) : 0
1147+
rawError = message.text.replace(/<retry_timer>(.*?)<\/retry_timer>/, "").trim()
1148+
retryInfo = retryTimer > 0 && (
1149+
<p
1150+
className={cn(
1151+
"mt-2 font-light text-xs text-vscode-descriptionForeground cursor-default flex items-center gap-1 transition-all duration-1000",
1152+
retryTimer === 0 ? "opacity-0 max-h-0" : "max-h-2 opacity-100",
1153+
)}>
1154+
<Repeat2 className="size-3" strokeWidth={1.5} />
1155+
<span>{retryTimer}s</span>
1156+
</p>
1157+
)
11421158
}
11431159
return (
11441160
<ErrorRow
@@ -1147,6 +1163,7 @@ export const ChatRowContent = ({
11471163
message={body}
11481164
docsURL={docsURL}
11491165
additionalContent={retryInfo}
1166+
errorDetails={rawError}
11501167
/>
11511168
)
11521169
case "api_req_finished":
@@ -1259,7 +1276,7 @@ export const ChatRowContent = ({
12591276
</div>
12601277
)
12611278
case "error":
1262-
return <ErrorRow type="error" message={message.text || ""} />
1279+
return <ErrorRow type="error" message={t("chat:error")} errorDetails={message.text || undefined} />
12631280
case "completion_result":
12641281
return (
12651282
<>

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

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { useState, useCallback, memo } from "react"
22
import { useTranslation } from "react-i18next"
33
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
4-
import { BookOpenText, MessageCircleWarning } from "lucide-react"
4+
import { BookOpenText, MessageCircleWarning, Info, Copy, Check } from "lucide-react"
55
import { useCopyToClipboard } from "@src/utils/clipboard"
66
import { vscode } from "@src/utils/vscode"
77
import CodeBlock from "../common/CodeBlock"
8+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@src/components/ui/dialog"
9+
import { Button, Tooltip, TooltipContent, TooltipTrigger } from "../ui"
810

911
/**
1012
* Unified error display component for all error types in the chat.
@@ -61,7 +63,8 @@ export interface ErrorRowProps {
6163
headerClassName?: string
6264
messageClassName?: string
6365
code?: number
64-
docsURL?: string // NEW: Optional documentation link
66+
docsURL?: string // Optional documentation link
67+
errorDetails?: string // Optional detailed error message shown in modal
6568
}
6669

6770
/**
@@ -80,10 +83,13 @@ export const ErrorRow = memo(
8083
messageClassName,
8184
docsURL,
8285
code,
86+
errorDetails,
8387
}: ErrorRowProps) => {
8488
const { t } = useTranslation()
8589
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
8690
const [showCopySuccess, setShowCopySuccess] = useState(false)
91+
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false)
92+
const [showDetailsCopySuccess, setShowDetailsCopySuccess] = useState(false)
8793
const { copyWithFeedback } = useCopyToClipboard()
8894

8995
// Default titles for different error types
@@ -130,6 +136,22 @@ export const ErrorRow = memo(
130136
[message, copyWithFeedback],
131137
)
132138

139+
const handleCopyDetails = useCallback(
140+
async (e: React.MouseEvent) => {
141+
e.stopPropagation()
142+
if (errorDetails) {
143+
const success = await copyWithFeedback(errorDetails)
144+
if (success) {
145+
setShowDetailsCopySuccess(true)
146+
setTimeout(() => {
147+
setShowDetailsCopySuccess(false)
148+
}, 1000)
149+
}
150+
}
151+
},
152+
[errorDetails, copyWithFeedback],
153+
)
154+
133155
const errorTitle = getDefaultTitle()
134156

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

169191
// Standard error display
170192
return (
171-
<div className="group pr-2">
172-
{errorTitle && (
173-
<div className={headerClassName || "flex items-center justify-between gap-2 break-words"}>
174-
<MessageCircleWarning className="w-4 text-vscode-errorForeground" />
175-
<span className="text-vscode-errorForeground font-bold grow cursor-default">{errorTitle}</span>
176-
{docsURL && (
177-
<a
178-
href={docsURL}
179-
className="text-sm flex items-center gap-1 transition-opacity opacity-0 group-hover:opacity-100"
180-
onClick={(e) => {
181-
e.preventDefault()
182-
vscode.postMessage({ type: "openExternal", url: docsURL })
183-
}}>
184-
<BookOpenText className="size-3 mt-[3px]" />
185-
{t("chat:apiRequest.errorMessage.docs")}
186-
</a>
187-
)}
193+
<>
194+
<div className="group pr-2">
195+
{errorTitle && (
196+
<div className={headerClassName || "flex items-center justify-between gap-2 break-words"}>
197+
<MessageCircleWarning className="w-4 text-vscode-errorForeground" />
198+
<span className="font-bold grow cursor-default">{errorTitle}</span>
199+
<div className="flex items-center gap-2">
200+
{docsURL && (
201+
<a
202+
href={docsURL}
203+
className="text-sm flex items-center gap-1 transition-opacity opacity-0 group-hover:opacity-100"
204+
onClick={(e) => {
205+
e.preventDefault()
206+
vscode.postMessage({ type: "openExternal", url: docsURL })
207+
}}>
208+
<BookOpenText className="size-3 mt-[3px]" />
209+
{t("chat:apiRequest.errorMessage.docs")}
210+
</a>
211+
)}
212+
{errorDetails && (
213+
<Tooltip>
214+
<TooltipTrigger asChild>
215+
<button
216+
onClick={() => setIsDetailsDialogOpen(true)}
217+
className="transition-opacity opacity-0 group-hover:opacity-100 cursor-pointer"
218+
aria-label={t("chat:errorDetails.title")}>
219+
<Info className="size-4" />
220+
</button>
221+
</TooltipTrigger>
222+
<TooltipContent>{t("chat:errorDetails.title")}</TooltipContent>
223+
</Tooltip>
224+
)}
225+
</div>
226+
</div>
227+
)}
228+
<div className="ml-2 pl-4 mt-1 pt-1 border-l border-vscode-errorForeground/50">
229+
<p
230+
className={
231+
messageClassName ||
232+
"my-0 font-light whitespace-pre-wrap break-words text-vscode-descriptionForeground"
233+
}>
234+
{message}
235+
</p>
236+
{additionalContent}
188237
</div>
189-
)}
190-
<div className="pl-6 py-1">
191-
<p
192-
className={
193-
messageClassName ||
194-
"my-0 font-light whitespace-pre-wrap break-words text-vscode-errorForeground"
195-
}>
196-
{message}
197-
</p>
198-
{additionalContent}
199238
</div>
200-
</div>
239+
240+
{/* Error Details Dialog */}
241+
{errorDetails && (
242+
<Dialog open={isDetailsDialogOpen} onOpenChange={setIsDetailsDialogOpen}>
243+
<DialogContent className="max-w-2xl">
244+
<DialogHeader>
245+
<DialogTitle>{t("chat:errorDetails.title")}</DialogTitle>
246+
</DialogHeader>
247+
<div className="max-h-96 overflow-auto px-3 bg-vscode-editor-background rounded-xl border border-vscode-editorGroup-border">
248+
<pre className="font-mono text-sm whitespace-pre-wrap break-words bg-transparent">
249+
{errorDetails}
250+
</pre>
251+
</div>
252+
<DialogFooter>
253+
<Button variant="secondary" onClick={handleCopyDetails}>
254+
{showDetailsCopySuccess ? (
255+
<>
256+
<Check className="size-3" />
257+
{t("chat:errorDetails.copied")}
258+
</>
259+
) : (
260+
<>
261+
<Copy className="size-3" />
262+
{t("chat:errorDetails.copyToClipboard")}
263+
</>
264+
)}
265+
</Button>
266+
</DialogFooter>
267+
</DialogContent>
268+
</Dialog>
269+
)}
270+
</>
201271
)
202272
},
203273
)

webview-ui/src/components/ui/dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
7878
return (
7979
<DialogPrimitive.Title
8080
data-slot="dialog-title"
81-
className={cn("text-lg leading-none font-semibold my-0", className)}
81+
className={cn("text-lg leading-none font-semibold my-0 cursor-default", className)}
8282
{...props}
8383
/>
8484
)

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"403": "Unauthorized. Your API key is valid, but the provider refused to complete this request.",
153153
"429": "Too many requests. You're being rate-limited by the provider. Please wait a bit before your next API call.",
154154
"500": "Provider server error. Something is wrong on the provider side, there's nothing wrong with your request.",
155+
"connection": "Connection error. Make sure you have a working internet connection.",
155156
"unknown": "Unknown API error. Please contact Roo Code support."
156157
}
157158
},
@@ -285,6 +286,11 @@
285286
},
286287
"taskCompleted": "Task Completed",
287288
"error": "Error",
289+
"errorDetails": {
290+
"title": "Error Details",
291+
"copyToClipboard": "Copy to Clipboard",
292+
"copied": "Copied!"
293+
},
288294
"diffError": {
289295
"title": "Edit Unsuccessful"
290296
},

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/chat.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/hi/chat.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)