mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
Full product rename across 109 files (~834 occurrences): - Env vars: CLAUDEMAN_* → CODEMAN_* - Data dirs: ~/.claudeman/ → ~/.codeman/, ~/claudeman-cases/ → ~/codeman-cases/ - tmux prefix: claudeman- → codeman- - localStorage: claudeman-* → codeman-* - Package/CLI: claudeman → codeman - GitHub repo: Ark0N/Claudeman → Ark0N/Codeman - systemd service: claudeman-web → codeman-web - Class: ClaudemanApp → CodemanApp Migration infrastructure for seamless transition: - state-store.ts: auto-migrates data directories on startup - tmux-manager.ts: dual-prefix detection (legacy claudeman- sessions) - app.js: localStorage key migration (preserves old keys) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
550 lines
19 KiB
Markdown
550 lines
19 KiB
Markdown
# Notification Blinking & Visual Alert System - Deep Dive
|
|
|
|
## Overview
|
|
|
|
Codeman implements a **4-layer notification system** managed by the `NotificationManager` class (app.js lines 860-1265). The layers are:
|
|
|
|
1. **In-app notification drawer** (Layer 1) - badge + list UI
|
|
2. **Document title flashing** (Layer 2) - tab title blinks when hidden
|
|
3. **Browser Web Notifications** (Layer 3) - OS-level popups
|
|
4. **Audio alerts** (Layer 4) - Web Audio API beeps
|
|
|
|
In addition, there are **CSS-based tab alert animations** that are independent of NotificationManager and driven by the pending hooks state machine.
|
|
|
|
---
|
|
|
|
## 1. Document Title Blinking
|
|
|
|
### Location
|
|
`src/web/public/app.js` lines 1082-1104
|
|
|
|
### Mechanism
|
|
The title blink uses `setInterval` to toggle `document.title` between two states every 1500ms:
|
|
|
|
```js
|
|
// Constants (line 14)
|
|
const TITLE_FLASH_INTERVAL_MS = 1500;
|
|
|
|
// updateTabTitle() - line 1082
|
|
updateTabTitle() {
|
|
if (this.unreadCount > 0 && !this.isTabVisible) {
|
|
if (!this.titleFlashInterval) {
|
|
this.titleFlashInterval = setInterval(() => {
|
|
this.titleFlashState = !this.titleFlashState;
|
|
document.title = this.titleFlashState
|
|
? `\u26A0\uFE0F (${this.unreadCount}) Codeman`
|
|
: this.originalTitle;
|
|
}, TITLE_FLASH_INTERVAL_MS);
|
|
// Set immediately
|
|
document.title = `\u26A0\uFE0F (${this.unreadCount}) Codeman`;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The title alternates between:
|
|
- Warning emoji + unread count: `"(3) Codeman"`
|
|
- Original title: `"Codeman"`
|
|
|
|
### What Triggers It
|
|
Title flashing starts when `notify()` is called **while the tab is not visible** (`!this.isTabVisible`). The check is at lines 1026-1028:
|
|
|
|
```js
|
|
if (!this.isTabVisible) {
|
|
this.updateTabTitle();
|
|
}
|
|
```
|
|
|
|
Every event that calls `notificationManager.notify()` can trigger title blinking. This includes:
|
|
- `session:error` - Session errors (critical)
|
|
- `session:exit` - Unexpected exits with non-zero codes (critical)
|
|
- `session:idle` - Stuck detection after threshold (warning)
|
|
- `hook:idle_prompt` - Claude waiting for input (warning)
|
|
- `hook:permission_prompt` - Tool approval needed (critical)
|
|
- `hook:elicitation_dialog` - Claude asking a question (critical)
|
|
- `hook:stop` - Response complete (info)
|
|
- `respawn:blocked` - Respawn blocked (critical)
|
|
- `respawn:autoAcceptSent` - Plan accepted (info)
|
|
- `session:autoClear` - Auto-cleared context (info)
|
|
- `session:ralphCompletionDetected` - Loop complete (warning)
|
|
- `session:circuitBreakerUpdate` (when OPEN) - Critical
|
|
- `session:exitGateMet` - Exit gate met (warning)
|
|
- Various Ralph/fix-plan operations
|
|
|
|
### What Stops It
|
|
Title flashing stops via `stopTitleFlash()` (lines 1097-1104):
|
|
|
|
```js
|
|
stopTitleFlash() {
|
|
if (this.titleFlashInterval) {
|
|
clearInterval(this.titleFlashInterval);
|
|
this.titleFlashInterval = null;
|
|
this.titleFlashState = false;
|
|
document.title = this.originalTitle;
|
|
}
|
|
}
|
|
```
|
|
|
|
Called from:
|
|
1. **`onTabVisible()`** (line 1242) - When tab becomes visible again
|
|
2. **`markAllRead()`** (line 1218) - When user marks all notifications read
|
|
3. **`clearAll()`** (line 1226) - When user clears all notifications
|
|
4. **`handleInit()` cleanup** (lines 3294-3298) - On SSE reconnect
|
|
|
|
### Interval Safety
|
|
The interval guard (`if (!this.titleFlashInterval)`) at line 1084 prevents stacking - only one interval can exist at a time. New notifications while blinking update the `unreadCount` displayed but don't create additional intervals. This is **correct and safe**.
|
|
|
|
### Memory Leak Risk: LOW
|
|
The interval is properly cleaned up in:
|
|
- `onTabVisible()` - on every tab return
|
|
- `handleInit()` - on SSE reconnect (lines 3294-3298)
|
|
- `markAllRead()` and `clearAll()` - user actions
|
|
|
|
The `handleInit()` cleanup is particularly important because SSE reconnects reset all state. The explicit cleanup at lines 3294-3298 prevents orphaned intervals:
|
|
|
|
```js
|
|
// Clear notification manager title flash interval to prevent memory leak
|
|
if (this.notificationManager?.titleFlashInterval) {
|
|
clearInterval(this.notificationManager.titleFlashInterval);
|
|
this.notificationManager.titleFlashInterval = null;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Favicon Changes
|
|
|
|
### Current State: NO dynamic favicon changes
|
|
|
|
The favicon is defined as an **inline SVG data URI** in `index.html` line 9:
|
|
|
|
```html
|
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,...">
|
|
```
|
|
|
|
It shows a lightning bolt icon on a dark background. The favicon is **static** and never changes programmatically.
|
|
|
|
Browser notifications reference `/favicon.ico` as their icon (line 1130):
|
|
```js
|
|
const notif = new Notification(`Codeman: ${title}`, {
|
|
body,
|
|
tag,
|
|
icon: '/favicon.ico',
|
|
silent: true,
|
|
});
|
|
```
|
|
|
|
This is for the OS notification popup icon, not the browser tab favicon. There is no code that manipulates `link[rel="icon"]` or swaps the favicon for attention.
|
|
|
|
---
|
|
|
|
## 3. CSS Animations for Attention
|
|
|
|
### 3.1 Tab Alert Animations (styles.css lines 328-345)
|
|
|
|
Two CSS animation classes are applied to session tabs:
|
|
|
|
```css
|
|
/* Red blinking tab - for action-required hooks */
|
|
.session-tab.tab-alert-action {
|
|
animation: tab-blink-red 2.5s ease-in-out infinite;
|
|
}
|
|
|
|
/* Yellow blinking tab - for idle hooks */
|
|
.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); }
|
|
}
|
|
```
|
|
|
|
- **Red blink (2.5s cycle)**: Permission prompts, elicitation dialogs - requires user action
|
|
- **Yellow blink (3.5s cycle)**: Idle prompt - Claude waiting for input
|
|
|
|
These are **CSS-only infinite animations** with no JavaScript timer overhead. They are performant and have zero memory leak risk.
|
|
|
|
### 3.2 Tab Switch Glow (styles.css lines 241-251)
|
|
|
|
```css
|
|
.session-tab.tab-glow {
|
|
animation: tab-glow 0.35s ease-out forwards;
|
|
}
|
|
```
|
|
|
|
A one-shot green glow burst when switching tabs. Applied in `selectSession()` (app.js line 3985) and cleaned up via `animationend` event with `{ once: true }`:
|
|
|
|
```js
|
|
activeTab.classList.add('tab-glow');
|
|
activeTab.addEventListener('animationend', () => activeTab.classList.remove('tab-glow'), { once: true });
|
|
```
|
|
|
|
This is **safe** - `{ once: true }` auto-removes the listener.
|
|
|
|
### 3.3 Notification Badge Pulse (styles.css lines 3978-4000)
|
|
|
|
```css
|
|
.notification-badge {
|
|
animation: notif-badge-pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes notif-badge-pulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.15); }
|
|
}
|
|
```
|
|
|
|
The red notification count badge in the header bell icon pulses continuously when visible. Pure CSS, no memory concerns.
|
|
|
|
### 3.4 Status Dot Pulse (styles.css lines 261-265, 347-350)
|
|
|
|
```css
|
|
.session-tab .tab-status.busy {
|
|
background: var(--green);
|
|
animation: pulse 1.5s infinite;
|
|
will-change: opacity;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
```
|
|
|
|
The green status dot pulses when a session is busy/working. Pure CSS.
|
|
|
|
### 3.5 Connection Status Animations (styles.css lines 405-413)
|
|
|
|
```css
|
|
.connection-dot.warning {
|
|
animation: connection-pulse 1.5s ease-in-out infinite;
|
|
}
|
|
.connection-dot.error {
|
|
animation: connection-pulse 0.8s ease-in-out infinite;
|
|
}
|
|
```
|
|
|
|
Connection indicator pulses differently for warning vs error states.
|
|
|
|
### 3.6 Other CSS Animations
|
|
|
|
| Animation | Location | Purpose |
|
|
|-----------|----------|---------|
|
|
| `respawn-blocked-pulse` | styles.css:798 | Respawn blocked indicator |
|
|
| `pulse-hook` | styles.css:899 | Hook event indicator |
|
|
| `ralph-pulse` | styles.css:1013 | Ralph tracker active state |
|
|
| `circuit-breaker-pulse` | styles.css:1057 | Circuit breaker warning |
|
|
| `wizard-pulse` | styles.css:5541 | Ralph wizard active indicator |
|
|
| `plan-subagent-pulse` | styles.css:5558 | Plan subagent active |
|
|
| `notif-slide-in` | styles.css:4088 | Notification drawer slide |
|
|
|
|
### 3.7 Mobile-Specific Animations (mobile.css)
|
|
|
|
Mobile CSS is minimal for animations:
|
|
- `slideUp` (line 1091) - Mobile toolbar slide animation
|
|
- `caseModalSlideUp` (line 1204) - Case modal bottom-sheet animation
|
|
|
|
No mobile-specific blinking or notification animations exist.
|
|
|
|
---
|
|
|
|
## 4. Tab Visibility API
|
|
|
|
### Implementation (app.js lines 881-893)
|
|
|
|
The `NotificationManager` constructor sets up two visibility listeners:
|
|
|
|
```js
|
|
// Standard visibility change
|
|
document.addEventListener('visibilitychange', () => {
|
|
this.isTabVisible = !document.hidden;
|
|
if (this.isTabVisible) {
|
|
this.onTabVisible();
|
|
}
|
|
});
|
|
|
|
// iOS Safari: pageshow fires on back-forward cache restore (bfcache)
|
|
window.addEventListener('pageshow', (e) => {
|
|
if (e.persisted) {
|
|
this.isTabVisible = true;
|
|
this.onTabVisible();
|
|
}
|
|
});
|
|
```
|
|
|
|
### Behavior When Tab Hidden
|
|
- `isTabVisible` set to `false`
|
|
- New notifications trigger `updateTabTitle()` which starts the title blink interval
|
|
- Browser notifications are sent (subject to per-event preferences)
|
|
|
|
### Behavior When Tab Becomes Visible (`onTabVisible()` - line 1241)
|
|
```js
|
|
onTabVisible() {
|
|
this.stopTitleFlash(); // Stop title blinking
|
|
if (this.isDrawerOpen) {
|
|
this.markAllRead(); // Mark all read if drawer is open
|
|
}
|
|
// Re-fit terminal dimensions
|
|
if (this.app?.fitAddon && this.app?.activeSessionId) {
|
|
this.app.fitAddon.fit();
|
|
this.app.sendResize(this.app.activeSessionId);
|
|
}
|
|
}
|
|
```
|
|
|
|
Key detail: Title flash always stops when tab becomes visible, but **unread count is NOT reset** unless the notification drawer is open. This means the badge count persists until the user interacts with it.
|
|
|
|
---
|
|
|
|
## 5. Focus/Blur Handling
|
|
|
|
### No window focus/blur listeners
|
|
Codeman does **not** use `window.addEventListener('focus')` or `window.addEventListener('blur')`. It relies solely on the Page Visibility API (`visibilitychange` + `pageshow`).
|
|
|
|
This is the correct modern approach. The `focus`/`blur` events are unreliable (fire for devtools, iframe changes, etc.) while `visibilitychange` accurately reflects whether the user can see the tab.
|
|
|
|
### Other focus-related listeners
|
|
- `document.addEventListener('focusin')` (line 238) - Mobile keyboard handler for scrolling inputs into view
|
|
- Various `element.focus()` calls for modal focus trapping (`FocusTrap` class, line 742)
|
|
- `window.focus()` in browser notification click handler (line 1135) to bring window to front
|
|
|
|
None of these are related to notification/blinking behavior.
|
|
|
|
---
|
|
|
|
## 6. Multiple Notification Stacking
|
|
|
|
### Can intervals stack? NO
|
|
|
|
The `updateTabTitle()` method has a guard (line 1084):
|
|
|
|
```js
|
|
if (!this.titleFlashInterval) {
|
|
this.titleFlashInterval = setInterval(() => { ... }, TITLE_FLASH_INTERVAL_MS);
|
|
}
|
|
```
|
|
|
|
Only one interval is ever created. Subsequent notifications while the tab is hidden simply update `this.unreadCount` which is read by the existing interval callback. The displayed count stays current without creating new intervals.
|
|
|
|
### Notification Grouping
|
|
|
|
Notifications within the same category + session within 5 seconds are **grouped** instead of creating new entries (lines 986-997):
|
|
|
|
```js
|
|
const groupKey = `${category}:${sessionId || 'global'}`;
|
|
const existing = this.groupingMap.get(groupKey);
|
|
if (existing) {
|
|
existing.notification.count = (existing.notification.count || 1) + 1;
|
|
existing.notification.message = message;
|
|
existing.notification.timestamp = Date.now();
|
|
clearTimeout(existing.timeout);
|
|
existing.timeout = setTimeout(() => this.groupingMap.delete(groupKey), GROUPING_TIMEOUT_MS);
|
|
this.scheduleRender();
|
|
return; // <-- Early return prevents duplicate badge/title/browser/audio triggers
|
|
}
|
|
```
|
|
|
|
The grouping early return prevents:
|
|
- Duplicate badge increments
|
|
- Duplicate title updates
|
|
- Duplicate browser notifications
|
|
- Duplicate audio alerts
|
|
|
|
### Browser Notification Rate Limiting
|
|
|
|
Even without grouping, browser notifications are rate-limited to 1 per 3 seconds (lines 1122-1125):
|
|
|
|
```js
|
|
const now = Date.now();
|
|
if (now - this.lastBrowserNotifTime < 3000) return;
|
|
this.lastBrowserNotifTime = now;
|
|
```
|
|
|
|
### Notification List Cap
|
|
|
|
The notification list is capped at 100 entries (line 1014):
|
|
```js
|
|
if (this.notifications.length > 100) this.notifications.pop();
|
|
```
|
|
|
|
### Grouping Timeout Cleanup
|
|
|
|
Grouping map entries self-clean after 5 seconds via `setTimeout`. These timeouts are also explicitly cleaned up in `handleInit()` (lines 3299-3304):
|
|
|
|
```js
|
|
if (this.notificationManager?.groupingMap) {
|
|
for (const { timeout } of this.notificationManager.groupingMap.values()) {
|
|
clearTimeout(timeout);
|
|
}
|
|
this.notificationManager.groupingMap.clear();
|
|
}
|
|
```
|
|
|
|
### Rapid Event Scenario
|
|
|
|
If 50 events fire while the tab is hidden:
|
|
1. First event: creates notification, starts title flash, sends browser notif
|
|
2. Events 2-N within 5s of same category+session: grouped (count increments, no new intervals)
|
|
3. Events of different categories: new notifications, but title flash interval is singular
|
|
4. Browser notifications: only 1 per 3s gets through
|
|
|
|
**Verdict**: Well-protected against stacking/compounding.
|
|
|
|
---
|
|
|
|
## 7. Team Agent Blinking
|
|
|
|
### Current State: NO team-specific blinking
|
|
|
|
Searching for `team:` SSE event listeners finds **none**. There are no `addListener('team:...')` handlers in app.js.
|
|
|
|
Team agent data (teammates, tasks, colors) is tracked via:
|
|
- `this.teammateMap` (Map of agent info)
|
|
- `this.teammatePanesByName` (Map of pane targets)
|
|
- `this.teammateTerminals` (Map of terminal instances)
|
|
|
|
But these are populated from **subagent data**, not dedicated team events. Teammates appear as standard subagents and are detected by `subagent-watcher.ts` (as noted in MEMORY.md: "Teammates appear as standard subagents").
|
|
|
|
### What team events could trigger blinking?
|
|
|
|
Currently, subagent events have their own notification category:
|
|
```js
|
|
// Default event type preferences (line 906-907)
|
|
subagent_spawn: { enabled: false, browser: false, audio: false },
|
|
subagent_complete: { enabled: false, browser: false, audio: false },
|
|
```
|
|
|
|
Both are **disabled by default**. Even if enabled, they go through the standard `notify()` path which would trigger title blinking only when the tab is hidden.
|
|
|
|
### Should team events trigger blinking?
|
|
|
|
The MEMORY.md implementation priority notes:
|
|
> 1. Team-aware idle detection (prevent premature respawn)
|
|
|
|
There is no `TeammateIdle` or `TaskCompleted` hook handler in the frontend. The hooks are mentioned in MEMORY.md as valid settings schema keys but have no frontend implementation yet.
|
|
|
|
If/when team hooks are implemented, they should:
|
|
- Potentially trigger tab-alert-action (red blink) for teammate stuck/blocked states
|
|
- Use the notification system for teammate task completions
|
|
- Consider a new notification category (e.g., `teammate_idle`, `team_task_complete`) with configurable per-event preferences
|
|
|
|
---
|
|
|
|
## 8. Cleanup Analysis
|
|
|
|
### All Interval/Timeout Cleanup Points
|
|
|
|
| Timer | Created | Cleared | Risk |
|
|
|-------|---------|---------|------|
|
|
| `titleFlashInterval` | `updateTabTitle()` L1085 | `stopTitleFlash()` L1099, `onTabVisible()` L1242, `handleInit()` L3296 | LOW - guarded + multi-path cleanup |
|
|
| Grouping timeouts | `notify()` L994/L1017 | Self-expire 5s, `handleInit()` L3301 | LOW - TTL + explicit cleanup |
|
|
| Browser notif auto-close | `sendBrowserNotif()` L1143 | Self-expire 8s (via `setTimeout`) | NONE - fires once |
|
|
| Audio oscillator | `playAudioAlert()` L1178 | Self-stops 0.15s | NONE - Web Audio manages it |
|
|
| Idle timer per session | `session:idle` handler L2384 | `session:working` L2415, `handleInit()` L3269, `removeSession()` L4138 | LOW - cleaned in all paths |
|
|
|
|
### handleInit Cleanup (SSE Reconnect)
|
|
|
|
The `handleInit()` method (called on every SSE reconnect) performs comprehensive cleanup (lines 3260-3321):
|
|
|
|
1. Clears all Maps (sessions, ralphStates, terminalBuffers, etc.)
|
|
2. Clears all idle timers
|
|
3. Clears flicker filter state
|
|
4. Clears pending terminal writes
|
|
5. Clears pending hooks and tab alerts
|
|
6. **Clears notification title flash interval** (L3295-3298)
|
|
7. **Clears notification grouping timeouts** (L3300-3304)
|
|
8. Disconnects terminal resize observer
|
|
9. Clears plan loading timers
|
|
10. Clears countdown intervals
|
|
11. Clears run summary auto-refresh timer
|
|
|
|
### removeSession Cleanup (line 4120-4139)
|
|
|
|
When a session is removed:
|
|
- `pendingHooks.delete(sessionId)` (L4128)
|
|
- `tabAlerts.delete(sessionId)` (L4129)
|
|
- Idle timer cleared (L4136-4139)
|
|
- All floating windows closed
|
|
|
|
### Browser Notification Auto-Close
|
|
|
|
The `setTimeout(() => notif.close(), 8000)` at line 1143 creates an anonymous closure over the `notif` variable. This is safe because:
|
|
1. The timeout fires once and is garbage collected
|
|
2. `notif.close()` is idempotent
|
|
3. 8 seconds is short enough to not accumulate
|
|
|
|
### Edge Case: AudioContext
|
|
|
|
The `AudioContext` (line 1167) is created once and reused:
|
|
```js
|
|
if (!this.audioCtx) {
|
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
```
|
|
|
|
This is never explicitly closed, but `AudioContext` is lightweight when idle and the singleton pattern prevents accumulation. Not a practical concern.
|
|
|
|
---
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
notify() called
|
|
|
|
|
+-----------+-----------+
|
|
| |
|
|
enabled check preferences check
|
|
| |
|
|
[Layer 1: Drawer] [Layer 2: Title Flash]
|
|
- Add to notifications[] - Only if tab hidden
|
|
- Cap at 100 - Single interval guard
|
|
- Update badge count - Toggles every 1500ms
|
|
- requestAnimationFrame
|
|
| |
|
|
[Layer 3: Browser Notif] [Layer 4: Audio]
|
|
- Per-event prefs - Per-event prefs
|
|
- Rate limit 3s - Web Audio API
|
|
- Auto-close 8s - 0.15s beep
|
|
- Permission check - Singleton AudioContext
|
|
|
|
INDEPENDENT:
|
|
[Tab CSS Alerts]
|
|
- Driven by pendingHooks state machine
|
|
- tab-alert-action (red, 2.5s cycle)
|
|
- tab-alert-idle (yellow, 3.5s cycle)
|
|
- Pure CSS animation, no JS timers
|
|
```
|
|
|
|
---
|
|
|
|
## Summary of Findings
|
|
|
|
| Aspect | Status | Notes |
|
|
|--------|--------|-------|
|
|
| Title blink interval safety | SAFE | Guard prevents stacking; 3-path cleanup |
|
|
| Favicon changes | NOT IMPLEMENTED | Static inline SVG; no dynamic swapping |
|
|
| CSS tab animations | SAFE | Pure CSS infinite animations; no JS timer cost |
|
|
| Visibility API usage | CORRECT | `visibilitychange` + `pageshow` (bfcache) |
|
|
| Focus/blur handling | NOT USED (correct) | Relies on Visibility API instead |
|
|
| Notification stacking | WELL PROTECTED | Grouping, rate limiting, single interval |
|
|
| Team agent blinking | NOT IMPLEMENTED | No `team:` SSE handlers; teammates use subagent path |
|
|
| Interval cleanup | COMPREHENSIVE | `handleInit`, `onTabVisible`, `removeSession`, `markAllRead`, `clearAll` |
|
|
| Memory leak risk | LOW | All timers have explicit cleanup paths |
|
|
|
|
### Potential Improvements
|
|
|
|
1. **Dynamic favicon**: Could swap favicon to a red/orange variant when there are unread critical notifications (common pattern in web apps).
|
|
|
|
2. **Team agent notifications**: When `TeammateIdle` and `TaskCompleted` hooks are implemented, add dedicated notification categories with configurable preferences in the per-event settings grid.
|
|
|
|
3. **Unread count on tab return**: Currently, returning to the tab stops the title flash but does NOT reset the unread count. The user must open the drawer or click individual notifications. Consider auto-marking as read after a brief delay when the tab becomes visible.
|
|
|
|
4. **Notification sound variety**: Currently all audio alerts use the same 660Hz sine wave. Different categories could use different tones (e.g., lower pitch for info, higher for critical).
|