From 6c886bde83cf1086eacb53c56aadeaa2bbe731fa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 10:36:25 +0000 Subject: [PATCH] Apply new app versions on their own when it cannot interrupt a run A waiting service worker stayed parked until every Codey tab closed, so anyone who missed the Reload toast kept seeing the old site. It now applies right after a page load, when the tab is hidden, or after 20 seconds without typing; tabs coming back into view also check for a new version. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018xNjY7412Rp5bFuw5h69yu --- src/components/PwaPrompt.tsx | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/components/PwaPrompt.tsx b/src/components/PwaPrompt.tsx index d4198d7..45f9b0d 100644 --- a/src/components/PwaPrompt.tsx +++ b/src/components/PwaPrompt.tsx @@ -3,9 +3,17 @@ import { useRegisterSW } from "virtual:pwa-register/react"; import { RefreshCw, WifiOff, X } from "lucide-react"; /** - * Registers the service worker and says when a new version is ready. It never - * reloads by itself, so an update cannot cut off a run mid-snippet. + * Registers the service worker and applies new versions. A waiting version + * would otherwise stay parked until every tab closes, so it is applied on its + * own whenever that cannot cut off a run: right after the page loads, when the + * tab is hidden, or after a while without typing. The toast lets you reload + * sooner. */ + +const IDLE_MS = 20_000; +const FRESH_LOAD_MS = 15_000; +let lastKeyAt = 0; +if (typeof window !== "undefined") window.addEventListener("keydown", () => { lastKeyAt = Date.now(); }, { capture: true, passive: true }); export function PwaPrompt() { const { needRefresh: [needRefresh, setNeedRefresh], @@ -14,7 +22,11 @@ export function PwaPrompt() { } = useRegisterSW({ onRegisteredSW(_url, registration) { // Long-lived tabs look for a new version every hour. - if (registration) window.setInterval(() => void registration.update().catch(() => {}), 60 * 60 * 1000); + if (!registration) return; + const check = () => void registration.update().catch(() => {}); + window.setInterval(check, 60 * 60 * 1000); + // Coming back to a tab left open for days also looks for a new version. + document.addEventListener("visibilitychange", () => document.visibilityState === "visible" && check()); }, }); const [online, setOnline] = useState(() => navigator.onLine); @@ -29,6 +41,23 @@ export function PwaPrompt() { }; }, []); + useEffect(() => { + if (!needRefresh) return; + const apply = () => void updateServiceWorker(true); + const safe = () => performance.now() < FRESH_LOAD_MS || document.visibilityState === "hidden" || Date.now() - lastKeyAt > IDLE_MS; + if (safe()) { + apply(); + return; + } + const timer = window.setInterval(() => safe() && apply(), 5000); + const onHide = () => document.visibilityState === "hidden" && apply(); + document.addEventListener("visibilitychange", onHide); + return () => { + window.clearInterval(timer); + document.removeEventListener("visibilitychange", onHide); + }; + }, [needRefresh, updateServiceWorker]); + useEffect(() => { if (!offlineReady) return; const timer = window.setTimeout(() => setOfflineReady(false), 6000);