Files
devdesk/web/src/notify.js
T
jaredandClaude Opus 5.5 be9b026a01
build-deb / Lint, test, package (push) Successful in 2m12s
build-deb / Deploy to CT 126 (push) Successful in 3s
build-deb / Notify on failure (push) Skipped
feat: v0.5.0 header, switcher, phone key bar, shared polling, Matrix "needs you"
- Header: no SSO chip ("Sign in again" only on expiry), state dot on the
  session chip, LIVE as a dot, ⋯ menu for text size/notifications/fullscreen,
  Panels badge for waiting sessions and alerts.
- Session switcher on the chip; bare URL reattaches the last session; + New
  opens the panel form with project folders.
- Notifications when other sessions start waiting; alerts polled with the
  panels closed; one tab polls for all tabs over BroadcastChannel.
- Sidebar: context bar per session instead of the Context panel; compact
  Repo/CI with "Ask Claude to fix CI"; legible labels.
- Phone: key bar, drawer with backdrop, visualViewport-aware layout, DOM
  renderer on touch, compact connection dot.
- devdesk-notify-matrix: one Matrix message per wait that lasts
  DEVDESK_MATRIX_DELAY seconds with nobody attached.

Closes #8
Closes #13
Closes #14
Closes #15
Closes #16
Closes #17
Closes #28

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:34:59 -04:00

79 lines
1.9 KiB
JavaScript

// 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;
function attentive() {
return !document.hidden && document.hasFocus();
}
function render() {
document.title = unread ? `(${unread}) ${baseTitle}` : baseTitle;
const link = document.getElementById('favicon');
if (link) link.href = unread ? FAVICON_BADGE : FAVICON;
}
export function setBaseTitle(title) {
baseTitle = title;
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);