Skip to content

Desktop: WebView2 RenderProcessGone crash — no recovery, UI freezes while sidecar stays healthy #24176

@pangxianggang

Description

@pangxianggang

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:

  1. Updates a lastHeartbeat timestamp on every animation frame
  2. Checks every 3s — if the heartbeat hasn't updated in 10s, the render thread is dead
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions