Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import type { Session } from './api';import { Attention, type AlertKind, type Mail } from './attention';import { boards } from './boards.svelte';import { layout } from './layout.svelte';
const KEY = 'albedo.notifications';
class Notifications { enabled = $state(localStorage.getItem(KEY) === 'on'); supported = typeof Notification !== 'undefined'; permission = $state<NotificationPermission>(this.supported ? Notification.permission : 'denied'); requesting = $state(false); error = $state(''); #attention = new Attention(); #open = new Map<string, Notification>(); #title = document.title; #count = -1; #icon = document.querySelector<HTMLLinkElement>('link[rel="icon"]'); #originalIcon = this.#icon?.href ?? '';
start() { const clear = () => { if (document.hidden) return; this.#attention.clear(); for (const notification of this.#open.values()) notification.close(); this.#open.clear(); this.#render(); if (this.supported) this.permission = Notification.permission; }; window.addEventListener('focus', clear); document.addEventListener('visibilitychange', clear); }
async toggle() { this.error = ''; if (this.enabled && this.permission === 'granted') { this.enabled = false; for (const notification of this.#open.values()) notification.close(); this.#open.clear(); } else if (this.supported) { this.requesting = true; try { this.permission = await Notification.requestPermission(); this.enabled = this.permission === 'granted'; } catch { this.error = 'the browser could not enable notifications. try again here.'; } finally { this.requesting = false; } } localStorage.setItem(KEY, this.enabled ? 'on' : 'off'); }
watched(session: Session) { return !session.preferences.archived && (session.preferences.pinned || boards.hasSession(session.id) || (layout.phone && layout.session === session.id)); }
changed(previous: Session | undefined, next: Session) { const kind = this.#attention.changed(previous, next, this.watched(next), document.hidden || !document.hasFocus()); if (kind) this.#notify(next, kind); this.#render(); }
mail(mail: Mail, sessions: Session[]) { const session = sessions.find((item) => item.id === mail.receiver_session_id); const kind = this.#attention.mail(mail, !!session && this.watched(session), document.hidden || !document.hasFocus()); if (kind && session) this.#notify(session, kind); this.#render(); }
prune(sessions: Session[]) { this.#attention.prune(new Set(sessions.filter((session) => this.watched(session)).map((session) => session.id))); this.#render(); }
#render() { const count = this.#attention.unread.size; if (count === this.#count) return; this.#count = count; document.title = count ? `(${count}) ${this.#title}` : this.#title; if (this.#icon) this.#icon.href = count ? `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="#e8e4dc" stroke="#333" stroke-width="1.5"/><circle cx="12" cy="4" r="3" fill="#a05886" stroke="#fff"/></svg>')}` : this.#originalIcon; }
#notify(session: Session, kind: AlertKind) { if (!document.hidden || !this.enabled || !this.supported || Notification.permission !== 'granted') return; try { const body = { finished: 'finished a turn', attention: 'needs your attention', mail: 'received agent mail' }[kind]; const notification = new Notification(session.name || 'albedo', { body, tag: `albedo:${session.id}` }); this.#open.set(session.id, notification); notification.onclose = () => this.#open.delete(session.id); notification.onclick = () => { window.focus(); boards.revealSession(session); notification.close(); }; } catch { this.error = 'this browser could not show a system notification. tab counts still work.'; } }}
export const notifications = new Notifications();