Files
Innei ac000cc0a6 ✨ feat(desktop): add chat completion sound settings (#19280)
* ✨ feat(desktop): add chat completion sound settings

Split completion feedback by window focus so the two sound sources can never
overlap: the foreground plays an in-app chime (off by default, own volume and
importable audio), while the background leaves the sound to the notification
banner. The main process owns the focus decision and the renderer only plays
when the banner was skipped or refused.

The banner can carry either the macOS default sound or LobeHub's own chime.
UNNotificationSound resolves a name only against ~/Library/Sounds and only in
AIFF, so the chime is installed there on demand rather than bundled.

Claude-Session: https://claude.ai/code/session_01SaZzuGjGHqADBxhWn4HjUa

* ✅ test: mock completion sound in agent run specs
2026-09-08 22:31:51 +08:00
..
…

@lobechat/electron-mac-notifications

Native N-API addon that posts macOS notifications through UNUserNotificationCenter, styling them as communication notifications (sender avatar as the primary icon, via INSendMessageIntent) when a sender is provided. Consumed by the desktop main process; falls back gracefully everywhere it can't run.

Build

pnpm --filter @lobechat/electron-mac-notifications build:native

No-op on non-darwin platforms. The desktop packaging pipeline runs this automatically in beforePack.

Runtime requirements (all empirically verified)

The avatar treatment only renders when the host app satisfies all of:

  1. Signed with a real certificate and launched as a regular app. Adhoc-signed dev Electron gets UNErrorDomain Code=1 from the notification center, and this package then reports isSupported() but fails at show-time — callers fall back to Electron's Notification.
  2. com.apple.developer.usernotifications.communication entitlement in the signature. Never add it without requirement 3: launchd refuses to spawn the app (POSIX 163).
  3. An Apple provisioning profile authorizing the entitlement, embedded at Contents/embedded.provisionprofile. Wired through MAC_PROVISIONING_PROFILE + APPLE_TEAM_ID in electron-builder.mjs.
  4. NSUserActivityTypes = [INSendMessageIntent] in Info.plist (set unconditionally via mac.extendInfo).

Without 2–3 the notification still shows as a plain banner — macOS silently drops the communication styling — so profile-less channels keep working.

Delegate ownership

The addon takes over the process-global UNUserNotificationCenter delegate. Notifications whose identifier lacks the lobehub- prefix are forwarded to whatever delegate was installed before (Electron's own Notification module), so the two paths can coexist during fallback.