Fixes #4885: Prevent completion sound from playing when reopening completed tasks

- Added state tracking for tasks that have already played completion sound
- Modified completion_result case to only play sound once per task
- Removed sound from resume_completed_task case entirely
- Added tests to verify the fix works correctly
- Clear sound tracking when starting new tasks
This commit is contained in:
Roo Code 2025-06-19 15:53:53 +00:00
parent 2e2f83be60
commit 4b20c22051
3 changed files with 731 additions and 4 deletions

588
roo-code-messages.log Normal file

File diff suppressed because one or more lines are too long

View file

@ -97,6 +97,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
soundVolume,
} = useExtensionState()
// Track which tasks have already played their completion sound
const [completedTasksWithSound, setCompletedTasksWithSound] = useState<Set<string>>(new Set())
const messagesRef = useRef(messages)
useEffect(() => {
messagesRef.current = messages
@ -330,7 +333,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
if (!isPartial) {
playSound("celebration")
const taskId = task?.ts?.toString() || "unknown"
if (!completedTasksWithSound.has(taskId)) {
playSound("celebration")
setCompletedTasksWithSound((prev) => {
const newSet = new Set(prev)
newSet.add(taskId)
return newSet
})
}
}
setSendingDisabled(isPartial)
setClineAsk("completion_result")
@ -350,9 +361,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setDidClickCancel(false) // special case where we reset the cancel button state
break
case "resume_completed_task":
if (!isPartial) {
playSound("celebration")
}
// Don't play celebration sound when reopening completed tasks from history
// The sound should only play when the task is first completed
setSendingDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
@ -406,6 +416,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
useEffect(() => {
setExpandedRows({})
everVisibleMessagesTsRef.current.clear() // Clear for new task
// Clear completed tasks sound tracking when starting a new task
setCompletedTasksWithSound(new Set())
}, [task?.ts])
useEffect(() => () => everVisibleMessagesTsRef.current.clear(), [])

View file

@ -935,6 +935,133 @@ describe("ChatView - Sound Playing Tests", () => {
})
})
it("does not play celebration sound when reopening completed tasks from history", async () => {
renderChatView()
// First hydrate state with a completed task from history
mockPostMessage({
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now() - 1000,
text: "Task completed successfully",
partial: false,
},
],
})
// Clear any previous sound calls
mockPlayFunction.mockClear()
// Then send the resume_completed_task message (reopening from history)
mockPostMessage({
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now() - 1000,
text: "Task completed successfully",
partial: false,
},
{
type: "ask",
ask: "resume_completed_task",
ts: Date.now(),
text: "Resuming completed task",
partial: false,
},
],
})
// Wait a bit to ensure any async operations complete
await new Promise((resolve) => setTimeout(resolve, 100))
// Verify celebration sound was NOT played for resume_completed_task
// (it should have been played only once for the original completion_result)
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
})
it("plays celebration sound only once per task completion", async () => {
renderChatView()
// First hydrate state with initial task
mockPostMessage({
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
],
})
// Clear any previous sound calls
mockPlayFunction.mockClear()
// Send the first completion result message
mockPostMessage({
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now(),
text: "Task completed successfully",
partial: false,
},
],
})
// Wait for the sound to be played
await waitFor(() => {
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
})
// Send the same completion result message again (simulating reopening)
mockPostMessage({
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now(),
text: "Task completed successfully",
partial: false,
},
],
})
// Wait a bit to ensure any async operations complete
await new Promise((resolve) => setTimeout(resolve, 100))
// Verify celebration sound was still only played once
expect(mockPlayFunction).toHaveBeenCalledTimes(1)
})
it("plays progress_loop sound for api failures", async () => {
renderChatView()