Skip to content

[Bug]: All windows become invisible/transparent frames after the WebView2 browser process exits (app must be restarted) #410

Description

@diogochaves

Existing issues

  • I searched existing issues and did not find a duplicate.

What happened?

I used an AI assistant to help track this one down, and I went through its claims and verified them against my own machine — the process and window-handle evidence below is reproducible with the commands included, I confirmed the failure deterministically by killing the WebView2 browser process on a fresh 1.5.36 instance, and everything it asserted about the code checks out against the sources it cites. I'm posting it here rather than opening a PR straight away because the right fix depends on how you'd rather structure webview lifecycle handling, and I don't want to guess. If you tell me which direction you prefer, I'm happy to implement it and send a PR.

What happened

Every so often the tray flyout opens as an empty, transparent, rounded frame — the window shadow and border are there, but you can see the desktop wallpaper through the middle, no content at all. The only way out is quitting and relaunching Ceiling.

Image

This time I did not restart, so we could inspect the live process. Two things turned out to be true that reframe the bug:

  1. It is not flyout-specific. Opening the main window from the tray gives the same empty transparent frame.
  2. The webview isn't failing to paint — it isn't there any more.

Evidence from the live broken process

Ceiling was running as PID 12840, started 30/08 18:13. It had zero WebView2 child processes:

Get-CimInstance Win32_Process -Filter "Name='msedgewebview2.exe' AND ParentProcessId=12840"
# → 0 results

Every msedgewebview2.exe on the machine at that moment belonged to SearchHost, OneDrive or WhatsApp. None to Ceiling.

Both of Ceiling's native windows were still alive:

HWND      Class         Title    Visible  Rect
0x450C7E  Tauri Window  Ceiling  False    491,48    360x233   ← flyout
0x20890   Tauri Window  Ceiling  False    1235,135  720x675   ← main window

…and neither had a single child window. A healthy Tauri window on Windows hosts this chain, which I confirmed on a fresh instance:

WRY_WEBVIEW, Chrome_WidgetWin_0, Chrome_WidgetWin_1, Chrome_RenderWidgetHostHWND, Intermediate D3D Window

EnumChildWindows returned nothing at all for either broken window.

So the WebView2 controller and browser process are gone, and Tauri is holding two bare native window frames. Since the flyout is built transparent(true) (shell/flyout_window.rs:81) with DwmExtendFrameIntoClientArea and the Win11 rounded-corner preference applied (shell/dwm.rs:257-277), an unpainted client area is exactly the wallpaper-through-a-rounded-frame look in the screenshot.

The Rust side was completely healthy throughout — 162s of CPU accumulated, tray icon still updating usage percentages. Only the UI layer was dead.

When it happened, and whether it was a crash

Ceiling's WebView2 profile directory %LOCALAPPDATA%\io.github.tsouth89.ceiling\EBWebView has LastWriteTime 31/08 09:50:05. At that same second, OneDrive's and SearchHost's WebView2 browser processes were created; WhatsApp's came up at 09:50:11. So every WebView2 browser process on the machine went down at 09:50:05 — three of the four hosts recreated theirs within seconds, Ceiling did not, and stayed a zombie UI for the ~2 hours until I looked.

Ceiling's Crashpad directory contains no reports (Crashpad\reports\ is empty; the only files are stale metadata from 11/08). The browser process therefore exited cleanly — it did not crash. That is WebView2's BrowserProcessExited failure kind, which the host app is expected to handle and recover from.

I could not pin down what caused the machine-wide shutdown, and I'd rather say so than guess:

  • No WebView2 runtime update — MicrosoftEdgeUpdate.log shows only hourly "Update check not needed at this time", and the runtime is still 151.0.4129.107 installed 26/08.
  • No sleep/resume — zero Kernel-Power 42/107 events since the 30/08 boot.
  • No GPU/display driver reset today. The LiveKernelEvent 0x117 / 0x141 and BlueScreen entries at 09:38:47 are April dumps (WATCHDOG-20260402-*, Minidump\042326-*) that WER merely archived at that time — a red herring worth naming so nobody else chases it.

I'd argue the trigger is secondary anyway. Whatever it was, it hit four WebView2 hosts equally and three of them recovered. The bug is that Ceiling can't.

Root cause in the code

Three decisions compound into a permanent failure:

1. Nothing handles webview process failure. grep -rn "ProcessFailed\|process_failed" apps/desktop-tauri/src-tauri/src returns nothing. There is no ICoreWebView2Controller::add_ProcessFailed registration anywhere, so a BrowserProcessExited goes completely unobserved.

2. Windows are hidden, never closed. flyout_window::hide() (shell/flyout_window.rs:132-146) calls window.hide() deliberately, to keep the WebView2 instance alive across opens. The rationale is sound, but the consequence is that a dead instance is kept just as faithfully, for the remaining lifetime of the process.

3. Reopening shows the window unconditionally. open_or_focus's existing-window branch (shell/flyout_window.rs:59-73) goes straight to window.show() + set_focus() with no liveness check. There is a careful first-build handshake — .visible(false), frontend measures, then reveal_tray_panel_window (commands/surface.rs:92) reveals it — precisely so no pre-layout blank frame is ever shown. But that guard only covers the first build; every later open trusts the cached webview blindly.

Net effect: one clean WebView2 shutdown and the entire UI is unusable until the app is restarted, with no diagnostic anywhere pointing at why.

Two secondary findings

Both are separate from the bug above; flagging them since they surfaced during the same read.

A. A race that can show the flyout before its first paint. open_flyout (hover, taskbar_widget.rs:1529) and toggle_flyout (click, taskbar_widget.rs:1615) each spawn an independent async task. If a click lands while a hover-open is still in flight, toggle_flyout reads is_visible()false (the window is built but still .visible(false), awaiting the frontend reveal), so it calls open_or_focus again, takes the existing-window branch, and shows the window immediately — bypassing the reveal handshake that exists specifically to prevent that. This one is transient rather than permanent, but it produces the same "empty frame" shape and defeats an intentional guard.

B. There is no ErrorBoundary anywhere in the frontend. grep -rn "ErrorBoundary\|componentDidCatch\|getDerivedStateFromError" apps/desktop-tauri/src returns nothing. React 18 unmounts the whole tree on an uncaught render error, so any render throw in a surface blanks that window with no message and no recovery path — a second, independent route to a window that looks broken for no visible reason.

Proposed fixes

1. Handle ProcessFailed (the real fix). Tauri v2 exposes the WebView2 controller on Windows via window.with_webview(|w| w.controller()), and webview2-com provides add_ProcessFailed. On BrowserProcessExited / RenderProcessExited, destroy the affected window(s) and rebuild them — which is exactly what OneDrive and SearchHost did at 09:50:05. Worth logging at warn when it fires, so this failure mode stops being invisible in reports.

2. Liveness guard before show() (cheap backstop). Before the existing-window branch shows anything, check whether the window still has a webview child HWND (GetWindow(hwnd, GW_CHILD) != 0). That is the exact condition used to diagnose this, it's one Win32 call, and it converts "show a blank frame forever" into "rebuild the window". Useful even with fix 1 in place, since it also covers any teardown path the event doesn't report.

3. Serialize the flyout toggle. Guard open_flyout/toggle_flyout with a shared in-flight flag so a click during a hover-open can't short-circuit the reveal handshake (secondary finding A).

4. Add an ErrorBoundary at the surface root, rendering a minimal "something went wrong — reload" panel with an opaque background, so a render throw is visible and recoverable rather than an invisible window (secondary finding B).

I'd suggest 1 + 2 as one PR and 3 + 4 separately, but happy to split or combine however you prefer.

Environment

  • Ceiling 1.5.36 (installer build, %LOCALAPPDATA%\Programs\Ceiling\ceiling.exe)
  • Windows 11 Pro 25H2, 10.0.26200
  • WebView2 Runtime 151.0.4129.107 (Evergreen, machine-wide)

Affected area

  • Tray panel
  • Settings UI
  • Config file / settings persistence
  • CLI
  • Provider-specific behavior
  • Installer / release packaging
  • Startup / background behavior
  • Other

Steps to reproduce

Reproduction — verified

The organic trigger isn't on-demand, but the failure reproduces deterministically by terminating the WebView2 browser process. I ran this end to end on 1.5.36:

$cp = (Get-Process ceiling).Id
# 1. open the flyout normally (tray widget click) and confirm it renders
# 2. find and kill the browser process
Get-CimInstance Win32_Process -Filter "Name='msedgewebview2.exe' AND ParentProcessId=$cp" |
  ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
# 3. click the tray widget again

Observed, step by step:

Ceiling alive WebView2 procs Flyout 0x2462BB2 (491,48 360x233) child HWNDs
Flyout open, healthy yes 1 WRY_WEBVIEW, Chrome_WidgetWin_0, Chrome_WidgetWin_1, Chrome_RenderWidgetHostHWND, Intermediate D3D Window
Right after killing the browser process yes 0 WRY_WEBVIEW only
After toggling the widget closed and open again yes 0 WRY_WEBVIEW only

The app survives and never spawns a replacement WebView2 process. The widget click still runs the full toggle_flyoutopen_or_focuswindow.show() path, so the window becomes visible again at the same rect — rendering nothing. A screen capture of that exact rect while "open" shows straight through to the windows behind it; the same capture taken 30 seconds earlier shows the normal flyout with its provider rows. Permanent until the app is restarted.

One honest difference between the forced and organic failures: killing the process leaves WRY's own WRY_WEBVIEW container HWND orphaned, whereas in the organic 09:50:05 failure even that was gone — consistent with a graceful BrowserProcessExited teardown that let WRY destroy its container too. Different teardown depth, identical user-visible result and identical absence of recovery.

Logs, screenshots, or recordings


App version

1.5.36

Windows version

Windows 11 Pro 25H2, 10.0.26200

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions