open-swe/ui/src/lib/notifications.ts
Johannes du Plessis d2c232cc54
feat: add browser notifications for agent run completion (#1558)
* feat: add browser notifications for agent run completion

Request notification permission when a user starts their first agent
run from the home page. A toggle in Profile Settings lets users
enable/disable desktop notifications. When a run transitions from
running to a terminal state (finished/error/interrupted), a browser
notification is fired — suppressed for the thread the user is
currently viewing.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: notify for active thread when page is in background tab

Only suppress the notification for the active thread when the page is
actually visible. If the user switched to another browser tab, the
notification fires even for the thread they have open.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-17 09:24:44 -07:00

62 lines
1.7 KiB
TypeScript

import type { AgentThread } from "./agents/types"
export const NOTIFICATIONS_PREF_KEY = "open-swe-notifications-enabled"
export function notificationsSupported(): boolean {
return typeof window !== "undefined" && "Notification" in window
}
export function getNotificationPermission(): NotificationPermission | null {
if (!notificationsSupported()) return null
return Notification.permission
}
export function notificationsEnabled(): boolean {
if (!notificationsSupported()) return false
if (Notification.permission !== "granted") return false
try {
return localStorage.getItem(NOTIFICATIONS_PREF_KEY) === "true"
} catch {
return false
}
}
export async function requestNotificationPermission(): Promise<NotificationPermission | null> {
if (!notificationsSupported()) return null
if (Notification.permission === "granted") return "granted"
if (Notification.permission === "denied") return "denied"
return Notification.requestPermission()
}
export function setNotificationsPref(enabled: boolean) {
try {
localStorage.setItem(NOTIFICATIONS_PREF_KEY, String(enabled))
} catch {
/* ignore */
}
}
export function showRunNotification(thread: AgentThread) {
if (!notificationsEnabled()) return
const statusLabel =
thread.status === "error"
? "encountered an error"
: thread.status === "interrupted"
? "was interrupted"
: "finished"
const title = thread.title || "Agent run"
const body = `Run ${statusLabel}.`
try {
const n = new Notification(title, {
body,
icon: "/logo-mark.png",
tag: `run-${thread.id}`,
})
n.onclick = () => {
window.focus()
n.close()
}
} catch {
/* ignore */
}
}