Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 20 additions & 7 deletions pnpm-lock.yaml

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

1 change: 1 addition & 0 deletions webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
105 changes: 66 additions & 39 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -169,60 +170,86 @@ 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 ? (
<WelcomeView />
<ErrorBoundary componentName="WelcomeView">
<WelcomeView />
</ErrorBoundary>
) : (
<>
{tab === "modes" && <ModesView onDone={() => switchTab("chat")} />}
{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
{tab === "modes" && (
<ErrorBoundary componentName="ModesView">
<ModesView onDone={() => switchTab("chat")} />
</ErrorBoundary>
)}
{tab === "mcp" && (
<ErrorBoundary componentName="McpView">
<McpView onDone={() => switchTab("chat")} />
</ErrorBoundary>
)}
{tab === "history" && (
<ErrorBoundary componentName="HistoryView">
<HistoryView onDone={() => switchTab("chat")} />
</ErrorBoundary>
)}
{tab === "settings" && (
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
<ErrorBoundary componentName="SettingsView">
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
</ErrorBoundary>
)}
{tab === "marketplace" && (
<MarketplaceView
stateManager={marketplaceStateManager}
onDone={() => switchTab("chat")}
targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
/>
<ErrorBoundary componentName="MarketplaceView">
<MarketplaceView
stateManager={marketplaceStateManager}
onDone={() => switchTab("chat")}
targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
/>
</ErrorBoundary>
)}
{tab === "account" && (
<AccountView
userInfo={cloudUserInfo}
isAuthenticated={cloudIsAuthenticated}
cloudApiUrl={cloudApiUrl}
onDone={() => switchTab("chat")}
/>
<ErrorBoundary componentName="AccountView">
<AccountView
userInfo={cloudUserInfo}
isAuthenticated={cloudIsAuthenticated}
cloudApiUrl={cloudApiUrl}
onDone={() => switchTab("chat")}
/>
</ErrorBoundary>
)}
<ChatView
ref={chatViewRef}
isHidden={tab !== "chat"}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => setShowAnnouncement(false)}
/>
<HumanRelayDialog
isOpen={humanRelayDialogState.isOpen}
requestId={humanRelayDialogState.requestId}
promptText={humanRelayDialogState.promptText}
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
/>
<ErrorBoundary componentName="ChatView">
<ChatView
ref={chatViewRef}
isHidden={tab !== "chat"}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => setShowAnnouncement(false)}
/>
</ErrorBoundary>
<ErrorBoundary componentName="HumanRelayDialog">
<HumanRelayDialog
isOpen={humanRelayDialogState.isOpen}
requestId={humanRelayDialogState.requestId}
promptText={humanRelayDialogState.promptText}
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
/>
</ErrorBoundary>
Comment on lines +178 to +234
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A cleaner pattern would be to just use the HoC so we don't need to litter the existing markup with a bunch of wrappers, the wrapping happens on component's file export

Instead of export default Component we'd do export default withErrorBoundary(Component) instead. That could be customised so we don't have repeated logic, all of the magic happens inside withErroBoundary wrapper.

</>
)
}

const queryClient = new QueryClient()

const AppWithProviders = () => (
<ExtensionStateContextProvider>
<TranslationProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
<App />
</TooltipProvider>
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>
<ErrorBoundary componentName="App">
<ExtensionStateContextProvider>
<TranslationProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
<App />
</TooltipProvider>
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>
</ErrorBoundary>
)

export default AppWithProviders
82 changes: 82 additions & 0 deletions webview-ui/src/components/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center justify-center p-6 bg-vscode-editor-background border border-vscode-widget-border rounded-md m-4">
<div className="flex items-center mb-4">
<span className="codicon codicon-error text-vscode-errorForeground text-2xl mr-3" />
<h2 className="text-lg font-semibold text-vscode-editor-foreground">
{t("errorBoundary.title", "Something went wrong")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using inline fallback strings in translation calls. Remove the second argument in t() (e.g. in t('errorBoundary.title', 'Something went wrong')) and rely solely on translations defined in the JSON files per our guidelines.

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

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

</h2>
</div>

{componentName && (
<p className="text-sm text-vscode-descriptionForeground mb-2">
{t("errorBoundary.componentError", "Error in {{componentName}} component", { componentName })}
</p>
)}

<p className="text-sm text-vscode-descriptionForeground mb-4 text-center max-w-md">
{t(
"errorBoundary.description",
"An error occurred in this part of the interface. You can try to recover by clicking the button below.",
)}
</p>

<details className="mb-4 w-full max-w-md">
<summary className="cursor-pointer text-sm text-vscode-descriptionForeground hover:text-vscode-editor-foreground">
{t("errorBoundary.showDetails", "Show error details")}
</summary>
<pre className="mt-2 p-3 bg-vscode-textCodeBlock-background border border-vscode-widget-border rounded text-xs text-vscode-editor-foreground overflow-auto max-h-32">
{error.message}
{error.stack && (
<>
{"\n\n"}
{error.stack}
</>
)}
</pre>
</details>

<VSCodeButton appearance="primary" onClick={resetErrorBoundary}>
{t("errorBoundary.retry", "Try again")}
</VSCodeButton>
</div>
)
}

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 (
<ReactErrorBoundary
FallbackComponent={(props) => <ErrorFallback {...props} componentName={componentName} />}
onError={handleError}>
{children}
</ReactErrorBoundary>
)
}

export default ErrorBoundary
Loading
Loading