mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: prevent double notification sound playback (#11283)
This commit is contained in:
parent
7da429da19
commit
f51e91ac76
2 changed files with 119 additions and 3 deletions
|
|
@ -244,9 +244,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
|
||||
|
||||
const volume = typeof soundVolume === "number" ? soundVolume : 0.5
|
||||
const [playNotification] = useSound(`${audioBaseUri}/notification.wav`, { volume, soundEnabled })
|
||||
const [playCelebration] = useSound(`${audioBaseUri}/celebration.wav`, { volume, soundEnabled })
|
||||
const [playProgressLoop] = useSound(`${audioBaseUri}/progress_loop.wav`, { volume, soundEnabled })
|
||||
const [playNotification] = useSound(`${audioBaseUri}/notification.wav`, { volume, soundEnabled, interrupt: true })
|
||||
const [playCelebration] = useSound(`${audioBaseUri}/celebration.wav`, { volume, soundEnabled, interrupt: true })
|
||||
const [playProgressLoop] = useSound(`${audioBaseUri}/progress_loop.wav`, { volume, soundEnabled, interrupt: true })
|
||||
|
||||
const lastPlayedRef = useRef<Record<string, number>>({})
|
||||
|
||||
const playSound = useCallback(
|
||||
(audioType: AudioType) => {
|
||||
|
|
@ -254,6 +256,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const lastPlayed = lastPlayedRef.current[audioType] ?? 0
|
||||
if (now - lastPlayed < 100) {
|
||||
return
|
||||
} // debounce: skip if played within 100ms
|
||||
lastPlayedRef.current[audioType] = now
|
||||
|
||||
switch (audioType) {
|
||||
case "notification":
|
||||
playNotification()
|
||||
|
|
|
|||
|
|
@ -520,3 +520,110 @@ describe("ChatView - Notification Sound with Queued Messages", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ChatView - Sound Debounce", () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it("should not play the same sound type twice within 100ms", async () => {
|
||||
const now = 1_000_000
|
||||
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now)
|
||||
|
||||
renderChatView()
|
||||
|
||||
// Hydrate with initial task
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [{ type: "say", say: "task", ts: now - 2000, text: "Initial task" }],
|
||||
})
|
||||
|
||||
// Clear any setup calls
|
||||
mockPlayFunction.mockClear()
|
||||
|
||||
// First completion_result — should trigger celebration sound
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [
|
||||
{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
|
||||
{ type: "ask", ask: "completion_result", ts: now, text: "Task completed", partial: false },
|
||||
],
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Simulate only 50ms passing — still inside the 100ms debounce window
|
||||
dateNowSpy.mockReturnValue(now + 50)
|
||||
|
||||
// Second completion_result with slightly different content to force useDeepCompareEffect re-fire
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [
|
||||
{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
|
||||
{ type: "ask", ask: "completion_result", ts: now + 50, text: "Task completed again", partial: false },
|
||||
],
|
||||
})
|
||||
|
||||
// Allow time for the second state update to propagate through React effects
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// Debounce should have prevented the second play
|
||||
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
|
||||
|
||||
dateNowSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should allow playing the same sound type again after 100ms", async () => {
|
||||
const now = 1_000_000
|
||||
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now)
|
||||
|
||||
renderChatView()
|
||||
|
||||
// Hydrate with initial task
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [{ type: "say", say: "task", ts: now - 2000, text: "Initial task" }],
|
||||
})
|
||||
|
||||
// Clear any setup calls
|
||||
mockPlayFunction.mockClear()
|
||||
|
||||
// First completion_result — triggers sound
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [
|
||||
{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
|
||||
{ type: "ask", ask: "completion_result", ts: now, text: "Task completed", partial: false },
|
||||
],
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Advance past the 100ms debounce window
|
||||
dateNowSpy.mockReturnValue(now + 101)
|
||||
|
||||
// Second completion_result with different content to trigger a fresh effect cycle
|
||||
mockPostMessage({
|
||||
soundEnabled: true,
|
||||
messageQueue: [],
|
||||
clineMessages: [
|
||||
{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
|
||||
{ type: "ask", ask: "completion_result", ts: now + 101, text: "Second task completed", partial: false },
|
||||
],
|
||||
})
|
||||
|
||||
// This time the debounce window has elapsed — sound should play again
|
||||
await waitFor(() => {
|
||||
expect(mockPlayFunction).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
dateNowSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue