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)
The `AudioContext` is lazily created and reused across alerts. Errors are silently caught.
### Potential Issues
1.**Autoplay policy**: Modern browsers block `AudioContext` creation until user interaction. The first `playAudioAlert()` 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.
2.**Mobile iOS restrictions**: iOS Safari requires `AudioContext.resume()` after user gesture. The current code does not call `resume()`, so audio alerts may never work on iOS unless the user has already interacted with an `AudioContext` (e.g., by clicking something that triggers audio).
3.**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:
| 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-error` is used (line 2343) but the eventType key is `session_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 to `idle_prompt`, but it does not match the key
- Category `hook-permission` (line 2843) -- should map to `permission_prompt`, but does not match
- Category `hook-elicitation` (line 2860) -- should map to `elicitation_dialog`, but does not match
- Category `hook-stop` (line 2877) -- should map to `stop`, 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:
1.**State tracking** (lines 1349-1354):
-`tabAlerts: Map<sessionId, 'action' | 'idle'>` -- current alert state per tab
- **`session:working` event** (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:stop` event** (line 2873): Clears ALL pending hooks for the session (response complete means all hooks resolved).
- **`selectSession()`** (line 3977): Clears `idle_prompt` hooks (viewing the session means you saw the idle state) but keeps `action` hooks.
- **`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:
Mobile uses a separate localStorage key (`codeman-notification-prefs-mobile`) so that enabling notifications on desktop does not accidentally enable them on a mobile device viewing the same Codeman instance.
### Mobile Drawer Styling (mobile.css lines 1049-1058)
```css
.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:
```js
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 |
**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()`.
`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 alerts
- `tabAlerts.clear()` -- prevents stale tab blinking