Skip to content
Closed
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
4 changes: 4 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
270 changes: 270 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<any>) => {
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<boolean>("hasCrashRecovery")
const lastCrashInfo = context.globalState.get<any>("lastCrashInfo")
const taskRecoveryInfo = context.globalState.get<any>("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)
}
}
15 changes: 15 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo/Incomplete text: The 'helpText' string reads 'If the problem persists, please'. Consider completing this sentence for clarity.

Suggested change
"helpText": "If the problem persists, please"
"helpText": "If the problem persists, please contact support."

}
}
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export interface WebviewMessage {
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "openExternal"
| "reloadWindow"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"
| "installMarketplaceItem"
Expand Down
Loading
Loading