Skip to content

Add custom desktop notification sounds#1430

Merged
nwparker merged 2 commits intostablyai:mainfrom
KayleeWilliams:KayleeWilliams/Starfish
May 5, 2026
Merged

Add custom desktop notification sounds#1430
nwparker merged 2 commits intostablyai:mainfrom
KayleeWilliams:KayleeWilliams/Starfish

Conversation

@KayleeWilliams
Copy link
Copy Markdown
Contributor

Summary

Adds one configurable custom desktop notification sound that applies to all delivered Orca desktop notifications: agent complete, terminal bell, and test notifications.

Changes

  • Adds notifications.customSoundPath with persistence defaults and migration-safe merging.
  • Adds an audio picker for common formats (ogg, mp3, wav, m4a, aac, flac).
  • Silences native Electron notifications when a custom sound is configured to avoid double playback.
  • Plays the selected sound through a hardened main/preload flow: main reads only the persisted custom sound path, validates absolute path, file type, regular file, and max size, then preload plays it as a Blob URL.
  • Updates Notifications settings with choose/change/clear controls and failure feedback for the test notification.

Validation

  • pnpm vitest run --config config/vitest.config.ts src/main/ipc/notifications.test.ts src/main/ipc/shell.test.ts src/main/persistence.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • pnpm exec tsgo --noEmit -p config/tsconfig.node.json
  • pnpm exec tsgo --noEmit -p config/tsconfig.tc.web.json
  • pnpm exec oxlint src/main/ipc/notifications.ts src/preload/index.ts src/renderer/src/lib/desktop-notification-sound.ts src/renderer/src/components/settings/NotificationsPane.tsx src/main/ipc/notifications.test.ts src/main/ipc/shell.ts src/main/ipc/shell.test.ts src/shared/types.ts src/shared/constants.ts
  • git diff --check

@KayleeWilliams KayleeWilliams marked this pull request as ready for review May 5, 2026 06:45
@nwparker
Copy link
Copy Markdown
Contributor

nwparker commented May 5, 2026

Reviewed for perf, correctness, and React hooks usage. Pushed one follow-up commit (442dbcae1a) addressing two real-world issues:

1. Per-notification 10MB IPC + disk read. Each playSound was invoking notifications:loadSound, which stat+readFiled the configured audio (up to 10MB) and structured-cloned it across the IPC bridge. For terminal-bell-heavy workflows that's significant overhead. The preload now caches the decoded blob URL keyed by the resolved sound path, and a new notifications:resolveSoundPath IPC method returns just the validated path so repeated dispatches with an unchanged sound skip the file read and transfer entirely. Cache is invalidated on path mismatch and on any error path.

2. Overlapping playback on bursts. Two concurrent triggers (e.g. agent-complete + terminal-bell on the same data chunk) each created an Audio element, causing them to stack. Added a 250ms dedupe window in the preload — well under the main process's 5-second cooldown so it only catches the genuinely-overlapping case. Returns a distinct 'deduped' reason so the renderer doesn't log it as a failure or surface a misleading error toast on the test button.

Other things I checked and consider fine:

  • No new useEffects — picker state uses useState correctly; no derived-state-via-effect anti-patterns.
  • silent: true on Electron Notification when a custom sound is set — correct on macOS; on Linux/Windows behavior depends on the notification daemon, but this is the documented Electron API.
  • Path validation in main (absolute, allowed extension, regular file, max 10MB) — reasonable guardrails, even though the file ends up loaded into the renderer that the user already trusts.
  • Cross-platform: no hardcoded path separators or platform-specific shortcuts touched.

Tests, tsgo (node + web), and oxlint all pass against the modified set.

@nwparker nwparker self-assigned this May 5, 2026
KayleeWilliams and others added 2 commits May 5, 2026 00:54
Avoids re-reading the configured audio file (up to 10MB) from disk and
re-transferring it over IPC on every notification. Adds a path-only
resolver so repeated dispatches with an unchanged sound skip the heavy
load entirely.

For burst handling, follows the VS Code AccessibilitySignalService /
GNOME canberra pattern: one shared HTMLAudioElement per sound, restarted
from t=0 on each play, with an in-flight guard that drops new plays
while the sound is still ringing. This self-dedupes by the sound's own
duration without any magic time constant — distinct sounds remain free
to overlap. The Test button passes force: true so an explicit user
action always plays through.

Co-authored-by: Orca <help@stably.ai>
@nwparker nwparker force-pushed the KayleeWilliams/Starfish branch from 442dbca to b1680fc Compare May 5, 2026 07:54
@nwparker
Copy link
Copy Markdown
Contributor

nwparker commented May 5, 2026

Updated b1680fc4dd after some research on how mature notification systems handle bursts. Replaced the time-window dedupe (250ms tunable I'd added) with the VS Code AccessibilitySignalService / GNOME canberra pattern: one shared HTMLAudioElement per sound, currentTime = 0 on every play, and an in-flight guard that drops new plays while the sound is still ringing.

Why this is better than my previous approach:

  • No magic constant — the dedup window is the sound's own duration, naturally. A 200ms ding self-dedupes for 200ms; a 1s sound for 1s.
  • Distinct sounds still overlap — keyed by path, so if Orca ever supports per-source sounds (agent vs bell), they'd be free to play simultaneously.
  • Test button uses force: true — explicit user actions always play through, even on rapid double-clicks.
  • Cited prior art: VS Code does exactly this in accessibilitySignalService.ts for terminal bell + accessibility signals; GNOME's libcanberra has the same per-event-id guard. Apple, Microsoft, and freedesktop don't formally specify burst behavior but each platform's notification daemon effectively coalesces.

Also rebased onto current main. Tests / tsgo (node + web) / oxlint all pass.

@nwparker nwparker merged commit f9120cb into stablyai:main May 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants