Add text-to-speech functionality (#1412)

* Add text-to-speech functionality

* Add speed config option to text-to-speech

* Fix test case for tts speed slider

* Fix test case for tts speed slider (really)

* Disabled error message logging in tts.ts

* ignore markdown and mermaid diagrams in TTS

* add ttsEnabled and ttsSpeed to GlobalStateKey

* fix failing webview test for save button

* Translations

* Fix tests

---------

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
Seth Miller 2025-03-17 22:47:16 -07:00 committed by GitHub
parent eb74f02094
commit f16a49de87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 392 additions and 14 deletions

18
package-lock.json generated
View file

@ -48,6 +48,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"say": "^0.16.0",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"sound-play": "^1.1.0",
@ -12546,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",
@ -12827,6 +12827,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",
@ -14121,6 +14126,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",

View file

@ -359,6 +359,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"say": "^0.16.0",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"sound-play": "^1.1.0",

View file

@ -39,6 +39,7 @@ import { BrowserSession } from "../../services/browser/BrowserSession"
import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
import { fileExistsAtPath } from "../../utils/fs"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { playTts, setTtsEnabled, setTtsSpeed } from "../../utils/tts"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { searchCommits } from "../../utils/git"
import { getDiffStrategy } from "../diff/DiffStrategy"
@ -356,6 +357,11 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
setSoundEnabled(soundEnabled ?? false)
})
// Initialize tts enabled state
this.getState().then(({ ttsEnabled }) => {
setTtsEnabled(ttsEnabled ?? false)
})
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
@ -1233,6 +1239,23 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
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 "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)
}
break
case "diffEnabled":
const diffEnabled = message.bool ?? true
await this.updateGlobalState("diffEnabled", diffEnabled)
@ -2333,6 +2356,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
soundEnabled,
ttsEnabled,
ttsSpeed,
diffEnabled,
enableCheckpoints,
checkpointStorage,
@ -2392,6 +2417,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
.filter((item: HistoryItem) => item.ts && item.task)
.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",
@ -2551,6 +2578,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
taskHistory: stateValues.taskHistory,
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",

View file

@ -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"
@ -271,6 +272,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,
@ -506,6 +512,7 @@ describe("ClineProvider", () => {
alwaysAllowMcp: false,
uriScheme: "vscode",
soundEnabled: false,
ttsEnabled: false,
diffEnabled: false,
enableCheckpoints: false,
checkpointStorage: "task",
@ -603,6 +610,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")
})
@ -666,6 +674,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 10 seconds", async () => {

View file

@ -207,6 +207,8 @@ export type GlobalStateKey =
| "openRouterUseMiddleOutTransform"
| "googleGeminiBaseUrl"
| "allowedCommands"
| "ttsEnabled"
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "diffEnabled"

View file

@ -124,6 +124,8 @@ export interface ExtensionState {
currentTaskItem?: HistoryItem
allowedCommands?: string[]
soundEnabled?: boolean
ttsEnabled?: boolean
ttsSpeed?: number
soundVolume?: number
diffEnabled?: boolean
enableCheckpoints: boolean

View file

@ -50,7 +50,10 @@ export interface WebviewMessage {
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
| "playSound"
| "playTts"
| "soundEnabled"
| "ttsEnabled"
| "ttsSpeed"
| "soundVolume"
| "diffEnabled"
| "enableCheckpoints"

View file

@ -76,6 +76,8 @@ export const GLOBAL_STATE_KEYS = [
"googleGeminiBaseUrl",
"allowedCommands",
"soundEnabled",
"ttsEnabled",
"ttsSpeed",
"soundVolume",
"diffEnabled",
"enableCheckpoints",

75
src/utils/tts.ts Normal file
View file

@ -0,0 +1,75 @@
import * as vscode from "vscode"
let isTtsEnabled = false
let speed = 1.0
let isSpeaking = false
const utteranceQueue: string[] = []
/**
* Set tts configuration
* @param enabled boolean
*/
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
*/
const processQueue = async (): Promise<void> => {
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<void>((resolve, reject) => {
say.speak(nextUtterance, null, speed, (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<void> => {
if (!isTtsEnabled) {
return
}
try {
utteranceQueue.push(message)
await processQueue()
} catch (error: any) {
//vscode.window.showErrorMessage(error.message)
}
}

View file

@ -44,6 +44,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",
@ -19478,6 +19479,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",

View file

@ -52,6 +52,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",

View file

@ -31,6 +31,7 @@ import { validateCommand } from "../../utils/command-validation"
import { getAllModes } from "../../../../src/shared/modes"
import TelemetryBanner from "../common/TelemetryBanner"
import { useAppTranslation } from "@/i18n/TranslationContext"
import removeMd from "remove-markdown"
interface ChatViewProps {
isHidden: boolean
@ -91,6 +92,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const disableAutoScrollRef = useRef(false)
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(false)
const lastTtsRef = useRef<string>("")
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
const [showCheckpointWarning, setShowCheckpointWarning] = useState<boolean>(false)
@ -104,6 +106,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.
@ -674,6 +680,34 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
)
useEffect(() => {
// this ensures the first message is not read, future user messages are labelled as user_feedback
if (lastMessage && messages.length > 1) {
//console.log(JSON.stringify(lastMessage))
if (
lastMessage.text && // has text
(lastMessage.say === "text" || lastMessage.say === "completion_result") && // is a text message
!lastMessage.partial && // not a partial message
!lastMessage.text.startsWith("{") // not a json object
) {
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)
}
}
}
}
// Only execute when isStreaming changes from true to false
if (wasStreaming && !isStreaming && lastMessage) {
// Play appropriate sound based on lastMessage content
@ -706,7 +740,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

View file

@ -16,9 +16,9 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => {
const onCopy = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
const tempDiv = document.createElement('div');
tempDiv.innerHTML = itemTask;
const text = tempDiv.textContent || tempDiv.innerText || "";
const tempDiv = document.createElement("div")
tempDiv.innerHTML = itemTask
const text = tempDiv.textContent || tempDiv.innerText || ""
!isCopied && copy(text)
},
[isCopied, copy, itemTask],

View file

@ -8,12 +8,16 @@ import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
ttsEnabled?: boolean
ttsSpeed?: number
soundEnabled?: boolean
soundVolume?: number
setCachedStateField: SetCachedStateField<"soundEnabled" | "soundVolume">
setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume">
}
export const NotificationSettings = ({
ttsEnabled,
ttsSpeed,
soundEnabled,
soundVolume,
setCachedStateField,
@ -30,6 +34,38 @@ export const NotificationSettings = ({
</SectionHeader>
<Section>
<div>
<VSCodeCheckbox
checked={ttsEnabled}
onChange={(e: any) => setCachedStateField("ttsEnabled", e.target.checked)}
data-testid="tts-enabled-checkbox">
<span className="font-medium">{t("settings:notifications.tts.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:notifications.tts.description")}
</p>
{ttsEnabled && (
<div className="pl-[10px] ml-0 border-l-2 border-l-vscode-button-background">
<div className="flex items-center gap-[5px]">
<input
type="range"
min="0.1"
max="2.0"
step="0.01"
value={ttsSpeed ?? 1.0}
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"
/>
<span className="min-w-[35px] text-left">{((ttsSpeed ?? 1.0) * 100).toFixed(0)}%</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.tts.speedLabel")}
</p>
</div>
)}
</div>
<div>
<VSCodeCheckbox
checked={soundEnabled}
@ -41,13 +77,8 @@ export const NotificationSettings = ({
{t("settings:notifications.sound.description")}
</p>
{soundEnabled && (
<div
style={{
marginLeft: 0,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<div className="pl-[10px] ml-0 border-l-2 border-l-vscode-button-background">
<div className="flex items-center gap-[5px]">
<input
type="range"
min="0"
@ -59,7 +90,7 @@ export const NotificationSettings = ({
aria-label="Volume"
data-testid="sound-volume-slider"
/>
<span style={{ minWidth: "35px", textAlign: "left" }}>
<span className="min-w-[35px] text-left">
{((soundVolume ?? 0.5) * 100).toFixed(0)}%
</span>
</div>

View file

@ -95,6 +95,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
remoteBrowserHost,
screenshotQuality,
soundEnabled,
ttsEnabled,
ttsSpeed,
soundVolume,
telemetrySetting,
terminalOutputLineLimit,
@ -185,6 +187,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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: "ttsSpeed", value: ttsSpeed })
vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
@ -430,6 +434,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
<div ref={notificationsRef}>
<NotificationSettings
ttsEnabled={ttsEnabled}
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
setCachedStateField={setCachedStateField}

View file

@ -116,6 +116,8 @@ const mockPostMessage = (state: any) => {
shouldShowAnnouncement: false,
allowedCommands: [],
alwaysAllowExecute: false,
ttsEnabled: false,
ttsSpeed: 1,
soundEnabled: false,
soundVolume: 0.5,
...state,
@ -148,6 +150,16 @@ describe("SettingsView - Sound Settings", () => {
jest.clearAllMocks()
})
it("initializes with tts disabled by default", () => {
renderSettingsView()
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
expect(ttsCheckbox).not.toBeChecked()
// Speed slider should not be visible when tts is disabled
expect(screen.queryByTestId("tts-speed-slider")).not.toBeInTheDocument()
})
it("initializes with sound disabled by default", () => {
renderSettingsView()
@ -158,6 +170,27 @@ describe("SettingsView - Sound Settings", () => {
expect(screen.queryByTestId("sound-volume-slider")).not.toBeInTheDocument()
})
it("toggles tts setting and sends message to VSCode", () => {
renderSettingsView()
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
// Enable tts
fireEvent.click(ttsCheckbox)
expect(ttsCheckbox).toBeChecked()
// Click Save to save settings
const saveButton = screen.getByTestId("save-button")
fireEvent.click(saveButton)
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "ttsEnabled",
bool: true,
}),
)
})
it("toggles sound setting and sends message to VSCode", () => {
renderSettingsView()
@ -179,6 +212,19 @@ describe("SettingsView - Sound Settings", () => {
)
})
it("shows tts slider when sound is enabled", () => {
renderSettingsView()
// Enable tts
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
fireEvent.click(ttsCheckbox)
// Speed slider should be visible
const speedSlider = screen.getByTestId("tts-speed-slider")
expect(speedSlider).toBeInTheDocument()
expect(speedSlider).toHaveValue("1")
})
it("shows volume slider when sound is enabled", () => {
renderSettingsView()
@ -192,6 +238,28 @@ 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.getByTestId("tts-enabled-checkbox")
fireEvent.click(ttsCheckbox)
// Change speed
const speedSlider = screen.getByTestId("tts-speed-slider")
fireEvent.change(speedSlider, { target: { value: "0.75" } })
// Click Save to save settings
const saveButton = screen.getByTestId("save-button")
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()

View file

@ -35,6 +35,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setAllowedCommands: (value: string[]) => void
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
@ -115,6 +117,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
allowedCommands: [],
soundEnabled: false,
soundVolume: 0.5,
ttsEnabled: false,
ttsSpeed: 1.0,
diffEnabled: false,
enableCheckpoints: true,
checkpointStorage: "task",
@ -232,6 +236,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,
@ -257,6 +262,8 @@ 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 })),
setTtsSpeed: (value) => setState((prevState) => ({ ...prevState, ttsSpeed: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
setBrowserViewportSize: (value: string) =>

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -241,6 +241,11 @@
"label": "ध्वनि प्रभाव सक्षम करें",
"description": "जब सक्षम होता है, तो Roo सूचनाओं और घटनाओं के लिए ध्वनि प्रभाव चलाएगा।",
"volumeLabel": "वॉल्यूम"
},
"tts": {
"label": "टेक्स्ट-टू-स्पीच सक्षम करें",
"description": "जब सक्षम होता है, तो Roo टेक्स्ट-टू-स्पीच का उपयोग करके अपनी प्रतिक्रियाओं को बोलकर पढ़ेगा।",
"speedLabel": "गति"
}
},
"contextManagement": {

View file

@ -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": {

View file

@ -241,6 +241,11 @@
"label": "サウンドエフェクトを有効化",
"description": "有効にすると、Rooは通知やイベントのためにサウンドエフェクトを再生します。",
"volumeLabel": "音量"
},
"tts": {
"label": "音声合成を有効化",
"description": "有効にすると、Rooは音声合成を使用して応答を音声で読み上げます。",
"speedLabel": "速度"
}
},
"contextManagement": {

View file

@ -241,6 +241,11 @@
"label": "사운드 효과 활성화",
"description": "활성화되면 Roo는 알림 및 이벤트에 대한 사운드 효과를 재생합니다.",
"volumeLabel": "볼륨"
},
"tts": {
"label": "음성 합성 활성화",
"description": "활성화되면 Roo는 음성 합성을 사용하여 응답을 소리내어 읽습니다.",
"speedLabel": "속도"
}
},
"contextManagement": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -241,6 +241,11 @@
"label": "启用音效",
"description": "启用后Roo 将为通知和事件播放音效。",
"volumeLabel": "音量"
},
"tts": {
"label": "启用文本转语音",
"description": "启用后Roo 将使用文本转语音功能朗读其响应。",
"speedLabel": "速度"
}
},
"contextManagement": {

View file

@ -241,6 +241,11 @@
"label": "啟用音效",
"description": "啟用後Roo 將為通知和事件播放音效。",
"volumeLabel": "音量"
},
"tts": {
"label": "啟用文字轉語音",
"description": "啟用後Roo 將使用文字轉語音功能朗讀其回應。",
"speedLabel": "速度"
}
},
"contextManagement": {