30 KiB
Claudeman Frontend Notification System -- Detailed Report
Generated: 2026-02-17 Source files analyzed:
/home/arkon/default/claudeman/src/web/public/app.js(main frontend, ~15k lines)/home/arkon/default/claudeman/src/web/public/index.html/home/arkon/default/claudeman/src/web/public/styles.css/home/arkon/default/claudeman/src/web/public/mobile.css
1. Architecture Overview
The notification system is a 4-layer design, all implemented in the NotificationManager class (lines 860-1268 of app.js). The layers are:
| Layer | Mechanism | When Active |
|---|---|---|
| 1 | In-app notification drawer | Always (when enabled) |
| 2 | Tab title flashing | When tab is hidden (background) |
| 3 | Browser Notification API (OS-level) | When enabled + permission granted |
| 4 | Audio alert (Web Audio API) | When enabled for specific events |
The NotificationManager is instantiated once at line 1404:
this.notificationManager = new NotificationManager(this);
2. NotificationManager Class (lines 860-1268)
2.1 Constructor (lines 861-893)
State initialized:
this.notifications = []-- in-memory log (max 100 items)this.unreadCount = 0-- badge counterthis.isTabVisible = !document.hidden-- visibility trackingthis.isDrawerOpen = falsethis.originalTitle = document.title-- saved for flash restorethis.titleFlashInterval = null-- interval ID for title flashingthis.titleFlashState = false-- toggle state for flashthis.lastBrowserNotifTime = 0-- rate-limit timestampthis.audioCtx = null-- lazily created Web Audio contextthis.groupingMap = new Map()-- debounce grouping (5s window)
Visibility listeners:
document.visibilitychange-- updatesisTabVisible, callsonTabVisible()when tab becomes visiblewindow.pageshow(withe.persistedcheck) -- handles iOS Safari back-forward cache (bfcache) restore
2.2 Preferences System (lines 896-960)
Default Event-Type Preferences (lines 897-908)
const defaultEventTypes = {
permission_prompt: { enabled: true, browser: true, audio: true },
elicitation_dialog: { enabled: true, browser: true, audio: true },
idle_prompt: { enabled: true, browser: true, audio: false },
stop: { enabled: true, browser: false, audio: false },
session_error: { enabled: true, browser: true, audio: false },
respawn_cycle: { enabled: true, browser: false, audio: false },
token_milestone: { enabled: true, browser: false, audio: false },
ralph_complete: { enabled: true, browser: true, audio: true },
subagent_spawn: { enabled: false, browser: false, audio: false },
subagent_complete: { enabled: false, browser: false, audio: false },
};
Device-Specific Defaults (lines 910-923)
Mobile devices (MobileDetection.getDeviceType() === 'mobile') get notifications disabled by default:
const isMobile = MobileDetection.getDeviceType() === 'mobile';
const defaults = {
enabled: !isMobile, // OFF on mobile
browserNotifications: !isMobile, // OFF on mobile
audioAlerts: false, // OFF everywhere
stuckThresholdMs: 600000, // 10 minutes
muteCritical: false, // Legacy urgency muting
muteWarning: false,
muteInfo: false,
eventTypes: defaultEventTypes,
_version: 3,
};
Storage Keys (lines 952-956)
Device-specific localStorage keys prevent mobile settings from overriding desktop settings:
- Desktop:
claudeman-notification-prefs - Mobile:
claudeman-notification-prefs-mobile
Version Migrations (lines 928-940)
- v1 -> v2:
browserNotificationsdefault changed fromfalsetotrue - v2 -> v3: Added
eventTypesobject with per-event-type preferences
Server Sync (lines 9470-9475, 9787-9828)
Notification preferences are saved to the server alongside app settings via PUT /api/settings:
body: JSON.stringify({ ...settings, notificationPreferences: notifPrefsToSave })
On load, server prefs are applied only if localStorage has none (line 9815-9818):
if (notificationPreferences && this.notificationManager) {
const localNotifPrefs = localStorage.getItem(this.notificationManager.getStorageKey());
if (!localNotifPrefs) {
this.notificationManager.preferences = notificationPreferences;
this.notificationManager.savePreferences();
}
}
This means localStorage always takes precedence over server-stored prefs.
3. The notify() Flow (lines 962-1038)
notify() called
|
+-- preferences.enabled === false? --> RETURN (no-op)
|
+-- Check per-event-type preferences (eventTypes[category])
| |
| +-- Found: eventPref.enabled === false? --> RETURN
| | shouldBrowserNotify = eventPref.browser && prefs.browserNotifications
| | shouldAudioAlert = eventPref.audio && prefs.audioAlerts
| |
| +-- Not found: fall back to legacy urgency-based muting
| if muteCritical/muteWarning/muteInfo matches --> RETURN
| shouldBrowserNotify = prefs.browserNotifications && (critical/warning/!tabVisible)
| shouldAudioAlert = critical && prefs.audioAlerts
|
+-- Grouping: same category+session within 5s? --> increment count, update message, RETURN
|
+-- Create notification object { id, urgency, category, sessionId, sessionName, title, message, timestamp, read, count }
|
+-- Add to this.notifications[] (max 100, FIFO eviction)
|
+-- Track in groupingMap (5s TTL)
|
+-- unreadCount++; updateBadge(); scheduleRender()
|
+-- Layer 2: if tab NOT visible --> updateTabTitle() (start title flashing)
|
+-- Layer 3: if shouldBrowserNotify --> sendBrowserNotif()
|
+-- Layer 4: if shouldAudioAlert --> playAudioAlert()
3.1 Notification Grouping (lines 986-997)
Within a 5-second window, notifications with the same category:sessionId key are grouped:
- Count is incremented on existing notification
- Message is updated to latest
- Timestamp refreshed
- No new notification entry is created
- The grouping timeout is reset (sliding window)
This prevents notification spam for rapid-fire events.
4. Layer 1: In-App Notification Drawer
4.1 HTML Structure (index.html lines 1394-1406)
<div class="notification-drawer" id="notifDrawer">
<div class="notif-drawer-header">
<span class="notif-drawer-title">Notifications</span>
<div class="notif-drawer-actions">
<button onclick="markAllRead()">checkmark</button>
<button onclick="clearAll()">trash</button>
<button onclick="toggleNotifications()">X</button>
</div>
</div>
<div class="notif-drawer-list" id="notifList"></div>
<div class="notif-drawer-empty" id="notifEmpty">No notifications</div>
</div>
4.2 Bell Button (index.html lines 63-66)
<button class="btn-icon-header btn-notifications" onclick="app.toggleNotifications()">
<svg><!-- bell icon --></svg>
<span class="notification-badge" id="notifBadge" style="display:none;">0</span>
</button>
The bell button visibility is controlled by the enabled preference (line 9647-9652):
const notifEnabled = this.notificationManager?.preferences?.enabled ?? true;
const notifBtn = document.querySelector('.btn-notifications');
if (notifBtn) {
notifBtn.style.display = notifEnabled ? '' : 'none';
}
4.3 Badge (lines 1230-1238)
The red badge on the bell shows unread count. It pulses via CSS animation:
.notification-badge {
position: absolute; top: 2px; right: 2px;
background: var(--red); color: #fff;
animation: notif-badge-pulse 2s ease-in-out infinite;
}
@keyframes notif-badge-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
Display logic: badge.style.display = unreadCount > 0 ? 'flex' : 'none'.
Shows 99+ if count exceeds 99.
4.4 Drawer Rendering (lines 1041-1079)
Uses requestAnimationFrame for debounced rendering. Each notification item shows:
- Urgency color (left border: red/yellow/blue)
- Title with count multiplier (e.g., "Permission Required x3")
- Relative timestamp ("now", "5m ago", "2h ago")
- Message (truncated with ellipsis)
- Session chip (session name)
- Unread highlight (subtle blue background)
- Slide-in animation (
notif-slide-in)
Clicking a notification: marks as read, decrements unread count, switches to the notification's session.
4.5 Drawer Toggle (lines 1183-1192)
toggleDrawer() adds/removes the open class. The drawer slides in from the right via CSS transform:
.notification-drawer {
transform: translateX(100%);
transition: transform 0.2s ease;
}
.notification-drawer.open {
transform: translateX(0);
}
4.6 CSS Styling (styles.css lines 3965-4151)
Drawer is 340px wide, fixed position, full height below header, z-index 10001. Items have color-coded left borders: red (critical), yellow (warning), blue (info). Mobile override: full-width with safe area padding (mobile.css lines 1049-1058).
5. Layer 2: Tab Title Flashing (lines 1082-1103)
Behavior
When the tab is not visible and there are unread notifications:
setIntervalat 1500ms toggles between:- Warning emoji + unread count:
"(3) Claudeman" - Original title:
"Claudeman"
- Warning emoji + unread count:
- Set immediately on first notification (no wait for first interval tick)
Stopping
onTabVisible() (line 1241) calls stopTitleFlash(), which:
- Clears the interval
- Resets
titleFlashState = false - Restores
document.title = this.originalTitle
If the drawer is open when tab becomes visible, all notifications are marked as read.
Memory Leak Prevention (lines 3294-3304)
On SSE reconnect (handleInit()), the title flash interval is explicitly cleared:
if (this.notificationManager?.titleFlashInterval) {
clearInterval(this.notificationManager.titleFlashInterval);
this.notificationManager.titleFlashInterval = null;
}
Grouping timeouts are also cleared to prevent orphaned timers.
Potential Issue
The title flash uses a Unicode warning emoji (\u26A0\uFE0F). This displays correctly on all modern browsers but may not render on very old terminals/browsers.
6. Layer 3: Browser Notification API (lines 1106-1161)
Permission Flow (lines 1107-1120)
sendBrowserNotif() called
|
+-- prefs.browserNotifications === false? --> RETURN
+-- Notification API undefined? --> RETURN
+-- Notification.permission === 'default'?
| --> Auto-request permission
| --> If granted, re-call sendBrowserNotif() recursively
| --> RETURN (wait for permission dialog)
+-- Notification.permission !== 'granted'? --> RETURN
+-- Rate limit: < 3s since last? --> RETURN
+-- Create Notification
Notification Object (lines 1127-1143)
new Notification(`Claudeman: ${title}`, {
body,
tag, // Groups same-tag notifications (replaces previous with same tag)
icon: '/favicon.ico',
silent: true, // We handle audio ourselves
});
- onclick: focuses window, switches to session, closes notification
- Auto-close: 8 seconds via
setTimeout(() => notif.close(), 8000) - Rate limit: Max 1 browser notification per 3 seconds (
BROWSER_NOTIF_RATE_LIMIT_MS)
Manual Permission Request (lines 1146-1161)
The settings UI has an "Ask" button that calls requestPermission():
- Shows toast on success/failure
- Updates permission status display (checkmark/X/?)
- Auto-enables
browserNotificationspreference on grant
Permission Status Display
In settings (index.html line 993), a <span class="settings-status" id="notifPermissionStatus">?</span> shows:
granted-> checkmark with green backgrounddenied-> X with red backgrounddefault->?
HTTPS Requirement
The settings UI shows a hint (index.html line 996):
For remote access, HTTPS is required. Start with: claudeman web --https
Browser Notification API requires a secure context (HTTPS or localhost). This hint warns users who access Claudeman remotely over HTTP.
7. Layer 4: Audio Alerts (lines 1163-1181)
Implementation
Uses Web Audio API to generate a short sine wave beep:
playAudioAlert() {
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(660, ctx.currentTime); // 660 Hz (high E)
gain.gain.setValueAtTime(0.15, ctx.currentTime); // Low volume
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15); // 150ms fade
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + 0.15); // 150ms duration
}
The AudioContext is lazily created and reused across alerts. Errors are silently caught.
Potential Issues
-
Autoplay policy: Modern browsers block
AudioContextcreation until user interaction. The firstplayAudioAlert()call may silently fail if the user hasn't clicked anything yet. The code handles this gracefully via try/catch, but the user gets no feedback that audio failed. -
Mobile iOS restrictions: iOS Safari requires
AudioContext.resume()after user gesture. The current code does not callresume(), so audio alerts may never work on iOS unless the user has already interacted with anAudioContext(e.g., by clicking something that triggers audio). -
No audio indicator: There is no visual feedback that an audio alert played (or failed to play).
8. SSE Event -> Notification Mapping
The following table maps every SSE event that triggers a notification, with exact line numbers:
| SSE Event | Category | Urgency | Line | Condition |
|---|---|---|---|---|
session:error |
session-error |
critical | 2341 | Always |
session:exit |
session-crash |
critical | 2360 | Non-zero exit code only |
session:idle |
session-stuck |
warning | 2386 | After stuck threshold timeout (default 10min), only if respawn not enabled |
respawn:blocked |
respawn-blocked |
critical | 2488 | Always (circuit breaker, exit signal, or status blocked) |
respawn:autoAcceptSent |
auto-accept |
info | 2513 | Always |
session:autoClear |
auto-clear |
info | 2623 | Always |
session:ralphCompletionDetected |
ralph-complete |
warning | 2746 | Deduped by completion key (30s cooldown) |
session:circuitBreakerUpdate |
circuit-breaker |
critical | 2772 | Only when state === 'OPEN' |
session:exitGateMet |
exit-gate |
warning | 2787 | Always |
hook:idle_prompt |
hook-idle |
warning | 2823 | Always |
hook:permission_prompt |
hook-permission |
critical | 2841 | Always |
hook:elicitation_dialog |
hook-elicitation |
critical | 2858 | Always |
hook:stop |
hook-stop |
info | 2875 | Always |
| Circuit breaker reset | circuit-breaker |
info | 10634 | On successful reset |
| Fix plan error | fix-plan |
error | 10657 | On API error |
| Fix plan copied | fix-plan |
info | 10719 | On clipboard copy |
| Fix plan written | fix-plan |
info | 10738 | On successful write |
| Fix plan write error | fix-plan |
error | 10746 | On write failure |
| Fix plan imported | fix-plan |
info | 10768 | On successful import |
| Fix plan not found | fix-plan |
warning | 10777 | When file not found |
Category-to-EventType Mapping Gap
Bug identified: The notification categories used in notify() calls do NOT always match the event type keys in preferences.eventTypes. For example:
- Category
session-erroris used (line 2343) but the eventType key issession_error(underscore, line 902) - Category
session-crash(line 2362) has no corresponding eventType entry - Category
session-stuck(line 2388) has no corresponding eventType entry - Category
respawn-blocked(line 2490) has no corresponding eventType entry - Category
auto-accept(line 2515) has no corresponding eventType entry - Category
auto-clear(line 2625) has no corresponding eventType entry - Category
circuit-breaker(lines 2774, 10636) has no corresponding eventType entry - Category
exit-gate(line 2789) has no corresponding eventType entry - Category
hook-idle(line 2825) -- should map toidle_prompt, but it does not match the key - Category
hook-permission(line 2843) -- should map topermission_prompt, but does not match - Category
hook-elicitation(line 2860) -- should map toelicitation_dialog, but does not match - Category
hook-stop(line 2877) -- should map tostop, but does not match - Category
fix-plan(lines 10659, 10721, etc.) has no corresponding eventType entry
Impact: When notify() is called with a category that does not exist in preferences.eventTypes, the code falls through to the legacy urgency-based muting path (lines 976-983). This means per-event-type browser/audio toggles in the settings UI have no effect on the actual hook events, because the hook events use different category strings (hook-permission) than the eventType keys (permission_prompt).
For example, unchecking "Browser" for "Permission prompts" in settings sets eventTypes.permission_prompt.browser = false. But the actual notification uses category hook-permission, which is not found in eventTypes, so it falls back to urgency-based logic where critical urgency always gets browser notifications when browserNotifications is enabled.
This is the most significant bug in the notification system.
9. Tab Alert System (Separate from NotificationManager)
How It Works
Tab alerts are a separate visual indicator system that shows blinking session tabs:
-
State tracking (lines 1349-1354):
tabAlerts: Map<sessionId, 'action' | 'idle'>-- current alert state per tabpendingHooks: Map<sessionId, Set<hookType>>-- pending hook events
-
setPendingHook() (lines 1445-1451): Adds hook type to session's pending set, calls
updateTabAlertFromHooks(). -
clearPendingHooks() (lines 1453-1464): Removes specific hook type or all hooks, calls
updateTabAlertFromHooks(). -
updateTabAlertFromHooks() (lines 1467-1477):
No hooks -> remove alert Has permission_prompt OR elicitation_dialog -> 'action' alert (red blink) Has idle_prompt -> 'idle' alert (yellow blink) -
Visual rendering (lines 3497-3504, 3581-3582): Tab elements get CSS classes
tab-alert-actionortab-alert-idle.
CSS Animations (styles.css lines 329-345)
.session-tab.tab-alert-action {
animation: tab-blink-red 2.5s ease-in-out infinite;
}
.session-tab.tab-alert-idle {
animation: tab-blink-yellow 3.5s ease-in-out infinite;
}
@keyframes tab-blink-red {
0%, 100% { background: transparent; border-color: transparent; }
50% { background: rgba(239, 68, 68, 0.12); border-color: var(--red); }
}
@keyframes tab-blink-yellow {
0%, 100% { background: transparent; border-color: transparent; }
50% { background: rgba(234, 179, 8, 0.1); border-color: var(--yellow); }
}
Alert Clearing
session:workingevent (line 2406-2408): Clears tab alert only if no pending hooks remain for that session. This correctly preserves alerts for permission prompts even when Claude starts working again.hook:stopevent (line 2873): Clears ALL pending hooks for the session (response complete means all hooks resolved).selectSession()(line 3977): Clearsidle_prompthooks (viewing the session means you saw the idle state) but keepsactionhooks.sendInput()(line 3148): Clears all pending hooks (user sent input, so hooks are resolved).- Session deletion (line 4128-4129): Clears both pending hooks and tab alerts.
- SSE reconnect (lines 3287-3289): Clears all pending hooks and tab alerts.
Tab Glow Effect (lines 3983-3986)
When switching sessions, the newly-active tab gets a brief green glow animation:
@keyframes tab-glow {
0% { box-shadow: none; }
10% { box-shadow: 0 0 18px 6px rgba(34, 197, 94, 0.7); }
100% { box-shadow: none; }
}
This is purely cosmetic and not notification-related.
10. Notification Settings UI (lines 9272-9461)
Settings Location
Under App Settings modal -> "Notifications" tab (if using tabbed settings layout).
Controls
Global Controls (index.html lines 981-1013):
| Setting | Element ID | Default | Purpose |
|---|---|---|---|
| Enabled | appSettingsNotifEnabled |
true (desktop), false (mobile) | Master switch |
| Browser | appSettingsNotifBrowser |
true (desktop), false (mobile) | OS-level notifications |
| Audio Alerts | appSettingsNotifAudio |
false | Beep sounds |
| Idle Threshold | appSettingsNotifStuckMins |
10 | Minutes before "stuck" warning |
Legacy Urgency Levels (index.html lines 1019-1039):
| Setting | Element ID | Default | Purpose |
|---|---|---|---|
| Critical | appSettingsNotifCritical |
checked | Show critical urgency |
| Warning | appSettingsNotifWarning |
checked | Show warning urgency |
| Info | appSettingsNotifInfo |
checked | Show info urgency |
These are stored inverted as muteCritical, muteWarning, muteInfo.
Per-Event-Type Grid (index.html lines 1046-1086): A 4-column grid (Event / On / Browser / Sound) for 7 event types:
| Event | On Default | Browser Default | Sound Default |
|---|---|---|---|
| Permission prompts | on | on | on |
| Questions from Claude | on | on | on |
| Session idle | on | on | off |
| Response complete | on | off | off |
| Respawn cycles | on | off | off |
| Task complete | on | on | on |
| Subagent activity | off | off | off |
Save Flow (lines 9362-9488)
- Collect all UI values into a
notifPrefsToSaveobject - Set
this.notificationManager.preferences = notifPrefsToSave - Call
this.notificationManager.savePreferences()(writes to localStorage) - Send to server via
PUT /api/settingswithnotificationPreferencesfield - Call
applyHeaderVisibilitySettings()which hides/shows the bell button
Session Error Event Type Bug (line 9425-9429)
The session_error event type in the save flow reuses the permission_prompt's browser checkbox:
session_error: {
enabled: true,
browser: document.getElementById('eventPermissionBrowser').checked, // BUG: wrong checkbox
audio: false,
},
This means toggling "Permission prompts -> Browser" also affects session_error browser notifications, which is likely unintentional.
11. Mobile-Specific Behavior
Device Detection (lines 119-198)
MobileDetection object detects:
- Touch capability (via
ontouchstart,maxTouchPoints, media query) - iOS devices
- Safari browser
- Screen size categories: mobile (<430px), tablet (430-768px), desktop (768px+)
Body classes set: device-mobile, device-tablet, device-desktop, touch-device, ios-device, safari-browser.
Mobile Notification Defaults (lines 910-914)
On mobile devices:
enabled: false-- notifications disabled by defaultbrowserNotifications: false-- browser notifications disabledaudioAlerts: false-- audio disabled (same as desktop)
Mobile Storage Key (lines 952-956)
Mobile uses a separate localStorage key (claudeman-notification-prefs-mobile) so that enabling notifications on desktop does not accidentally enable them on a mobile device viewing the same Claudeman instance.
Mobile Drawer Styling (mobile.css lines 1049-1058)
.notification-drawer {
width: 100%;
max-width: 100%;
right: 0;
border-radius: 0;
padding-left: var(--safe-area-left);
padding-right: var(--safe-area-right);
padding-bottom: var(--safe-area-bottom);
}
The drawer takes full width on mobile and respects iOS safe areas (notch, home indicator).
Mobile Tab Ordering (lines 3566-3568)
On mobile, the active session tab is always rendered first:
if (MobileDetection.getDeviceType() === 'mobile' && this.activeSessionId) {
tabOrder = [this.activeSessionId, ...this.sessionOrder.filter(id => id !== this.activeSessionId)];
}
This ensures the active tab's alert animation is always visible.
12. Team/Agent Notifications
Subagent Event Handling
Subagent events (subagent:discovered, subagent:updated, etc.) do NOT directly call notificationManager.notify(). There are no direct notification calls for subagent spawn/complete events in the SSE handlers.
The subagent_spawn and subagent_complete event types exist in the preferences (lines 906-907) and settings UI, but no code currently dispatches notifications with these categories.
Teammate Badges (lines 13063-13095)
Teammate badges are purely visual UI elements on subagent windows -- they are not part of the notification system. They show @name with team color (blue, green, yellow).
Missing Subagent Notifications
Despite having settings for "Subagent activity" (on/browser/sound), the system never dispatches notifications with category subagent_spawn or subagent_complete. The UI settings exist but are non-functional for these event types.
13. Visual Indicators Summary
Notification-Related
| Indicator | Element | Behavior |
|---|---|---|
| Bell badge | #notifBadge |
Red circle with count, pulsing animation |
| Tab blink (action) | .tab-alert-action |
Red blink, 2.5s cycle |
| Tab blink (idle) | .tab-alert-idle |
Yellow blink, 3.5s cycle |
| Title flash | document.title |
Alternates with unread count, 1.5s interval |
| Drawer slide-in | .notification-drawer.open |
Right-to-left slide, 0.2s |
| Item slide-in | .notif-item |
Right-to-left slide, 0.2s |
| Item urgency border | .notif-item-critical/warning/info |
Red/yellow/blue left border |
| Unread highlight | .notif-item.unread |
Subtle blue background |
Non-Notification Visual Indicators
| Indicator | Element | Purpose |
|---|---|---|
| Tab status dot | .tab-status |
Green (idle), pulsing green (busy), red (error) |
| Tab glow | .tab-glow |
Brief green glow on tab switch |
| Connection indicator | #connectionIndicator |
Shows offline/reconnecting/draining state |
| Ralph status badge | #ralphStatusBadge |
Active/completed/tracking state |
| Subagent count badge | #subagentCountBadge |
Active agent count |
| Task badge | .tab-badge |
Running task count on session tab |
14. Bugs and Issues
14.1 Category/EventType Mismatch (CRITICAL)
Location: Lines 962-984 (notify flow) vs lines 897-908 (eventTypes definition)
The categories used in notify() calls (hook-permission, hook-idle, session-error, etc.) do not match the eventType keys in preferences (permission_prompt, idle_prompt, session_error, etc.). This means the per-event-type checkboxes in settings have no effect on most notifications.
Impact: Users who disable "Permission prompts -> Browser" in settings still get browser notifications for permission prompts, because the notification uses category hook-permission which falls through to urgency-based logic.
Fix: Either change the categories in notify() calls to match the eventType keys, or add a mapping layer in notify().
14.2 Session Error Browser Setting Reuse (MINOR)
Location: Line 9427
session_error.browser reuses eventPermissionBrowser checkbox instead of having its own control.
14.3 No Subagent Notifications Dispatched (MINOR)
Location: Event types subagent_spawn and subagent_complete exist in defaults (lines 906-907) and UI (lines 1082-1085), but no code ever calls notify() with these categories.
14.4 AudioContext Autoplay Policy (MINOR)
Location: Line 1167
AudioContext creation may be blocked by browser autoplay policy. No resume() call is made. First audio alert after page load may silently fail.
14.5 Rate Limit Applies Across All Events (MINOR)
Location: Line 1124
The 3-second rate limit for browser notifications is global -- a rapid succession of different event types (e.g., permission prompt + session error) will only show the first browser notification.
14.6 Title Flash Shows Emoji That May Not Gate Properly
Location: Line 1088
Title flash always shows when tab is hidden and there are unread notifications, regardless of which event types are enabled/disabled. If a user disables all event types but one, the title flash still fires for all unread items.
This is correct behavior (the flash indicates unread items in the drawer), but it could be confusing if a user thinks disabling an event type should prevent all visual indicators.
14.7 onTabVisible Marks All Read If Drawer Open
Location: Lines 1243-1246
When the tab becomes visible and the drawer is open, ALL notifications are marked as read. This could be surprising if the user quickly switches tabs and back -- they lose their unread state.
15. Cleanup and Memory Safety
SSE Reconnect Cleanup (lines 3280-3305)
On handleInit() (SSE reconnect), the following notification state is cleaned up:
pendingHooks.clear()-- prevents stale hook alertstabAlerts.clear()-- prevents stale tab blinking_shownCompletions.clear()-- allows re-notificationtitleFlashIntervalcleared -- prevents orphaned intervalsgroupingMaptimeouts cleared -- prevents orphaned timeouts
Session Deletion Cleanup (lines 4128-4129)
When a session is deleted:
pendingHooks.delete(sessionId)tabAlerts.delete(sessionId)
Idle Timer Cleanup (lines 2412-2417)
When session starts working, its stuck detection timer is cleared:
const timer = this.idleTimers.get(data.id);
if (timer) {
clearTimeout(timer);
this.idleTimers.delete(data.id);
}
16. Constants Reference
| Constant | Value | Purpose |
|---|---|---|
GROUPING_TIMEOUT_MS |
5000 | Notification grouping window |
NOTIFICATION_LIST_CAP |
100 | Max notifications in drawer |
TITLE_FLASH_INTERVAL_MS |
1500 | Title blink rate |
BROWSER_NOTIF_RATE_LIMIT_MS |
3000 | Min time between browser notifications |
AUTO_CLOSE_NOTIFICATION_MS |
8000 | Browser notification auto-dismiss |
STUCK_THRESHOLD_DEFAULT_MS |
600000 | Default idle-stuck detection (10 min) |
THROTTLE_DELAY_MS |
100 | General UI throttle |