feat: add Web Push API support and PWA integration (#189)
* feat: add Web Push API support and PWA integration Add web Push API with subscribe/unsubscribe and visibility endpoints Introduce usePushVisibilityBeacon and useSessionDeepLink hooks Integrate PWA with service worker, registerSW, and VAPID key persistence * feat: add heartbeat visibility beacon for web runtime Add a 10s heartbeat to ping visibility while visible Subscribe to visibilitychange, focus, blur, pageshow, and pagehide events to report state Clear heartbeat interval on unmount to avoid leaks
This commit is contained in:
committed by
GitHub
parent
06c5e821a4
commit
1f23b63c0b
@@ -1202,18 +1202,32 @@ export const useEventStream = () => {
|
||||
useSessionStore.getState().sessions.find((s) => s.id === request.sessionID)?.title ||
|
||||
'Session';
|
||||
|
||||
import('sonner').then(({ toast }) => {
|
||||
toast.warning('Permission required', {
|
||||
description: sessionTitle,
|
||||
action: {
|
||||
label: 'Open',
|
||||
onClick: () => {
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
void useSessionStore.getState().setCurrentSession(request.sessionID);
|
||||
import('sonner').then(({ toast }) => {
|
||||
toast.warning('Permission required', {
|
||||
description: sessionTitle,
|
||||
action: {
|
||||
label: 'Open',
|
||||
onClick: () => {
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
void useSessionStore.getState().setCurrentSession(request.sessionID);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (isWebRuntime() && nativeNotificationsEnabled) {
|
||||
const shouldNotify = notificationMode === 'always' || visibilityStateRef.current === 'hidden';
|
||||
if (shouldNotify) {
|
||||
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||
if (runtimeAPIs?.notifications) {
|
||||
void runtimeAPIs.notifications.notifyAgentCompletion({
|
||||
title: 'Permission required',
|
||||
body: sessionTitle,
|
||||
tag: `permission-${toastKey}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
|
||||
const HEARTBEAT_MS = 10000;
|
||||
|
||||
const sendVisibility = (visible: boolean) => {
|
||||
if (!isWebRuntime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apis = getRegisteredRuntimeAPIs();
|
||||
if (!apis?.push?.setVisibility) {
|
||||
return;
|
||||
}
|
||||
|
||||
void apis.push.setVisibility({ visible });
|
||||
};
|
||||
|
||||
export const usePushVisibilityBeacon = () => {
|
||||
React.useEffect(() => {
|
||||
if (!isWebRuntime() || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const report = () => {
|
||||
sendVisibility(document.visibilityState === 'visible');
|
||||
};
|
||||
|
||||
const reportVisibleOnly = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
sendVisibility(true);
|
||||
}
|
||||
};
|
||||
|
||||
report();
|
||||
|
||||
// Heartbeat while visible so server TTL (30s) never expires.
|
||||
const interval = window.setInterval(reportVisibleOnly, HEARTBEAT_MS);
|
||||
|
||||
document.addEventListener('visibilitychange', report);
|
||||
window.addEventListener('pagehide', report);
|
||||
window.addEventListener('pageshow', report);
|
||||
window.addEventListener('focus', report);
|
||||
window.addEventListener('blur', report);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(interval);
|
||||
document.removeEventListener('visibilitychange', report);
|
||||
window.removeEventListener('pagehide', report);
|
||||
window.removeEventListener('pageshow', report);
|
||||
window.removeEventListener('focus', report);
|
||||
window.removeEventListener('blur', report);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
export const useSessionDeepLink = () => {
|
||||
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionId: string | null = null;
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
sessionId = params.get('session');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionId || sessionId.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
await setCurrentSession(sessionId as string);
|
||||
} finally {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('session');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
}, [setCurrentSession]);
|
||||
};
|
||||
Reference in New Issue
Block a user