import webPush from 'web-push'; import { notificationPreferencesDb } from '@/shared/database/repositories/notification-preferences.js'; import { pushSubscriptionsDb } from '@/shared/database/repositories/push-subscriptions.js'; import { sessionsDb } from '@/shared/database/repositories/sessions.db.js'; type NotificationKind = 'action_required' | 'stop' | 'error' | 'info' | string; type NotificationEvent = { provider: string; sessionId?: string | null; kind?: NotificationKind; code?: string; meta?: Record; severity?: string; dedupeKey?: string | null; requiresUserAction?: boolean; createdAt?: string; }; type NotificationPreferences = { channels?: { inApp?: boolean; webPush?: boolean; }; events?: { actionRequired?: boolean; stop?: boolean; error?: boolean; }; }; const KIND_TO_PREF_KEY: Record> = { action_required: 'actionRequired', stop: 'stop', error: 'error', }; const PROVIDER_LABELS: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', gemini: 'Gemini', system: 'System', }; const recentEventKeys = new Map(); const DEDUPE_WINDOW_MS = 20_000; const cleanupOldEventKeys = (): void => { const now = Date.now(); for (const [key, timestamp] of recentEventKeys.entries()) { if (now - timestamp > DEDUPE_WINDOW_MS) { recentEventKeys.delete(key); } } }; function shouldSendPush( preferences: NotificationPreferences | null | undefined, event: NotificationEvent ): boolean { const webPushEnabled = Boolean(preferences?.channels?.webPush); const prefEventKey = KIND_TO_PREF_KEY[event.kind ?? '']; const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true; return webPushEnabled && eventEnabled; } function isDuplicate(event: NotificationEvent): boolean { cleanupOldEventKeys(); const key = event.dedupeKey ?? `${event.provider}:${event.kind ?? 'info'}:${event.code ?? 'generic'}:${event.sessionId ?? 'none'}`; if (recentEventKeys.has(key)) { return true; } recentEventKeys.set(key, Date.now()); return false; } function normalizeErrorMessage(error: unknown): string { if (typeof error === 'string') { return error; } if ( error && typeof error === 'object' && 'message' in error && typeof (error as { message?: unknown }).message === 'string' ) { return (error as { message: string }).message; } if (error == null) { return 'Unknown error'; } return String(error); } function normalizeSessionName(sessionName: unknown): string | null { if (typeof sessionName !== 'string') { return null; } const normalized = sessionName.replace(/\s+/g, ' ').trim(); if (!normalized) { return null; } return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized; } function resolveSessionName(event: NotificationEvent): string | null { const explicitSessionName = normalizeSessionName(event.meta?.sessionName); if (explicitSessionName) { return explicitSessionName; } if (!event.sessionId || !event.provider) { return null; } return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider)); } function buildPushBody(event: NotificationEvent) { const codeMap: Record = { 'permission.required': event.meta?.toolName ? `Action Required: Tool "${String(event.meta.toolName)}" needs approval` : 'Action Required: A tool needs your approval', 'run.stopped': (typeof event.meta?.stopReason === 'string' && event.meta.stopReason) || 'Run Stopped: The run has stopped', 'run.failed': event.meta?.error ? `Run Failed: ${String(event.meta.error)}` : 'Run Failed: The run encountered an error', 'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification', 'push.enabled': 'Push notifications are now enabled!', }; const providerLabel = PROVIDER_LABELS[event.provider] ?? 'Assistant'; const sessionName = resolveSessionName(event); const message = codeMap[event.code ?? ''] ?? 'You have a new notification'; return { title: sessionName ?? 'Claude Code UI', body: `${providerLabel}: ${message}`, data: { sessionId: event.sessionId ?? null, code: event.code ?? null, provider: event.provider ?? null, sessionName, tag: `${event.provider ?? 'assistant'}:${event.sessionId ?? 'none'}:${event.code ?? 'generic.info'}`, }, }; } async function sendWebPush(userId: number, event: NotificationEvent): Promise { const subscriptions = pushSubscriptionsDb.getPushSubscriptions(userId); if (!subscriptions.length) return; const payload = JSON.stringify(buildPushBody(event)); const results = await Promise.allSettled( subscriptions.map((sub) => webPush.sendNotification( { endpoint: sub.endpoint, keys: { p256dh: sub.keys_p256dh, auth: sub.keys_auth, }, }, payload ) ) ); // Clean up gone subscriptions (410 Gone or 404). results.forEach((result, index) => { if (result.status === 'rejected') { const statusCode = (result.reason as { statusCode?: number } | undefined) ?.statusCode; if (statusCode === 410 || statusCode === 404) { pushSubscriptionsDb.deletePushSubscription(subscriptions[index].endpoint); } } }); } function createNotificationEvent({ provider, sessionId = null, kind = 'info', code = 'generic.info', meta = {}, severity = 'info', dedupeKey = null, requiresUserAction = false, }: { provider: string; sessionId?: string | null; kind?: NotificationKind; code?: string; meta?: Record; severity?: string; dedupeKey?: string | null; requiresUserAction?: boolean; }): NotificationEvent { return { provider, sessionId, kind, code, meta, severity, requiresUserAction, dedupeKey, createdAt: new Date().toISOString(), }; } function notifyUserIfEnabled({ userId, event, }: { userId: number | null | undefined; event: NotificationEvent | null | undefined; }): void { if (!userId || !event) { return; } const preferences = notificationPreferencesDb.getNotificationPreferences(userId); if (!shouldSendPush(preferences, event)) { return; } if (isDuplicate(event)) { return; } sendWebPush(userId, event).catch((error) => { console.error('Web push send error:', error); }); } function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null, }: { userId: number; provider: string; sessionId?: string | null; stopReason?: string; sessionName?: string | null; }): void { notifyUserIfEnabled({ userId, event: createNotificationEvent({ provider, sessionId, kind: 'stop', code: 'run.stopped', meta: { stopReason, sessionName }, severity: 'info', dedupeKey: `${provider}:run:stop:${sessionId ?? 'none'}:${stopReason}`, }), }); } function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null, }: { userId: number; provider: string; sessionId?: string | null; error: unknown; sessionName?: string | null; }): void { const errorMessage = normalizeErrorMessage(error); notifyUserIfEnabled({ userId, event: createNotificationEvent({ provider, sessionId, kind: 'error', code: 'run.failed', meta: { error: errorMessage, sessionName }, severity: 'error', dedupeKey: `${provider}:run:error:${sessionId ?? 'none'}:${errorMessage}`, }), }); } export { createNotificationEvent, notifyUserIfEnabled, notifyRunStopped, notifyRunFailed, };