Jelajahi Sumber

debug: Add ErrorBoundary component for better error handling (#5085)

Co-authored-by: Eric Wheeler <[email protected]>
KJ7LNW 6 bulan lalu
induk
melakukan
3e89b06f25
35 mengubah file dengan 1175 tambahan dan 16 penghapusan
  1. 14 0
      pnpm-lock.yaml
  2. 1 1
      src/.vscodeignore
  3. 2 2
      src/core/webview/ClineProvider.ts
  4. 3 1
      src/core/webview/__tests__/ClineProvider.spec.ts
  5. 1 1
      src/esbuild.mjs
  6. 2 0
      webview-ui/package.json
  7. 27 9
      webview-ui/src/App.tsx
  8. 84 0
      webview-ui/src/__tests__/App.spec.tsx
  9. 88 0
      webview-ui/src/__tests__/ErrorBoundary.spec.tsx
  10. 98 0
      webview-ui/src/components/ErrorBoundary.tsx
  11. 97 0
      webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx
  12. 31 1
      webview-ui/src/components/settings/About.tsx
  13. 8 0
      webview-ui/src/i18n/locales/ca/common.json
  14. 8 0
      webview-ui/src/i18n/locales/de/common.json
  15. 8 0
      webview-ui/src/i18n/locales/en/common.json
  16. 8 0
      webview-ui/src/i18n/locales/es/common.json
  17. 8 0
      webview-ui/src/i18n/locales/fr/common.json
  18. 8 0
      webview-ui/src/i18n/locales/hi/common.json
  19. 8 0
      webview-ui/src/i18n/locales/id/common.json
  20. 8 0
      webview-ui/src/i18n/locales/it/common.json
  21. 8 0
      webview-ui/src/i18n/locales/ja/common.json
  22. 8 0
      webview-ui/src/i18n/locales/ko/common.json
  23. 8 0
      webview-ui/src/i18n/locales/nl/common.json
  24. 8 0
      webview-ui/src/i18n/locales/pl/common.json
  25. 8 0
      webview-ui/src/i18n/locales/pt-BR/common.json
  26. 8 0
      webview-ui/src/i18n/locales/ru/common.json
  27. 8 0
      webview-ui/src/i18n/locales/tr/common.json
  28. 8 0
      webview-ui/src/i18n/locales/vi/common.json
  29. 8 0
      webview-ui/src/i18n/locales/zh-CN/common.json
  30. 8 0
      webview-ui/src/i18n/locales/zh-TW/common.json
  31. 88 0
      webview-ui/src/utils/__tests__/sourceMapUtils.spec.ts
  32. 180 0
      webview-ui/src/utils/sourceMapInitializer.ts
  33. 190 0
      webview-ui/src/utils/sourceMapUtils.ts
  34. 116 0
      webview-ui/src/vite-plugins/sourcemapPlugin.ts
  35. 9 1
      webview-ui/vite.config.ts

+ 14 - 0
pnpm-lock.yaml

@@ -1036,6 +1036,9 @@ importers:
       source-map:
         specifier: ^0.7.4
         version: 0.7.4
+      stacktrace-js:
+        specifier: ^2.0.2
+        version: 2.0.2
       styled-components:
         specifier: ^6.1.13
         version: 6.1.18([email protected]([email protected]))([email protected])
@@ -1097,6 +1100,9 @@ importers:
       '@types/shell-quote':
         specifier: ^1.7.5
         version: 1.7.5
+      '@types/stacktrace-js':
+        specifier: ^2.0.3
+        version: 2.0.3
       '@types/vscode-webview':
         specifier: ^1.57.5
         version: 1.57.5
@@ -3888,6 +3894,10 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
 
+  '@types/[email protected]':
+    resolution: {integrity: sha512-B6JnMic4NAZ4mLWmRi4RvayCN2HZQvpcVF0MkoqubtuZx1AQB0/kRlrngGiocEPyO7R+TFocTEoLKQ0HzmEOPw==}
+    deprecated: This is a stub types definition. stacktrace-js provides its own type definitions, so you do not need this installed.
+
   '@types/[email protected]':
     resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==}
 
@@ -13090,6 +13100,10 @@ snapshots:
 
   '@types/[email protected]': {}
 
+  '@types/[email protected]':
+    dependencies:
+      stacktrace-js: 2.0.2
+
   '@types/[email protected]':
     dependencies:
       '@types/node': 20.19.1

+ 1 - 1
src/.vscodeignore

@@ -14,7 +14,7 @@
 !dist
 
 # Include the built webview
-**/*.map
+!**/*.map
 !webview-ui/audio
 !webview-ui/build/assets/*.js
 !webview-ui/build/assets/*.ttf

+ 2 - 2
src/core/webview/ClineProvider.ts

@@ -682,7 +682,7 @@ export class ClineProvider
 			`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
 			`media-src ${webview.cspSource}`,
 			`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
-			`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
+			`connect-src ${webview.cspSource} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
 		]
 
 		return /*html*/ `
@@ -764,7 +764,7 @@ export class ClineProvider
             <meta charset="utf-8">
             <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
             <meta name="theme-color" content="#000000">
-            <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
+            <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src ${webview.cspSource} https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
             <link rel="stylesheet" type="text/css" href="${stylesUri}">
 			<link href="${codiconsUri}" rel="stylesheet" />
 			<script nonce="${nonce}">

+ 3 - 1
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -400,6 +400,7 @@ describe("ClineProvider", () => {
 				options: {},
 				onDidReceiveMessage: vi.fn(),
 				asWebviewUri: vi.fn(),
+				cspSource: "vscode-webview://test-csp-source",
 			},
 			visible: true,
 			onDidDispose: vi.fn().mockImplementation((callback) => {
@@ -473,7 +474,7 @@ describe("ClineProvider", () => {
 
 		// Verify Content Security Policy contains the necessary PostHog domains
 		expect(mockWebviewView.webview.html).toContain(
-			"connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
+			"connect-src vscode-webview://test-csp-source https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
 		)
 
 		// Extract the script-src directive section and verify required security elements
@@ -1986,6 +1987,7 @@ describe("Project MCP Settings", () => {
 				options: {},
 				onDidReceiveMessage: vi.fn(),
 				asWebviewUri: vi.fn(),
+				cspSource: "vscode-webview://test-csp-source",
 			},
 			visible: true,
 			onDidDispose: vi.fn(),

+ 1 - 1
src/esbuild.mjs

@@ -15,7 +15,7 @@ async function main() {
 	const production = process.argv.includes("--production")
 	const watch = process.argv.includes("--watch")
 	const minify = production
-	const sourcemap = !production
+	const sourcemap = true // Always generate source maps for error handling
 
 	/**
 	 * @type {import('esbuild').BuildOptions}

+ 2 - 0
webview-ui/package.json

@@ -68,6 +68,7 @@
 		"shell-quote": "^1.8.2",
 		"shiki": "^3.2.1",
 		"source-map": "^0.7.4",
+		"stacktrace-js": "^2.0.2",
 		"styled-components": "^6.1.13",
 		"tailwind-merge": "^3.0.0",
 		"tailwindcss": "^4.0.0",
@@ -90,6 +91,7 @@
 		"@types/react": "^18.3.23",
 		"@types/react-dom": "^18.3.5",
 		"@types/shell-quote": "^1.7.5",
+		"@types/stacktrace-js": "^2.0.3",
 		"@types/vscode-webview": "^1.57.5",
 		"@vitejs/plugin-react": "^4.3.4",
 		"@vitest/ui": "^3.2.3",

+ 27 - 9
webview-ui/src/App.tsx

@@ -9,6 +9,7 @@ import { MarketplaceViewStateManager } from "./components/marketplace/Marketplac
 import { vscode } from "./utils/vscode"
 import { telemetryClient } from "./utils/TelemetryClient"
 import { TelemetryEventName } from "@roo-code/types"
+import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer"
 import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
 import ChatView, { ChatViewRef } from "./components/chat/ChatView"
 import HistoryView from "./components/history/HistoryView"
@@ -19,6 +20,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView"
 import ModesView from "./components/modes/ModesView"
 import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
 import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
+import ErrorBoundary from "./components/ErrorBoundary"
 import { AccountView } from "./components/account/AccountView"
 import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
 import { TooltipProvider } from "./components/ui/tooltip"
@@ -191,6 +193,20 @@ const App = () => {
 	// Tell the extension that we are ready to receive messages.
 	useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
 
+	// Initialize source map support for better error reporting
+	useEffect(() => {
+		// Initialize source maps for better error reporting in production
+		initializeSourceMaps()
+
+		// Expose source map debugging utilities in production
+		if (process.env.NODE_ENV === "production") {
+			exposeSourceMapsForDebugging()
+		}
+
+		// Log initialization for debugging
+		console.debug("App initialized with source map support")
+	}, [])
+
 	// Focus the WebView when non-interactive content is clicked (only in editor/tab mode)
 	useAddNonInteractiveClickListener(
 		useCallback(() => {
@@ -283,15 +299,17 @@ const App = () => {
 const queryClient = new QueryClient()
 
 const AppWithProviders = () => (
-	<ExtensionStateContextProvider>
-		<TranslationProvider>
-			<QueryClientProvider client={queryClient}>
-				<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
-					<App />
-				</TooltipProvider>
-			</QueryClientProvider>
-		</TranslationProvider>
-	</ExtensionStateContextProvider>
+	<ErrorBoundary>
+		<ExtensionStateContextProvider>
+			<TranslationProvider>
+				<QueryClientProvider client={queryClient}>
+					<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
+						<App />
+					</TooltipProvider>
+				</QueryClientProvider>
+			</TranslationProvider>
+		</ExtensionStateContextProvider>
+	</ErrorBoundary>
 )
 
 export default AppWithProviders

+ 84 - 0
webview-ui/src/__tests__/App.spec.tsx

@@ -11,6 +11,20 @@ vi.mock("@src/utils/vscode", () => ({
 	},
 }))
 
+// Mock the ErrorBoundary component
+vi.mock("@src/components/ErrorBoundary", () => ({
+	__esModule: true,
+	default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+}))
+
+// Mock the telemetry client
+vi.mock("@src/utils/TelemetryClient", () => ({
+	telemetryClient: {
+		capture: vi.fn(),
+		updateTelemetryState: vi.fn(),
+	},
+}))
+
 vi.mock("@src/components/chat/ChatView", () => ({
 	__esModule: true,
 	default: function ChatView({ isHidden }: { isHidden: boolean }) {
@@ -88,11 +102,81 @@ vi.mock("@src/components/account/AccountView", () => ({
 
 const mockUseExtensionState = vi.fn()
 
+// Mock the HumanRelayDialog component
+vi.mock("@src/components/human-relay/HumanRelayDialog", () => ({
+	HumanRelayDialog: ({ _children, isOpen, onClose }: any) => (
+		<div data-testid="human-relay-dialog" data-open={isOpen} onClick={onClose}>
+			Human Relay Dialog
+		</div>
+	),
+}))
+
+// Mock i18next and react-i18next
+vi.mock("i18next", () => {
+	const tFunction = (key: string) => key
+	const i18n = {
+		t: tFunction,
+		use: () => i18n,
+		init: () => Promise.resolve(tFunction),
+		changeLanguage: vi.fn(() => Promise.resolve()),
+	}
+	return { default: i18n }
+})
+
+vi.mock("react-i18next", () => {
+	const tFunction = (key: string) => key
+	return {
+		withTranslation: () => (Component: any) => {
+			const MockedComponent = (props: any) => {
+				return <Component t={tFunction} i18n={{ t: tFunction }} tReady {...props} />
+			}
+			MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})`
+			return MockedComponent
+		},
+		Trans: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+		useTranslation: () => {
+			return {
+				t: tFunction,
+				i18n: {
+					t: tFunction,
+					changeLanguage: vi.fn(() => Promise.resolve()),
+				},
+			}
+		},
+		initReactI18next: {
+			type: "3rdParty",
+			init: vi.fn(),
+		},
+	}
+})
+
+// Mock TranslationProvider to pass through children
+vi.mock("@src/i18n/TranslationContext", () => {
+	const tFunction = (key: string) => key
+	return {
+		__esModule: true,
+		default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+		useAppTranslation: () => ({
+			t: tFunction,
+			i18n: {
+				t: tFunction,
+				changeLanguage: vi.fn(() => Promise.resolve()),
+			},
+		}),
+	}
+})
+
 vi.mock("@src/context/ExtensionStateContext", () => ({
 	useExtensionState: () => mockUseExtensionState(),
 	ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
 }))
 
+// Mock environment variables
+vi.mock("process.env", () => ({
+	NODE_ENV: "test",
+	PKG_VERSION: "1.0.0-test",
+}))
+
 describe("App", () => {
 	beforeEach(() => {
 		vi.clearAllMocks()

+ 88 - 0
webview-ui/src/__tests__/ErrorBoundary.spec.tsx

@@ -0,0 +1,88 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import ErrorBoundary from "../components/ErrorBoundary"
+
+// Mock telemetry client
+vi.mock("@src/utils/TelemetryClient", () => ({
+	telemetryClient: {
+		capture: vi.fn(),
+	},
+}))
+
+// Mock translation function
+vi.mock("react-i18next", () => {
+	const tFunction = (key: string) => key
+	return {
+		withTranslation: () => (Component: any) => {
+			const MockedComponent = (props: any) => {
+				return <Component t={tFunction} i18n={{ t: tFunction }} tReady {...props} />
+			}
+			MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})`
+			return MockedComponent
+		},
+	}
+})
+
+// Test component that can throw errors on demand
+const ErrorThrower = ({ shouldThrow = false, message = "Test error" }: { shouldThrow?: boolean; message?: string }) => {
+	if (shouldThrow) {
+		throw new Error(message)
+	}
+	return <div>No error</div>
+}
+
+describe("ErrorBoundary", () => {
+	// Suppress console errors during tests
+	beforeEach(() => {
+		vi.spyOn(console, "error").mockImplementation(() => {})
+	})
+
+	afterEach(() => {
+		vi.restoreAllMocks()
+	})
+
+	it("renders children when there is no error", () => {
+		render(
+			<ErrorBoundary>
+				<div data-testid="test-child">Test Content</div>
+			</ErrorBoundary>,
+		)
+
+		expect(screen.getByTestId("test-child")).toBeInTheDocument()
+		expect(screen.getByText("Test Content")).toBeInTheDocument()
+	})
+
+	it("renders error UI when a child component throws", () => {
+		vi.stubEnv("PKG_VERSION", "1.2.3")
+
+		// Using the React testing library's render method with an error boundary is tricky
+		// We need to catch and ignore the error during the test
+		const spy = vi.spyOn(console, "error").mockImplementation(() => {})
+
+		render(
+			<ErrorBoundary>
+				<ErrorThrower shouldThrow={true} message="Test component error" />
+			</ErrorBoundary>,
+		)
+
+		// Verify error boundary elements are displayed - using partial matchers to account for version info
+		expect(screen.getByText(/errorBoundary.title/)).toBeInTheDocument()
+
+		// Check for the GitHub link
+		const githubLink = screen.getByRole("link", { name: /errorBoundary.githubText/ })
+		expect(githubLink).toBeInTheDocument()
+		expect(githubLink).toHaveAttribute("href", "https://github.com/RooCodeInc/Roo-Code/issues")
+
+		// Check for other error boundary elements
+		expect(screen.getByText(/errorBoundary.copyInstructions/)).toBeInTheDocument()
+		expect(screen.getByText(/errorBoundary.errorStack/)).toBeInTheDocument()
+
+		// In test environments, the componentStack might not always be available
+		// so we don't check for it to make the test more reliable
+
+		// The test error message should be included in the error display
+		expect(screen.getByText(/Test component error/)).toBeInTheDocument()
+
+		spy.mockRestore()
+	})
+})

+ 98 - 0
webview-ui/src/components/ErrorBoundary.tsx

@@ -0,0 +1,98 @@
+import React, { Component } from "react"
+import { telemetryClient } from "@src/utils/TelemetryClient"
+import { withTranslation, WithTranslation } from "react-i18next"
+import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils"
+
+type ErrorProps = {
+	children: React.ReactNode
+} & WithTranslation
+
+type ErrorState = {
+	error?: string
+	componentStack?: string | null
+	timestamp?: number
+}
+
+class ErrorBoundary extends Component<ErrorProps, ErrorState> {
+	constructor(props: ErrorProps) {
+		super(props)
+		this.state = {}
+
+		this.state = {}
+	}
+
+	static getDerivedStateFromError(error: unknown) {
+		let errorMessage = ""
+
+		if (error instanceof Error) {
+			errorMessage = error.stack ?? error.message
+		} else {
+			errorMessage = `${error}`
+		}
+
+		return {
+			error: errorMessage,
+			timestamp: Date.now(),
+		}
+	}
+
+	async componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+		const componentStack = errorInfo.componentStack || ""
+		const enhancedError = await enhanceErrorWithSourceMaps(error, componentStack)
+
+		telemetryClient.capture("error_boundary_caught_error", {
+			error: enhancedError.message,
+			stack: enhancedError.sourceMappedStack || enhancedError.stack,
+			componentStack: enhancedError.sourceMappedComponentStack || componentStack,
+			timestamp: Date.now(),
+			errorType: enhancedError.name,
+		})
+
+		this.setState({
+			error: enhancedError.sourceMappedStack || enhancedError.stack,
+			componentStack: enhancedError.sourceMappedComponentStack || componentStack,
+		})
+	}
+
+	render() {
+		const { t } = this.props
+
+		if (!this.state.error) {
+			return this.props.children
+		}
+
+		const errorDisplay = this.state.error
+		const componentStackDisplay = this.state.componentStack
+
+		const version = process.env.PKG_VERSION || "unknown"
+
+		return (
+			<div>
+				<h2 className="text-lg font-bold mt-0 mb-2">
+					{t("errorBoundary.title")} (v{version})
+				</h2>
+				<p className="mb-4">
+					{t("errorBoundary.reportText")}{" "}
+					<a href="https://github.com/RooCodeInc/Roo-Code/issues" target="_blank" rel="noreferrer">
+						{t("errorBoundary.githubText")}
+					</a>
+				</p>
+				<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
+
+				<div className="mb-4">
+					<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
+					<pre className="p-2 border rounded text-sm overflow-auto">{errorDisplay}</pre>
+				</div>
+
+				{componentStackDisplay && (
+					<div>
+						<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
+						<pre className="p-2 border rounded text-sm overflow-auto">{componentStackDisplay}</pre>
+					</div>
+				)}
+			</div>
+		)
+	}
+}
+
+export default withTranslation("common")(ErrorBoundary)

+ 97 - 0
webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx

@@ -0,0 +1,97 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import { vi } from "vitest"
+import ErrorBoundary from "../ErrorBoundary"
+
+// Mock telemetryClient
+vi.mock("@src/utils/TelemetryClient", () => ({
+	telemetryClient: {
+		capture: vi.fn(),
+	},
+}))
+
+// Mock translation
+vi.mock("react-i18next", () => ({
+	withTranslation: () => (Component: any) => {
+		Component.defaultProps = {
+			...Component.defaultProps,
+			t: (key: string) => {
+				// Mock translations for tests
+				const translations: Record<string, string> = {
+					"errorBoundary.title": "Something went wrong",
+					"errorBoundary.reportText": "Please help us improve by reporting this error on",
+					"errorBoundary.githubText": "GitHub",
+					"errorBoundary.copyInstructions": "Please copy and paste the following error message:",
+				}
+				return translations[key] || key
+			},
+		}
+		return Component
+	},
+}))
+
+// Test component that throws an error
+const ErrorThrowingComponent = ({ shouldThrow = false }) => {
+	if (shouldThrow) {
+		throw new Error("Test error")
+	}
+	return <div data-testid="normal-render">Content rendered normally</div>
+}
+
+describe("ErrorBoundary", () => {
+	// Suppress console errors during tests
+	const originalConsoleError = console.error
+	beforeAll(() => {
+		console.error = vi.fn()
+	})
+	afterAll(() => {
+		console.error = originalConsoleError
+	})
+
+	test("renders children when no error occurs", () => {
+		render(
+			<ErrorBoundary>
+				<ErrorThrowingComponent shouldThrow={false} />
+			</ErrorBoundary>,
+		)
+
+		expect(screen.getByTestId("normal-render")).toBeInTheDocument()
+	})
+
+	test("renders error UI when an error occurs", () => {
+		// React will log the error to the console - we're just testing the UI behavior
+		render(
+			<ErrorBoundary>
+				<ErrorThrowingComponent shouldThrow={true} />
+			</ErrorBoundary>,
+		)
+
+		// Verify error message is displayed using a more flexible approach
+		const errorTitle = screen.getByRole("heading", { level: 2 })
+		expect(errorTitle.textContent).toContain("Something went wrong")
+		expect(screen.getByText(/please copy and paste the following error message/i)).toBeInTheDocument()
+	})
+
+	test("error boundary renders error UI when component changes but still in error state", () => {
+		const { rerender } = render(
+			<ErrorBoundary>
+				<ErrorThrowingComponent shouldThrow={true} />
+			</ErrorBoundary>,
+		)
+
+		// Verify error message is displayed using a more flexible approach
+		const errorTitle = screen.getByRole("heading", { level: 2 })
+		expect(errorTitle.textContent).toContain("Something went wrong")
+
+		// Update the component to not throw
+		rerender(
+			<ErrorBoundary>
+				<ErrorThrowingComponent shouldThrow={false} />
+			</ErrorBoundary>,
+		)
+
+		// The error boundary should still show the error since it doesn't automatically reset
+		const errorTitleAfterRerender = screen.getByRole("heading", { level: 2 })
+		expect(errorTitleAfterRerender.textContent).toContain("Something went wrong")
+	})
+})

+ 31 - 1
webview-ui/src/components/settings/About.tsx

@@ -1,4 +1,4 @@
-import { HTMLAttributes } from "react"
+import { HTMLAttributes, useState } from "react"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { Trans } from "react-i18next"
 import { Info, Download, Upload, TriangleAlert } from "lucide-react"
@@ -22,9 +22,33 @@ type AboutProps = HTMLAttributes<HTMLDivElement> & {
 
 export const About = ({ telemetrySetting, setTelemetrySetting, className, ...props }: AboutProps) => {
 	const { t } = useAppTranslation()
+	const [shouldThrowError, setShouldThrowError] = useState(false)
+
+	// Function to trigger error for testing ErrorBoundary
+	const triggerTestError = () => {
+		setShouldThrowError(true)
+	}
+
+	// Named function to make it easier to identify in stack traces
+	function throwTestError() {
+		// Intentionally cause a type error by accessing a property on undefined
+		const obj: any = undefined
+		obj.nonExistentMethod()
+	}
+
+	// Test component that throws an error when shouldThrow is true
+	const ErrorThrower = ({ shouldThrow = false }) => {
+		if (shouldThrow) {
+			// Use a named function to make it easier to identify in stack traces
+			throwTestError()
+		}
+		return null
+	}
 
 	return (
 		<div className={cn("flex flex-col gap-2", className)} {...props}>
+			{/* Test component that throws an error when shouldThrow is true */}
+			<ErrorThrower shouldThrow={shouldThrowError} />
 			<SectionHeader
 				description={
 					Package.sha
@@ -84,6 +108,12 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro
 						<TriangleAlert className="p-0.5" />
 						{t("settings:footer.settings.reset")}
 					</Button>
+
+					{/* Test button for ErrorBoundary - only visible in development */}
+					<Button variant="destructive" onClick={triggerTestError} className="w-auto">
+						<TriangleAlert className="p-0.5" />
+						Test ErrorBoundary
+					</Button>
 				</div>
 			</Section>
 		</div>

+ 8 - 0
webview-ui/src/i18n/locales/ca/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Alguna cosa ha anat malament",
+		"reportText": "Si us plau, ajuda'ns a millorar informant d'aquest error a",
+		"githubText": "la nostra pàgina d'incidències de GitHub",
+		"copyInstructions": "Copia i enganxa el següent missatge d'error per incloure'l com a part de la teva presentació:",
+		"errorStack": "Pila d'errors:",
+		"componentStack": "Pila de components:"
+	},
 	"answers": {
 		"yes": "Sí",
 		"no": "No",

+ 8 - 0
webview-ui/src/i18n/locales/de/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Etwas ist schiefgelaufen",
+		"reportText": "Bitte hilf uns, besser zu werden, indem du diesen Fehler meldest auf",
+		"githubText": "unserer GitHub Issues-Seite",
+		"copyInstructions": "Kopiere die folgende Fehlermeldung und füge sie deiner Meldung bei:",
+		"errorStack": "Fehler-Stack:",
+		"componentStack": "Komponenten-Stack:"
+	},
 	"answers": {
 		"yes": "Ja",
 		"no": "Nein",

+ 8 - 0
webview-ui/src/i18n/locales/en/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Something went wrong",
+		"reportText": "Please help us improve by reporting this error on",
+		"githubText": "our GitHub Issues page",
+		"copyInstructions": "Copy and paste the following error message to include it as part of your submission:",
+		"errorStack": "Error Stack:",
+		"componentStack": "Component Stack:"
+	},
 	"answers": {
 		"yes": "Yes",
 		"no": "No",

+ 8 - 0
webview-ui/src/i18n/locales/es/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Algo salió mal",
+		"reportText": "Ayúdanos a mejorar informando de este error en",
+		"githubText": "nuestra página de Issues de GitHub",
+		"copyInstructions": "Copia y pega el siguiente mensaje de error para incluirlo como parte de tu informe:",
+		"errorStack": "Pila de errores:",
+		"componentStack": "Pila de componentes:"
+	},
 	"answers": {
 		"yes": "Sí",
 		"no": "No",

+ 8 - 0
webview-ui/src/i18n/locales/fr/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Une erreur s'est produite",
+		"reportText": "Aidez-nous à nous améliorer en signalant cette erreur sur",
+		"githubText": "notre page GitHub Issues",
+		"copyInstructions": "Copiez et collez le message d'erreur suivant pour l'inclure dans votre rapport :",
+		"errorStack": "Pile d'erreurs :",
+		"componentStack": "Pile de composants :"
+	},
 	"answers": {
 		"yes": "Oui",
 		"no": "Non",

+ 8 - 0
webview-ui/src/i18n/locales/hi/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "कुछ गलत हो गया",
+		"reportText": "कृपया इस त्रुटि की रिपोर्ट करके हमें सुधारने में मदद करें",
+		"githubText": "हमारे GitHub Issues पेज पर",
+		"copyInstructions": "अपनी सबमिशन के हिस्से के रूप में शामिल करने के लिए निम्नलिखित त्रुटि संदेश को कॉपी और पेस्ट करें:",
+		"errorStack": "त्रुटि स्टैक:",
+		"componentStack": "कंपोनेंट स्टैक:"
+	},
 	"answers": {
 		"yes": "हाँ",
 		"no": "नहीं",

+ 8 - 0
webview-ui/src/i18n/locales/id/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Terjadi kesalahan",
+		"reportText": "Bantu kami meningkatkan dengan melaporkan kesalahan ini di",
+		"githubText": "halaman GitHub Issues kami",
+		"copyInstructions": "Salin dan tempel pesan kesalahan berikut untuk menyertakannya sebagai bagian dari laporan Anda:",
+		"errorStack": "Stack Error:",
+		"componentStack": "Stack Komponen:"
+	},
 	"answers": {
 		"yes": "Ya",
 		"no": "Tidak",

+ 8 - 0
webview-ui/src/i18n/locales/it/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Qualcosa è andato storto",
+		"reportText": "Aiutaci a migliorare segnalando questo errore su",
+		"githubText": "la nostra pagina GitHub Issues",
+		"copyInstructions": "Copia e incolla il seguente messaggio di errore per includerlo come parte della tua segnalazione:",
+		"errorStack": "Stack di errore:",
+		"componentStack": "Stack dei componenti:"
+	},
 	"answers": {
 		"yes": "Sì",
 		"no": "No",

+ 8 - 0
webview-ui/src/i18n/locales/ja/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "問題が発生しました",
+		"reportText": "このエラーを報告して改善にご協力ください",
+		"githubText": "GitHub Issuesページ",
+		"copyInstructions": "以下のエラーメッセージをコピーして報告に含めてください:",
+		"errorStack": "エラースタック:",
+		"componentStack": "コンポーネントスタック:"
+	},
 	"answers": {
 		"yes": "はい",
 		"no": "いいえ",

+ 8 - 0
webview-ui/src/i18n/locales/ko/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "문제가 발생했습니다",
+		"reportText": "이 오류를 보고하여 개선에 도움을 주세요",
+		"githubText": "GitHub 이슈 페이지",
+		"copyInstructions": "다음 오류 메시지를 복사하여 보고서에 포함해 주세요:",
+		"errorStack": "오류 스택:",
+		"componentStack": "컴포넌트 스택:"
+	},
 	"answers": {
 		"yes": "예",
 		"no": "아니오",

+ 8 - 0
webview-ui/src/i18n/locales/nl/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Er is iets misgegaan",
+		"reportText": "Help ons te verbeteren door deze fout te melden op",
+		"githubText": "onze GitHub Issues-pagina",
+		"copyInstructions": "Kopieer en plak het volgende foutbericht om het als onderdeel van je melding op te nemen:",
+		"errorStack": "Foutstack:",
+		"componentStack": "Componentstack:"
+	},
 	"answers": {
 		"yes": "Ja",
 		"no": "Nee",

+ 8 - 0
webview-ui/src/i18n/locales/pl/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Coś poszło nie tak",
+		"reportText": "Pomóż nam ulepszyć aplikację, zgłaszając ten błąd na",
+		"githubText": "naszej stronie GitHub Issues",
+		"copyInstructions": "Skopiuj i wklej poniższy komunikat o błędzie, aby dołączyć go do zgłoszenia:",
+		"errorStack": "Stos błędu:",
+		"componentStack": "Stos komponentów:"
+	},
 	"number_format": {
 		"thousand_suffix": "k",
 		"million_suffix": "m",

+ 8 - 0
webview-ui/src/i18n/locales/pt-BR/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Algo deu errado",
+		"reportText": "Ajude-nos a melhorar relatando este erro em",
+		"githubText": "nossa página de Issues no GitHub",
+		"copyInstructions": "Copie e cole a seguinte mensagem de erro para incluí-la como parte do seu relatório:",
+		"errorStack": "Pilha de Erro:",
+		"componentStack": "Pilha de Componentes:"
+	},
 	"number_format": {
 		"thousand_suffix": "k",
 		"million_suffix": "m",

+ 8 - 0
webview-ui/src/i18n/locales/ru/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Что-то пошло не так",
+		"reportText": "Пожалуйста, помогите нам улучшить приложение, сообщив об этой ошибке на",
+		"githubText": "нашей странице GitHub Issues",
+		"copyInstructions": "Скопируйте и вставьте следующее сообщение об ошибке, чтобы включить его в ваше сообщение:",
+		"errorStack": "Стек ошибки:",
+		"componentStack": "Стек компонентов:"
+	},
 	"number_format": {
 		"thousand_suffix": "тыс",
 		"million_suffix": "млн",

+ 8 - 0
webview-ui/src/i18n/locales/tr/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Bir şeyler yanlış gitti",
+		"reportText": "Bu hatayı bildirerek iyileştirmemize yardımcı olun",
+		"githubText": "GitHub Issues sayfamızda",
+		"copyInstructions": "Gönderiminize dahil etmek için aşağıdaki hata mesajını kopyalayıp yapıştırın:",
+		"errorStack": "Hata Yığını:",
+		"componentStack": "Bileşen Yığını:"
+	},
 	"answers": {
 		"yes": "Evet",
 		"no": "Hayır",

+ 8 - 0
webview-ui/src/i18n/locales/vi/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "Đã xảy ra lỗi",
+		"reportText": "Vui lòng giúp chúng tôi cải thiện bằng cách báo cáo lỗi này trên",
+		"githubText": "trang GitHub Issues của chúng tôi",
+		"copyInstructions": "Sao chép và dán thông báo lỗi sau đây để đưa vào báo cáo của bạn:",
+		"errorStack": "Stack lỗi:",
+		"componentStack": "Stack thành phần:"
+	},
 	"answers": {
 		"yes": "Có",
 		"no": "Không",

+ 8 - 0
webview-ui/src/i18n/locales/zh-CN/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "出现了错误",
+		"reportText": "请通过在以下位置报告此错误来帮助我们改进",
+		"githubText": "我们的 GitHub Issues 页面",
+		"copyInstructions": "复制并粘贴以下错误信息,将其作为提交内容的一部分:",
+		"errorStack": "错误堆栈:",
+		"componentStack": "组件堆栈:"
+	},
 	"answers": {
 		"yes": "是",
 		"no": "否",

+ 8 - 0
webview-ui/src/i18n/locales/zh-TW/common.json

@@ -1,4 +1,12 @@
 {
+	"errorBoundary": {
+		"title": "發生錯誤",
+		"reportText": "請幫助我們改進,在以下位置回報此錯誤",
+		"githubText": "我們的 GitHub Issues 頁面",
+		"copyInstructions": "複製並貼上以下錯誤訊息,將其作為提交內容的一部分:",
+		"errorStack": "錯誤堆疊:",
+		"componentStack": "元件堆疊:"
+	},
 	"answers": {
 		"yes": "是",
 		"no": "否",

+ 88 - 0
webview-ui/src/utils/__tests__/sourceMapUtils.spec.ts

@@ -0,0 +1,88 @@
+import { vi, describe, test, expect, beforeEach } from "vitest"
+import { parseStackTrace, applySourceMapsToStack, enhanceErrorWithSourceMaps } from "../sourceMapUtils"
+
+// Mock console.debug to avoid cluttering test output
+beforeEach(() => {
+	vi.spyOn(console, "debug").mockImplementation(() => {})
+})
+
+describe("sourceMapUtils", () => {
+	describe("parseStackTrace", () => {
+		// Note: parseStackTrace is now a compatibility function
+		test("should correctly parse a Chrome-style stack trace", async () => {
+			const stackTrace = `Error: Test error
+    at Function.execute (webpack:///./src/components/App.tsx:123:45)
+    at Object.next (webpack:///./node_modules/react/index.js:76:21)
+    at eval (webpack:///./src/utils/helpers.ts:89:10)`
+
+			const frames = await parseStackTrace(stackTrace)
+
+			// Verify it still returns an array of frame objects
+			expect(frames).toBeInstanceOf(Array)
+			expect(frames.length).toBeGreaterThan(0)
+
+			// Check that the first frame has the expected properties
+			const firstFrame = frames[0]
+			expect(firstFrame).toHaveProperty("functionName")
+			expect(firstFrame).toHaveProperty("fileName")
+			expect(firstFrame).toHaveProperty("lineNumber")
+			expect(firstFrame).toHaveProperty("columnNumber")
+			expect(firstFrame).toHaveProperty("source")
+
+			// Verify the first frame has the correct values
+			expect(firstFrame.fileName).toBe("webpack:///./src/components/App.tsx")
+			expect(firstFrame.lineNumber).toBe(123)
+			expect(firstFrame.columnNumber).toBe(45)
+		})
+
+		test("should return empty array for empty stack", async () => {
+			expect(await parseStackTrace("")).toEqual([])
+			expect(await parseStackTrace(undefined as unknown as string)).toEqual([])
+		})
+	})
+
+	describe("applySourceMapsToStack", () => {
+		test("should return original stack when source maps cannot be applied", async () => {
+			const stackTrace = `Error: Test error
+    at Function.execute (webpack:///./src/components/App.tsx:123:45)`
+
+			const result = await applySourceMapsToStack(stackTrace)
+
+			// For now, we expect it to return the original stack
+			// since we haven't implemented actual source map application
+			expect(result).toBe(stackTrace)
+		})
+
+		test("should handle empty stack", async () => {
+			const emptyStack = ""
+			const result = await applySourceMapsToStack(emptyStack)
+			expect(result).toBe(emptyStack)
+		})
+	})
+
+	describe("enhanceErrorWithSourceMaps", () => {
+		test("should add sourceMappedStack property to error", async () => {
+			const error = new Error("Test error")
+			error.stack = `Error: Test error
+    at Function.execute (webpack:///./src/components/App.tsx:123:45)`
+
+			// Mock the applySourceMapsToStack function
+			vi.spyOn(global.console, "error").mockImplementation(() => {})
+
+			const enhancedError = await enhanceErrorWithSourceMaps(error)
+
+			expect(enhancedError).toBe(error) // Should return the same error object
+			expect("sourceMappedStack" in enhancedError).toBe(true)
+		})
+
+		test("should handle errors without stack", async () => {
+			const error = new Error("Test error")
+			error.stack = undefined
+
+			const enhancedError = await enhanceErrorWithSourceMaps(error)
+
+			expect(enhancedError).toBe(error)
+			expect("sourceMappedStack" in enhancedError).toBe(false)
+		})
+	})
+})

+ 180 - 0
webview-ui/src/utils/sourceMapInitializer.ts

@@ -0,0 +1,180 @@
+/**
+ * Source Map Initializer
+ *
+ * This utility ensures source maps are properly loaded in production builds.
+ * It attempts to preload source maps for all scripts on the page and
+ * sets up global error handlers to enhance errors with source maps.
+ *
+ * This implementation is compatible with VSCode's Content Security Policy.
+ */
+
+import { enhanceErrorWithSourceMaps } from "./sourceMapUtils"
+
+/**
+ * Initialize source map support for production builds
+ */
+export function initializeSourceMaps(): void {
+	if (process.env.NODE_ENV !== "production") {
+		// Only needed in production builds
+		return
+	}
+
+	console.debug("Initializing CSP-compatible source map support for production build")
+
+	// Set up global error handler
+	window.addEventListener("error", async (event) => {
+		if (event.error && event.error instanceof Error) {
+			try {
+				// Apply source maps to the error
+				const enhancedError = await enhanceErrorWithSourceMaps(event.error)
+
+				// Log the enhanced error
+				console.error("Source mapped error:", enhancedError)
+
+				// Don't prevent default handling - let the ErrorBoundary catch it
+			} catch (e) {
+				console.error("Error enhancing error with source maps:", e)
+			}
+		}
+	})
+
+	// Set up unhandled promise rejection handler
+	window.addEventListener("unhandledrejection", async (event) => {
+		if (event.reason && event.reason instanceof Error) {
+			try {
+				// Apply source maps to the error
+				const enhancedError = await enhanceErrorWithSourceMaps(event.reason)
+
+				// Log the enhanced error
+				console.error("Source mapped rejection:", enhancedError)
+			} catch (e) {
+				console.error("Error enhancing rejection with source maps:", e)
+			}
+		}
+	})
+
+	// Preload source maps for all scripts
+	try {
+		const scripts = document.getElementsByTagName("script")
+		for (let i = 0; i < scripts.length; i++) {
+			const script = scripts[i]
+			if (script.src) {
+				// Try multiple source map locations
+				const possibleMapUrls = [
+					`${script.src}.map`,
+					`${script.src}?source-map=true`,
+					script.src.replace(/\.js$/, ".js.map"),
+					script.src.replace(/\.js$/, ".map.json"),
+					script.src.replace(/\.js$/, ".sourcemap"),
+				]
+
+				// Preload all possible source map locations
+				for (const mapUrl of possibleMapUrls) {
+					const link = document.createElement("link")
+					link.rel = "preload"
+					link.as = "fetch"
+					link.href = mapUrl
+					link.crossOrigin = "anonymous"
+					document.head.appendChild(link)
+				}
+
+				// Also check for inline sourceMappingURL comments
+				fetch(script.src)
+					.then((response) => response.text())
+					.then((content) => {
+						const sourceMappingURLMatch = content.match(/\/\/[#@]\s*sourceMappingURL=([^\s]+)/)
+						if (sourceMappingURLMatch && sourceMappingURLMatch[1]) {
+							const sourceMappingURL = sourceMappingURLMatch[1]
+
+							// If it's not a data: URL, preload it
+							if (!sourceMappingURL.startsWith("data:")) {
+								const scriptUrlObj = new URL(script.src)
+								const baseUrl = scriptUrlObj.href.substring(0, scriptUrlObj.href.lastIndexOf("/") + 1)
+								const fullUrl = new URL(sourceMappingURL, baseUrl).href
+
+								const link = document.createElement("link")
+								link.rel = "preload"
+								link.as = "fetch"
+								link.href = fullUrl
+								link.crossOrigin = "anonymous"
+								document.head.appendChild(link)
+							}
+						}
+					})
+					.catch((e) => console.debug("Error checking for inline sourceMappingURL:", e))
+			}
+		}
+	} catch (e) {
+		console.error("Error preloading source maps:", e)
+	}
+}
+
+/**
+ * Expose source maps on the window object for debugging
+ */
+export function exposeSourceMapsForDebugging(): void {
+	if (process.env.NODE_ENV !== "production") {
+		return
+	}
+
+	try {
+		// Add a global function to manually apply source maps to an error
+		;(window as any).__applySourceMaps = async (error: Error) => {
+			if (!(error instanceof Error)) {
+				console.error("Not an Error object:", error)
+				return error
+			}
+			return await enhanceErrorWithSourceMaps(error)
+		}
+
+		// Add a global function to test source map functionality
+		;(window as any).__testSourceMaps = () => {
+			try {
+				// Intentionally cause an error
+				const obj: any = undefined
+				obj.nonExistentMethod()
+			} catch (e) {
+				if (e instanceof Error) {
+					console.log("Original error:", e)
+					;(window as any).__applySourceMaps(e).then((enhanced: Error) => {
+						console.log("Enhanced error:", enhanced)
+
+						// Log the source mapped stack if available
+						if ("sourceMappedStack" in enhanced) {
+							console.log("Source mapped stack:", enhanced.sourceMappedStack)
+						}
+
+						// Log the source mapped component stack if available
+						if ("sourceMappedComponentStack" in enhanced) {
+							console.log("Source mapped component stack:", enhanced.sourceMappedComponentStack)
+						}
+					})
+				}
+			}
+		}
+
+		// Add a global function to check if source maps are available for a script
+		;(window as any).__checkSourceMap = async (scriptUrl: string) => {
+			try {
+				const response = await fetch(`${scriptUrl}.map`)
+				if (response.ok) {
+					const sourceMap = await response.json()
+					const originalFileName =
+						sourceMap.sources && sourceMap.sources.length > 0 ? sourceMap.sources[0] : "unknown"
+					console.log(`Source map found for ${scriptUrl}. Original file: ${originalFileName}`)
+					return true
+				} else {
+					console.log(`No source map found for ${scriptUrl}`)
+					return false
+				}
+			} catch (e) {
+				console.error(`Error checking source map for ${scriptUrl}:`, e)
+				return false
+			}
+		}
+
+		console.debug("Source map debugging utilities exposed on window object")
+	} catch (e) {
+		console.error("Error exposing source maps for debugging:", e)
+	}
+}

+ 190 - 0
webview-ui/src/utils/sourceMapUtils.ts

@@ -0,0 +1,190 @@
+import * as StackTrace from "stacktrace-js"
+
+/**
+ * Extended Error interface with source mapped stack trace
+ */
+export interface EnhancedError extends Error {
+	sourceMappedStack?: string
+	sourceMappedComponentStack?: string
+}
+
+/**
+ * Apply source maps to a stack trace using StackTrace.js
+ * Returns the original stack trace if source maps can't be applied
+ */
+export async function applySourceMapsToStack(stack: string): Promise<string> {
+	if (!stack) {
+		console.debug("applySourceMapsToStack: Empty stack trace provided")
+		return stack
+	}
+
+	console.debug("Original stack trace:", stack)
+
+	try {
+		// Create a temporary Error object with the provided stack
+		const tempError = new Error()
+		tempError.stack = stack
+
+		// Extract the error message (first line)
+		const errorMessage = stack.split("\n")[0]
+		console.debug("Error message:", errorMessage)
+
+		// Use StackTrace.js to get source mapped stack frames
+		const stackFrames = await StackTrace.fromError(tempError)
+		console.debug("StackTrace.js parsed frames:", stackFrames)
+
+		// Convert stack frames back to string format
+		const mappedFrames = stackFrames.map((frame: StackTrace.StackFrame) => {
+			const functionName = frame.functionName || "<anonymous>"
+			const fileName = frame.fileName || "unknown"
+			const lineNumber = frame.lineNumber || 0
+			const columnNumber = frame.columnNumber || 0
+
+			return `    at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`
+		})
+
+		// Reconstruct the stack trace with the error message
+		const result = [errorMessage, ...mappedFrames].join("\n")
+		console.debug("Final mapped stack trace:", result)
+		return result
+	} catch (error) {
+		console.error("Error applying source maps with StackTrace.js:", error)
+		return stack // Return original stack on error
+	}
+}
+
+/**
+ * Apply source maps to a React component stack trace using StackTrace.js
+ */
+export async function applySourceMapsToComponentStack(componentStack: string): Promise<string> {
+	if (!componentStack) {
+		console.debug("applySourceMapsToComponentStack: Empty component stack provided")
+		return componentStack
+	}
+
+	console.debug("Original component stack:", componentStack)
+
+	try {
+		// Component stack has a different format than error stack
+		// Example: at ComponentName (file:///path/to/file.tsx:123:45)
+		const lines = componentStack.split("\n")
+		const mappedLines = await Promise.all(
+			lines.map(async (line) => {
+				// Skip empty lines
+				if (!line.trim()) return line
+
+				// Extract file path, line and column numbers
+				const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/)
+				if (!match) return line
+
+				const [_, componentName, fileName, lineNumber, columnNumber] = match
+				console.debug(`Processing component stack line:`, { componentName, fileName, lineNumber, columnNumber })
+
+				try {
+					// Create a synthetic stack frame for StackTrace.js
+					const syntheticError = new Error()
+					syntheticError.stack = `Error\n    at ${componentName} (${fileName}:${lineNumber}:${columnNumber})`
+
+					// Use StackTrace.js to resolve source maps
+					const stackFrames = await StackTrace.fromError(syntheticError)
+
+					if (stackFrames.length > 0) {
+						const frame = stackFrames[0]
+						const mappedFileName = frame.fileName || fileName
+						const mappedLineNumber = frame.lineNumber || parseInt(lineNumber, 10)
+						const mappedColumnNumber = frame.columnNumber || parseInt(columnNumber, 10)
+
+						return `at ${componentName} (${mappedFileName}:${mappedLineNumber}:${mappedColumnNumber})`
+					}
+				} catch (e) {
+					console.debug(`Error processing component stack line with StackTrace.js:`, e)
+				}
+
+				return line
+			}),
+		)
+
+		const result = mappedLines.join("\n")
+		console.debug("Final mapped component stack:", result)
+		return result
+	} catch (error) {
+		console.error("Error applying source maps to component stack with StackTrace.js:", error)
+		return componentStack
+	}
+}
+
+/**
+ * Enhance an Error object with source mapped stack trace and component stack
+ */
+export function enhanceErrorWithSourceMaps(error: Error, componentStack?: string): Promise<EnhancedError> {
+	console.debug("Enhancing error with source maps using StackTrace.js:", error)
+
+	return new Promise<EnhancedError>((resolve) => {
+		if (!error.stack) {
+			console.debug("Error has no stack trace")
+			resolve(error as EnhancedError)
+			return
+		}
+
+		// Process both stacks in parallel
+		const stackPromise = applySourceMapsToStack(error.stack)
+		const componentStackPromise = componentStack
+			? applySourceMapsToComponentStack(componentStack)
+			: Promise.resolve(undefined)
+
+		Promise.all([stackPromise, componentStackPromise])
+			.then(([sourceMappedStack, sourceMappedComponentStack]) => {
+				console.debug("Source mapped stacks applied successfully with StackTrace.js")
+
+				// Extend the error object with the source mapped stack
+				Object.defineProperty(error, "sourceMappedStack", {
+					value: sourceMappedStack,
+					writable: true,
+					configurable: true,
+				})
+
+				// Add the source mapped component stack if available
+				if (sourceMappedComponentStack) {
+					Object.defineProperty(error, "sourceMappedComponentStack", {
+						value: sourceMappedComponentStack,
+						writable: true,
+						configurable: true,
+					})
+				}
+
+				resolve(error)
+			})
+			.catch((mapError) => {
+				console.error("Error applying source maps with StackTrace.js:", mapError)
+				// If anything fails, just return the original error
+				resolve(error)
+			})
+	})
+}
+
+/**
+ * Parse a stack trace string into structured stack frames
+ * This is kept for backward compatibility with tests
+ */
+export async function parseStackTrace(stack: string): Promise<any[]> {
+	if (!stack) return []
+
+	try {
+		// Create a temporary Error object with the provided stack
+		const tempError = new Error()
+		tempError.stack = stack
+
+		// Use StackTrace.js to parse the stack
+		const frames = await StackTrace.fromError(tempError)
+		return frames.map((frame: StackTrace.StackFrame) => ({
+			functionName: frame.functionName || "<anonymous>",
+			fileName: frame.fileName,
+			lineNumber: frame.lineNumber,
+			columnNumber: frame.columnNumber,
+			source: `at ${frame.functionName || "<anonymous>"} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`,
+		}))
+	} catch (error) {
+		console.error("Error parsing stack trace with StackTrace.js:", error)
+		return [] // Return empty array if parsing fails
+	}
+}

+ 116 - 0
webview-ui/src/vite-plugins/sourcemapPlugin.ts

@@ -0,0 +1,116 @@
+import { Plugin } from "vite"
+import fs from "fs"
+import path from "path"
+
+/**
+ * Custom Vite plugin to ensure source maps are properly included in the build
+ * This plugin copies source maps to the build directory and ensures they're accessible
+ */
+export function sourcemapPlugin(): Plugin {
+	return {
+		name: "vite-plugin-sourcemap",
+		apply: "build",
+
+		// After the build is complete, ensure source maps are included in the build
+		closeBundle: {
+			order: "post",
+			handler: async () => {
+				console.log("Ensuring source maps are included in build...")
+
+				// Determine the correct output directory based on the build mode
+				const mode = process.env.NODE_ENV
+				let outDir
+
+				if (mode === "nightly") {
+					outDir = path.resolve("../apps/vscode-nightly/build/webview-ui/build")
+				} else {
+					outDir = path.resolve("../src/webview-ui/build")
+				}
+
+				const assetsDir = path.join(outDir, "assets")
+
+				console.log(`Source map processing for ${mode} build in ${outDir}`)
+
+				// Check if build directory exists
+				if (!fs.existsSync(outDir)) {
+					console.warn("Build directory not found:", outDir)
+					return
+				}
+
+				// Check if assets directory exists
+				if (!fs.existsSync(assetsDir)) {
+					console.warn("Assets directory not found:", assetsDir)
+					return
+				}
+
+				// Find JS files in the assets directory
+				const jsFiles = fs.readdirSync(assetsDir).filter((file) => file.endsWith(".js"))
+
+				console.log(`Found ${jsFiles.length} JS files in assets directory`)
+
+				// Check for source maps
+				for (const jsFile of jsFiles) {
+					const jsPath = path.join(assetsDir, jsFile)
+					const mapPath = jsPath + ".map"
+
+					// If source map exists, ensure it's properly referenced in the JS file
+					if (fs.existsSync(mapPath)) {
+						console.log(`Source map found for ${jsFile}`)
+
+						// Read the JS file
+						let jsContent = fs.readFileSync(jsPath, "utf8")
+
+						// Check if the source map is already referenced
+						if (!jsContent.includes("//# sourceMappingURL=")) {
+							console.log(`Adding source map reference to ${jsFile}`)
+
+							// Add source map reference
+							jsContent += `\n//# sourceMappingURL=${jsFile}.map\n`
+
+							// Write the updated JS file
+							fs.writeFileSync(jsPath, jsContent)
+						}
+
+						// Make sure map file is in the correct format and has proper sourceRoot
+						try {
+							const mapContent = JSON.parse(fs.readFileSync(mapPath, "utf8"))
+
+							// Ensure the sourceRoot is set correctly for VSCode webview
+							if (!mapContent.sourceRoot) {
+								mapContent.sourceRoot = ""
+							}
+
+							// Make sure "sources" paths are relative
+							if (mapContent.sources) {
+								mapContent.sources = mapContent.sources.map((source: string) => {
+									// Remove absolute paths to ensure they work in VSCode webview context
+									return source.replace(/^\//, "")
+								})
+							}
+
+							// Write back the updated source map
+							fs.writeFileSync(mapPath, JSON.stringify(mapContent))
+							console.log(`Updated source map for ${jsFile}`)
+						} catch (error) {
+							console.error(`Error processing source map for ${jsFile}:`, error)
+						}
+					} else {
+						console.log(`No source map found for ${jsFile}`)
+					}
+				}
+
+				// Create a special file to enable source map loading in production
+				fs.writeFileSync(
+					path.join(outDir, "sourcemap-manifest.json"),
+					JSON.stringify({
+						enabled: true,
+						version: process.env.PKG_VERSION || "unknown",
+						buildTime: new Date().toISOString(),
+					}),
+				)
+
+				console.log("Source map processing complete")
+			},
+		},
+	}
+}

+ 9 - 1
webview-ui/vite.config.ts

@@ -5,6 +5,7 @@ import { execSync } from "child_process"
 import { defineConfig, type PluginOption, type Plugin } from "vite"
 import react from "@vitejs/plugin-react"
 import tailwindcss from "@tailwindcss/vite"
+import { sourcemapPlugin } from "./src/vite-plugins/sourcemapPlugin"
 
 function getGitSha() {
 	let gitSha: string | undefined = undefined
@@ -79,7 +80,7 @@ export default defineConfig(({ mode }) => {
 		define["process.env.PKG_OUTPUT_CHANNEL"] = JSON.stringify("Roo-Code-Nightly")
 	}
 
-	const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin()]
+	const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin(), sourcemapPlugin()]
 
 	return {
 		plugins,
@@ -94,7 +95,10 @@ export default defineConfig(({ mode }) => {
 			outDir,
 			emptyOutDir: true,
 			reportCompressedSize: false,
+			// Generate complete source maps with original TypeScript sources
 			sourcemap: true,
+			// Ensure source maps are properly included in the build
+			minify: mode === "production" ? "esbuild" : false,
 			rollupOptions: {
 				output: {
 					entryFileNames: `assets/[name].js`,
@@ -114,6 +118,10 @@ export default defineConfig(({ mode }) => {
 						) {
 							return "assets/fonts/[name][extname]"
 						}
+						// Ensure source maps are included in the build
+						if (assetInfo.name && assetInfo.name.endsWith(".map")) {
+							return "assets/[name]"
+						}
 						return "assets/[name][extname]"
 					},
 					manualChunks: (id, { getModuleInfo }) => {