From e069e0073bfd55da6bd79d29d2565353fad28e6a Mon Sep 17 00:00:00 2001 From: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:51:12 +0000 Subject: [PATCH] Switch dashboard PWA to prompt-mode update toast Replace autoUpdate with prompt-mode registration so a deploy no longer forces open tabs to reload mid-session. The new toast lets users choose when to reload, protecting in-flight agent runs and unsaved compose state. Refs: #99 --- ui/src/client.tsx | 38 ++++++++++++++++------ ui/src/components/PwaUpdateToast.tsx | 47 ++++++++++++++++++++++++++++ ui/vite.config.ts | 2 +- 3 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 ui/src/components/PwaUpdateToast.tsx diff --git a/ui/src/client.tsx b/ui/src/client.tsx index 4bdeadd3..096d57cf 100644 --- a/ui/src/client.tsx +++ b/ui/src/client.tsx @@ -1,21 +1,41 @@ import { StartClient } from "@tanstack/react-start/client" -import { StrictMode } from "react" +import { StrictMode, useState } from "react" import { hydrateRoot } from "react-dom/client" import { registerSW } from "virtual:pwa-register" -// autoUpdate: vite-plugin-pwa generates a SW that calls skipWaiting and -// clientsClaim, so a new deploy activates for existing tabs. Open dashboard -// tabs may reload when the service worker takes over; this is acceptable for -// a dev/internal dashboard where silent updates are preferred over stale -// bundles. Skip in dev — the SW precaches production-only build artifacts that -// 404 against the dev server. -if (import.meta.env.PROD) { - registerSW() +import { PwaUpdateToast } from "@/components/PwaUpdateToast" + +// Prompt-mode PWA: vite-plugin-pwa still generates a SW that precaches the +// production bundle, but it does not force skipWaiting/clientsClaim. Instead, +// the new SW waits until the user accepts the update, so open dashboard tabs +// are not reloaded mid-session. This protects in-flight agent runs and any +// unsaved text in the compose box. Skip in dev — the SW precaches production-only +// build artifacts that 404 against the dev server. +function PwaUpdateProvider() { + const [needRefresh, setNeedRefresh] = useState(false) + const [updateSW, setUpdateSW] = useState<(() => Promise) | undefined>(undefined) + + if (import.meta.env.PROD) { + const updateSWRef = registerSW({ + onNeedRefresh() { + setNeedRefresh(true) + }, + }) + setUpdateSW(() => updateSWRef) + } + + return ( + + ) } hydrateRoot( document, + ) diff --git a/ui/src/components/PwaUpdateToast.tsx b/ui/src/components/PwaUpdateToast.tsx new file mode 100644 index 00000000..792c7207 --- /dev/null +++ b/ui/src/components/PwaUpdateToast.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react" + +import { Button } from "@/components/ui/button" + +interface PwaUpdateToastProps { + needRefresh: boolean + updateServiceWorker: (() => Promise) | undefined +} + +export function PwaUpdateToast({ + needRefresh, + updateServiceWorker, +}: PwaUpdateToastProps) { + const [dismissed, setDismissed] = useState(false) + + useEffect(() => { + if (needRefresh) { + setDismissed(false) + } + }, [needRefresh]) + + if (!needRefresh || dismissed) { + return null + } + + return ( +
+
+

A new version is available.

+

Reload to update the dashboard.

+
+
+ + +
+
+ ) +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 4e2ea978..7b377067 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -173,7 +173,7 @@ const config = defineConfig({ tanstackStart({ spa: { enabled: true } }), VitePWA({ injectRegister: false, - registerType: "autoUpdate", + registerType: "prompt", outDir: ".output/public", devOptions: { enabled: true,