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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

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

2 changes: 1 addition & 1 deletion src/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
!dist

# Include the built webview
**/*.map
!**/*.map
!webview-ui/audio
!webview-ui/build/assets/*.js
!webview-ui/build/assets/*.ttf
Expand Down
4 changes: 2 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,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*/ `
Expand Down Expand Up @@ -762,7 +762,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}">
Expand Down
4 changes: 3 additions & 1 deletion src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1985,6 +1986,7 @@ describe("Project MCP Settings", () => {
options: {},
onDidReceiveMessage: vi.fn(),
asWebviewUri: vi.fn(),
cspSource: "vscode-webview://test-csp-source",
},
visible: true,
onDidDispose: vi.fn(),
Expand Down
2 changes: 1 addition & 1 deletion src/esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
36 changes: 27 additions & 9 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 changes: 84 additions & 0 deletions webview-ui/src/__tests__/App.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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()
Expand Down
88 changes: 88 additions & 0 deletions webview-ui/src/__tests__/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading