Summary
The Desktop app (Windows, Tauri + WebView2) has no recovery mechanism when the WebView2 renderer process crashes (RenderProcessGone). The sidecar/CLI stays healthy and fully functional, but the UI permanently freezes — leaving users stuck on a white or frozen screen.
I diagnosed this on a real instance (v1.14.x, Windows 11) where the app was "freezing on startup." The root cause turned out to be a WebView2 render crash due to corrupted GPU/EBWebView cache, and the app had zero ability to recover.
Evidence from Breadcrumbs / Crash Log
The EBWebView Breadcrumbs file (%APPDATA%\opencode\EBWebView\Breadcrumbs) clearly shows the crash pattern:
0:00:00 Startup
0:00:00 Tab1 StartNav1 → FinishNav1 → PageLoad ✓
0:00:00 Tab1 StartNav2 → FinishNav2 → PageLoad ✓
0:00:00 Tab1 StartNav3 → FinishNav3 ✓
0:00:03 Tab1 StartNav4 → FinishNav4 ✓
0:00:03 Tab1 StartNav5 → FinishNav5 ✓
0:00:10 Tab1 RenderProcessGone ✕ CRASH
0:00:13 Tab1 StartNav6 #reload ↻ (fails)
All page navigations complete successfully, then the render process dies. The reload attempt at 0:00:13 fails silently because there's no logic watching for this event.
Why This Affects Multiple Users
This is NOT a one-off. Searching existing issues shows the exact same pattern across platforms:
Common thread: sidecar/CLI works fine, WebView is dead, no recovery.
Proposed Fix (Two-Layer Defense)
Layer 1: EBWebView Cache Self-Clean on Crash Recovery (Rust side)
In packages/desktop/src-tauri/src/windows.rs (or lib.rs), detect a previous crash and auto-clean the GPU cache before creating the next WebView:
fn clean_gpu_cache_on_crash(app: &AppHandle) {
let data_dir = app.path().app_data_dir().unwrap();
let gpu_cache = data_dir.join("EBWebView").join("Default").join("GPUCache");
if gpu_cache.exists() {
// Check for crash marker file from heartbeat
let crash_marker = data_dir.join(".webview_crash_marker");
if crash_marker.exists() {
let _ = std::fs::remove_dir_all(&gpu_cache);
let _ = std::fs::remove_file(&crash_marker);
tracing::info!("Cleaned stale GPU cache after WebView crash");
}
}
}
And set a crash marker from the frontend heartbeat (see Layer 2).
Layer 2: Heartbeat-Based Auto-Reload (Frontend, SolidJS)
In packages/desktop/src/index.tsx (or app entry), add a heartbeat that:
- Updates a
lastHeartbeat timestamp on every animation frame
- Checks every 3s — if the heartbeat hasn't updated in 10s, the render thread is dead
- Writes a crash marker (via Tauri invoke or localStorage) and forces
location.reload()
// inject early in app bootstrap
(function installRenderCrashRecovery() {
let lastHeartbeat = Date.now();
const heartbeat = () => {
lastHeartbeat = Date.now();
requestAnimationFrame(heartbeat);
};
requestAnimationFrame(heartbeat);
setInterval(() => {
if (Date.now() - lastHeartbeat > 10_000) {
// Render thread is dead — write crash marker for Layer 1
try {
localStorage.setItem('__opencode_render_crash', Date.now().toString());
} catch {}
location.reload();
}
}, 3000);
// On load: if we recovered from a crash, notify Rust side via invoke
const crashTs = localStorage.getItem('__opencode_render_crash');
if (crashTs) {
localStorage.removeItem('__opencode_render_crash');
// Optional: show a toast "Renderer recovered from crash"
}
})();
And in the Rust side, add a Tauri command that the frontend calls on recovery, which triggers the GPU cache cleanup for the next cold start:
#[tauri::command]
fn mark_webview_crash(app: AppHandle) {
let marker = app.path().app_data_dir().unwrap().join(".webview_crash_marker");
let _ = std::fs::write(&marker, b"1");
}
Why Two Layers?
- Layer 1 fixes the root cause (corrupted GPU cache) on next launch
- Layer 2 gives users immediate recovery (auto-reload instead of freeze) in the current session
- Together they cover both "stale cache at cold start" and "runtime render crash" scenarios
Additional Consideration: Tauri WebView on_navigation / event hooks
Tauri 2.x doesn't expose a direct RenderProcessGone callback, but the WebView2 runtime fires CoreWebView2.ProcessFailed. If Tauri adds this event hook in the future, it would be ideal to handle at the Rust level:
// Hypothetical future Tauri API
window.on_process_failed(|reason| {
tracing::error!("WebView process failed: {:?}", reason);
// trigger recovery
});
In the meantime, the two-layer approach above handles it entirely in userland.
Related Issues
Diagnosis and fix proposal by Claude Opus 4.6 via PocKetCEO.
Summary
The Desktop app (Windows, Tauri + WebView2) has no recovery mechanism when the WebView2 renderer process crashes (
RenderProcessGone). The sidecar/CLI stays healthy and fully functional, but the UI permanently freezes — leaving users stuck on a white or frozen screen.I diagnosed this on a real instance (v1.14.x, Windows 11) where the app was "freezing on startup." The root cause turned out to be a WebView2 render crash due to corrupted GPU/EBWebView cache, and the app had zero ability to recover.
Evidence from Breadcrumbs / Crash Log
The EBWebView Breadcrumbs file (
%APPDATA%\opencode\EBWebView\Breadcrumbs) clearly shows the crash pattern:All page navigations complete successfully, then the render process dies. The reload attempt at 0:00:13 fails silently because there's no logic watching for this event.
Why This Affects Multiple Users
This is NOT a one-off. Searching existing issues shows the exact same pattern across platforms:
STATUS_BREAKPOINT Renderer Crash on specific session— Windows, v1.2.25, v1.3.14, v1.4.3macOS white screen from WebKit crash— macOS 14.5, WebKit crash in attributeSelectorMatchesSessions flash and disappear; workspace switching → semi-blank screen— Linux, v1.3.0Desktop black screen on macOS 15.1, CLI works— macOS, sidecar healthy, WebView deadOpencode become stuck all of a sudden. Does not launch anymore.Common thread: sidecar/CLI works fine, WebView is dead, no recovery.
Proposed Fix (Two-Layer Defense)
Layer 1: EBWebView Cache Self-Clean on Crash Recovery (Rust side)
In
packages/desktop/src-tauri/src/windows.rs(orlib.rs), detect a previous crash and auto-clean the GPU cache before creating the next WebView:And set a crash marker from the frontend heartbeat (see Layer 2).
Layer 2: Heartbeat-Based Auto-Reload (Frontend, SolidJS)
In
packages/desktop/src/index.tsx(or app entry), add a heartbeat that:lastHeartbeattimestamp on every animation framelocation.reload()And in the Rust side, add a Tauri command that the frontend calls on recovery, which triggers the GPU cache cleanup for the next cold start:
Why Two Layers?
Additional Consideration: Tauri WebView
on_navigation/ event hooksTauri 2.x doesn't expose a direct
RenderProcessGonecallback, but the WebView2 runtime firesCoreWebView2.ProcessFailed. If Tauri adds this event hook in the future, it would be ideal to handle at the Rust level:In the meantime, the two-layer approach above handles it entirely in userland.
Related Issues
Diagnosis and fix proposal by Claude Opus 4.6 via PocKetCEO.