/** * @file Browser-notification preferences — the single definition of the shape, * the defaults, and the `localStorage` read/write. Settings (which edits them) * and `useNotifications` (which obeys them) both import from here; they used to * carry private copies, so adding a notification type meant editing the same * literal twice and a miss went unnoticed until a toggle did nothing. A * shown notification also plays a sound: `sound` is the default, and * `soundByType` may override it per notification type (`soundFor`). * @author Nguyễn Ngọc Trí Vĩ */ export const NOTIF_KEY = "agent-monitor-notifications"; export const NOTIF_SOUNDS = ["none", "chime", "pop", "alert"] as const; export type NotifSound = (typeof NOTIF_SOUNDS)[number]; /** The per-type flags — every key a notification is gated on. */ export type NotifType = | "onNewSession" | "onSessionError" | "onAwaitingInput" | "onTurnComplete" | "onSessionComplete" | "onSubagentSpawn"; export interface NotifPrefs { /** Master switch; when false no notification fires, whatever the flags say. */ enabled: boolean; onNewSession: boolean; onSessionError: boolean; /** Claude is BLOCKED on the user — a permission prompt or a question. */ onAwaitingInput: boolean; /** Claude finished its turn and is waiting for the next prompt (`Stop`). */ onTurnComplete: boolean; /** The whole session closed (`SessionEnd`), not just one turn. */ onSessionComplete: boolean; onSubagentSpawn: boolean; /** The sound a shown notification plays, unless its type overrides it. */ sound: NotifSound; soundByType?: Partial>; } // The two "Claude is waiting for you" types default ON: they are the reason // notifications exist (issue #29 — the user walks away during a long run and // needs to know when they are the blocker). The rest stay as they were. export const defaultNotifPrefs: NotifPrefs = { enabled: false, onNewSession: false, onSessionError: true, onAwaitingInput: true, onTurnComplete: true, onSessionComplete: false, onSubagentSpawn: false, sound: "none", }; /** The sound for one notification type: its override, else the default. */ export function soundFor(prefs: NotifPrefs, type: NotifType): NotifSound { return prefs.soundByType?.[type] ?? prefs.sound; } /** Reads prefs, merged over the defaults so a stored blob written by an older * build (missing the newer flags) still yields a complete object. */ export function loadNotifPrefs(): NotifPrefs { try { const raw = localStorage.getItem(NOTIF_KEY); if (!raw) return { ...defaultNotifPrefs }; return { ...defaultNotifPrefs, ...(JSON.parse(raw) as Partial) }; } catch { return { ...defaultNotifPrefs }; } } export function saveNotifPrefs(prefs: NotifPrefs) { try { localStorage.setItem(NOTIF_KEY, JSON.stringify(prefs)); } catch { /* private mode / quota — the toggle still applies for this tab */ } }