diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5030055fea..02d0bae85f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -994,6 +994,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.0(react@18.3.1) react-i18next: specifier: ^15.4.1 version: 15.5.1(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) @@ -8047,6 +8050,11 @@ packages: peerDependencies: react: ^18.3.1 + react-error-boundary@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + react-hook-form@7.57.0: resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==} engines: {node: '>=18.0.0'} @@ -11120,14 +11128,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -13295,7 +13303,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -14494,7 +14502,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 csstype: 3.1.3 dom-serializer@2.0.0: @@ -15931,7 +15939,7 @@ snapshots: is-it-type@5.1.2: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 globalthis: 1.0.4 is-map@2.0.3: {} @@ -17931,6 +17939,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@6.0.0(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + react-hook-form@7.57.0(react@18.3.1): dependencies: react: 18.3.1 @@ -18031,7 +18044,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -18355,7 +18368,7 @@ snapshots: rtl-css-js@1.16.1: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.27.6 run-applescript@7.0.0: {} diff --git a/webview-ui/package.json b/webview-ui/package.json index 4c6edc7a2b..4cc998e837 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -54,6 +54,7 @@ "pretty-bytes": "^7.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^6.0.0", "react-i18next": "^15.4.1", "react-markdown": "^9.0.3", "react-remark": "^2.1.0", diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 332ef18511..d71868b6a6 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -22,6 +22,7 @@ import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" +import { ErrorBoundary } from "./components/common/ErrorBoundary" type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -169,44 +170,68 @@ const App = () => { // Do not conditionally load ChatView, it's expensive and there's state we // don't want to lose (user input, disableInput, askResponse promise, etc.) return showWelcome ? ( - + + + ) : ( <> - {tab === "modes" && switchTab("chat")} />} - {tab === "mcp" && switchTab("chat")} />} - {tab === "history" && switchTab("chat")} />} + {tab === "modes" && ( + + switchTab("chat")} /> + + )} + {tab === "mcp" && ( + + switchTab("chat")} /> + + )} + {tab === "history" && ( + + switchTab("chat")} /> + + )} {tab === "settings" && ( - setTab("chat")} targetSection={currentSection} /> + + setTab("chat")} targetSection={currentSection} /> + )} {tab === "marketplace" && ( - switchTab("chat")} - targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined} - /> + + switchTab("chat")} + targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined} + /> + )} {tab === "account" && ( - switchTab("chat")} - /> + + switchTab("chat")} + /> + )} - setShowAnnouncement(false)} - /> - setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} - onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} - onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} - /> + + setShowAnnouncement(false)} + /> + + + setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} + onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} + onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} + /> + ) } @@ -214,15 +239,17 @@ const App = () => { const queryClient = new QueryClient() const AppWithProviders = () => ( - - - - - - - - - + + + + + + + + + + + ) export default AppWithProviders diff --git a/webview-ui/src/components/common/ErrorBoundary.tsx b/webview-ui/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000000..12cef5135a --- /dev/null +++ b/webview-ui/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,82 @@ +import React from "react" +import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary" +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { useTranslation } from "react-i18next" +import { errorReporter } from "../../utils/errorReporting" + +interface ErrorFallbackProps extends FallbackProps { + componentName?: string +} + +function ErrorFallback({ error, resetErrorBoundary, componentName }: ErrorFallbackProps) { + const { t } = useTranslation("common") + + return ( +
+
+ +

+ {t("errorBoundary.title", "Something went wrong")} +

+
+ + {componentName && ( +

+ {t("errorBoundary.componentError", "Error in {{componentName}} component", { componentName })} +

+ )} + +

+ {t( + "errorBoundary.description", + "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + )} +

+ +
+ + {t("errorBoundary.showDetails", "Show error details")} + +
+					{error.message}
+					{error.stack && (
+						<>
+							{"\n\n"}
+							{error.stack}
+						
+					)}
+				
+
+ + + {t("errorBoundary.retry", "Try again")} + +
+ ) +} + +interface ErrorBoundaryProps { + children: React.ReactNode + componentName?: string + onError?: (error: Error, errorInfo: React.ErrorInfo) => void +} + +export function ErrorBoundary({ children, componentName, onError }: ErrorBoundaryProps) { + const handleError = (error: Error, errorInfo: React.ErrorInfo) => { + // Report error using our error reporting utility + errorReporter.reportError(error, errorInfo, componentName) + + // Call custom error handler if provided (for potential Sentry integration) + onError?.(error, errorInfo) + } + + return ( + } + onError={handleError}> + {children} + + ) +} + +export default ErrorBoundary diff --git a/webview-ui/src/components/common/__tests__/ErrorBoundary.spec.tsx b/webview-ui/src/components/common/__tests__/ErrorBoundary.spec.tsx new file mode 100644 index 0000000000..958d64a0d9 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/ErrorBoundary.spec.tsx @@ -0,0 +1,142 @@ +import React from "react" +import { render, screen, fireEvent } from "@testing-library/react" +import { vi, beforeEach, afterEach, describe, it, expect } from "vitest" +import { ErrorBoundary } from "../ErrorBoundary" +import { errorReporter } from "../../../utils/errorReporting" + +// Mock the error reporter +vi.mock("../../../utils/errorReporting", () => ({ + errorReporter: { + reportError: vi.fn(), + }, +})) + +// Mock react-i18next +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, defaultValue?: string, options?: any) => { + // Handle interpolation for componentError + if (key === "errorBoundary.componentError" && options?.componentName) { + return `Error in ${options.componentName} component` + } + return defaultValue || key + }, + }), +})) + +// Component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error("Test error") + } + return
No error
+} + +describe("ErrorBoundary", () => { + beforeEach(() => { + vi.clearAllMocks() + // Suppress console.error for these tests + vi.spyOn(console, "error").mockImplementation(() => {}) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("renders children when there is no error", () => { + render( + + + , + ) + + expect(screen.getByText("No error")).toBeInTheDocument() + }) + + it("renders error fallback when there is an error", () => { + render( + + + , + ) + + expect(screen.getByText("Something went wrong")).toBeInTheDocument() + expect(screen.getByText("Error in TestComponent component")).toBeInTheDocument() + expect(screen.getByText("Try again")).toBeInTheDocument() + }) + + it("shows error details when expanded", () => { + render( + + + , + ) + + const detailsButton = screen.getByText("Show error details") + fireEvent.click(detailsButton) + + // Look for the error message in the details section (it's part of a larger text block) + expect(screen.getByText(/Test error/)).toBeInTheDocument() + }) + + it("calls error reporter when error occurs", () => { + const mockReportError = errorReporter.reportError as ReturnType + + render( + + + , + ) + + expect(mockReportError).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Test error", + }), + expect.any(Object), + "TestComponent", + ) + }) + + it("calls custom onError handler when provided", () => { + const mockOnError = vi.fn() + + render( + + + , + ) + + expect(mockOnError).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Test error", + }), + expect.any(Object), + ) + }) + + it("resets error boundary when retry button is clicked", () => { + const TestComponent = () => { + const [shouldThrow, setShouldThrow] = React.useState(true) + + return ( + + + + + ) + } + + render() + + // Error should be shown initially + expect(screen.getByText("Something went wrong")).toBeInTheDocument() + + // Click retry button + const retryButton = screen.getByText("Try again") + fireEvent.click(retryButton) + + // Component should be reset and try to render again + // Since we haven't fixed the error, it should show the error again + expect(screen.getByText("Something went wrong")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/common.json b/webview-ui/src/i18n/locales/ca/common.json index 267e0a62d7..b4f4fbfd75 100644 --- a/webview-ui/src/i18n/locales/ca/common.json +++ b/webview-ui/src/i18n/locales/ca/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI de dades de la imatge copiada al porta-retalls" } + }, + "errorBoundary": { + "title": "Alguna cosa ha anat malament", + "componentError": "Error al component {{componentName}}", + "description": "S'ha produït un error en aquesta part de la interfície. Podeu intentar recuperar-vos fent clic al botó de sota.", + "showDetails": "Mostrar detalls de l'error", + "retry": "Tornar a intentar" } } diff --git a/webview-ui/src/i18n/locales/de/common.json b/webview-ui/src/i18n/locales/de/common.json index 76b9064bc3..d6c5cd4c9a 100644 --- a/webview-ui/src/i18n/locales/de/common.json +++ b/webview-ui/src/i18n/locales/de/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "Bild-Daten-URI in die Zwischenablage kopiert" } + }, + "errorBoundary": { + "title": "Etwas ist schiefgelaufen", + "componentError": "Fehler in der {{componentName}} Komponente", + "description": "In diesem Teil der Benutzeroberfläche ist ein Fehler aufgetreten. Sie können versuchen, sich zu erholen, indem Sie auf die Schaltfläche unten klicken.", + "showDetails": "Fehlerdetails anzeigen", + "retry": "Erneut versuchen" } } diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index 1488f00f46..2fdd2a748c 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "Image data URI copied to clipboard" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/es/common.json b/webview-ui/src/i18n/locales/es/common.json index 5fe624372f..3d20773835 100644 --- a/webview-ui/src/i18n/locales/es/common.json +++ b/webview-ui/src/i18n/locales/es/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI de datos de imagen copiada al portapapeles" } + }, + "errorBoundary": { + "title": "Algo salió mal", + "componentError": "Error en el componente {{componentName}}", + "description": "Ocurrió un error en esta parte de la interfaz. Puedes intentar recuperarte haciendo clic en el botón de abajo.", + "showDetails": "Mostrar detalles del error", + "retry": "Intentar de nuevo" } } diff --git a/webview-ui/src/i18n/locales/fr/common.json b/webview-ui/src/i18n/locales/fr/common.json index 677116ff2a..f5d2093dcc 100644 --- a/webview-ui/src/i18n/locales/fr/common.json +++ b/webview-ui/src/i18n/locales/fr/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI de données d'image copiée dans le presse-papiers" } + }, + "errorBoundary": { + "title": "Quelque chose s'est mal passé", + "componentError": "Erreur dans le composant {{componentName}}", + "description": "Une erreur s'est produite dans cette partie de l'interface. Vous pouvez essayer de récupérer en cliquant sur le bouton ci-dessous.", + "showDetails": "Afficher les détails de l'erreur", + "retry": "Réessayer" } } diff --git a/webview-ui/src/i18n/locales/hi/common.json b/webview-ui/src/i18n/locales/hi/common.json index 77876eb274..12261f3d97 100644 --- a/webview-ui/src/i18n/locales/hi/common.json +++ b/webview-ui/src/i18n/locales/hi/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "इमेज डेटा URI क्लिपबोर्ड में कॉपी हो गया" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/id/common.json b/webview-ui/src/i18n/locales/id/common.json index d50246ced2..f9a9ce6dd6 100644 --- a/webview-ui/src/i18n/locales/id/common.json +++ b/webview-ui/src/i18n/locales/id/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "Data URI gambar disalin ke clipboard" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/it/common.json b/webview-ui/src/i18n/locales/it/common.json index 9d5426aa0e..79f1e3a77e 100644 --- a/webview-ui/src/i18n/locales/it/common.json +++ b/webview-ui/src/i18n/locales/it/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI dati immagine copiato negli appunti" } + }, + "errorBoundary": { + "title": "Qualcosa è andato storto", + "componentError": "Errore nel componente {{componentName}}", + "description": "Si è verificato un errore in questa parte dell'interfaccia. Puoi provare a recuperare cliccando il pulsante qui sotto.", + "showDetails": "Mostra dettagli errore", + "retry": "Riprova" } } diff --git a/webview-ui/src/i18n/locales/ja/common.json b/webview-ui/src/i18n/locales/ja/common.json index 975ea67834..9a6ee15f18 100644 --- a/webview-ui/src/i18n/locales/ja/common.json +++ b/webview-ui/src/i18n/locales/ja/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "画像データURIをクリップボードにコピーしました" } + }, + "errorBoundary": { + "title": "問題が発生しました", + "componentError": "{{componentName}}コンポーネントでエラーが発生しました", + "description": "インターフェースのこの部分でエラーが発生しました。下のボタンをクリックして回復を試すことができます。", + "showDetails": "エラーの詳細を表示", + "retry": "再試行" } } diff --git a/webview-ui/src/i18n/locales/ko/common.json b/webview-ui/src/i18n/locales/ko/common.json index 276f2cb20b..3bf09dec6d 100644 --- a/webview-ui/src/i18n/locales/ko/common.json +++ b/webview-ui/src/i18n/locales/ko/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "이미지 데이터 URI가 클립보드에 복사됨" } + }, + "errorBoundary": { + "title": "문제가 발생했습니다", + "componentError": "{{componentName}} 컴포넌트에서 오류 발생", + "description": "인터페이스의 이 부분에서 오류가 발생했습니다. 아래 버튼을 클릭하여 복구를 시도할 수 있습니다.", + "showDetails": "오류 세부사항 표시", + "retry": "다시 시도" } } diff --git a/webview-ui/src/i18n/locales/nl/common.json b/webview-ui/src/i18n/locales/nl/common.json index 012808e51a..f6ca28f58e 100644 --- a/webview-ui/src/i18n/locales/nl/common.json +++ b/webview-ui/src/i18n/locales/nl/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "Afbeelding data-URI gekopieerd naar klembord" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/pl/common.json b/webview-ui/src/i18n/locales/pl/common.json index c72b046c42..6c608cbc45 100644 --- a/webview-ui/src/i18n/locales/pl/common.json +++ b/webview-ui/src/i18n/locales/pl/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI danych obrazu skopiowane do schowka" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/common.json b/webview-ui/src/i18n/locales/pt-BR/common.json index a911b2366f..fe77fffdc1 100644 --- a/webview-ui/src/i18n/locales/pt-BR/common.json +++ b/webview-ui/src/i18n/locales/pt-BR/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI de dados da imagem copiada para a área de transferência" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/ru/common.json b/webview-ui/src/i18n/locales/ru/common.json index e68899a2db..9f4856fd4f 100644 --- a/webview-ui/src/i18n/locales/ru/common.json +++ b/webview-ui/src/i18n/locales/ru/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI данных изображения скопирован в буфер обмена" } + }, + "errorBoundary": { + "title": "Что-то пошло не так", + "componentError": "Ошибка в компоненте {{componentName}}", + "description": "В этой части интерфейса произошла ошибка. Вы можете попытаться восстановить работу, нажав кнопку ниже.", + "showDetails": "Показать детали ошибки", + "retry": "Попробовать снова" } } diff --git a/webview-ui/src/i18n/locales/tr/common.json b/webview-ui/src/i18n/locales/tr/common.json index 23344ca966..e32a8d398a 100644 --- a/webview-ui/src/i18n/locales/tr/common.json +++ b/webview-ui/src/i18n/locales/tr/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "Görsel veri URI'si panoya kopyalandı" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/vi/common.json b/webview-ui/src/i18n/locales/vi/common.json index 16952117ef..d96627c864 100644 --- a/webview-ui/src/i18n/locales/vi/common.json +++ b/webview-ui/src/i18n/locales/vi/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "URI dữ liệu hình ảnh đã được sao chép vào clipboard" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/common.json b/webview-ui/src/i18n/locales/zh-CN/common.json index 29f11c7f2f..c59c2b0fc6 100644 --- a/webview-ui/src/i18n/locales/zh-CN/common.json +++ b/webview-ui/src/i18n/locales/zh-CN/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "图片数据 URI 已复制到剪贴板" } + }, + "errorBoundary": { + "title": "出现错误", + "componentError": "{{componentName}} 组件出错", + "description": "界面的这一部分发生了错误。您可以尝试点击下面的按钮来恢复。", + "showDetails": "显示错误详情", + "retry": "重试" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/common.json b/webview-ui/src/i18n/locales/zh-TW/common.json index b8ec7f998e..0796aa3f3a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/common.json +++ b/webview-ui/src/i18n/locales/zh-TW/common.json @@ -51,5 +51,12 @@ "success": { "imageDataUriCopied": "圖片資料 URI 已複製到剪貼簿" } + }, + "errorBoundary": { + "title": "Something went wrong", + "componentError": "Error in {{componentName}} component", + "description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.", + "showDetails": "Show error details", + "retry": "Try again" } } diff --git a/webview-ui/src/utils/errorReporting.ts b/webview-ui/src/utils/errorReporting.ts new file mode 100644 index 0000000000..f8c6fc16b1 --- /dev/null +++ b/webview-ui/src/utils/errorReporting.ts @@ -0,0 +1,93 @@ +/** + * Error reporting utility for the webview + * This can be extended to integrate with services like Sentry in the future + */ + +export interface ErrorReport { + error: Error + errorInfo?: React.ErrorInfo + componentName?: string + timestamp: number + userAgent: string + url: string +} + +class ErrorReporter { + private errors: ErrorReport[] = [] + private maxErrors = 50 // Keep only the last 50 errors + + /** + * Report an error that occurred in a React component + */ + reportError(error: Error, errorInfo?: React.ErrorInfo, componentName?: string): void { + const errorReport: ErrorReport = { + error: { + name: error.name, + message: error.message, + stack: error.stack, + } as Error, + errorInfo, + componentName, + timestamp: Date.now(), + userAgent: navigator.userAgent, + url: window.location.href, + } + + // Add to local storage for debugging + this.errors.push(errorReport) + if (this.errors.length > this.maxErrors) { + this.errors.shift() + } + + // Log to console for development + console.error(`Error in ${componentName || "component"}:`, error, errorInfo) + + // TODO: In the future, this could send errors to Sentry or another service + // Example: + // if (window.Sentry) { + // window.Sentry.captureException(error, { + // tags: { component: componentName }, + // extra: errorInfo + // }) + // } + } + + /** + * Get all stored error reports (useful for debugging) + */ + getErrors(): ErrorReport[] { + return [...this.errors] + } + + /** + * Clear all stored errors + */ + clearErrors(): void { + this.errors = [] + } + + /** + * Get error statistics + */ + getErrorStats(): { total: number; byComponent: Record } { + const byComponent: Record = {} + + this.errors.forEach((error) => { + const component = error.componentName || "unknown" + byComponent[component] = (byComponent[component] || 0) + 1 + }) + + return { + total: this.errors.length, + byComponent, + } + } +} + +// Export a singleton instance +export const errorReporter = new ErrorReporter() + +// Make it available globally for debugging in development +if (typeof window !== "undefined" && process.env.NODE_ENV === "development") { + ;(window as any).errorReporter = errorReporter +} diff --git a/webview-ui/vitest.config.ts b/webview-ui/vitest.config.ts index b9455584bf..c5e597521a 100644 --- a/webview-ui/vitest.config.ts +++ b/webview-ui/vitest.config.ts @@ -11,6 +11,9 @@ export default defineConfig({ environment: "jsdom", include: ["src/**/*.spec.ts", "src/**/*.spec.tsx"], }, + define: { + "process.env.NODE_ENV": '"development"', + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"),