diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 82c780adf4..1729da84ea 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -707,6 +707,10 @@ export const webviewMessageHandler = async ( vscode.env.openExternal(vscode.Uri.parse(message.url)) } break + case "reloadWindow": + // Reload the VS Code window to recover from crash + vscode.commands.executeCommand("workbench.action.reloadWindow") + break case "checkpointDiff": const result = checkoutDiffPayloadSchema.safeParse(message.payload) diff --git a/src/extension.ts b/src/extension.ts index bd43bcbf8a..d2dd39a415 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,6 +59,12 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(outputChannel) outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`) + // Set up global error handlers for crash recovery + setupGlobalErrorHandlers(context, outputChannel) + + // Check for crash recovery + await checkForCrashRecovery(context, outputChannel) + // Migrate old settings to new await migrateSettings(context, outputChannel) @@ -218,3 +224,267 @@ export async function deactivate() { TelemetryService.instance.shutdown() TerminalRegistry.cleanup() } + +// Global error handlers for crash recovery +function setupGlobalErrorHandlers(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { + // Handle uncaught exceptions + process.on("uncaughtException", async (error: Error) => { + const errorMessage = `[CRASH] Uncaught Exception: ${error.message}\nStack: ${error.stack}` + outputChannel.appendLine(errorMessage) + console.error(errorMessage) + + // Save crash information + await saveCrashInfo(context, error, "uncaughtException") + + // Attempt to save current task state + await saveTaskStateOnCrash(context) + + // Log telemetry + // Log crash telemetry + try { + if (TelemetryService.hasInstance()) { + TelemetryService.instance.captureEvent("extension_crash" as any, { + type: "uncaughtException", + error: error.message, + stack: error.stack, + platform: process.platform, + }) + } + } catch (e) { + console.error("Failed to log telemetry:", e) + } + + // Show user-friendly error message + vscode.window + .showErrorMessage( + "Roo Code encountered an unexpected error. Your work has been saved. Please restart VS Code.", + "Restart VS Code", + ) + .then((selection) => { + if (selection === "Restart VS Code") { + vscode.commands.executeCommand("workbench.action.reloadWindow") + } + }) + }) + + // Handle unhandled promise rejections + process.on("unhandledRejection", async (reason: any, promise: Promise) => { + const errorMessage = `[CRASH] Unhandled Promise Rejection: ${reason}\nPromise: ${promise}` + outputChannel.appendLine(errorMessage) + console.error(errorMessage) + + // Save crash information + await saveCrashInfo(context, reason, "unhandledRejection") + + // Attempt to save current task state + await saveTaskStateOnCrash(context) + + // Log telemetry + // Log crash telemetry + try { + if (TelemetryService.hasInstance()) { + TelemetryService.instance.captureEvent("extension_crash" as any, { + type: "unhandledRejection", + reason: String(reason), + platform: process.platform, + }) + } + } catch (e) { + console.error("Failed to log telemetry:", e) + } + }) + + // Windows-specific error handling + if (process.platform === "win32") { + // Handle Windows-specific errors + process.on("SIGTERM", async () => { + outputChannel.appendLine("[CRASH] Received SIGTERM signal (Windows termination)") + await saveCrashInfo(context, new Error("SIGTERM received"), "SIGTERM") + await saveTaskStateOnCrash(context) + }) + + process.on("SIGINT", async () => { + outputChannel.appendLine("[CRASH] Received SIGINT signal (Windows interruption)") + await saveCrashInfo(context, new Error("SIGINT received"), "SIGINT") + await saveTaskStateOnCrash(context) + }) + + // Handle Windows-specific exit events + process.on("exit", async (code) => { + if (code !== 0) { + outputChannel.appendLine(`[CRASH] Process exiting with code ${code}`) + await saveCrashInfo(context, new Error(`Process exit with code ${code}`), "exit") + await saveTaskStateOnCrash(context) + } + }) + + // Handle Windows-specific errors that might cause crashes + process.on("uncaughtExceptionMonitor", (error: Error, origin: string) => { + // This event is emitted before uncaughtException, useful for logging + outputChannel.appendLine(`[CRASH] Uncaught exception monitor: ${error.message} from ${origin}`) + + // Check for Windows-specific error patterns + if (error.message.includes("EPERM") || error.message.includes("EACCES")) { + outputChannel.appendLine("[CRASH] Windows permission error detected") + } else if (error.message.includes("ENOENT")) { + outputChannel.appendLine("[CRASH] Windows file not found error detected") + } else if (error.message.includes("spawn") || error.message.includes("ENOBUFS")) { + outputChannel.appendLine("[CRASH] Windows process spawn error detected") + } + }) + } +} + +// Save crash information for recovery +async function saveCrashInfo(context: vscode.ExtensionContext, error: any, type: string) { + try { + const crashInfo = { + timestamp: new Date().toISOString(), + type, + error: + error instanceof Error + ? { + message: error.message, + stack: error.stack, + name: error.name, + } + : String(error), + platform: process.platform, + vscodeVersion: vscode.version, + extensionVersion: context.extension?.packageJSON?.version, + } + + await context.globalState.update("lastCrashInfo", crashInfo) + await context.globalState.update("hasCrashRecovery", true) + } catch (e) { + console.error("Failed to save crash info:", e) + } +} + +// Save current task state on crash +async function saveTaskStateOnCrash(context: vscode.ExtensionContext) { + try { + const provider = ClineProvider.getVisibleInstance() + if (provider) { + const currentTask = provider.getCurrentCline() + if (currentTask) { + // Save current task state + // Force save current state + await provider.postStateToWebview() + + // Save task recovery info + const recoveryInfo = { + taskId: currentTask.taskId, + parentTaskId: currentTask.parentTask?.taskId, + taskStack: provider.getCurrentTaskStack(), + timestamp: new Date().toISOString(), + } + + await context.globalState.update("taskRecoveryInfo", recoveryInfo) + outputChannel.appendLine(`[CRASH] Saved task recovery info for task ${currentTask.taskId}`) + } + } + } catch (e) { + console.error("Failed to save task state on crash:", e) + } +} + +// Check for crash recovery on startup +async function checkForCrashRecovery(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { + try { + const hasCrashRecovery = context.globalState.get("hasCrashRecovery") + const lastCrashInfo = context.globalState.get("lastCrashInfo") + const taskRecoveryInfo = context.globalState.get("taskRecoveryInfo") + + if (hasCrashRecovery && lastCrashInfo) { + outputChannel.appendLine( + `[RECOVERY] Detected previous crash: ${lastCrashInfo.type} at ${lastCrashInfo.timestamp}`, + ) + + // Clear the crash flag + await context.globalState.update("hasCrashRecovery", false) + + // Show recovery notification with Windows-specific messaging if applicable + const isWindows = process.platform === "win32" + const crashMessage = + isWindows && (lastCrashInfo.type === "SIGTERM" || lastCrashInfo.type === "SIGINT") + ? "Roo Code was terminated unexpectedly on Windows. Would you like to restore your last session?" + : "Roo Code recovered from a previous crash. Would you like to restore your last session?" + + const selection = await vscode.window.showInformationMessage(crashMessage, "Restore Session", "Start Fresh") + + if (selection === "Restore Session" && taskRecoveryInfo) { + outputChannel.appendLine(`[RECOVERY] Attempting to restore task ${taskRecoveryInfo.taskId}`) + + // Delay to ensure extension is fully initialized + setTimeout(async () => { + try { + const provider = ClineProvider.getVisibleInstance() + if (provider && taskRecoveryInfo.taskId) { + // Check if this was a subtask + if (taskRecoveryInfo.parentTaskId) { + outputChannel.appendLine( + `[RECOVERY] Detected subtask recovery. Parent task: ${taskRecoveryInfo.parentTaskId}`, + ) + + // First, try to restore the parent task + try { + await provider.showTaskWithId(taskRecoveryInfo.parentTaskId) + outputChannel.appendLine( + `[RECOVERY] Restored parent task ${taskRecoveryInfo.parentTaskId}`, + ) + + // Then show information about the subtask that was interrupted + vscode.window + .showInformationMessage( + `Restored to parent task. The subtask that was running during the crash has been saved and can be resumed.`, + "View Subtask", + ) + .then(async (selection) => { + if (selection === "View Subtask") { + // Show the subtask that was interrupted + await provider.showTaskWithId(taskRecoveryInfo.taskId) + } + }) + } catch (parentError) { + // If parent task can't be restored, just restore the subtask + outputChannel.appendLine( + `[RECOVERY] Failed to restore parent task, restoring subtask instead`, + ) + await provider.showTaskWithId(taskRecoveryInfo.taskId) + + vscode.window.showInformationMessage( + `Restored subtask from before the crash. The parent task context may need to be re-established.`, + ) + } + } else { + // Regular task recovery + await provider.showTaskWithId(taskRecoveryInfo.taskId) + + vscode.window.showInformationMessage(`Restored task from before the crash.`) + } + + // If there was a task stack, log it for debugging + if (taskRecoveryInfo.taskStack && taskRecoveryInfo.taskStack.length > 1) { + outputChannel.appendLine( + `[RECOVERY] Task stack at crash: ${taskRecoveryInfo.taskStack.join(" -> ")}`, + ) + } + } + } catch (e) { + console.error("Failed to restore task:", e) + vscode.window.showErrorMessage( + "Could not restore the previous task, but your work has been saved.", + ) + } + }, 2000) + } + + // Clear recovery info + await context.globalState.update("taskRecoveryInfo", undefined) + await context.globalState.update("lastCrashInfo", undefined) + } + } catch (e) { + console.error("Error checking for crash recovery:", e) + } +} diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index db6341c312..259d6bf4e3 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -168,5 +168,20 @@ "preventCompletionWithOpenTodos": { "description": "Prevent task completion when there are incomplete todos in the todo list" } + }, + "errorBoundary": { + "title": "Something went wrong", + "reportText": "Please help us improve by reporting this error on", + "githubText": "GitHub", + "copyInstructions": "Please copy and paste the following error message:", + "errorStack": "Error Stack", + "componentStack": "Component Stack", + "windowsNote": "This crash occurred on Windows. Your work has been automatically saved.", + "crashRecoveryText": "Don't worry! Your work has been saved and can be recovered.", + "restarting": "Restarting...", + "restartVSCode": "Restart VS Code", + "reportIssue": "Report Issue", + "technicalDetails": "Technical Details", + "helpText": "If the problem persists, please" } } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e276522..0aa576d7f6 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -182,6 +182,7 @@ export interface WebviewMessage { | "profileThresholds" | "setHistoryPreviewCollapsed" | "openExternal" + | "reloadWindow" | "filterMarketplaceItems" | "marketplaceButtonClicked" | "installMarketplaceItem" diff --git a/webview-ui/src/components/ErrorBoundary.tsx b/webview-ui/src/components/ErrorBoundary.tsx index 283171ea6f..76464e61c9 100644 --- a/webview-ui/src/components/ErrorBoundary.tsx +++ b/webview-ui/src/components/ErrorBoundary.tsx @@ -2,6 +2,7 @@ import React, { Component } from "react" import { telemetryClient } from "@src/utils/TelemetryClient" import { withTranslation, WithTranslation } from "react-i18next" import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils" +import { vscode } from "@src/utils/vscode" type ErrorProps = { children: React.ReactNode @@ -11,14 +12,15 @@ type ErrorState = { error?: string componentStack?: string | null timestamp?: number + isRecovering?: boolean } class ErrorBoundary extends Component { constructor(props: ErrorProps) { super(props) - this.state = {} - - this.state = {} + this.state = { + isRecovering: false, + } } static getDerivedStateFromError(error: unknown) { @@ -54,6 +56,31 @@ class ErrorBoundary extends Component { }) } + handleRestart = () => { + this.setState({ isRecovering: true }) + vscode.postMessage({ type: "reloadWindow" }) + } + + handleReportIssue = () => { + const errorInfo = encodeURIComponent( + ` +**Error:** ${this.state.error || "Unknown error"} +**Timestamp:** ${new Date(this.state.timestamp || Date.now()).toISOString()} +**Version:** ${process.env.PKG_VERSION || "unknown"} +**Platform:** ${navigator.platform} +**User Agent:** ${navigator.userAgent} + +**Component Stack:** +\`\`\` +${this.state.componentStack || "Not available"} +\`\`\` + `.trim(), + ) + + const issueUrl = `https://github.com/RooCodeInc/Roo-Code/issues/new?title=Crash%20Report&body=${errorInfo}` + window.open(issueUrl, "_blank") + } + render() { const { t } = this.props @@ -63,33 +90,96 @@ class ErrorBoundary extends Component { const errorDisplay = this.state.error const componentStackDisplay = this.state.componentStack - const version = process.env.PKG_VERSION || "unknown" + const isWindows = navigator.platform.toLowerCase().includes("win") return ( -
-

- {t("errorBoundary.title")} (v{version}) -

-

- {t("errorBoundary.reportText")}{" "} - - {t("errorBoundary.githubText")} - -

-

{t("errorBoundary.copyInstructions")}

- -
-

{t("errorBoundary.errorStack")}

-
{errorDisplay}
+
+
+

+ {t("errorBoundary.title")} (v{version}) +

+ + {isWindows && ( +
+ + {t( + "errorBoundary.windowsNote", + "This crash occurred on Windows. Your work has been automatically saved.", + )} +
+ )} + +

+ {t( + "errorBoundary.crashRecoveryText", + "Don't worry! Your work has been saved and can be recovered.", + )} +

+ +
+ + + +
- {componentStackDisplay && ( -
-

{t("errorBoundary.componentStack")}

-
{componentStackDisplay}
+
+ + {t("errorBoundary.technicalDetails", "Technical Details")} + + +
+

{t("errorBoundary.copyInstructions")}

+ +
+

{t("errorBoundary.errorStack")}

+
{errorDisplay}
+
+ + {componentStackDisplay && ( +
+

{t("errorBoundary.componentStack")}

+
+									{componentStackDisplay}
+								
+
+ )}
- )} +
+ +
+

+ {t("errorBoundary.helpText", "If the problem persists, please")}{" "} + + {t("errorBoundary.githubText")} + +

+
) }