Files
hermes-web/packages/client/src/utils/completion-notification.ts
T
toor 4701bf0ad0 Phase 004: remove the desktop shell, website tooling, and bundled hermes CLI
- Drop all desktop/Electron branches, scripts, and UI (title bar, pets,
  desktop bridge); LAN endpoint kind is now web/custom only
- Replace the bundled hermes CLI with user-side installation: add
  GET /api/hermes/cli-status plus a HermesMissingView onboarding guide
- Remove the version-management UI (snapshots handled by git) and prune
  dead website/mcu/claude scripts and lockfile entries
2026-10-16 15:33:56 +08:00

104 lines
3.2 KiB
TypeScript

interface CompletionNotificationPayload {
title: string
body?: string
icon?: string
tag?: string
}
export interface CompletionNotificationPermissionResult {
granted: boolean
reason?: 'unsupported' | 'insecure' | 'denied'
}
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, fallback: T): Promise<T> {
return new Promise<T>((resolve) => {
const timer = window.setTimeout(() => resolve(fallback), timeoutMs)
promise.then(
value => {
window.clearTimeout(timer)
resolve(value)
},
() => {
window.clearTimeout(timer)
resolve(fallback)
},
)
})
}
function supportsBrowserNotification(): boolean {
return typeof window !== 'undefined' && 'Notification' in window
}
function isBrowserNotificationSecureContext(): boolean {
if (typeof window === 'undefined') return false
return window.isSecureContext
}
function browserNotificationOptions(payload: CompletionNotificationPayload): NotificationOptions {
return {
body: payload.body,
icon: payload.icon ? new URL(payload.icon, window.location.origin).href : undefined,
tag: payload.tag,
}
}
async function showServiceWorkerNotification(payload: CompletionNotificationPayload): Promise<boolean> {
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return false
try {
const registration = await withTimeout(
navigator.serviceWorker.register('/notification-sw.js'),
3000,
null,
)
if (!registration) return false
await withTimeout(navigator.serviceWorker.ready, 3000, registration)
await registration.showNotification(payload.title, browserNotificationOptions(payload))
return true
} catch (err) {
console.warn('Failed to show service worker notification:', err)
return false
}
}
export async function requestCompletionNotificationPermission(): Promise<CompletionNotificationPermissionResult> {
if (!supportsBrowserNotification()) return { granted: false, reason: 'unsupported' }
if (!isBrowserNotificationSecureContext()) return { granted: false, reason: 'insecure' }
if (Notification.permission === 'granted') return { granted: true }
if (Notification.permission === 'denied') return { granted: false, reason: 'denied' }
try {
const permission = await withTimeout(
Notification.requestPermission(),
5000,
'default' as NotificationPermission,
)
return permission === 'granted'
? { granted: true }
: { granted: false, reason: permission === 'denied' ? 'denied' : 'unsupported' }
} catch {
return { granted: false, reason: 'unsupported' }
}
}
export async function showCompletionNotification(payload: CompletionNotificationPayload): Promise<boolean> {
if (!supportsBrowserNotification() || !isBrowserNotificationSecureContext() || Notification.permission !== 'granted') {
return false
}
try {
if (await showServiceWorkerNotification(payload)) return true
const notification = new Notification(payload.title, browserNotificationOptions(payload))
notification.onclick = () => {
window.focus()
notification.close()
}
return true
} catch (err) {
console.warn('Failed to show browser completion notification:', err)
return false
}
}