← Back to PRs

#19828: feat: reply notifications for macOS and web UI

by fal3 open 2026-02-18 06:45 View on GitHub →
app: macos app: web-ui size: L
## 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 ![2026-02-18 14 29 22](https://github.com/user-attachments/assets/536f224f-2b56-4fc0-a7f1-0bef6efa4e86) <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(&quot;chat&quot;, {state:&quot;final&quot;})"| 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 === &quot;final&quot;"| 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