feat(web): in-app PWA update prompt when new service worker is available (#946)

* feat(web): in-app PWA update prompt when new service worker is available (closes #938)

User-controlled reload with a persistent banner, visibility-triggered SW
checks, and an expandable rationale. Switches registerType to prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): align vite.config with soup layers for clean driver merge

Keeps registerType prompt while matching garden IWER stubs and PWA
share_target shape expected by feat/pwa-share-target in the manifest.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert "fix(web): align vite.config with soup layers for clean driver merge"

This reverts commit 6f0915b0884d029a2413d8819a4dfe81d7c4e595.

* fix(web): make PWA reload apply waiting service worker updates

Handle SKIP_WAITING in injectManifest sw.ts and reload via controllerchange
with a timed fallback when vite-plugin-pwa prompt mode does not navigate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): satisfy setTimeout mock typing in PWA reload tests

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): register PWA service worker before auth gates

Mount PwaUpdateProvider at app root and show the update banner on login
and error screens so registerSW runs for logged-out users too.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): offset PWA update banner below top status banners

Reserve top-12 when syncing or reconnecting so the reload prompt stays
visible above SyncingBanner and ReconnectingBanner.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): offset PWA update banner below voice error banner

Use PwaUpdateBannerWithStatusOffset inside VoiceProvider so voice errors
share the same top-12 reservation as sync and reconnect banners.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-06-18 10:15:10 +08:00
committed by GitHub
co-authored by Cursor
parent 78155a9d27
commit 5f27abddd4
12 changed files with 649 additions and 41 deletions
+5
View File
@@ -466,6 +466,11 @@ export default {
'reconnecting.reason.closed': 'stream closed',
'reconnecting.reason.heartbeatTimeout': 'heartbeat timeout',
'reconnecting.reason.visibilityRecovery': 'resuming after background',
'pwa.update.title': 'New version available',
'pwa.update.body': 'Reload to get the latest HAPI',
'pwa.update.reload': 'Reload',
'pwa.update.whyToggle': "Why can't I dismiss this?",
'pwa.update.whyBody': 'HAPI will not reload your tab automatically while you may have an agent running, a permission waiting, or a message in progress. Running an old web build against the current server can cause sync bugs and failed actions. This banner stays visible until you reload so you are not stuck on a stale version by accident — but you choose when to tap Reload and finish what you are doing first.',
// Send blocked
'send.blocked.title': 'Cannot send message',
+5
View File
@@ -470,6 +470,11 @@ export default {
'reconnecting.reason.closed': '流连接已关闭',
'reconnecting.reason.heartbeatTimeout': '心跳超时',
'reconnecting.reason.visibilityRecovery': '后台恢复中',
'pwa.update.title': '新版本可用',
'pwa.update.body': '重新加载以获取最新版 HAPI',
'pwa.update.reload': '重新加载',
'pwa.update.whyToggle': '为什么不能关闭此提示?',
'pwa.update.whyBody': '当你可能有正在运行的智能体、待处理的权限请求或未发送的消息时,HAPI 不会自动重新加载标签页。旧版网页与当前服务器一起运行可能导致同步错误和操作失败。此横幅会一直保持显示,直到你重新加载,以免你在不知情的情况下停留在旧版本 — 但何时点击「重新加载」由你决定,可以先完成手头的工作。',
// Send blocked
'send.blocked.title': '无法发送消息',
+24
View File
@@ -0,0 +1,24 @@
import { createContext, useContext, type ReactNode } from 'react'
import { usePwaUpdate } from '@/hooks/usePwaUpdate'
type PwaUpdateContextValue = ReturnType<typeof usePwaUpdate>
const PwaUpdateContext = createContext<PwaUpdateContextValue | null>(null)
export function PwaUpdateProvider({ children }: { children: ReactNode }) {
const value = usePwaUpdate()
return (
<PwaUpdateContext.Provider value={value}>
{children}
</PwaUpdateContext.Provider>
)
}
export function usePwaUpdateContext() {
const value = useContext(PwaUpdateContext)
if (!value) {
throw new Error('usePwaUpdateContext must be used within PwaUpdateProvider')
}
return value
}