From 0d4a7437a2b8f6caf1bf1d49974dfeaa7150228b Mon Sep 17 00:00:00 2001 From: heyseth Date: Wed, 5 Mar 2025 18:20:26 -0800 Subject: [PATCH 01/10] Add text-to-speech functionality --- package-lock.json | 17 +++++ package.json | 1 + src/core/webview/ClineProvider.ts | 20 ++++++ .../webview/__tests__/ClineProvider.test.ts | 20 ++++++ src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 2 + src/shared/globalState.ts | 1 + src/utils/tts.ts | 66 +++++++++++++++++++ webview-ui/src/components/chat/ChatView.tsx | 26 +++++++- .../settings/NotificationSettings.tsx | 14 +++- .../src/components/settings/SettingsView.tsx | 3 + .../src/context/ExtensionStateContext.tsx | 3 + 12 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/utils/tts.ts diff --git a/package-lock.json b/package-lock.json index 228a51a720a..b72e7cfbf5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "pretty-bytes": "^6.1.1", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", + "say": "^0.16.0", "serialize-error": "^11.0.3", "simple-git": "^3.27.0", "sound-play": "^1.1.0", @@ -12349,6 +12350,11 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha512-qAMrwuk2xLEutlASoiPiAMW3EN3K96Ka/ilSXYr6qR1zSVXw2j7+yDSqGTC4T9apfLYxM3tLLjKvgPdAUK7kYQ==" + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -13544,6 +13550,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/say": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/say/-/say-0.16.0.tgz", + "integrity": "sha512-yEfncNu3I6lcZ6RIrXgE9DqbrEmvV5uQQ8ReM14u/DodlvJYpveqNphO55RLMSj77b06ZKNif/FLmhzQxcuUXg==", + "dependencies": { + "one-time": "0.0.4" + }, + "engines": { + "node": ">=6.9" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", diff --git a/package.json b/package.json index 136c91cfa41..f6843cd6a2f 100644 --- a/package.json +++ b/package.json @@ -298,6 +298,7 @@ "pretty-bytes": "^6.1.1", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", + "say": "^0.16.0", "serialize-error": "^11.0.3", "simple-git": "^3.27.0", "sound-play": "^1.1.0", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6883eb4fdfa..15321441502 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -30,6 +30,7 @@ import { McpHub } from "../../services/mcp/McpHub" import { McpServerManager } from "../../services/mcp/McpServerManager" import { fileExistsAtPath } from "../../utils/fs" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" +import { playTts, setTtsEnabled } from "../../utils/tts" import { singleCompletionHandler } from "../../utils/single-completion-handler" import { searchCommits } from "../../utils/git" import { getDiffStrategy } from "../diff/DiffStrategy" @@ -394,6 +395,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { setSoundEnabled(soundEnabled ?? false) }) + // Initialize tts enabled state + this.getState().then(({ ttsEnabled }) => { + setTtsEnabled(ttsEnabled ?? false) + }) + webviewView.webview.options = { // Allow scripts in the webview enableScripts: true, @@ -1204,6 +1210,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { setSoundVolume(soundVolume) await this.postStateToWebview() break + case "ttsEnabled": + const ttsEnabled = message.bool ?? true + await this.updateGlobalState("ttsEnabled", ttsEnabled) + setTtsEnabled(ttsEnabled) // Add this line to update the tts utility + await this.postStateToWebview() + break + case "playTts": + if (message.text) { + playTts(message.text) + } + break case "diffEnabled": const diffEnabled = message.bool ?? true await this.updateGlobalState("diffEnabled", diffEnabled) @@ -2125,6 +2142,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowMcp, alwaysAllowModeSwitch, soundEnabled, + ttsEnabled, diffEnabled, enableCheckpoints, checkpointStorage, @@ -2176,6 +2194,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { .filter((item: HistoryItem) => item.ts && item.task) .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts), soundEnabled: soundEnabled ?? false, + ttsEnabled: ttsEnabled ?? false, diffEnabled: diffEnabled ?? true, enableCheckpoints: enableCheckpoints ?? true, checkpointStorage: checkpointStorage ?? "task", @@ -2326,6 +2345,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { taskHistory: stateValues.taskHistory, allowedCommands: stateValues.allowedCommands, soundEnabled: stateValues.soundEnabled ?? false, + ttsEnabled: stateValues.ttsEnabled ?? false, diffEnabled: stateValues.diffEnabled ?? true, enableCheckpoints: stateValues.enableCheckpoints ?? false, checkpointStorage: stateValues.checkpointStorage ?? "task", diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index d92ca0ff642..e9d5540f5e7 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -7,6 +7,7 @@ import { ClineProvider } from "../ClineProvider" import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" import { GlobalStateKey, SecretKey } from "../../../shared/globalState" import { setSoundEnabled } from "../../../utils/sound" +import { setTtsEnabled } from "../../../utils/tts" import { defaultModeSlug } from "../../../shared/modes" import { experimentDefault } from "../../../shared/experiments" import { Cline } from "../../Cline" @@ -193,6 +194,11 @@ jest.mock("../../../utils/sound", () => ({ setSoundEnabled: jest.fn(), })) +// Mock tts utility +jest.mock("../../../utils/tts", () => ({ + setTtsEnabled: jest.fn(), +})) + // Mock ESM modules jest.mock("p-wait-for", () => ({ __esModule: true, @@ -423,6 +429,7 @@ describe("ClineProvider", () => { alwaysAllowMcp: false, uriScheme: "vscode", soundEnabled: false, + ttsEnabled: false, diffEnabled: false, enableCheckpoints: false, checkpointStorage: "task", @@ -517,6 +524,7 @@ describe("ClineProvider", () => { expect(state).toHaveProperty("alwaysAllowBrowser") expect(state).toHaveProperty("taskHistory") expect(state).toHaveProperty("soundEnabled") + expect(state).toHaveProperty("ttsEnabled") expect(state).toHaveProperty("diffEnabled") expect(state).toHaveProperty("writeDelayMs") }) @@ -588,6 +596,18 @@ describe("ClineProvider", () => { expect(setSoundEnabled).toHaveBeenCalledWith(false) expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false) expect(mockPostMessage).toHaveBeenCalled() + + // Simulate setting tts to enabled + await messageHandler({ type: "ttsEnabled", bool: true }) + expect(setTtsEnabled).toHaveBeenCalledWith(true) + expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", true) + expect(mockPostMessage).toHaveBeenCalled() + + // Simulate setting tts to disabled + await messageHandler({ type: "ttsEnabled", bool: false }) + expect(setTtsEnabled).toHaveBeenCalledWith(false) + expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", false) + expect(mockPostMessage).toHaveBeenCalled() }) test("requestDelaySeconds defaults to 5 seconds", async () => { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9a10e04a08f..01da5d07804 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -115,6 +115,7 @@ export interface ExtensionState { currentTaskItem?: HistoryItem allowedCommands?: string[] soundEnabled?: boolean + ttsEnabled?: boolean soundVolume?: number diffEnabled?: boolean enableCheckpoints: boolean diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 9592b559e26..4a31ca9f5af 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -49,7 +49,9 @@ export interface WebviewMessage { | "alwaysAllowMcp" | "alwaysAllowModeSwitch" | "playSound" + | "playTts" | "soundEnabled" + | "ttsEnabled" | "soundVolume" | "diffEnabled" | "enableCheckpoints" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index fd7bd1adb9f..6528475d7a5 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -57,6 +57,7 @@ export const GLOBAL_STATE_KEYS = [ "openRouterUseMiddleOutTransform", "allowedCommands", "soundEnabled", + "ttsEnabled", "soundVolume", "diffEnabled", "enableCheckpoints", diff --git a/src/utils/tts.ts b/src/utils/tts.ts new file mode 100644 index 00000000000..a002da1d140 --- /dev/null +++ b/src/utils/tts.ts @@ -0,0 +1,66 @@ +import * as vscode from "vscode" + +let isTtsEnabled = false +let isSpeaking = false +const utteranceQueue: string[] = [] + +/** + * Set tts configuration + * @param enabled boolean + */ +export const setTtsEnabled = (enabled: boolean): void => { + isTtsEnabled = enabled +} + +/** + * Process the next item in the utterance queue + */ +const processQueue = async (): Promise => { + if (!isTtsEnabled || isSpeaking || utteranceQueue.length === 0) { + return + } + + try { + isSpeaking = true + const nextUtterance = utteranceQueue.shift()! + const say = require("say") + + // Wrap say.speak in a promise to handle completion + await new Promise((resolve, reject) => { + say.speak(nextUtterance, null, null, (err: Error) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + + isSpeaking = false + // Process next item in queue if any + await processQueue() + } catch (error: any) { + isSpeaking = false + vscode.window.showErrorMessage(error.message) + // Try to continue with next item despite error + await processQueue() + } +} + +/** + * Queue a tts message to be spoken + * @param message string + * @return void + */ +export const playTts = async (message: string): Promise => { + if (!isTtsEnabled) { + return + } + + try { + utteranceQueue.push(message) + await processQueue() + } catch (error: any) { + vscode.window.showErrorMessage(error.message) + } +} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d56594a6f84..b1509f206ec 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -86,6 +86,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const disableAutoScrollRef = useRef(false) const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [isAtBottom, setIsAtBottom] = useState(false) + const lastTtsRef = useRef("") const [wasStreaming, setWasStreaming] = useState(false) const [showCheckpointWarning, setShowCheckpointWarning] = useState(false) @@ -99,6 +100,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie vscode.postMessage({ type: "playSound", audioType }) } + function playTts(text: string) { + vscode.postMessage({ type: "playTts", text }) + } + useDeepCompareEffect(() => { // if last message is an ask, show user ask UI // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost. @@ -659,6 +664,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie ) useEffect(() => { + // skip input message + if (lastMessage && messages.length > 1) { + let text = lastMessage?.text || "" + + if ( + lastMessage.type === "say" && // is a say message + !lastMessage.partial && // not a partial message + !text.startsWith("{") && // not a json object + text !== lastTtsRef.current // not the same as last TTS message + ) { + try { + playTts(text) + lastTtsRef.current = text + } catch (error) { + console.error("Failed to execute text-to-speech:", error) + } + } + } + // Only execute when isStreaming changes from true to false if (wasStreaming && !isStreaming && lastMessage) { // Play appropriate sound based on lastMessage content @@ -691,7 +715,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie } // Update previous value setWasStreaming(isStreaming) - }, [isStreaming, lastMessage, wasStreaming, isAutoApproved]) + }, [isStreaming, lastMessage, wasStreaming, isAutoApproved, messages.length]) const isBrowserSessionMessage = (message: ClineMessage): boolean => { // which of visible messages are browser session messages, see above diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index 1fba9dd412e..ca694b51b73 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -7,12 +7,14 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" type NotificationSettingsProps = HTMLAttributes & { + ttsEnabled?: boolean soundEnabled?: boolean soundVolume?: number - setCachedStateField: SetCachedStateField<"soundEnabled" | "soundVolume"> + setCachedStateField: SetCachedStateField<"ttsEnabled" | "soundEnabled" | "soundVolume"> } export const NotificationSettings = ({ + ttsEnabled, soundEnabled, soundVolume, setCachedStateField, @@ -28,6 +30,16 @@ export const NotificationSettings = ({
+
+ setCachedStateField("ttsEnabled", e.target.checked)}> + Enable text-to-speech + +

+ When enabled, Roo will read aloud its responses using text-to-speech. +

+
(({ onDone }, requestDelaySeconds, screenshotQuality, soundEnabled, + ttsEnabled, soundVolume, terminalOutputLineLimit, writeDelayMs, @@ -149,6 +150,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled }) vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) + vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled }) vscode.postMessage({ type: "soundVolume", value: soundVolume }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints }) @@ -370,6 +372,7 @@ const SettingsView = forwardRef(({ onDone },
void setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void + setTtsEnabled: (value: boolean) => void setDiffEnabled: (value: boolean) => void setEnableCheckpoints: (value: boolean) => void setBrowserViewportSize: (value: string) => void @@ -105,6 +106,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode allowedCommands: [], soundEnabled: false, soundVolume: 0.5, + ttsEnabled: false, diffEnabled: false, enableCheckpoints: true, checkpointStorage: "task", @@ -242,6 +244,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), + setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })), setBrowserViewportSize: (value: string) => From 88cf10616a261874044d70a952030bb531b5ee57 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sat, 8 Mar 2025 14:19:02 -0800 Subject: [PATCH 02/10] Add speed config option to text-to-speech --- src/core/webview/ClineProvider.ts | 11 ++- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + src/shared/globalState.ts | 1 + src/utils/tts.ts | 11 ++- .../settings/NotificationSettings.tsx | 29 ++++++- .../src/components/settings/SettingsView.tsx | 3 + .../settings/__tests__/SettingsView.test.tsx | 76 +++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 4 + 9 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index c4f61a2cfc4..e9892430c9f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -32,7 +32,7 @@ import { McpServerManager } from "../../services/mcp/McpServerManager" import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService" import { fileExistsAtPath } from "../../utils/fs" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" -import { playTts, setTtsEnabled } from "../../utils/tts" +import { playTts, setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { singleCompletionHandler } from "../../utils/single-completion-handler" import { searchCommits } from "../../utils/git" import { getDiffStrategy } from "../diff/DiffStrategy" @@ -1252,6 +1252,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { setTtsEnabled(ttsEnabled) // Add this line to update the tts utility await this.postStateToWebview() break + case "ttsSpeed": + const ttsSpeed = message.value ?? 1.0 + await this.updateGlobalState("ttsSpeed", ttsSpeed) + setTtsSpeed(ttsSpeed) + await this.postStateToWebview() + break case "playTts": if (message.text) { playTts(message.text) @@ -2196,6 +2202,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowModeSwitch, soundEnabled, ttsEnabled, + ttsSpeed, diffEnabled, enableCheckpoints, checkpointStorage, @@ -2252,6 +2259,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts), soundEnabled: soundEnabled ?? false, ttsEnabled: ttsEnabled ?? false, + ttsSpeed: ttsSpeed ?? 1.0, diffEnabled: diffEnabled ?? true, enableCheckpoints: enableCheckpoints ?? true, checkpointStorage: checkpointStorage ?? "task", @@ -2408,6 +2416,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { allowedCommands: stateValues.allowedCommands, soundEnabled: stateValues.soundEnabled ?? false, ttsEnabled: stateValues.ttsEnabled ?? false, + ttsSpeed: stateValues.ttsSpeed ?? 1.0, diffEnabled: stateValues.diffEnabled ?? true, enableCheckpoints: stateValues.enableCheckpoints ?? true, checkpointStorage: stateValues.checkpointStorage ?? "task", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 33eb5b4ae64..dbcfe86885b 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -117,6 +117,7 @@ export interface ExtensionState { allowedCommands?: string[] soundEnabled?: boolean ttsEnabled?: boolean + ttsSpeed?: number soundVolume?: number diffEnabled?: boolean enableCheckpoints: boolean diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1956a3faf43..18603b55471 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -52,6 +52,7 @@ export interface WebviewMessage { | "playTts" | "soundEnabled" | "ttsEnabled" + | "ttsSpeed" | "soundVolume" | "diffEnabled" | "enableCheckpoints" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 7168355c031..3ecf642da32 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -60,6 +60,7 @@ export const GLOBAL_STATE_KEYS = [ "allowedCommands", "soundEnabled", "ttsEnabled", + "ttsSpeed", "soundVolume", "diffEnabled", "enableCheckpoints", diff --git a/src/utils/tts.ts b/src/utils/tts.ts index a002da1d140..e08c1454dee 100644 --- a/src/utils/tts.ts +++ b/src/utils/tts.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode" let isTtsEnabled = false +let speed = 1.0 let isSpeaking = false const utteranceQueue: string[] = [] @@ -12,6 +13,14 @@ export const setTtsEnabled = (enabled: boolean): void => { isTtsEnabled = enabled } +/** + * Set tts speed + * @param speed number + */ +export const setTtsSpeed = (newSpeed: number): void => { + speed = newSpeed +} + /** * Process the next item in the utterance queue */ @@ -27,7 +36,7 @@ const processQueue = async (): Promise => { // Wrap say.speak in a promise to handle completion await new Promise((resolve, reject) => { - say.speak(nextUtterance, null, null, (err: Error) => { + say.speak(nextUtterance, null, speed, (err: Error) => { if (err) { reject(err) } else { diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index ca694b51b73..8615992f9b8 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -8,13 +8,15 @@ import { Section } from "./Section" type NotificationSettingsProps = HTMLAttributes & { ttsEnabled?: boolean + ttsSpeed?: number soundEnabled?: boolean soundVolume?: number - setCachedStateField: SetCachedStateField<"ttsEnabled" | "soundEnabled" | "soundVolume"> + setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume"> } export const NotificationSettings = ({ ttsEnabled, + ttsSpeed, soundEnabled, soundVolume, setCachedStateField, @@ -39,6 +41,31 @@ export const NotificationSettings = ({

When enabled, Roo will read aloud its responses using text-to-speech.

+ {ttsEnabled && ( +
+
+ setCachedStateField("ttsSpeed", parseFloat(e.target.value))} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + aria-label="Speed" + /> + + {((ttsSpeed ?? 1.0) * 100).toFixed(0)}% + +
+

Speed

+
+ )}
(({ onDone }, screenshotQuality, soundEnabled, ttsEnabled, + ttsSpeed, soundVolume, telemetrySetting, terminalOutputLimit, @@ -168,6 +169,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled }) vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled }) + vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed }) vscode.postMessage({ type: "soundVolume", value: soundVolume }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints }) @@ -392,6 +394,7 @@ const SettingsView = forwardRef(({ onDone },
{ shouldShowAnnouncement: false, allowedCommands: [], alwaysAllowExecute: false, + ttsEnabled: false, + ttsSpeed: 1.0, soundEnabled: false, soundVolume: 0.5, ...state, @@ -132,6 +134,18 @@ describe("SettingsView - Sound Settings", () => { jest.clearAllMocks() }) + it("initializes with tts disabled by default", () => { + renderSettingsView() + + const ttsCheckbox = screen.getByRole("checkbox", { + name: /Enable text-to-speech/i, + }) + expect(ttsCheckbox).not.toBeChecked() + + // Speed slider should not be visible when tts is disabled + expect(screen.queryByRole("slider", { name: /speed/i })).not.toBeInTheDocument() + }) + it("initializes with sound disabled by default", () => { renderSettingsView() @@ -144,6 +158,29 @@ describe("SettingsView - Sound Settings", () => { expect(screen.queryByRole("slider", { name: /volume/i })).not.toBeInTheDocument() }) + it("toggles tts setting and sends message to VSCode", () => { + renderSettingsView() + + const ttsCheckbox = screen.getByRole("checkbox", { + name: /Enable text-to-speech/i, + }) + + // Enable tts + fireEvent.click(ttsCheckbox) + expect(ttsCheckbox).toBeChecked() + + // Click Save to save settings + const saveButton = screen.getByText("Save") + fireEvent.click(saveButton) + + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "ttsEnabled", + bool: true, + }), + ) + }) + it("toggles sound setting and sends message to VSCode", () => { renderSettingsView() @@ -167,6 +204,21 @@ describe("SettingsView - Sound Settings", () => { ) }) + it("shows tts slider when sound is enabled", () => { + renderSettingsView() + + // Enable tts + const ttsCheckbox = screen.getByRole("checkbox", { + name: /Enable text-to-speech/i, + }) + fireEvent.click(ttsCheckbox) + + // Speed slider should be visible + const speedSlider = screen.getByRole("slider", { name: /speed/i }) + expect(speedSlider).toBeInTheDocument() + expect(speedSlider).toHaveValue("1.0") + }) + it("shows volume slider when sound is enabled", () => { renderSettingsView() @@ -182,6 +234,30 @@ describe("SettingsView - Sound Settings", () => { expect(volumeSlider).toHaveValue("0.5") }) + it("updates speed and sends message to VSCode when slider changes", () => { + renderSettingsView() + + // Enable tts + const ttsCheckbox = screen.getByRole("checkbox", { + name: /Enable text-to-speech/i, + }) + fireEvent.click(ttsCheckbox) + + // Change speed + const speedSlider = screen.getByRole("slider", { name: /speed/i }) + fireEvent.change(speedSlider, { target: { value: "0.75" } }) + + // Click Save to save settings + const saveButton = screen.getByText("Save") + fireEvent.click(saveButton) + + // Verify message sent to VSCode + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "ttsSpeed", + value: 0.75, + }) + }) + it("updates volume and sends message to VSCode when slider changes", () => { renderSettingsView() diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 0602208aad4..cae67b9788a 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -38,6 +38,7 @@ export interface ExtensionStateContextType extends ExtensionState { setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void setTtsEnabled: (value: boolean) => void + setTtsSpeed: (value: number) => void setDiffEnabled: (value: boolean) => void setEnableCheckpoints: (value: boolean) => void setBrowserViewportSize: (value: string) => void @@ -114,6 +115,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode soundEnabled: false, soundVolume: 0.5, ttsEnabled: false, + ttsSpeed: 1.0, diffEnabled: false, enableCheckpoints: true, checkpointStorage: "task", @@ -229,6 +231,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode filePaths, openedTabs, soundVolume: state.soundVolume, + ttsSpeed: state.ttsSpeed, fuzzyMatchThreshold: state.fuzzyMatchThreshold, writeDelayMs: state.writeDelayMs, screenshotQuality: state.screenshotQuality, @@ -254,6 +257,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })), + setTtsSpeed: (value) => setState((prevState) => ({ ...prevState, ttsSpeed: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })), setBrowserViewportSize: (value: string) => From 8f1938709365bc6281d09ed043972be094ce37b7 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sat, 8 Mar 2025 14:36:40 -0800 Subject: [PATCH 03/10] Fix test case for tts speed slider --- .../src/components/settings/__tests__/SettingsView.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 3fcf184ab77..904f24999e3 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -107,7 +107,7 @@ const mockPostMessage = (state: any) => { allowedCommands: [], alwaysAllowExecute: false, ttsEnabled: false, - ttsSpeed: 1.0, + ttsSpeed: 0.5, soundEnabled: false, soundVolume: 0.5, ...state, @@ -216,7 +216,7 @@ describe("SettingsView - Sound Settings", () => { // Speed slider should be visible const speedSlider = screen.getByRole("slider", { name: /speed/i }) expect(speedSlider).toBeInTheDocument() - expect(speedSlider).toHaveValue("1.0") + expect(speedSlider).toHaveValue("0.5") }) it("shows volume slider when sound is enabled", () => { From a734d517aad3e20c32d4310142b080a552eb1da2 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sat, 8 Mar 2025 15:07:22 -0800 Subject: [PATCH 04/10] Fix test case for tts speed slider (really) --- .../src/components/settings/__tests__/SettingsView.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 904f24999e3..0c7c9c2ad34 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -107,7 +107,7 @@ const mockPostMessage = (state: any) => { allowedCommands: [], alwaysAllowExecute: false, ttsEnabled: false, - ttsSpeed: 0.5, + ttsSpeed: 1, soundEnabled: false, soundVolume: 0.5, ...state, @@ -216,7 +216,7 @@ describe("SettingsView - Sound Settings", () => { // Speed slider should be visible const speedSlider = screen.getByRole("slider", { name: /speed/i }) expect(speedSlider).toBeInTheDocument() - expect(speedSlider).toHaveValue("0.5") + expect(speedSlider).toHaveValue("1") }) it("shows volume slider when sound is enabled", () => { From be9e57e99e5810350fd9ea004d599b179dd1f8d4 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sat, 8 Mar 2025 19:32:11 -0800 Subject: [PATCH 05/10] Disabled error message logging in tts.ts --- src/utils/tts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/tts.ts b/src/utils/tts.ts index e08c1454dee..cc567b1b1a6 100644 --- a/src/utils/tts.ts +++ b/src/utils/tts.ts @@ -50,7 +50,7 @@ const processQueue = async (): Promise => { await processQueue() } catch (error: any) { isSpeaking = false - vscode.window.showErrorMessage(error.message) + //vscode.window.showErrorMessage(error.message) // Try to continue with next item despite error await processQueue() } @@ -70,6 +70,6 @@ export const playTts = async (message: string): Promise => { utteranceQueue.push(message) await processQueue() } catch (error: any) { - vscode.window.showErrorMessage(error.message) + //vscode.window.showErrorMessage(error.message) } } From 1b6b8307e047db532e39f7f2b8c0a9f8d3820458 Mon Sep 17 00:00:00 2001 From: heyseth Date: Mon, 17 Mar 2025 14:56:33 -0700 Subject: [PATCH 06/10] ignore markdown and mermaid diagrams in TTS --- webview-ui/package-lock.json | 6 ++++ webview-ui/package.json | 1 + webview-ui/src/components/chat/ChatView.tsx | 32 ++++++++++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 97960851c97..33d4305f15f 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -40,6 +40,7 @@ "react-virtuoso": "^4.7.13", "rehype-highlight": "^7.0.0", "remark-gfm": "^4.0.1", + "remove-markdown": "^0.6.0", "shell-quote": "^1.8.2", "styled-components": "^6.1.13", "tailwind-merge": "^2.6.0", @@ -19636,6 +19637,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-markdown": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.6.0.tgz", + "integrity": "sha512-B9g8yo5Zp1wXfZ77M1RLpqI7xrBBERkp7+3/Btm9N/uZV5xhXZjzIxDbCKz7CSj141lWDuCnQuH12DKLUv4Ghw==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 589ccbf6743..e092c2ae43c 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -47,6 +47,7 @@ "react-virtuoso": "^4.7.13", "rehype-highlight": "^7.0.0", "remark-gfm": "^4.0.1", + "remove-markdown": "^0.6.0", "shell-quote": "^1.8.2", "styled-components": "^6.1.13", "tailwind-merge": "^2.6.0", diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index f81f07d4413..3d43f0972f4 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -30,6 +30,7 @@ import { AudioType } from "../../../../src/shared/WebviewMessage" import { validateCommand } from "../../utils/command-validation" import { getAllModes } from "../../../../src/shared/modes" import TelemetryBanner from "../common/TelemetryBanner" +import removeMd from "remove-markdown" interface ChatViewProps { isHidden: boolean @@ -674,21 +675,30 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie ) useEffect(() => { - // skip input message + // this ensures the first message is not read, future user messages are labelled as user_feedback if (lastMessage && messages.length > 1) { - let text = lastMessage?.text || "" - + //console.log(JSON.stringify(lastMessage)) if ( - lastMessage.type === "say" && // is a say message + lastMessage.text && // has text + (lastMessage.say === "text" || lastMessage.say === "completion_result") && // is a text message !lastMessage.partial && // not a partial message - !text.startsWith("{") && // not a json object - text !== lastTtsRef.current // not the same as last TTS message + !lastMessage.text.startsWith("{") // not a json object ) { - try { - playTts(text) - lastTtsRef.current = text - } catch (error) { - console.error("Failed to execute text-to-speech:", error) + let text = lastMessage?.text || "" + const mermaidRegex = /```mermaid[\s\S]*?```/g + // remove mermaid diagrams from text + text = text.replace(mermaidRegex, "") + // remove markdown from text + text = removeMd(text) + + // ensure message is not a duplicate of last read message + if (text !== lastTtsRef.current) { + try { + playTts(text) + lastTtsRef.current = text + } catch (error) { + console.error("Failed to execute text-to-speech:", error) + } } } } From 552022d9808c17c6a99cdecf62d95cd6cd32ed74 Mon Sep 17 00:00:00 2001 From: heyseth Date: Mon, 17 Mar 2025 15:05:43 -0700 Subject: [PATCH 07/10] add ttsEnabled and ttsSpeed to GlobalStateKey --- src/exports/roo-code.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index f9764b91a5d..db0a21bd3d1 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -175,6 +175,8 @@ export type GlobalStateKey = | "openRouterUseMiddleOutTransform" | "googleGeminiBaseUrl" | "allowedCommands" + | "ttsEnabled" + | "ttsSpeed" | "soundEnabled" | "soundVolume" | "diffEnabled" From 1d7de4b32a7bd1d4284b12faed573c20e207417a Mon Sep 17 00:00:00 2001 From: heyseth Date: Mon, 17 Mar 2025 15:36:03 -0700 Subject: [PATCH 08/10] fix failing webview test for save button --- .../src/components/settings/__tests__/SettingsView.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 36b13b3fbd6..becdc4c95e8 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -175,7 +175,7 @@ describe("SettingsView - Sound Settings", () => { expect(ttsCheckbox).toBeChecked() // Click Save to save settings - const saveButton = screen.getByText("Save") + const saveButton = screen.getByTestId("save-button") fireEvent.click(saveButton) expect(vscode.postMessage).toHaveBeenCalledWith( @@ -249,7 +249,7 @@ describe("SettingsView - Sound Settings", () => { fireEvent.change(speedSlider, { target: { value: "0.75" } }) // Click Save to save settings - const saveButton = screen.getByText("Save") + const saveButton = screen.getByTestId("save-button") fireEvent.click(saveButton) // Verify message sent to VSCode From d867e0ec6bb619fc25e11c1ec259e1c2640bf0b8 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 18 Mar 2025 01:36:00 -0400 Subject: [PATCH 09/10] Translations --- package-lock.json | 1 - .../settings/NotificationSettings.tsx | 32 +++++++------------ webview-ui/src/i18n/locales/ca/settings.json | 5 +++ webview-ui/src/i18n/locales/de/settings.json | 5 +++ webview-ui/src/i18n/locales/en/settings.json | 5 +++ webview-ui/src/i18n/locales/es/settings.json | 5 +++ webview-ui/src/i18n/locales/fr/settings.json | 5 +++ webview-ui/src/i18n/locales/hi/settings.json | 5 +++ webview-ui/src/i18n/locales/it/settings.json | 5 +++ webview-ui/src/i18n/locales/ja/settings.json | 5 +++ webview-ui/src/i18n/locales/ko/settings.json | 5 +++ webview-ui/src/i18n/locales/pl/settings.json | 5 +++ .../src/i18n/locales/pt-BR/settings.json | 5 +++ webview-ui/src/i18n/locales/tr/settings.json | 5 +++ webview-ui/src/i18n/locales/vi/settings.json | 5 +++ .../src/i18n/locales/zh-CN/settings.json | 5 +++ .../src/i18n/locales/zh-TW/settings.json | 5 +++ 17 files changed, 86 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 703318a72a3..9f949bc3ed7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12547,7 +12547,6 @@ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "chalk": "^2.4.1", diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index b27d625ee02..434d7c358b7 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -38,19 +38,14 @@ export const NotificationSettings = ({ setCachedStateField("ttsEnabled", e.target.checked)}> - Enable text-to-speech + {t("settings:notifications.tts.label")}

- When enabled, Roo will read aloud its responses using text-to-speech. + {t("settings:notifications.tts.description")}

{ttsEnabled && ( -
-
+
+
- - {((ttsSpeed ?? 1.0) * 100).toFixed(0)}% - + {((ttsSpeed ?? 1.0) * 100).toFixed(0)}%
-

Speed

+

+ {t("settings:notifications.tts.speedLabel")} +

)}
@@ -80,13 +75,8 @@ export const NotificationSettings = ({ {t("settings:notifications.sound.description")}

{soundEnabled && ( -
-
+
+
- + {((soundVolume ?? 0.5) * 100).toFixed(0)}%
diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d93e69670e6..cb743d7a660 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -241,6 +241,11 @@ "label": "Habilitar efectes de so", "description": "Quan està habilitat, Roo reproduirà efectes de so per a notificacions i esdeveniments.", "volumeLabel": "Volum" + }, + "tts": { + "label": "Habilitar text a veu", + "description": "Quan està habilitat, Roo llegirà en veu alta les seves respostes utilitzant text a veu.", + "speedLabel": "Velocitat" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 34b81c678a0..0c93810977e 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -241,6 +241,11 @@ "label": "Soundeffekte aktivieren", "description": "Wenn aktiviert, spielt Roo Soundeffekte für Benachrichtigungen und Ereignisse ab.", "volumeLabel": "Lautstärke" + }, + "tts": { + "label": "Text-zu-Sprache aktivieren", + "description": "Wenn aktiviert, liest Roo seine Antworten mit Text-zu-Sprache laut vor.", + "speedLabel": "Geschwindigkeit" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c73b144eece..a44cc32b572 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -241,6 +241,11 @@ "label": "Enable sound effects", "description": "When enabled, Roo will play sound effects for notifications and events.", "volumeLabel": "Volume" + }, + "tts": { + "label": "Enable text-to-speech", + "description": "When enabled, Roo will read aloud its responses using text-to-speech.", + "speedLabel": "Speed" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 91e2cdfdde5..fb7d1f56773 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -241,6 +241,11 @@ "label": "Habilitar efectos de sonido", "description": "Cuando está habilitado, Roo reproducirá efectos de sonido para notificaciones y eventos.", "volumeLabel": "Volumen" + }, + "tts": { + "label": "Habilitar texto a voz", + "description": "Cuando está habilitado, Roo leerá en voz alta sus respuestas usando texto a voz.", + "speedLabel": "Velocidad" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index cd957897811..b06265c95cf 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -241,6 +241,11 @@ "label": "Activer les effets sonores", "description": "Lorsque cette option est activée, Roo jouera des effets sonores pour les notifications et les événements.", "volumeLabel": "Volume" + }, + "tts": { + "label": "Activer la synthèse vocale", + "description": "Lorsque cette option est activée, Roo lira ses réponses à haute voix en utilisant la synthèse vocale.", + "speedLabel": "Vitesse" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index b05778af1dc..4c2abed9118 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -241,6 +241,11 @@ "label": "ध्वनि प्रभाव सक्षम करें", "description": "जब सक्षम होता है, तो Roo सूचनाओं और घटनाओं के लिए ध्वनि प्रभाव चलाएगा।", "volumeLabel": "वॉल्यूम" + }, + "tts": { + "label": "टेक्स्ट-टू-स्पीच सक्षम करें", + "description": "जब सक्षम होता है, तो Roo टेक्स्ट-टू-स्पीच का उपयोग करके अपनी प्रतिक्रियाओं को बोलकर पढ़ेगा।", + "speedLabel": "गति" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 37b5b8583d3..823a7e16d3d 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -241,6 +241,11 @@ "label": "Abilita effetti sonori", "description": "Quando abilitato, Roo riprodurrà effetti sonori per notifiche ed eventi.", "volumeLabel": "Volume" + }, + "tts": { + "label": "Abilita sintesi vocale", + "description": "Quando abilitato, Roo leggerà ad alta voce le sue risposte utilizzando la sintesi vocale.", + "speedLabel": "Velocità" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 14e93195f39..a0ff866923d 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -241,6 +241,11 @@ "label": "サウンドエフェクトを有効化", "description": "有効にすると、Rooは通知やイベントのためにサウンドエフェクトを再生します。", "volumeLabel": "音量" + }, + "tts": { + "label": "音声合成を有効化", + "description": "有効にすると、Rooは音声合成を使用して応答を音声で読み上げます。", + "speedLabel": "速度" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 369dd62a1a6..86dc687930d 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -241,6 +241,11 @@ "label": "사운드 효과 활성화", "description": "활성화되면 Roo는 알림 및 이벤트에 대한 사운드 효과를 재생합니다.", "volumeLabel": "볼륨" + }, + "tts": { + "label": "음성 합성 활성화", + "description": "활성화되면 Roo는 음성 합성을 사용하여 응답을 소리내어 읽습니다.", + "speedLabel": "속도" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 22c7d41fec8..a07db6fc2b8 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -241,6 +241,11 @@ "label": "Włącz efekty dźwiękowe", "description": "Gdy włączone, Roo będzie odtwarzać efekty dźwiękowe dla powiadomień i zdarzeń.", "volumeLabel": "Głośność" + }, + "tts": { + "label": "Włącz syntezę mowy", + "description": "Gdy włączone, Roo będzie czytać na głos swoje odpowiedzi za pomocą syntezy mowy.", + "speedLabel": "Szybkość" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 14ec9ee4802..e76a2dca43d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -241,6 +241,11 @@ "label": "Ativar efeitos sonoros", "description": "Quando ativado, o Roo reproduzirá efeitos sonoros para notificações e eventos.", "volumeLabel": "Volume" + }, + "tts": { + "label": "Ativar texto para fala", + "description": "Quando ativado, o Roo lerá em voz alta suas respostas usando texto para fala.", + "speedLabel": "Velocidade" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 9478b03e89e..b9e1400aed3 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -241,6 +241,11 @@ "label": "Ses efektlerini etkinleştir", "description": "Etkinleştirildiğinde, Roo bildirimler ve olaylar için ses efektleri çalacaktır.", "volumeLabel": "Ses Düzeyi" + }, + "tts": { + "label": "Metinden sese özelliğini etkinleştir", + "description": "Etkinleştirildiğinde, Roo yanıtlarını metinden sese teknolojisi kullanarak sesli okuyacaktır.", + "speedLabel": "Hız" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 103d3ace664..2bff3fb1f4a 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -241,6 +241,11 @@ "label": "Bật hiệu ứng âm thanh", "description": "Khi được bật, Roo sẽ phát hiệu ứng âm thanh cho thông báo và sự kiện.", "volumeLabel": "Âm lượng" + }, + "tts": { + "label": "Bật chuyển văn bản thành giọng nói", + "description": "Khi được bật, Roo sẽ đọc to các phản hồi của nó bằng chức năng chuyển văn bản thành giọng nói.", + "speedLabel": "Tốc độ" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 4bed5509598..c28550d9204 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -241,6 +241,11 @@ "label": "启用音效", "description": "启用后,Roo 将为通知和事件播放音效。", "volumeLabel": "音量" + }, + "tts": { + "label": "启用文本转语音", + "description": "启用后,Roo 将使用文本转语音功能朗读其响应。", + "speedLabel": "速度" } }, "contextManagement": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 7fe534bf3d2..cdbbd6d3100 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -241,6 +241,11 @@ "label": "啟用音效", "description": "啟用後,Roo 將為通知和事件播放音效。", "volumeLabel": "音量" + }, + "tts": { + "label": "啟用文字轉語音", + "description": "啟用後,Roo 將使用文字轉語音功能朗讀其回應。", + "speedLabel": "速度" } }, "contextManagement": { From 730548ef1b9edd2173d879ec90591acd33947b74 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 18 Mar 2025 01:44:35 -0400 Subject: [PATCH 10/10] Fix tests --- .../settings/NotificationSettings.tsx | 4 +++- .../settings/__tests__/SettingsView.test.tsx | 22 ++++++------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index 434d7c358b7..4dbb3ef36b1 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -37,7 +37,8 @@ export const NotificationSettings = ({
setCachedStateField("ttsEnabled", e.target.checked)}> + onChange={(e: any) => setCachedStateField("ttsEnabled", e.target.checked)} + data-testid="tts-enabled-checkbox"> {t("settings:notifications.tts.label")}

@@ -55,6 +56,7 @@ export const NotificationSettings = ({ onChange={(e) => setCachedStateField("ttsSpeed", parseFloat(e.target.value))} className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" aria-label="Speed" + data-testid="tts-speed-slider" /> {((ttsSpeed ?? 1.0) * 100).toFixed(0)}%

diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 08aa427fedc..eb1689eef98 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -153,13 +153,11 @@ describe("SettingsView - Sound Settings", () => { it("initializes with tts disabled by default", () => { renderSettingsView() - const ttsCheckbox = screen.getByRole("checkbox", { - name: /Enable text-to-speech/i, - }) + const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") expect(ttsCheckbox).not.toBeChecked() // Speed slider should not be visible when tts is disabled - expect(screen.queryByRole("slider", { name: /speed/i })).not.toBeInTheDocument() + expect(screen.queryByTestId("tts-speed-slider")).not.toBeInTheDocument() }) it("initializes with sound disabled by default", () => { @@ -175,9 +173,7 @@ describe("SettingsView - Sound Settings", () => { it("toggles tts setting and sends message to VSCode", () => { renderSettingsView() - const ttsCheckbox = screen.getByRole("checkbox", { - name: /Enable text-to-speech/i, - }) + const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") // Enable tts fireEvent.click(ttsCheckbox) @@ -220,13 +216,11 @@ describe("SettingsView - Sound Settings", () => { renderSettingsView() // Enable tts - const ttsCheckbox = screen.getByRole("checkbox", { - name: /Enable text-to-speech/i, - }) + const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") fireEvent.click(ttsCheckbox) // Speed slider should be visible - const speedSlider = screen.getByRole("slider", { name: /speed/i }) + const speedSlider = screen.getByTestId("tts-speed-slider") expect(speedSlider).toBeInTheDocument() expect(speedSlider).toHaveValue("1") }) @@ -248,13 +242,11 @@ describe("SettingsView - Sound Settings", () => { renderSettingsView() // Enable tts - const ttsCheckbox = screen.getByRole("checkbox", { - name: /Enable text-to-speech/i, - }) + const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") fireEvent.click(ttsCheckbox) // Change speed - const speedSlider = screen.getByRole("slider", { name: /speed/i }) + const speedSlider = screen.getByTestId("tts-speed-slider") fireEvent.change(speedSlider, { target: { value: "0.75" } }) // Click Save to save settings