From 00a3c56f8091dee04961feb0a8f7caff0d4132c9 Mon Sep 17 00:00:00 2001 From: broisnischal Date: Thu, 8 Oct 2026 12:46:07 +0545 Subject: [PATCH 1/5] Keep F6 from crashing the WebView2 browser process F6 is Chromium's "focus next pane". Stroke binds nothing to it, so WebView2 ran its own action, and on WebView2 154.0.4258.62 that action dereferences null in the browser process (MultiContentsView::GetActiveContentsContainerView under chrome::FocusNextPane). The whole WebView2 process tree exits, the page goes blank, and the custom titlebar's close button goes with it. An AcceleratorKeyPressed handler on every WebView2 controller now switches off the browser's action for F6 key-downs. The key still reaches the page. --- src-tauri/src/commands.rs | 1 + src-tauri/src/lib.rs | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 5ea34bc9..ea0cbf74 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -318,6 +318,7 @@ pub async fn open_new_window(app: tauri::AppHandle) -> Result<(), String> { let surface = crate::surface_for_theme(window.theme().unwrap_or(tauri::Theme::Dark)); let _ = window.set_background_color(Some(surface)); crate::set_webview_backdrop(&window, surface); + crate::guard_webview(&window); crate::arm_reveal_failsafe(&window); Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8d47abbf..2997da8b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -134,6 +134,71 @@ fn set_windows_webview_backdrop(window: &tauri::WebviewWindow, color: tauri::win }); } +/// Keep the webview alive through F6. +/// +/// Only WebView2 needs this. WebKitGTK and WKWebView bind nothing to F6, so +/// there the key goes to the page and nowhere else. +pub(crate) fn guard_webview(window: &tauri::WebviewWindow) { + #[cfg(target_os = "windows")] + guard_windows_webview(window); + #[cfg(not(target_os = "windows"))] + let _ = window; +} + +/// F6 is Chromium's "focus next pane". WebView2 154.0.4258.62 runs it into a +/// null dereference in the browser process (`MultiContentsView:: +/// GetActiveContentsContainerView`, under `chrome::FocusNextPane`), and the +/// whole WebView2 process tree exits. The page goes with it, leaving the bare +/// window surface on screen, and with it TitleBar.svelte's close button: Windows +/// gets no native decorations, so the window looked frozen and could not be +/// closed. Same crash, same fix: NeuralNomadsAI/CodeNomad#875 and #881. +/// +/// Only the browser's own action is switched off, per key press, through +/// `ICoreWebView2AcceleratorKeyPressedEventArgs2`. F6 still reaches the page +/// (the Terminal tab sends it on to psql and friends) and every other browser +/// accelerator is untouched. A runtime too old to carry that interface +/// swallows F6 instead. +#[cfg(target_os = "windows")] +fn guard_windows_webview(window: &tauri::WebviewWindow) { + use webview2_com::Microsoft::Web::WebView2::Win32::{ + ICoreWebView2AcceleratorKeyPressedEventArgs2, COREWEBVIEW2_KEY_EVENT_KIND, + COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN, COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN, + }; + use webview2_com::AcceleratorKeyPressedEventHandler; + use windows_core::Interface; + + const VK_F6: u32 = 0x75; + + let _ = window.with_webview(|webview| unsafe { + let controller = webview.controller(); + let mut token = 0; + + let on_key = AcceleratorKeyPressedEventHandler::create(Box::new(|_, args| { + let Some(args) = args else { return Ok(()) }; + let mut kind = COREWEBVIEW2_KEY_EVENT_KIND::default(); + args.KeyEventKind(&mut kind)?; + // Alt+F6 arrives as a system key. + if kind != COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN + && kind != COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN + { + return Ok(()); + } + let mut key = 0; + args.VirtualKey(&mut key)?; + if key != VK_F6 { + return Ok(()); + } + match args.cast::() { + Ok(args) => args.SetIsBrowserAcceleratorKeyEnabled(false), + Err(_) => args.SetHandled(true), + } + })); + if let Err(e) = controller.add_AcceleratorKeyPressed(&on_key, &mut token) { + log::warn!("webview2: F6 guard not installed: {e}"); + } + }); +} + /// WebKitGTK's pre-paint colour. #[cfg(target_os = "linux")] fn set_linux_webview_backdrop(window: &tauri::WebviewWindow, color: tauri::window::Color) { @@ -418,6 +483,7 @@ pub fn run() { let surface = surface_for_theme(window_theme); let _ = window.set_background_color(Some(surface)); set_webview_backdrop(&window, surface); + guard_webview(&window); arm_reveal_failsafe(&window); #[cfg(target_os = "macos")] From c30b3eb57fdc329fc3ace7474b92bb280f85c710 Mon Sep 17 00:00:00 2001 From: broisnischal Date: Thu, 8 Oct 2026 12:46:07 +0545 Subject: [PATCH 2/5] Bring the native frame back when WebView2's browser process dies Windows builds have no native decorations; the close button lives in the page. If the browser process exits for any reason, the window was left with no way to close it. A ProcessFailed handler now turns decorations back on. --- src-tauri/src/lib.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2997da8b..41b653df 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -134,7 +134,7 @@ fn set_windows_webview_backdrop(window: &tauri::WebviewWindow, color: tauri::win }); } -/// Keep the webview alive through F6. +/// Keep the webview alive through F6, and the window closable if it dies anyway. /// /// Only WebView2 needs this. WebKitGTK and WKWebView bind nothing to F6, so /// there the key goes to the page and nowhere else. @@ -158,18 +158,23 @@ pub(crate) fn guard_webview(window: &tauri::WebviewWindow) { /// (the Terminal tab sends it on to psql and friends) and every other browser /// accelerator is untouched. A runtime too old to carry that interface /// swallows F6 instead. +/// +/// If the browser process exits for any other reason, the native frame comes +/// back so the window can still be moved and closed. #[cfg(target_os = "windows")] fn guard_windows_webview(window: &tauri::WebviewWindow) { use webview2_com::Microsoft::Web::WebView2::Win32::{ ICoreWebView2AcceleratorKeyPressedEventArgs2, COREWEBVIEW2_KEY_EVENT_KIND, COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN, COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN, + COREWEBVIEW2_PROCESS_FAILED_KIND, COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED, }; - use webview2_com::AcceleratorKeyPressedEventHandler; + use webview2_com::{AcceleratorKeyPressedEventHandler, ProcessFailedEventHandler}; use windows_core::Interface; const VK_F6: u32 = 0x75; - let _ = window.with_webview(|webview| unsafe { + let host = window.clone(); + let _ = window.with_webview(move |webview| unsafe { let controller = webview.controller(); let mut token = 0; @@ -196,6 +201,31 @@ fn guard_windows_webview(window: &tauri::WebviewWindow) { if let Err(e) = controller.add_AcceleratorKeyPressed(&on_key, &mut token) { log::warn!("webview2: F6 guard not installed: {e}"); } + + let core = match controller.CoreWebView2() { + Ok(core) => core, + Err(e) => { + log::warn!("webview2: crash guard not installed: {e}"); + return; + } + }; + let on_failed = ProcessFailedEventHandler::create(Box::new(move |_, args| { + let Some(args) = args else { return Ok(()) }; + let mut kind = COREWEBVIEW2_PROCESS_FAILED_KIND::default(); + args.ProcessFailedKind(&mut kind)?; + if kind == COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED { + log::error!("webview2: browser process exited, restoring the native frame"); + // Through the event loop, not from inside this COM callback. + let host = host.clone(); + tauri::async_runtime::spawn(async move { + let _ = host.set_decorations(true); + }); + } + Ok(()) + })); + if let Err(e) = core.add_ProcessFailed(&on_failed, &mut token) { + log::warn!("webview2: crash guard not installed: {e}"); + } }); } From 56db3def1f4334dae70a39c929bbef4c393811c5 Mon Sep 17 00:00:00 2001 From: broisnischal Date: Thu, 8 Oct 2026 12:46:07 +0545 Subject: [PATCH 3/5] Add the changeset for the F6 hotfix --- .changeset/f6-hotfix.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changeset/f6-hotfix.md diff --git a/.changeset/f6-hotfix.md b/.changeset/f6-hotfix.md new file mode 100644 index 00000000..1b037748 --- /dev/null +++ b/.changeset/f6-hotfix.md @@ -0,0 +1,3 @@ +### Bug Fixes +- On Windows, pressing F6 no longer blanks the Stroke window and leaves it impossible to close +- On Windows, if the webview crashes for any other reason, the window gets its native title bar back so it can still be closed From 0749382af011ded3729a94e72ee6904d736e621a Mon Sep 17 00:00:00 2001 From: broisnischal Date: Thu, 8 Oct 2026 12:51:36 +0545 Subject: [PATCH 4/5] Keep WebView2's other browser-only keys from running F6 was the worst of a set of keys WebView2 acts on whenever the page leaves them unhandled. Hotkeys skip plain keys in text fields and match modifiers exactly, so F5 in the SQL editor, Shift+F5, Ctrl+F5, and Ctrl+R with a dialog open reloaded the whole app; F7 asked about caret browsing; F3 and Ctrl+G opened the find bar and Ctrl+S a Save As dialog. The key guard now switches off the browser's action for F3, F5, F7, F10 and Ctrl/Alt with D, E, G, L, R and S, as well as F6. The page still gets every key, so Stroke's own F5, Ctrl+R and Ctrl+S are unchanged. --- src-tauri/src/lib.rs | 45 +++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 41b653df..dddf4061 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -134,10 +134,11 @@ fn set_windows_webview_backdrop(window: &tauri::WebviewWindow, color: tauri::win }); } -/// Keep the webview alive through F6, and the window closable if it dies anyway. +/// Keep browser-only keys away from the webview, and the window closable if it +/// dies anyway. /// -/// Only WebView2 needs this. WebKitGTK and WKWebView bind nothing to F6, so -/// there the key goes to the page and nowhere else. +/// Only WebView2 needs this. WebKitGTK and WKWebView bind none of these keys, +/// so there they go to the page and nowhere else. pub(crate) fn guard_webview(window: &tauri::WebviewWindow) { #[cfg(target_os = "windows")] guard_windows_webview(window); @@ -153,11 +154,22 @@ pub(crate) fn guard_webview(window: &tauri::WebviewWindow) { /// gets no native decorations, so the window looked frozen and could not be /// closed. Same crash, same fix: NeuralNomadsAI/CodeNomad#875 and #881. /// +/// F6 is the worst of a set of keys WebView2 acts on whenever the page leaves +/// them unhandled, and hotkeys leave plenty: plain keys are skipped in text +/// fields and modifiers have to match exactly. So: +/// - F5 in any text field (the SQL editor too), Shift+F5, Ctrl+F5, and Ctrl+R +/// with a dialog open reloaded the whole app, dropping the connection and +/// any staged edits +/// - F7 asked to turn on caret browsing +/// - F3 and Ctrl+G opened the browser's find bar, Ctrl+S its Save As dialog +/// - F10, Ctrl+L, Ctrl+E and Alt+D are Chromium's other focus commands, aimed +/// at browser chrome an embedded page doesn't have +/// /// Only the browser's own action is switched off, per key press, through -/// `ICoreWebView2AcceleratorKeyPressedEventArgs2`. F6 still reaches the page -/// (the Terminal tab sends it on to psql and friends) and every other browser -/// accelerator is untouched. A runtime too old to carry that interface -/// swallows F6 instead. +/// `ICoreWebView2AcceleratorKeyPressedEventArgs2`. Every key still reaches the +/// page, so Stroke's own F5, Ctrl+R and Ctrl+S keep working and the Terminal +/// tab still gets its function keys. A runtime too old to carry that interface +/// swallows F6 alone instead: swallowing takes the key from the page too. /// /// If the browser process exits for any other reason, the native frame comes /// back so the window can still be moved and closed. @@ -171,7 +183,17 @@ fn guard_windows_webview(window: &tauri::WebviewWindow) { use webview2_com::{AcceleratorKeyPressedEventHandler, ProcessFailedEventHandler}; use windows_core::Interface; + const VK_F3: u32 = 0x72; + const VK_F5: u32 = 0x74; const VK_F6: u32 = 0x75; + const VK_F7: u32 = 0x76; + const VK_F10: u32 = 0x79; + /// A letter's virtual-key code is its capital, and a letter only reaches the + /// handler with Ctrl or Alt held, so `R` here is Ctrl+R or Alt+R, never typing. + const BROWSER_ONLY: &[u32] = &[ + VK_F3, VK_F5, VK_F6, VK_F7, VK_F10, + b'D' as u32, b'E' as u32, b'G' as u32, b'L' as u32, b'R' as u32, b'S' as u32, + ]; let host = window.clone(); let _ = window.with_webview(move |webview| unsafe { @@ -182,7 +204,7 @@ fn guard_windows_webview(window: &tauri::WebviewWindow) { let Some(args) = args else { return Ok(()) }; let mut kind = COREWEBVIEW2_KEY_EVENT_KIND::default(); args.KeyEventKind(&mut kind)?; - // Alt+F6 arrives as a system key. + // Anything with Alt arrives as a system key. if kind != COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN && kind != COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN { @@ -190,16 +212,17 @@ fn guard_windows_webview(window: &tauri::WebviewWindow) { } let mut key = 0; args.VirtualKey(&mut key)?; - if key != VK_F6 { + if !BROWSER_ONLY.contains(&key) { return Ok(()); } match args.cast::() { Ok(args) => args.SetIsBrowserAcceleratorKeyEnabled(false), - Err(_) => args.SetHandled(true), + Err(_) if key == VK_F6 => args.SetHandled(true), + Err(_) => Ok(()), } })); if let Err(e) = controller.add_AcceleratorKeyPressed(&on_key, &mut token) { - log::warn!("webview2: F6 guard not installed: {e}"); + log::warn!("webview2: key guard not installed: {e}"); } let core = match controller.CoreWebView2() { From 9f8ca10ad0f227f2a5ebd114e169d74cdbdcd4cc Mon Sep 17 00:00:00 2001 From: broisnischal Date: Thu, 8 Oct 2026 12:51:36 +0545 Subject: [PATCH 5/5] List the other Windows keys in the F6 changeset --- .changeset/f6-hotfix.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/f6-hotfix.md b/.changeset/f6-hotfix.md index 1b037748..247f2820 100644 --- a/.changeset/f6-hotfix.md +++ b/.changeset/f6-hotfix.md @@ -1,3 +1,5 @@ ### Bug Fixes - On Windows, pressing F6 no longer blanks the Stroke window and leaves it impossible to close +- On Windows, F5 in a text field, Shift+F5, Ctrl+F5, and Ctrl+R with a dialog open no longer reload the whole app and drop the connection +- On Windows, F7, F3, Ctrl+G and Ctrl+S no longer open the browser's caret browsing prompt, find bar or Save As dialog - On Windows, if the webview crashes for any other reason, the window gets its native title bar back so it can still be closed