import React, { useState, useCallback, memo, useMemo } from "react" import { useTranslation } from "react-i18next" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { BookOpenText, MessageCircleWarning, Copy, Check, Microscope, Info } 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 } from "../ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import { PROVIDERS } from "../settings/constants" /** * 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 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 * * * @example * // Error with documentation link * * * @example * // Expandable error with code * {errorDetails}} * /> */ export interface ErrorRowProps { type: | "error" | "mistake_limit" | "api_failure" | "diff_error" | "streaming_failed" | "cancelled" | "api_req_retry_delayed" title?: string message: string showCopyButton?: boolean expandable?: boolean defaultExpanded?: boolean additionalContent?: React.ReactNode headerClassName?: string messageClassName?: string code?: number docsURL?: string // Optional documentation link errorDetails?: string // Optional detailed error message shown in modal } /** * Unified error display component for all error types in the chat */ export const ErrorRow = memo( ({ type, title, message, showCopyButton = false, expandable = false, defaultExpanded = false, additionalContent, headerClassName, 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() const { version, apiConfiguration } = useExtensionState() const { provider, id: modelId } = useSelectedModel(apiConfiguration) const usesProxy = PROVIDERS.find((p) => p.value === provider)?.proxy ?? false // Format error details with metadata prepended const formattedErrorDetails = useMemo(() => { if (!errorDetails) return undefined const metadata = [ `Date/time: ${new Date().toISOString()}`, `Extension version: ${version}`, `Provider: ${provider}${usesProxy ? " (proxy)" : ""}`, `Model: ${modelId}`, "", "", ].join("\n") return metadata + errorDetails }, [errorDetails, version, provider, modelId, usesProxy]) const handleDownloadDiagnostics = useCallback( (e: React.MouseEvent) => { e.stopPropagation() vscode.postMessage({ type: "downloadErrorDiagnostics", values: { timestamp: new Date().toISOString(), version, provider, model: modelId, details: errorDetails || "", }, }) }, [version, provider, modelId, errorDetails], ) // Default titles for different error types const getDefaultTitle = () => { if (title) return title switch (type) { case "error": return t("chat:error") case "mistake_limit": 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": return t("chat:apiRequest.cancelled") case "diff_error": return t("chat:diffError.title") default: return null } } const handleToggleExpand = useCallback(() => { if (expandable) { setIsExpanded(!isExpanded) } }, [expandable, isExpanded]) const handleCopy = useCallback( async (e: React.MouseEvent) => { e.stopPropagation() const success = await copyWithFeedback(message) if (success) { setShowCopySuccess(true) setTimeout(() => { setShowCopySuccess(false) }, 1000) } }, [message, copyWithFeedback], ) const handleCopyDetails = useCallback( async (e: React.MouseEvent) => { e.stopPropagation() if (formattedErrorDetails) { const success = await copyWithFeedback(formattedErrorDetails) if (success) { setShowDetailsCopySuccess(true) setTimeout(() => { setShowDetailsCopySuccess(false) }, 1000) } } }, [formattedErrorDetails, copyWithFeedback], ) const errorTitle = getDefaultTitle() // For diff_error type with expandable content if (type === "diff_error" && expandable) { return (
{errorTitle}
{showCopyButton && ( )}
{isExpanded && (
)}
) } // Standard error display return ( <>
{errorTitle && (
{errorTitle}
)}

{message} {formattedErrorDetails && ( )}

{additionalContent}
{/* Error Details Dialog */} {formattedErrorDetails && ( {t("chat:errorDetails.title")}
									{formattedErrorDetails}
								
{usesProxy && (
{t("chat:errorDetails.proxyProvider")}
)}
)} ) }, ) export default ErrorRow