From 701108c9d5c72b59bf40b1ba11039ac7c88687b8 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 5 May 2026 13:49:48 +0000 Subject: [PATCH] fix(gui): inject farewell overlay into parent DOM on shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the data:-URL navigation (blocked by Chrome since v60 for top-frame navigation) with a direct DOM-append of a full-screen overlay onto the parent document. Uses z-index 2147483647 so it sits above Streamlit's connection-error banner when the websocket drops. Note: still doesn't fully suppress the connection-error banner in testing — the next iteration will render the overlay through Streamlit's own page rather than via a component iframe. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/components/_legacy.py | 60 ++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 1f59d11..4a06989 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -80,6 +80,53 @@ def hide_streamlit_chrome() -> None: # Clean shutdown # --------------------------------------------------------------------------- +_FAREWELL_SCRIPT = """ + +""" + + def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> None: """Render a Quit button that terminates the Streamlit server. @@ -87,12 +134,15 @@ def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> Non process (SIGTERM/SIGINT) does not reliably terminate it — Streamlit installs its own handlers and the tornado/asyncio loop swallows or defers the signal, so the browser sees the websocket drop while the - python process stays alive. To shut down cleanly we schedule - ``os._exit(0)`` on a daemon thread after a short delay; the delay - lets the current rerun finish painting the "shutting down" message - before the process is hard-killed. + python process stays alive. We schedule ``os._exit(0)`` on a daemon + thread to hard-kill the process, and inject a small JS shim that + either closes the tab (when allowed) or replaces the top frame with + a self-contained "App closed" page so the user never sees + Streamlit's red connection-error overlay. """ if st.session_state.get("_app_shutting_down"): + from streamlit.components.v1 import html as _components_html + _components_html(_FAREWELL_SCRIPT, height=0) st.success("Shutting down… you can close this browser tab.") st.stop() @@ -100,7 +150,7 @@ def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> Non st.session_state["_app_shutting_down"] = True def _hard_exit() -> None: - time.sleep(0.5) + time.sleep(1.0) os._exit(0) threading.Thread(target=_hard_exit, daemon=True).start()