From 31d1d2df82063411a12efa3b6c46db5a82f9f144 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Tue, 13 May 2025 06:59:45 +0700 Subject: [PATCH 1/5] feat: move play audio to webview to ensure cross-platform replace sound-play with use-sound for audio handling and add new sound files --- package-lock.json | 6 -- package.json | 1 - src/core/webview/ClineProvider.ts | 3 - .../webview/__tests__/ClineProvider.test.ts | 6 -- src/core/webview/webviewMessageHandler.ts | 10 +-- src/utils/sound.ts | 75 ------------------ {audio => webview-ui/audio}/celebration.wav | Bin {audio => webview-ui/audio}/notification.wav | Bin {audio => webview-ui/audio}/progress_loop.wav | Bin webview-ui/package-lock.json | 19 +++++ webview-ui/package.json | 1 + webview-ui/src/components/chat/ChatView.tsx | 41 +++++++++- .../chat/__tests__/ChatView.test.tsx | 33 ++++---- webview-ui/vite.config.ts | 2 +- 14 files changed, 78 insertions(+), 119 deletions(-) delete mode 100644 src/utils/sound.ts rename {audio => webview-ui/audio}/celebration.wav (100%) rename {audio => webview-ui/audio}/notification.wav (100%) rename {audio => webview-ui/audio}/progress_loop.wav (100%) diff --git a/package-lock.json b/package-lock.json index ef24bf36f28..c42b38ee7d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -18679,11 +18678,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", diff --git a/package.json b/package.json index 2cdc344c1a3..7cbfb8a0e2c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 84366274aaf..cf555869e72 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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 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 implements terminalPowershellCounter = false, terminalZdotdir = false, }) => { - setSoundEnabled(soundEnabled) Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout) Terminal.setShellIntegrationDisabled(terminalShellIntegrationDisabled) Terminal.setCommandDelay(terminalCommandDelay) diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 3bd3a11db45..b4a462a7cd1 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -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,9 +172,6 @@ jest.mock("vscode", () => ({ }, })) -jest.mock("../../../utils/sound", () => ({ - setSoundEnabled: jest.fn(), -})) jest.mock("../../../utils/tts", () => ({ setTtsEnabled: jest.fn(), @@ -545,14 +541,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() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1f17d221a62..c46b7e1fc1a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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" @@ -483,22 +482,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": diff --git a/src/utils/sound.ts b/src/utils/sound.ts deleted file mode 100644 index 877a041cd38..00000000000 --- a/src/utils/sound.ts +++ /dev/null @@ -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) - } -} diff --git a/audio/celebration.wav b/webview-ui/audio/celebration.wav similarity index 100% rename from audio/celebration.wav rename to webview-ui/audio/celebration.wav diff --git a/audio/notification.wav b/webview-ui/audio/notification.wav similarity index 100% rename from audio/notification.wav rename to webview-ui/audio/notification.wav diff --git a/audio/progress_loop.wav b/webview-ui/audio/progress_loop.wav similarity index 100% rename from audio/progress_loop.wav rename to webview-ui/audio/progress_loop.wav diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 81105d26a26..08168051cbe 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -54,6 +54,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" }, @@ -12778,6 +12779,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", @@ -21524,6 +21531,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", diff --git a/webview-ui/package.json b/webview-ui/package.json index 0f7c6236fb7..a14adc1e19f 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -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" }, diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 90cdd147531..a86614b36b8 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -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, @@ -85,6 +86,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction 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, + } + + // Helper function to get audio URLs that works in both development and Jest environments + const getAudioUrl = (path: string) => { + if (typeof import.meta !== 'undefined') { + return new URL(`/audio/${path}`, import.meta.url).href; + } + // Fallback for Jest environment + return `/audio/${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) { diff --git a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx index cb801806871..1adffb298f0 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx @@ -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() }) }) }) diff --git a/webview-ui/vite.config.ts b/webview-ui/vite.config.ts index 69163efc3b7..2ac074c6f3d 100644 --- a/webview-ui/vite.config.ts +++ b/webview-ui/vite.config.ts @@ -85,5 +85,5 @@ export default defineConfig({ optimizeDeps: { exclude: ["@vscode/codicons", "vscode-oniguruma", "shiki"], }, - assetsInclude: ["**/*.wasm"], + assetsInclude: ["**/*.wasm", "**/*.wav"], }) From b549d4294e62ebc57ebf6793da120a8936bed6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=AE=E0=AE=A9=E0=AF=8B=E0=AE=9C=E0=AF=8D=E0=AE=95?= =?UTF-8?q?=E0=AF=81=E0=AE=AE=E0=AE=BE=E0=AE=B0=E0=AF=8D=20=E0=AE=AA?= =?UTF-8?q?=E0=AE=B4=E0=AE=A9=E0=AE=BF=E0=AE=9A=E0=AF=8D=E0=AE=9A=E0=AE=BE?= =?UTF-8?q?=E0=AE=AE=E0=AE=BF?= Date: Wed, 14 May 2025 19:10:10 +0530 Subject: [PATCH 2/5] fix getAudioUrl and CSP --- src/core/webview/ClineProvider.ts | 7 ++++++- src/core/webview/__tests__/ClineProvider.test.ts | 3 +-- webview-ui/src/components/chat/ChatView.tsx | 14 ++++++-------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index cf555869e72..bb05885bb57 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -595,6 +595,7 @@ export class ClineProvider extends EventEmitter 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}` @@ -614,6 +615,7 @@ export class ClineProvider extends EventEmitter 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}`, ] @@ -629,6 +631,7 @@ export class ClineProvider extends EventEmitter implements Roo Code @@ -688,6 +691,7 @@ export class ClineProvider extends EventEmitter 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")) @@ -718,11 +722,12 @@ export class ClineProvider extends EventEmitter implements - + Roo Code diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index b4a462a7cd1..85e9a29be9a 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -172,7 +172,6 @@ jest.mock("vscode", () => ({ }, })) - jest.mock("../../../utils/tts", () => ({ setTtsEnabled: jest.fn(), setTtsSpeed: jest.fn(), @@ -361,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 https://file+.vscode-resource.vscode-cdn.net;", ) // Extract the script-src directive section and verify required security elements diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a86614b36b8..163b0e14356 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -150,17 +150,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (typeof import.meta !== 'undefined') { - return new URL(`/audio/${path}`, import.meta.url).href; - } - // Fallback for Jest environment - return `/audio/${path}`; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return `${window.AUDIO_BASE_URI}/${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) + 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) { // Play the appropriate sound based on type From 70f3ba9f7c736b209d6c657fa1422ffb36230e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=AE=E0=AE=A9=E0=AF=8B=E0=AE=9C=E0=AF=8D=E0=AE=95?= =?UTF-8?q?=E0=AF=81=E0=AE=AE=E0=AE=BE=E0=AE=B0=E0=AF=8D=20=E0=AE=AA?= =?UTF-8?q?=E0=AE=B4=E0=AE=A9=E0=AE=BF=E0=AE=9A=E0=AF=8D=E0=AE=9A=E0=AE=BE?= =?UTF-8?q?=E0=AE=AE=E0=AE=BF?= Date: Fri, 16 May 2025 11:24:36 +0530 Subject: [PATCH 3/5] remove invalid source --- src/core/webview/ClineProvider.ts | 2 +- src/core/webview/__tests__/ClineProvider.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bb05885bb57..471a5e0aa44 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -722,7 +722,7 @@ export class ClineProvider extends EventEmitter implements - +