Skip to content

fix(deeplink): recreate a destroyed main window — deep links must never land in a windowless app - #493

Merged
runyourempire merged 1 commit into
mainfrom
fix/deeplink-window-recreate
Aug 18, 2026
Merged

fix(deeplink): recreate a destroyed main window — deep links must never land in a windowless app#493
runyourempire merged 1 commit into
mainfrom
fix/deeplink-window-recreate

Conversation

@runyourempire

Copy link
Copy Markdown
Collaborator

The relay delivered — into a windowless app

Operator's live repro (screenshots, 2026-08-19): Chrome's protocol dialog fired, the secondary's console flashed, then nothing. The primary's log shows Deep-link received via relay at 15:50:31 — the #492 handoff worked — but the main window had been destroyed, so get_webview_window("main") was None and the emit went nowhere. Window introspection confirmed only the hidden briefing window remained.

The killer left no trace: no CloseRequested (hide-to-tray never logged), no app-side destroy() caller exists for main (only briefing/notification recreate themselves), and the notification window died the same way minutes earlier ("JS never loaded — recreating") — suspected WebView2-level death. Whatever kills it, nothing in the app could ever bring main back: deep links, tray Show 4DA, and tray left-click all silently no-op until an app restart.

Fix: ensure_main_window() — return main, or rebuild it from its config

  • Deep-link delivery (plugin event listener + relay poll, unified in deliver_deep_link): window present → front it and emit; window gone → park the URL via the cold-start pending mechanism and recreate the window — the fresh frontend collects the parked URL on mount via take_pending_deep_link, so no emit races the webview boot.
  • All three tray show paths use the same primitive — a destroyed window is no longer unrecoverable.
  • Forensic hook: WindowEvent::Destroyed is now logged with its label, so the next silent window death names its moment.

Second bug from the same trace: the rejected instance opened the shared DB

The secondary's argv scan ran validate_deep_link_url, whose rejection path writes a security event via get_database() — the operator's console shows the REJECTED second instance logging Initializing database ... 4da.db (argv[0], the exe path, always fails validation → a bogus "rejected deep-link" security event on every relay). That is the exact WAL hazard the pre-Tauri lock exists to prevent. The scan is now a bare scheme-prefix check on args().skip(1) with zero DB contact; the primary validates on receipt, where it always did.

Verification

  • clippy -D warnings clean on the plain lib target; single_instance 8/8, utils::url 20/20; size + doc gates pass.
  • Post-merge live plan (canonical layout, real key): (1) warm path with window present; (2) destroy the main window, fire the deep link, watch it recreate and activate — the operator's exact failure; (3) secondary console shows no DB init and no bogus security event.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EnyENPW2GcG8GU7x1rJ8wN

…ver land in a windowless app

Live failure 2026-08-19: the relay delivered ("Deep-link received via relay"
logged) but the click did nothing — the main window had been DESTROYED, with
no CloseRequested fired and no app-side destroy caller (suspected
WebView2-level death; the notification window died the same way minutes
earlier, "JS never loaded — recreating"). The emit went into a windowless
app, and nothing in the codebase could ever bring main back: deep links,
tray "Show 4DA", and tray left-click all get_webview_window("main") and
silently no-op until an app restart.

ensure_main_window() is the missing primitive: return main, or rebuild it
from its tauri.conf definition. Deep-link delivery (plugin event listener
and relay poll, unified in deliver_deep_link) fronts an existing window and
emits — or, when main is gone, PARKS the URL via the cold-start pending
mechanism and recreates the window; the fresh frontend collects the parked
URL on mount, so no emit races the webview boot. All three tray show paths
use the same primitive. A Destroyed forensic log names the next window
death instead of letting it vanish silently.

Also from the same live trace: the rejected second instance's argv scan ran
validate_deep_link_url, whose rejection path writes a security event via
get_database() — the REJECTED instance was initializing the shared database
(argv[0], the exe path, always fails validation and logged a bogus security
event on every relay). The scan is now a bare scheme-prefix check on
args().skip(1); the primary validates on receipt, where it always did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnyENPW2GcG8GU7x1rJ8wN
@runyourempire
runyourempire enabled auto-merge (squash) August 18, 2026 16:49
@runyourempire
runyourempire merged commit 5cecf34 into main Aug 18, 2026
33 of 37 checks passed
@runyourempire
runyourempire deleted the fix/deeplink-window-recreate branch August 18, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant