feat: play notification sound on agent response#707
Conversation
Play a short ascending two-tone beep (Web Audio API) when the agent completes a response, so users can switch away from the app and still know when an answer is ready. Changes: - Chat: add useEffect watching isLoading transitions, generates a brief sine-wave beep (880→1100 Hz, 0.2s) using AudioContext. No external audio files needed. Fails silently if audio is blocked.
Greptile SummaryAdds a Web Audio API notification beep that fires when
Confidence Score: 3/5Safe to merge as a non-breaking change, but the notification feature will silently stop working after a few responses and will fire for background sessions. Two issues exist in the new code path: the AudioContext is never closed, so after roughly 6 responses the browser instance limit is hit and the beep silently stops working; and the missing active guard means background sessions also trigger audio, which is almost certainly unintended behaviour. src/renderer/src/screens/Chat/Chat.tsx — the notification useEffect needs both an active guard and an osc.onended / ctx.close() call. Important Files Changed
Reviews (1): Last reviewed commit: "feat: play notification sound when agent..." | Re-trigger Greptile |
| try { | ||
| const ctx = new AudioContext(); | ||
| const osc = ctx.createOscillator(); | ||
| const gain = ctx.createGain(); | ||
| osc.connect(gain); | ||
| gain.connect(ctx.destination); | ||
| osc.type = "sine"; | ||
| // Play two quick ascending tones | ||
| osc.frequency.setValueAtTime(880, ctx.currentTime); | ||
| osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.08); | ||
| gain.gain.setValueAtTime(0.12, ctx.currentTime); | ||
| gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2); | ||
| osc.start(ctx.currentTime); | ||
| osc.stop(ctx.currentTime + 0.2); | ||
| } catch { | ||
| // AudioContext may fail in some environments — silently ignore | ||
| } |
There was a problem hiding this comment.
AudioContext is never closed — stops working after a few responses. Browsers cap the number of simultaneously-live
AudioContext instances (Chrome: ~6, Safari: lower). Each agent response allocates a new one that is never released, so after ~6 chats the constructor starts throwing and the sound silently stops working — exactly the regression users won't notice until it's been broken for a while.
| try { | |
| const ctx = new AudioContext(); | |
| const osc = ctx.createOscillator(); | |
| const gain = ctx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(ctx.destination); | |
| osc.type = "sine"; | |
| // Play two quick ascending tones | |
| osc.frequency.setValueAtTime(880, ctx.currentTime); | |
| osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.08); | |
| gain.gain.setValueAtTime(0.12, ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2); | |
| osc.start(ctx.currentTime); | |
| osc.stop(ctx.currentTime + 0.2); | |
| } catch { | |
| // AudioContext may fail in some environments — silently ignore | |
| } | |
| try { | |
| const ctx = new AudioContext(); | |
| const osc = ctx.createOscillator(); | |
| const gain = ctx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(ctx.destination); | |
| osc.type = "sine"; | |
| // Play two quick ascending tones | |
| osc.frequency.setValueAtTime(880, ctx.currentTime); | |
| osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.08); | |
| gain.gain.setValueAtTime(0.12, ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2); | |
| osc.start(ctx.currentTime); | |
| osc.stop(ctx.currentTime + 0.2); | |
| // Close the context once playback ends to avoid exhausting the browser limit | |
| osc.onended = () => { void ctx.close(); }; | |
| } catch { | |
| // AudioContext may fail in some environments — silently ignore | |
| } |
| // Play a notification sound when the agent finishes responding | ||
| const prevLoadingRef = useRef(isLoading); | ||
| useEffect(() => { | ||
| const wasLoading = prevLoadingRef.current; | ||
| prevLoadingRef.current = isLoading; | ||
| if (!wasLoading || isLoading) return; |
There was a problem hiding this comment.
Sound fires for every mounted
Chat instance, including background sessions. Every other audio/keyboard side effect in this component guards on active (see the Cmd+N handler, context-menu handlers, etc.), but the notification sound doesn't. If a user has two background sessions finishing simultaneously they'll both beep, and a background session finishing while the user is reading the active chat will emit a misleading sound.
| // Play a notification sound when the agent finishes responding | |
| const prevLoadingRef = useRef(isLoading); | |
| useEffect(() => { | |
| const wasLoading = prevLoadingRef.current; | |
| prevLoadingRef.current = isLoading; | |
| if (!wasLoading || isLoading) return; | |
| // Play a notification sound when the agent finishes responding | |
| const prevLoadingRef = useRef(isLoading); | |
| useEffect(() => { | |
| const wasLoading = prevLoadingRef.current; | |
| prevLoadingRef.current = isLoading; | |
| if (!wasLoading || isLoading) return; | |
| if (!active) return; |
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!
Feature
Play a short notification sound when the agent finishes responding, so users who switch away from the app know when an answer is ready.
Implementation
Uses the Web Audio API to generate a brief ascending two-tone beep (880 → 1100 Hz, 0.2 seconds, low volume). No external audio files needed.
The sound plays when
isLoadingtransitions fromtruetofalse— indicating the agent has completed its response.If AudioContext is unavailable or blocked (e.g., browser autoplay policy), the error is silently caught — the app continues normally.