feat: add multi-layer browser notification system

Implement a 4-layer notification system for alerting users when Claude
Code sessions need attention:

1. Notification Drawer - persistent in-page log with bell icon + badge
2. Tab Title Updates - flashing title with unread count when unfocused
3. Web Notifications - browser push notifications when tab hidden
4. Audio Alerts - Web Audio API beep for critical events

Notification triggers:
- Critical: session errors, crashes (non-zero exit), agent failures/timeouts
- Warning: Ralph loop completions, budget warnings, stuck sessions (idle >10min)
- Info: auto-accepts, auto-clears, agent completions

Features:
- NotificationManager class with debouncing/grouping (5s window)
- Settings tab in App Settings with per-layer/per-urgency toggles
- Configurable idle threshold for stuck session detection
- Click notification to switch to relevant session
- Notification permission request flow
- Rate-limited browser notifications (max 1/3s)
- Idle timer cleanup on session delete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-24 01:22:53 +01:00
co-authored by Claude Opus 4.5
parent e40849da79
commit 0fc52902cc
3 changed files with 752 additions and 0 deletions
+79
View File
@@ -46,6 +46,10 @@
<span class="stat-value" id="statMem">--</span>
</div>
</div>
<button class="btn-icon-header btn-notifications" onclick="app.toggleNotifications()" title="Notifications">
&#x1F514;
<span class="notification-badge" id="notifBadge" style="display:none;">0</span>
</button>
<button class="btn-icon-header btn-settings" onclick="app.openAppSettings()" title="App Settings">&#x2699;</button>
<div class="header-tokens" id="headerTokens" title="Total tokens across all sessions">0 tokens</div>
</div>
@@ -467,6 +471,7 @@
<button class="modal-tab-btn active" data-tab="settings-display">Display</button>
<button class="modal-tab-btn" data-tab="settings-claude">Claude CLI</button>
<button class="modal-tab-btn" data-tab="settings-paths">Paths</button>
<button class="modal-tab-btn" data-tab="settings-notifications">Notifications</button>
</div>
<div class="modal-body">
<!-- Display Tab -->
@@ -542,6 +547,66 @@
<span class="form-hint">Default directory for new sessions.</span>
</div>
</div>
<!-- Notifications Tab -->
<div class="modal-tab-content hidden" id="settings-notifications">
<div class="form-row form-row-switch">
<label>Enable Notifications</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifEnabled" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Master toggle for all notification layers</span>
</div>
<div class="form-row form-row-switch">
<label>Browser Notifications</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifBrowser">
<span class="slider"></span>
</label>
<span class="form-hint">Show OS-level notifications when tab is hidden</span>
</div>
<div class="form-row">
<button class="btn-toolbar btn-sm" onclick="app.notificationManager?.requestPermission()">Request Permission</button>
<span class="form-hint" id="notifPermissionStatus">Status: unknown</span>
</div>
<div class="form-row form-row-switch">
<label>Audio Alerts</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifAudio">
<span class="slider"></span>
</label>
<span class="form-hint">Play a short beep for critical events</span>
</div>
<div class="form-row">
<label>Idle Threshold (minutes)</label>
<input type="number" id="appSettingsNotifStuckMins" value="10" min="1" max="120" style="width: 80px;">
<span class="form-hint">Notify when a session is idle longer than this</span>
</div>
<div class="form-row form-row-switch">
<label>Critical Notifications</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifCritical" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Errors, crashes, agent failures</span>
</div>
<div class="form-row form-row-switch">
<label>Warning Notifications</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifWarning" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Completions, budget warnings, stuck sessions</span>
</div>
<div class="form-row form-row-switch">
<label>Info Notifications</label>
<label class="switch">
<input type="checkbox" id="appSettingsNotifInfo" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Auto-accepts, auto-clears, agent completions</span>
</div>
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeAppSettings()">Cancel</button>
@@ -623,6 +688,20 @@
</div>
</div>
<!-- Notification Drawer -->
<div class="notification-drawer" id="notifDrawer">
<div class="notif-drawer-header">
<span class="notif-drawer-title">Notifications</span>
<div class="notif-drawer-actions">
<button class="btn-notif-action" onclick="app.notificationManager?.markAllRead()" title="Mark all read">&#x2713;</button>
<button class="btn-notif-action" onclick="app.notificationManager?.clearAll()" title="Clear all">&#x1F5D1;</button>
<button class="btn-notif-action" onclick="app.toggleNotifications()" title="Close">&times;</button>
</div>
</div>
<div class="notif-drawer-list" id="notifList"></div>
<div class="notif-drawer-empty" id="notifEmpty">No notifications</div>
</div>
<!-- Help Button -->
<button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button>
</div>