Skip to content

feat(notifications): record a custom completion sound#2783

Draft
andrewm4894 wants to merge 2 commits into
mainfrom
posthog-code/custom-notification-sound
Draft

feat(notifications): record a custom completion sound#2783
andrewm4894 wants to merge 2 commits into
mainfrom
posthog-code/custom-notification-sound

Conversation

@andrewm4894

Copy link
Copy Markdown
Member

Problem

The notification sound that plays when the agent finishes a task or needs your input is limited to a fixed list of built-in sounds. Users wanted to add their own — ideally recording one on the fly right in the app.

Changes

Adds a Custom (record) option to the Sound effect picker in General settings. Selecting it reveals a small recorder that captures up to 5 seconds from the microphone via MediaRecorder, stores it as a data URL in the persisted settings store, and plays it like any other completion sound (Test button, volume slider, and the OS-notification silencing all work with it).

  • playCompletionSound takes an optional custom data URL and plays it when the sound is "custom".
  • New CustomSoundRecorder component: Record / Re-record / Stop / Clear, with mic-permission error handling.
  • Threads customCompletionSound through the notification settings provider so the agent-completion/permission notifications use it.
  • macOS: adds the audio-input entitlement and NSMicrophoneUsageDescription so recording works in the packaged, sandboxed app.

How did you test this?

  • pnpm --filter @posthog/ui exec vitest run src/features/notifications/notifications.test.ts — 14 pass, including new cases for the custom sound (plays the recorded URL + silences the OS notification; not silenced when nothing is recorded yet).
  • Typechecked the changed files and ran Biome on all touched files (clean).
  • Did not manually drive the packaged app / live mic recording.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code from a Slack thread

Add a "Custom (record)" option to the notification sound picker so users
can record their own sound (up to 5s) from the microphone in-app via
MediaRecorder. The recording is stored as a data URL in the persisted
settings store and played when the agent finishes a task or needs input.

Adds the macOS microphone entitlement and NSMicrophoneUsageDescription so
recording works in the packaged, sandboxed app.

Generated-By: PostHog Code
Task-Id: 1d32a904-f0b9-4439-9900-d24d12be04a8
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/settings/sections/CustomSoundRecorder.tsx

Reviewed by React Doctor for commit 44903ce.

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/ui/src/features/notifications/notifications.test.ts:159-187
**New tests should be parameterized**

The two new `custom` sound tests check the same scenario (custom sound selected + `notifyPromptComplete` + `silent` flag outcome) differing only in whether `customCompletionSound` is set. Per the team's review standard, these should be expressed as a single `it.each` entry alongside the existing parameterised block. Keeping them as separate prose tests means a third case (e.g. `"custom"` with an empty string) would require another standalone block.

### Issue 2 of 3
packages/ui/src/features/settings/sections/GeneralSettings.tsx:417-419
**Volume slider appears active when no custom recording exists**

When `completionSound === "custom"` but no recording has been made yet, `completionSound !== "none"` is true so the volume slider is rendered — but no sound will actually play (the Test button is correctly hidden via `canTestSound`). This presents a functioning-looking control that has no audible effect. The slider condition should mirror `canTestSound` (or at least exclude the `custom`+no-recording state) to avoid the inconsistency.

### Issue 3 of 3
packages/ui/src/features/settings/sections/CustomSoundRecorder.tsx:56-60
**Silent failure when `FileReader` errors**

`onloadend` fires for both successful reads and read errors. When a read error occurs `reader.result` is `null`, so the branch `typeof reader.result === "string" ? reader.result : null` quietly calls `setCustomCompletionSound(null)` — effectively silently discarding what was just recorded with no feedback. Adding a `reader.onerror` handler (or checking `reader.error` inside `onloadend`) and showing a toast would give users visibility into a recording that failed to save.

Reviews (1): Last reviewed commit: "feat(notifications): let users record a ..." | Re-trigger Greptile

Comment on lines +159 to +187
it("plays a recorded custom sound and silences the OS notification", () => {
const { service, notify, play } = makeService({
hasFocus: false,
settings: {
completionSound: "custom",
customCompletionSound: "data:audio/webm;base64,AAAA",
},
});
service.notifyPromptComplete("My task", "end_turn", TASK_ID);
expect(play).toHaveBeenCalledWith(
"custom",
80,
"data:audio/webm;base64,AAAA",
);
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ silent: true }),
);
});

it("is not silent when custom is selected but nothing was recorded", () => {
const { service, notify } = makeService({
hasFocus: false,
settings: { completionSound: "custom", customCompletionSound: null },
});
service.notifyPromptComplete("My task", "end_turn", TASK_ID);
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ silent: false }),
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New tests should be parameterized

The two new custom sound tests check the same scenario (custom sound selected + notifyPromptComplete + silent flag outcome) differing only in whether customCompletionSound is set. Per the team's review standard, these should be expressed as a single it.each entry alongside the existing parameterised block. Keeping them as separate prose tests means a third case (e.g. "custom" with an empty string) would require another standalone block.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/notifications/notifications.test.ts
Line: 159-187

Comment:
**New tests should be parameterized**

The two new `custom` sound tests check the same scenario (custom sound selected + `notifyPromptComplete` + `silent` flag outcome) differing only in whether `customCompletionSound` is set. Per the team's review standard, these should be expressed as a single `it.each` entry alongside the existing parameterised block. Keeping them as separate prose tests means a third case (e.g. `"custom"` with an empty string) would require another standalone block.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 417 to 419
{completionSound !== "none" && (
<SettingRow label="Sound volume" noBorder>
<Flex align="center" gap="3">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Volume slider appears active when no custom recording exists

When completionSound === "custom" but no recording has been made yet, completionSound !== "none" is true so the volume slider is rendered — but no sound will actually play (the Test button is correctly hidden via canTestSound). This presents a functioning-looking control that has no audible effect. The slider condition should mirror canTestSound (or at least exclude the custom+no-recording state) to avoid the inconsistency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/settings/sections/GeneralSettings.tsx
Line: 417-419

Comment:
**Volume slider appears active when no custom recording exists**

When `completionSound === "custom"` but no recording has been made yet, `completionSound !== "none"` is true so the volume slider is rendered — but no sound will actually play (the Test button is correctly hidden via `canTestSound`). This presents a functioning-looking control that has no audible effect. The slider condition should mirror `canTestSound` (or at least exclude the `custom`+no-recording state) to avoid the inconsistency.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +56 to +60
reader.onloadend = () => {
setCustomCompletionSound(
typeof reader.result === "string" ? reader.result : null,
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent failure when FileReader errors

onloadend fires for both successful reads and read errors. When a read error occurs reader.result is null, so the branch typeof reader.result === "string" ? reader.result : null quietly calls setCustomCompletionSound(null) — effectively silently discarding what was just recorded with no feedback. Adding a reader.onerror handler (or checking reader.error inside onloadend) and showing a toast would give users visibility into a recording that failed to save.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/settings/sections/CustomSoundRecorder.tsx
Line: 56-60

Comment:
**Silent failure when `FileReader` errors**

`onloadend` fires for both successful reads and read errors. When a read error occurs `reader.result` is `null`, so the branch `typeof reader.result === "string" ? reader.result : null` quietly calls `setCustomCompletionSound(null)` — effectively silently discarding what was just recorded with no feedback. Adding a `reader.onerror` handler (or checking `reader.error` inside `onloadend`) and showing a toast would give users visibility into a recording that failed to save.

How can I resolve this? If you propose a fix, please make it concise.

- Surface a toast when reading the recording fails instead of silently
  clearing it (split FileReader onloadend into onload/onerror).
- Hide the volume slider when "custom" is selected but nothing is recorded
  yet, mirroring the Test button's canTestSound gating.
- Parameterize the two custom-sound notification tests into a single
  it.each block.

Generated-By: PostHog Code
Task-Id: 1d32a904-f0b9-4439-9900-d24d12be04a8
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.

1 participant