#19828: feat: reply notifications for macOS and web UI
app: macos
app: web-ui
size: L
Cluster:
macOS Notification and Menu Fixes
## Summary
Fire native notifications when the agent finishes a reply and the user isn't looking at it — macOS menu bar panel hidden, or browser tab backgrounded.
### Demo

<img width="374" height="779" alt="image" src="https://github.com/user-attachments/assets/b0c72119-7744-4ab2-8de4-c5d826ef9e0b" />
### Files changed (12 production + tests)
| File | Role |
|---|---|
| `ReplyNotificationObserver.swift` | Core observer — subscribes to gateway events, fires `NotificationManager.send` on `state:"final"` |
| `AppState.swift` | `replyNotificationsEnabled` persisted preference (UserDefaults) |
| `Constants.swift` | `replyNotificationsEnabledKey` |
| `MenuBar.swift` | Wires observer `start()`/`stop()`/`setPanelVisible()` into app lifecycle |
| `MenuContentView.swift` | Toggle in menu bar settings pane |
| `NotificationManager.swift` | Bundle ID guard — prevents crash in contexts without a bundle identifier |
| `PermissionManager.swift` | Bundle ID guard — same safety for permission checks |
| `reply-notifications.ts` | Browser Notification API module (permission, visibility, enable/disable) |
| `app-gateway.ts` | Hooks `notifyReplyComplete()` when gateway pushes `state:"final"` |
| `app-lifecycle.ts` | Requests browser notification permission on connect |
### Architecture
```mermaid
flowchart TD
subgraph Gateway
GW[Gateway server]
GW -->|"broadcast("chat", {state:"final"})"| WS[WebSocket]
end
subgraph macOS["macOS app"]
WS -->|push event| GC[GatewayConnection]
GC -->|subscribe stream| RNO[ReplyNotificationObserver]
RNO -->|"panel hidden?\nenabled?"| NM[NotificationManager]
NM -->|UNUserNotificationCenter| SYS_MAC[macOS notification]
MB[MenuBar] -->|"start / stop\nsetPanelVisible"| RNO
MCV[MenuContentView] -->|toggle| AS[AppState]
AS -->|replyNotificationsEnabled| RNO
end
subgraph Web["Web UI"]
WS -->|SSE / WS event| AG[app-gateway.ts]
AG -->|"state === "final""| RN[reply-notifications.ts]
RN -->|"tab hidden?\npermission granted?"| SYS_WEB[Browser notification]
AL[app-lifecycle.ts] -->|requestPermission| RN
end
style GW fill:#2d2d2d,stroke:#666,color:#fff
style RNO fill:#1a5276,stroke:#2980b9,color:#fff
style RN fill:#1a5276,stroke:#2980b9,color:#fff
style SYS_MAC fill:#0e6655,stroke:#1abc9c,color:#fff
style SYS_WEB fill:#0e6655,stroke:#1abc9c,color:#fff
```
## Test plan
- [x] Swift tests for `extractPreview` edge cases and `AppState.replyNotificationsEnabled` persistence
- [x] Vitest tests for `reply-notifications.ts` — permission, visibility, enable/disable, truncation, auto-close, click
- [ ] Build macOS app, send a message, close panel → verify notification
- [ ] Toggle off in menu bar settings → verify no notification fires
- [ ] Open web UI, background the tab, send a message → verify browser notification
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Adds native notification support for reply completion on both macOS (via `UNUserNotificationCenter`) and web (via Browser Notification API). Notifications fire only when the user isn't actively looking at the conversation — macOS menu bar panel hidden, or browser tab backgrounded.
- **macOS**: New `ReplyNotificationObserver` singleton subscribes to gateway chat events, checks for `state:"final"`, and dispatches notifications through the existing `NotificationManager`. Includes a toggle persisted in `UserDefaults`, panel visibility tracking, run-ID deduplication, and markdown stripping for preview text. Bundle ID guards added to `NotificationManager` and `PermissionManager` prevent crashes in test/CLI contexts.
- **Web**: New `reply-notifications.ts` module wraps the Browser Notification API with permission management, visibility checks, auto-close (5s), and click-to-focus. Integrated into `app-gateway.ts` on `state:"final"` events, with early permission request in `app-lifecycle.ts`.
- Both platforms default to enabled, with the macOS side offering a user-facing toggle and the web side controlled by a module-level flag (no UI toggle exposed yet).
- Comprehensive test coverage on both sides: Swift tests for payload parsing, preview extraction, markdown stripping, and AppState persistence; Vitest tests for permission states, visibility, truncation, auto-close, and click handling.
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge — the changes are additive with no modifications to existing logic paths.
- Clean, well-tested feature addition that follows existing codebase patterns closely. All new code is additive (no existing behavior modified). The macOS observer properly handles concurrency with MainActor isolation and weak self. The web implementation correctly guards against missing Notification API and visibility states. One minor concern: the Set-based runId deduplication cache evicts randomly rather than by age, which could theoretically cause a rare duplicate notification. Tests are thorough on both platforms.
- apps/macos/Sources/OpenClaw/ReplyNotificationObserver.swift deserves a second look at the notifiedRunIds eviction strategy (lines 86-88).
<sub>Last reviewed commit: 750311e</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#15909: Guard notifications on macOS; fix focus issue and build fixes
by jasonkneen · 2026-02-14
79.2%
#22458: Codex/macos chat corner clip
by apethree · 2026-02-21
72.4%
#9926: fix(macos): guard UNUserNotificationCenter when no bundle identifier
by webcpu · 2026-02-05
72.3%
#3474: fix(macos): menu bar activity badge not showing during agent work
by elektricM · 2026-01-28
71.2%
#22329: Android : Add Notification Intelligence -- cross-app AI triage
by VikrantSingh01 · 2026-02-21
70.9%
#23636: iOS: normalize watch quick actions and fix test signing
by mbelinky · 2026-02-22
70.8%
#10898: fix(mac): adopt canonical session key and add reset triggers
by Nachx639 · 2026-02-07
70.7%
#22454: fix(macos): add re-subscribe loop to gateway stream subscribers
by mandofever78 · 2026-02-21
70.6%
#12157: feat(macos): add Granola-style meeting notes with live transcription
by npow · 2026-02-08
70.4%
#8260: fix(macOS): gateway readiness detection + reversible Configure later
by xksteven · 2026-02-03
70.4%