From 6015f8669b8fb9d1f17ddd787963fa651cb12d23 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Mon, 30 Mar 2026 15:04:30 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20WebviewUrl::App=20hash=20fragment=20bug?= =?UTF-8?q?=20=E2=80=94=20tool=20windows=20loading=20empty=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE FOUND: WebviewUrl::App takes a PathBuf, not a URL. Passing "index.html#/tool/ping?sessionId=abc" treated the ENTIRE string including # and ? as a file path. Tauri looked for a file literally named "index.html#/tool/ping?sessionId=abc" which doesn't exist. The webview loaded an empty/404 page and WKWebView killed the content process, closing the window instantly. Fix: - Rust: split URL at '#' — pass only "index.html" to WebviewUrl::App, then set the hash fragment via window.eval() after build() - Vue: App.vue now listens for 'hashchange' event in addition to checking hash on mount, so the eval-injected hash triggers the correct tool/detached mode This was NEVER a CSP issue, focus issue, crossorigin issue, or async chunk loading issue. It was always a bad file path. Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/commands/window_commands.rs | 24 +++++++++++++++++++---- src/App.vue | 24 ++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands/window_commands.rs b/src-tauri/src/commands/window_commands.rs index 8273528..0d37af3 100644 --- a/src-tauri/src/commands/window_commands.rs +++ b/src-tauri/src/commands/window_commands.rs @@ -2,9 +2,12 @@ use tauri::AppHandle; use tauri::WebviewWindowBuilder; /// Open a child window from the Rust side using WebviewWindowBuilder. -/// This is more reliable than JS-side WebviewWindow on macOS WKWebView. +/// +/// The `url` parameter supports hash fragments (e.g. "index.html#/tool/ping?sessionId=abc"). +/// WebviewUrl::App takes a PathBuf and cannot handle hash/query — so we load plain +/// index.html and set the hash via JS after the window is created. #[tauri::command] -pub async fn open_child_window( +pub fn open_child_window( app_handle: AppHandle, label: String, title: String, @@ -12,13 +15,26 @@ pub async fn open_child_window( width: f64, height: f64, ) -> Result<(), String> { - let webview_url = tauri::WebviewUrl::App(url.into()); - WebviewWindowBuilder::new(&app_handle, &label, webview_url) + // Split "index.html#/tool/ping?sessionId=abc" into path and fragment + let (path, hash) = match url.split_once('#') { + Some((p, h)) => (p.to_string(), Some(format!("#{}", h))), + None => (url.clone(), None), + }; + + let webview_url = tauri::WebviewUrl::App(path.into()); + let window = WebviewWindowBuilder::new(&app_handle, &label, webview_url) .title(&title) .inner_size(width, height) .resizable(true) .center() .build() .map_err(|e| format!("Failed to create window '{}': {}", label, e))?; + + // Set the hash fragment after the window loads — this triggers App.vue's + // onMounted hash detection to render the correct tool/detached component. + if let Some(hash) = hash { + let _ = window.eval(&format!("window.location.hash = '{}';", hash)); + } + Ok(()) } diff --git a/src/App.vue b/src/App.vue index e2e914e..fed6728 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,7 +16,6 @@ const DetachedSession = defineAsyncComponent({ const app = useAppStore(); const appError = ref(null); -// Tool window mode — detected from URL hash: #/tool/network-scanner?sessionId=abc const isToolMode = ref(false); const isDetachedMode = ref(false); const toolName = ref(""); @@ -28,32 +27,39 @@ onErrorCaptured((err) => { return false; }); -onMounted(async () => { - const hash = window.location.hash; +/** Parse hash and set mode flags. Called on mount and on hashchange. */ +function applyHash(hash: string): void { if (hash.startsWith("#/tool/")) { isToolMode.value = true; - const rest = hash.substring(7); // after "#/tool/" + const rest = hash.substring(7); const [name, query] = rest.split("?"); toolName.value = name; toolSessionId.value = new URLSearchParams(query || "").get("sessionId") || ""; } else if (hash.startsWith("#/detached-session")) { isDetachedMode.value = true; - } else { + } +} + +onMounted(async () => { + // Check hash at load time (present if JS-side WebviewWindow set it in the URL) + applyHash(window.location.hash); + + // Also listen for hash changes (Rust-side window sets hash via eval after load) + window.addEventListener("hashchange", () => applyHash(window.location.hash)); + + // Only init vault for the main app window (no hash) + if (!isToolMode.value && !isDetachedMode.value) { await app.checkVaultState(); } });