Version: 1.0 Date: 2025-10-19 Status: Design Phase Target: WaveTerm Fork v0.12.4+
Implement a user-facing crash handler modal that displays when WaveTerm encounters an uncaught exception. The modal will show the error details, stack trace, and system information with copy-to-clipboard functionality, allowing users to easily report crashes before the application exits.
Core Problem: Currently, when WaveTerm crashes due to an uncaught exception, the error is only logged to waveapp.log and the application silently quits, leaving users confused about what went wrong.
Solution: Display a modal dialog with comprehensive crash information that users can copy and paste for bug reports.
Location: emain/emain.ts:655-672
Current Implementation:
process.on("uncaughtException", (error) => {
if (caughtException) {
return;
}
// Check if the error is related to QUIC protocol, if so, ignore
if (error?.message?.includes("net::ERR_QUIC_PROTOCOL_ERROR")) {
console.log("Ignoring QUIC protocol error:", error.message);
console.log("Stack Trace:", error.stack);
return;
}
caughtException = true;
console.log("Uncaught Exception, shutting down: ", error);
console.log("Stack Trace:", error.stack);
// Optionally, handle cleanup or exit the app
electronApp.quit();
});Issues:
- No user feedback when crash occurs
- Users don't know what caused the crash
- No easy way to copy error details for bug reports
- Application exits immediately without explanation
The codebase already uses electron.dialog.showMessageBoxSync() in several places:
- Multi-instance dialogs (
emain/emain-wavesrv.ts:76) - Window close confirmations (
emain/emain-window.ts:276) - Workspace deletion confirmations (
emain/emain-window.ts:740) - ARM64 translation warnings (
emain/platform.ts:119)
We will follow the same pattern for crash handling.
Modal Components:
- Error Icon - Standard error icon (type: "error")
- Title - "WaveTerm Encountered an Error"
- Message - Brief user-friendly description
- Details Section - Expandable/scrollable text area with:
- Error message
- Stack trace
- System information
- Session information
- Buttons:
- "Copy to Clipboard" - Copy all crash details
- "View Logs" - Open the log file location
- "Report Issue" - Open GitHub issues page
- "Close" - Exit the application
Crash Details:
interface CrashReport {
timestamp: string; // ISO 8601 timestamp
error: {
name: string; // Error name (e.g., "TypeError")
message: string; // Error message
stack: string; // Full stack trace
};
system: {
platform: string; // OS (from unamePlatform)
arch: string; // Architecture (from unameArch)
electronVersion: string; // Electron version
waveVersion: string; // Wave version
waveBuildTime: number; // Build timestamp
nodeVersion: string; // Node.js version
chromeVersion: string; // Chrome version
};
session: {
dataDir: string; // Wave data directory
configDir: string; // Wave config directory
instanceId?: string; // Multi-instance ID (if applicable)
uptime: number; // Process uptime in seconds
windowCount: number; // Number of open windows
};
logs: {
logFile: string; // Path to current log file
recentLogs: string[]; // Last 50 lines from log
};
}┌─────────────────────────────────────────────────────────────┐
│ ⚠️ WaveTerm Encountered an Error │
│ │
│ WaveTerm has crashed due to an unexpected error. Please │
│ copy the details below and report this issue. │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ERROR DETAILS │ │
│ │ │ │
│ │ Timestamp: 2025-10-19T14:23:45.123Z │ │
│ │ │ │
│ │ Error: TypeError: Cannot read property 'foo' of null │ │
│ │ │ │
│ │ Stack Trace: │ │
│ │ at Object.<anonymous> (/path/to/file.js:123:45) │ │
│ │ at Module._compile (internal/modules/cjs:456:78) │ │
│ │ ... (scrollable) │ │
│ │ │ │
│ │ System Information: │ │
│ │ Platform: win32 │ │
│ │ Architecture: x64 │ │
│ │ Wave Version: v0.12.3 │ │
│ │ Electron: 33.2.0 │ │
│ │ Node: 20.18.0 │ │
│ │ │ │
│ │ Session Information: │ │
│ │ Instance ID: test │ │
│ │ Uptime: 3h 45m 12s │ │
│ │ Open Windows: 2 │ │
│ │ │ │
│ │ Log File: C:\Users\...\waveterm\waveapp.log │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Copy to Clipboard] [View Logs] [Report Issue] │
│ [Close] (default) │
└─────────────────────────────────────────────────────────────┘
Create a dedicated module for crash handling:
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";
import * as fs from "fs";
import * as path from "path";
import { getWaveDataDir, getWaveConfigDir, unamePlatform, unameArch, getMultiInstanceInfo } from "./platform";
import { getWaveVersion } from "./emain-wavesrv";
import { getAllWaveWindows } from "./emain-window";
import { log } from "./log";
interface CrashReport {
timestamp: string;
error: {
name: string;
message: string;
stack: string;
};
system: {
platform: string;
arch: string;
electronVersion: string;
waveVersion: string;
waveBuildTime: number;
nodeVersion: string;
chromeVersion: string;
};
session: {
dataDir: string;
configDir: string;
instanceId?: string;
uptime: number;
windowCount: number;
};
logs: {
logFile: string;
recentLogs: string[];
};
}
function formatUptime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
}
function getRecentLogs(logFile: string, lines: number = 50): string[] {
try {
if (!fs.existsSync(logFile)) {
return ["Log file not found"];
}
const content = fs.readFileSync(logFile, "utf-8");
const allLines = content.split("\n");
return allLines.slice(-lines);
} catch (e) {
return [`Error reading log file: ${e.message}`];
}
}
function generateCrashReport(error: Error): CrashReport {
const waveDataDir = getWaveDataDir();
const waveConfigDir = getWaveConfigDir();
const multiInstanceInfo = getMultiInstanceInfo();
const waveVersion = getWaveVersion();
const logFile = path.join(waveDataDir, "waveapp.log");
return {
timestamp: new Date().toISOString(),
error: {
name: error.name || "Error",
message: error.message || "Unknown error",
stack: error.stack || "No stack trace available",
},
system: {
platform: unamePlatform,
arch: unameArch,
electronVersion: process.versions.electron,
waveVersion: waveVersion.version,
waveBuildTime: waveVersion.buildTime,
nodeVersion: process.versions.node,
chromeVersion: process.versions.chrome,
},
session: {
dataDir: waveDataDir,
configDir: waveConfigDir,
instanceId: multiInstanceInfo.instanceId,
uptime: process.uptime(),
windowCount: getAllWaveWindows().length,
},
logs: {
logFile: logFile,
recentLogs: getRecentLogs(logFile),
},
};
}
function formatCrashReportForDisplay(report: CrashReport): string {
return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WAVETERM CRASH REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Timestamp: ${report.timestamp}
━━━ ERROR DETAILS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Error: ${report.error.name}: ${report.error.message}
Stack Trace:
${report.error.stack}
━━━ SYSTEM INFORMATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Platform: ${report.system.platform}
Architecture: ${report.system.arch}
Wave Version: ${report.system.waveVersion}
Build Time: ${new Date(report.system.waveBuildTime * 1000).toISOString()}
Electron: ${report.system.electronVersion}
Node.js: ${report.system.nodeVersion}
Chrome: ${report.system.chromeVersion}
━━━ SESSION INFORMATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Data Directory: ${report.session.dataDir}
Config Directory: ${report.session.configDir}
Instance ID: ${report.session.instanceId || "default"}
Uptime: ${formatUptime(report.session.uptime)}
Open Windows: ${report.session.windowCount}
━━━ LOG FILE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Location: ${report.logs.logFile}
Recent Log Entries (last ${report.logs.recentLogs.length} lines):
${report.logs.recentLogs.join("\n")}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`;
}
export async function showCrashDialog(error: Error): Promise<void> {
try {
// Generate crash report
const crashReport = generateCrashReport(error);
const reportText = formatCrashReportForDisplay(crashReport);
// Log to file
log("═════════════════════════════════════════════════════");
log("CRASH REPORT");
log("═════════════════════════════════════════════════════");
log(reportText);
log("═════════════════════════════════════════════════════");
// Wait for app to be ready (in case crash happens during startup)
await electron.app.whenReady();
const { dialog, shell, clipboard } = electron;
// Show dialog with crash details
const dialogOpts: Electron.MessageBoxOptions = {
type: "error",
buttons: ["Copy to Clipboard", "View Logs", "Report Issue", "Close"],
defaultId: 3, // Close is default
cancelId: 3,
title: "WaveTerm Encountered an Error",
message: "WaveTerm has crashed due to an unexpected error.",
detail: `${error.name}: ${error.message}\n\nClick "Copy to Clipboard" to copy full crash details for reporting this issue.\n\nLog file: ${crashReport.logs.logFile}`,
noLink: true,
};
const choice = dialog.showMessageBoxSync(dialogOpts);
switch (choice) {
case 0: // Copy to Clipboard
clipboard.writeText(reportText);
// Show confirmation
dialog.showMessageBoxSync({
type: "info",
buttons: ["OK"],
title: "Copied",
message: "Crash details copied to clipboard",
detail: "You can now paste this information when reporting the issue on GitHub.",
});
break;
case 1: // View Logs
// Open the log directory
shell.showItemInFolder(crashReport.logs.logFile);
break;
case 2: // Report Issue
// Open GitHub issues page with pre-filled template
const issueTitle = encodeURIComponent(`Crash: ${error.message}`);
const issueBody = encodeURIComponent(
`**Crash Report**\n\n\`\`\`\n${reportText}\n\`\`\`\n\n` +
`**Steps to Reproduce**\n1. \n2. \n3. \n\n` +
`**Expected Behavior**\n\n` +
`**Actual Behavior**\nApplication crashed with the error above.`
);
shell.openExternal(
`https://github.com/a5af/waveterm/issues/new?title=${issueTitle}&body=${issueBody}`
);
break;
case 3: // Close
default:
// Just close
break;
}
} catch (e) {
// If crash dialog itself crashes, just log and exit
console.error("Error showing crash dialog:", e);
log("Error showing crash dialog:", e);
}
}Update the uncaught exception handler:
// At top of file, add import:
import { showCrashDialog } from "./crash-handler";
// Replace lines 655-672 with:
let caughtException = false;
process.on("uncaughtException", async (error) => {
if (caughtException) {
return;
}
// Check if the error is related to QUIC protocol, if so, ignore (can happen with the updater)
if (error?.message?.includes("net::ERR_QUIC_PROTOCOL_ERROR")) {
console.log("Ignoring QUIC protocol error:", error.message);
console.log("Stack Trace:", error.stack);
return;
}
caughtException = true;
console.log("Uncaught Exception, showing crash dialog:", error);
console.log("Stack Trace:", error.stack);
// Show crash dialog to user
await showCrashDialog(error);
// Exit application
electronApp.quit();
});Also handle unhandled promise rejections:
process.on("unhandledRejection", async (reason, promise) => {
const error = reason instanceof Error ? reason : new Error(String(reason));
console.log("Unhandled Promise Rejection:", error);
console.log("Stack Trace:", error.stack);
// Show crash dialog for unhandled rejections too
await showCrashDialog(error);
electronApp.quit();
});- User Action → Application encounters uncaught exception
- Crash Handler → Catches exception, generates crash report
- Modal Display → Shows error dialog with details
- User Options:
- Copy to Clipboard → Copies full report, shows confirmation
- View Logs → Opens file explorer to log file location
- Report Issue → Opens GitHub with pre-filled issue template
- Close → Exits application
- Application Exit → App quits gracefully after user dismisses dialog
- Transparency: Users know exactly what went wrong
- Easy Reporting: One-click copy to clipboard for bug reports
- Quick Access: Direct access to log files
- Streamlined Issues: Pre-filled GitHub issue template
Test Case 1: Trigger Crash During Runtime
// Add temporary test code in emain.ts
electron.ipcMain.on("test-crash", () => {
throw new Error("Test crash from main process");
});- Trigger crash from renderer
- Verify modal appears
- Test all buttons (Copy, View Logs, Report Issue, Close)
Test Case 2: Crash During Startup
// Add temporary code early in appMain()
setTimeout(() => {
throw new Error("Test startup crash");
}, 1000);- Verify modal appears even during startup
- Verify all information is captured
Test Case 3: Unhandled Promise Rejection
// Add temporary test code
electron.ipcMain.on("test-rejection", () => {
Promise.reject(new Error("Test unhandled rejection"));
});- Verify modal appears for rejections
- Verify stack trace is captured
- Log File Missing: Crash before log file created
- Long Stack Trace: Verify scrolling works
- Unicode in Error: Test special characters
- Multiple Windows: Verify modal appears on correct window
- No Windows Open: Verify modal can show without parent window
- Clipboard Failure: Handle clipboard write errors
- ✅ Modal appears within 500ms of crash
- ✅ All crash details are captured and displayed
- ✅ Copy to Clipboard works and shows confirmation
- ✅ View Logs opens correct directory
- ✅ Report Issue opens GitHub with pre-filled template
- ✅ Close button exits application
- ✅ No crashes in crash handler itself (graceful degradation)
Add optional settings for crash handling:
{
"crash:show-dialog": true, // Show dialog (vs. just log)
"crash:auto-report": false, // Auto-submit crash reports
"crash:include-recent-logs": true, // Include logs in report
"crash:log-lines": 50 // Number of log lines to include
}For testing and debugging:
# Disable crash dialog (just log and exit)
WAVE_CRASH_DIALOG=false Wave.exe
# Save crash report to file
WAVE_CRASH_SAVE_PATH=D:\crashes Wave.exeSafe to Include:
- Error message and stack trace
- System information (OS, versions)
- Session metadata (uptime, window count)
- Log file path
Never Include:
- User credentials or API keys
- File contents or terminal output
- Personal identifiable information
- SSH keys or certificates
When including recent logs, filter out sensitive patterns:
- Authentication tokens
- API keys (common patterns)
- File paths with usernames (truncate)
Example:
function sanitizeLog(log: string): string {
return log
.replace(/Bearer\s+[A-Za-z0-9_-]+/g, "Bearer [REDACTED]")
.replace(/api[_-]?key[=:]\s*[^\s]+/gi, "api_key=[REDACTED]")
.replace(/C:\\Users\\[^\\]+/g, "C:\\Users\\[USER]");
}- Create
emain/crash-handler.ts - Implement
generateCrashReport()function - Implement
formatCrashReportForDisplay()function - Implement
showCrashDialog()function - Update
emain/emain.tsuncaughtException handler - Add unhandledRejection handler
- Add manual test triggers
- Test all button actions
- Test edge cases (no logs, startup crash, etc.)
- Verify clipboard functionality
- Test GitHub issue template
- Add log sanitization
- Improve error message formatting
- Add configuration options
- Document crash handling in README
- Update BUILD.md with crash testing instructions
- Add crash reporting guide to docs
- Create GitHub issue template for crashes
This crash handler is similar to:
- Multi-instance dialog (
emain/emain-wavesrv.ts:54-84) - ARM64 translation warning (
emain/platform.ts:109-128)
- Crash Analytics: Send anonymous crash reports to telemetry
- Auto-Recovery: Attempt to restart with safe mode
- Session Restore: Restore windows/tabs after crash
- Crash History: Keep history of crashes for pattern detection
- User Awareness: 100% of crashes show modal (vs. silent exit)
- Bug Reports: 50%+ increase in quality bug reports with full details
- Time to Report: Reduce time from crash to GitHub issue by 80%
- User Satisfaction: Positive feedback on crash transparency
- Current Crash Handler:
emain/emain.ts:655-672 - Dialog Examples:
emain/emain-wavesrv.ts,emain/emain-window.ts - Logging System:
emain/log.ts - Electron Dialog API: https://www.electronjs.org/docs/latest/api/dialog
- Clipboard API: https://www.electronjs.org/docs/latest/api/clipboard
Status: Ready for implementation Priority: High - Improves user experience and bug reporting Estimated Effort: 1-2 days development + 1 day testing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WAVETERM CRASH REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Timestamp: 2025-10-19T14:23:45.123Z
━━━ ERROR DETAILS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Error: TypeError: Cannot read property 'workspaceId' of null
Stack Trace:
at WaveBrowserWindow.getWorkspaceId (D:\Code\waveterm\dist\main\emain-window.js:145:32)
at handleWSEvent (D:\Code\waveterm\dist\main\emain.js:125:18)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
━━━ SYSTEM INFORMATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Platform: win32
Architecture: x64
Wave Version: v0.12.3
Build Time: 2025-10-18T20:15:30.000Z
Electron: 33.2.0
Node.js: 20.18.0
Chrome: 130.0.6723.59
━━━ SESSION INFORMATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Data Directory: C:\Users\asafe\AppData\Local\waveterm\Data
Config Directory: C:\Users\asafe\.config\waveterm
Instance ID: test
Uptime: 3h 45m 12s
Open Windows: 2
━━━ LOG FILE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Location: C:\Users\asafe\AppData\Local\waveterm\Data\waveapp.log
Recent Log Entries (last 50 lines):
2025-10-19 14:23:40.123 waveterm-app starting, data_dir=C:\Users\asafe\AppData\Local\waveterm\Data...
2025-10-19 14:23:41.456 wavesrv started successfully
2025-10-19 14:23:42.789 created new window workspace:abc123
2025-10-19 14:23:45.012 handleWSEvent electron:updateactivetab
2025-10-19 14:23:45.123 Uncaught Exception, showing crash dialog: TypeError: Cannot read property 'workspaceId' of null
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━