// Bell → attention: desktop notification, favicon badge and a title prefix while // the tab is in the background. Claude rings the bell (preferredNotifChannel: // terminal_bell) when it finishes a turn or needs input. import { FAVICON, FAVICON_BADGE } from './icon.js'; let unread = 0; let baseTitle = document.title; let statusPrefix = ''; function attentive() { return !document.hidden && document.hasFocus(); } function render() { document.title = `${unread ? `(${unread}) ` : ''}${statusPrefix}${baseTitle}`; const link = document.getElementById('favicon'); if (link) link.href = unread ? FAVICON_BADGE : FAVICON; } export function setBaseTitle(title) { baseTitle = title; render(); } // "⏳ 3m · " while Claude works on a turn in this tab's session (devdesk#48). export function setStatusPrefix(prefix) { if (prefix === statusPrefix) return; statusPrefix = prefix; render(); } export function notificationsState() { if (!('Notification' in window)) return 'unsupported'; return Notification.permission; // granted | denied | default } export async function requestNotifications() { if (notificationsState() !== 'default') return notificationsState(); return Notification.requestPermission(); } const MUTE_KEY = 'devdesk.notify.muted'; export function isMuted() { try { return localStorage.getItem(MUTE_KEY) === '1'; } catch { return false; } } export function setMuted(muted) { try { localStorage.setItem(MUTE_KEY, muted ? '1' : '0'); } catch { // Private mode: the mute just won't persist. } } export function ring(session, detail) { if (attentive()) return; unread += 1; render(); if (notificationsState() === 'granted' && !isMuted()) { const note = new Notification(`Claude · ${session || 'devdesk'}`, { body: detail || 'Claude needs your attention', tag: `devdesk-${session}`, renotify: true, icon: FAVICON, }); note.onclick = () => { window.focus(); note.close(); }; } } function clear() { if (!unread || !attentive()) return; unread = 0; render(); } window.addEventListener('focus', clear); document.addEventListener('visibilitychange', clear);