2026-10-06 23:11:07 -04:00
|
|
|
// 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;
|
2026-10-11 23:28:11 -04:00
|
|
|
let statusPrefix = '';
|
2026-10-06 23:11:07 -04:00
|
|
|
|
|
|
|
|
function attentive() {
|
|
|
|
|
return !document.hidden && document.hasFocus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render() {
|
2026-10-11 23:28:11 -04:00
|
|
|
document.title = `${unread ? `(${unread}) ` : ''}${statusPrefix}${baseTitle}`;
|
2026-10-06 23:11:07 -04:00
|
|
|
const link = document.getElementById('favicon');
|
|
|
|
|
if (link) link.href = unread ? FAVICON_BADGE : FAVICON;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setBaseTitle(title) {
|
|
|
|
|
baseTitle = title;
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 23:28:11 -04:00
|
|
|
// "⏳ 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 23:11:07 -04:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 23:34:59 -04:00
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 23:11:07 -04:00
|
|
|
export function ring(session, detail) {
|
|
|
|
|
if (attentive()) return;
|
|
|
|
|
unread += 1;
|
|
|
|
|
render();
|
2026-10-07 23:34:59 -04:00
|
|
|
if (notificationsState() === 'granted' && !isMuted()) {
|
2026-10-06 23:11:07 -04:00
|
|
|
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);
|