mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: move play audio to webview to ensure cross-platform (#3659)
Co-authored-by: sam hoang <samhv.ict@gmail.com>
This commit is contained in:
parent
a0735bb414
commit
628661c5c5
14 changed files with 84 additions and 122 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -55,7 +55,6 @@
|
|||
"say": "^0.16.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-bom": "^5.0.0",
|
||||
|
|
@ -18747,11 +18746,6 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/sound-play": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sound-play/-/sound-play-1.1.0.tgz",
|
||||
"integrity": "sha512-Bd/L0AoCwITFeOnpNLMsfPXrV5GG5NhrC/T6odveahYbhPZkdTnrFXRia9FCC5WBWdUTw1d+yvLBvi4wnD1xOA=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
|
|
|||
|
|
@ -411,7 +411,6 @@
|
|||
"say": "^0.16.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-bom": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { McpHub } from "../../services/mcp/McpHub"
|
|||
import { McpServerManager } from "../../services/mcp/McpServerManager"
|
||||
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { setSoundEnabled } from "../../utils/sound"
|
||||
import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
|
||||
import { ContextProxy } from "../config/ContextProxy"
|
||||
import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
|
||||
|
|
@ -327,7 +326,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
// Initialize out-of-scope variables that need to recieve persistent global state values
|
||||
this.getState().then(
|
||||
({
|
||||
soundEnabled = false,
|
||||
terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout,
|
||||
terminalShellIntegrationDisabled = false,
|
||||
terminalCommandDelay = 0,
|
||||
|
|
@ -337,7 +335,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
terminalPowershellCounter = false,
|
||||
terminalZdotdir = false,
|
||||
}) => {
|
||||
setSoundEnabled(soundEnabled)
|
||||
Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout)
|
||||
Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled)
|
||||
Terminal.setCommandDelay(terminalCommandDelay)
|
||||
|
|
@ -579,6 +576,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
])
|
||||
|
||||
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
|
||||
const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
|
||||
|
||||
const file = "src/index.tsx"
|
||||
const scriptUri = `http://${localServerUrl}/${file}`
|
||||
|
|
@ -598,6 +596,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
`font-src ${webview.cspSource}`,
|
||||
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
`img-src ${webview.cspSource} data:`,
|
||||
`media-src ${webview.cspSource}`,
|
||||
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
|
||||
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
]
|
||||
|
|
@ -613,6 +612,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<script nonce="${nonce}">
|
||||
window.IMAGES_BASE_URI = "${imagesUri}"
|
||||
window.AUDIO_BASE_URI = "${audioUri}"
|
||||
window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
|
||||
</script>
|
||||
<title>Roo Code</title>
|
||||
|
|
@ -672,6 +672,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
])
|
||||
|
||||
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
|
||||
const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
|
||||
|
||||
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
|
||||
|
||||
|
|
@ -702,11 +703,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<script nonce="${nonce}">
|
||||
window.IMAGES_BASE_URI = "${imagesUri}"
|
||||
window.AUDIO_BASE_URI = "${audioUri}"
|
||||
window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}"
|
||||
</script>
|
||||
<title>Roo Code</title>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import axios from "axios"
|
|||
|
||||
import { ClineProvider } from "../ClineProvider"
|
||||
import { ProviderSettingsEntry, ClineMessage, ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
|
||||
import { setSoundEnabled } from "../../../utils/sound"
|
||||
import { setTtsEnabled } from "../../../utils/tts"
|
||||
import { defaultModeSlug } from "../../../shared/modes"
|
||||
import { experimentDefault } from "../../../shared/experiments"
|
||||
|
|
@ -173,10 +172,6 @@ jest.mock("vscode", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
jest.mock("../../../utils/sound", () => ({
|
||||
setSoundEnabled: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("../../../utils/tts", () => ({
|
||||
setTtsEnabled: jest.fn(),
|
||||
setTtsSpeed: jest.fn(),
|
||||
|
|
@ -365,7 +360,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Verify Content Security Policy contains the necessary PostHog domains
|
||||
expect(mockWebviewView.webview.html).toContain(
|
||||
"connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;",
|
||||
"connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
|
||||
)
|
||||
|
||||
// Extract the script-src directive section and verify required security elements
|
||||
|
|
@ -545,14 +540,12 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Simulate setting sound to enabled
|
||||
await messageHandler({ type: "soundEnabled", bool: true })
|
||||
expect(setSoundEnabled).toHaveBeenCalledWith(true)
|
||||
expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true)
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
|
||||
expect(mockPostMessage).toHaveBeenCalled()
|
||||
|
||||
// Simulate setting sound to disabled
|
||||
await messageHandler({ type: "soundEnabled", bool: false })
|
||||
expect(setSoundEnabled).toHaveBeenCalledWith(false)
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false)
|
||||
expect(mockPostMessage).toHaveBeenCalled()
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { getTheme } from "../../integrations/theme/getTheme"
|
|||
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
|
||||
import { searchWorkspaceFiles } from "../../services/search/file-search"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
|
||||
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
|
|
@ -486,22 +485,15 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "playSound":
|
||||
if (message.audioType) {
|
||||
const soundPath = path.join(provider.context.extensionPath, "audio", `${message.audioType}.wav`)
|
||||
playSound(soundPath)
|
||||
}
|
||||
break
|
||||
// playSound handler removed - now handled directly in the webview
|
||||
case "soundEnabled":
|
||||
const soundEnabled = message.bool ?? true
|
||||
await updateGlobalState("soundEnabled", soundEnabled)
|
||||
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "soundVolume":
|
||||
const soundVolume = message.value ?? 0.5
|
||||
await updateGlobalState("soundVolume", soundVolume)
|
||||
setSoundVolume(soundVolume)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "ttsEnabled":
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Minimum interval (in milliseconds) to prevent continuous playback
|
||||
*/
|
||||
const MIN_PLAY_INTERVAL = 500
|
||||
|
||||
/**
|
||||
* Timestamp of when sound was last played
|
||||
*/
|
||||
let lastPlayedTime = 0
|
||||
|
||||
/**
|
||||
* Determine if a file is a WAV file
|
||||
* @param filepath string
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isWAV = (filepath: string): boolean => {
|
||||
return path.extname(filepath).toLowerCase() === ".wav"
|
||||
}
|
||||
|
||||
let isSoundEnabled = false
|
||||
let volume = 0.5
|
||||
|
||||
/**
|
||||
* Set sound configuration
|
||||
* @param enabled boolean
|
||||
*/
|
||||
export const setSoundEnabled = (enabled: boolean): void => {
|
||||
isSoundEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sound volume
|
||||
* @param volume number
|
||||
*/
|
||||
export const setSoundVolume = (newVolume: number): void => {
|
||||
volume = newVolume
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound file
|
||||
* @param filepath string
|
||||
* @return void
|
||||
*/
|
||||
export const playSound = (filepath: string): void => {
|
||||
try {
|
||||
if (!isSoundEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!filepath) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isWAV(filepath)) {
|
||||
throw new Error("Only wav files are supported.")
|
||||
}
|
||||
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - lastPlayedTime < MIN_PLAY_INTERVAL) {
|
||||
return // Skip playback within minimum interval to prevent continuous playback
|
||||
}
|
||||
|
||||
const sound = require("sound-play")
|
||||
sound.play(filepath, volume).catch(() => {
|
||||
throw new Error("Failed to play sound effect")
|
||||
})
|
||||
|
||||
lastPlayedTime = currentTime
|
||||
} catch (error: any) {
|
||||
vscode.window.showErrorMessage(error.message)
|
||||
}
|
||||
}
|
||||
19
webview-ui/package-lock.json
generated
19
webview-ui/package-lock.json
generated
|
|
@ -55,6 +55,7 @@
|
|||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-sound": "^5.0.0",
|
||||
"vscrui": "^0.2.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
|
@ -12799,6 +12800,12 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/howler": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
|
||||
"integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||
|
|
@ -21575,6 +21582,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sound": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sound/-/use-sound-5.0.0.tgz",
|
||||
"integrity": "sha512-MNHT3FFC5HxNCrgZtrnpIMJI2cw/0D2xismcrtyht8BTuF5FhFhb57xO/jlQr2xJaFrc/0btzRQvGyHQwB7PVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"howler": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-sound": "^5.0.0",
|
||||
"vscrui": "^0.2.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
|||
import removeMd from "remove-markdown"
|
||||
import { Trans } from "react-i18next"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import useSound from "use-sound"
|
||||
|
||||
import {
|
||||
ClineAsk,
|
||||
|
|
@ -60,6 +61,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
{ isHidden, showAnnouncement, hideAnnouncement },
|
||||
ref,
|
||||
) => {
|
||||
const [audioBaseUri] = useState(() => {
|
||||
const w = window as any
|
||||
return w.AUDIO_BASE_URI || ""
|
||||
})
|
||||
const { t } = useAppTranslation()
|
||||
const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}`
|
||||
const {
|
||||
|
|
@ -85,6 +90,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
telemetrySetting,
|
||||
hasSystemPromptOverride,
|
||||
historyPreviewCollapsed, // Added historyPreviewCollapsed
|
||||
soundEnabled,
|
||||
soundVolume,
|
||||
} = useExtensionState()
|
||||
|
||||
const { tasks } = useTaskSearch()
|
||||
|
|
@ -137,8 +144,39 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const lastMessage = useMemo(() => messages.at(-1), [messages])
|
||||
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
|
||||
|
||||
// Setup sound hooks with use-sound
|
||||
const volume = typeof soundVolume === "number" ? soundVolume : 0.5
|
||||
const soundConfig = {
|
||||
volume,
|
||||
// useSound expects 'disabled' property, not 'soundEnabled'
|
||||
soundEnabled,
|
||||
}
|
||||
|
||||
const getAudioUrl = (path: string) => {
|
||||
return `${audioBaseUri}/${path}`
|
||||
}
|
||||
|
||||
// Use the getAudioUrl helper function
|
||||
const [playNotification] = useSound(getAudioUrl("notification.wav"), soundConfig)
|
||||
const [playCelebration] = useSound(getAudioUrl("celebration.wav"), soundConfig)
|
||||
const [playProgressLoop] = useSound(getAudioUrl("progress_loop.wav"), soundConfig)
|
||||
|
||||
function playSound(audioType: AudioType) {
|
||||
vscode.postMessage({ type: "playSound", audioType })
|
||||
// Play the appropriate sound based on type
|
||||
// The disabled state is handled by the useSound hook configuration
|
||||
switch (audioType) {
|
||||
case "notification":
|
||||
playNotification()
|
||||
break
|
||||
case "celebration":
|
||||
playCelebration()
|
||||
break
|
||||
case "progress_loop":
|
||||
playProgressLoop()
|
||||
break
|
||||
default:
|
||||
console.warn(`Unknown audio type: ${audioType}`)
|
||||
}
|
||||
}
|
||||
|
||||
function playTts(text: string) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ jest.mock("@src/utils/vscode", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
// Mock use-sound hook
|
||||
const mockPlayFunction = jest.fn()
|
||||
jest.mock("use-sound", () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return [mockPlayFunction]
|
||||
})
|
||||
})
|
||||
|
||||
// Mock components that use ESM dependencies
|
||||
jest.mock("../BrowserSessionRow", () => ({
|
||||
__esModule: true,
|
||||
|
|
@ -773,7 +781,10 @@ describe("ChatView - Auto Approval Tests", () => {
|
|||
})
|
||||
|
||||
describe("ChatView - Sound Playing Tests", () => {
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPlayFunction.mockClear()
|
||||
})
|
||||
|
||||
it("does not play sound for auto-approved browser actions", async () => {
|
||||
renderChatView()
|
||||
|
|
@ -821,10 +832,7 @@ describe("ChatView - Sound Playing Tests", () => {
|
|||
})
|
||||
|
||||
// Verify no sound was played
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "playSound",
|
||||
audioType: expect.any(String),
|
||||
})
|
||||
expect(mockPlayFunction).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("plays notification sound for non-auto-approved browser actions", async () => {
|
||||
|
|
@ -874,10 +882,7 @@ describe("ChatView - Sound Playing Tests", () => {
|
|||
|
||||
// Verify notification sound was played
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "playSound",
|
||||
audioType: "notification",
|
||||
})
|
||||
expect(mockPlayFunction).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -924,10 +929,7 @@ describe("ChatView - Sound Playing Tests", () => {
|
|||
|
||||
// Verify celebration sound was played
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "playSound",
|
||||
audioType: "celebration",
|
||||
})
|
||||
expect(mockPlayFunction).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -974,10 +976,7 @@ describe("ChatView - Sound Playing Tests", () => {
|
|||
|
||||
// Verify progress_loop sound was played
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "playSound",
|
||||
audioType: "progress_loop",
|
||||
})
|
||||
expect(mockPlayFunction).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,5 +85,5 @@ export default defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: ["@vscode/codicons", "vscode-oniguruma", "shiki"],
|
||||
},
|
||||
assetsInclude: ["**/*.wasm"],
|
||||
assetsInclude: ["**/*.wasm", "**/*.wav"],
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue