From 7de8cd4c5f67b5b17e64392fac5fdf1b3a986983 Mon Sep 17 00:00:00 2001 From: chyuwei Date: Sat, 23 May 2026 22:36:08 +0800 Subject: [PATCH] fix(tui): clear TTS env var on voice off, and add TTS indicator to status bar Bug 1: /voice off in TUI mode did not clear HERMES_VOICE_TTS, leaving TTS stuck ON with no way to disable it (the voice.toggle tts handler requires voice mode to be ON). Bug 2: TUI status bar only showed 'voice on/off' without any indication of whether TTS speech output is active, because the frontend never tracked voiceTts state. - tui_gateway/server.py: clear HERMES_VOICE_TTS when voice is turned off - ui-tui/src/app/useMainApp.ts: add voiceTts state, thread setVoiceTts through voice contexts, display [tts] in status bar - ui-tui/src/app/slash/commands/session.ts: sync tts from voice.toggle response - ui-tui/src/app/interfaces.ts: add setVoiceTts to all voice context interfaces --- tui_gateway/server.py | 3 +++ ui-tui/src/app/interfaces.ts | 3 +++ ui-tui/src/app/slash/commands/session.ts | 1 + ui-tui/src/app/useMainApp.ts | 12 ++++++++---- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 67b1f96d264..35b13a65914 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -5869,6 +5869,9 @@ def _(rid, params: dict) -> dict: except Exception as e: logger.warning("voice: stop_continuous failed during toggle off: %s", e) + # Clear TTS so it can be toggled independently after voice is off. + os.environ["HERMES_VOICE_TTS"] = "0" + return _ok( rid, { diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index b71e34188ef..cb2788bbf4f 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -216,6 +216,7 @@ export interface InputHandlerContext { setProcessing: StateSetter setRecording: StateSetter setVoiceEnabled: StateSetter + setVoiceTts: StateSetter } wheelStep: number } @@ -254,6 +255,7 @@ export interface GatewayEventHandlerContext { setProcessing: StateSetter setRecording: StateSetter setVoiceEnabled: StateSetter + setVoiceTts: StateSetter } } @@ -296,6 +298,7 @@ export interface SlashHandlerContext { voice: { setVoiceEnabled: StateSetter setVoiceRecordKey: (v: ParsedVoiceRecordKey) => void + setVoiceTts: StateSetter } } diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 30ae7d8204e..fb990ef11be 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -232,6 +232,7 @@ export const sessionCommands: SlashCommand[] = [ ctx.gateway.rpc('voice.toggle', { action }).then( ctx.guarded(r => { ctx.voice.setVoiceEnabled(!!r.enabled) + ctx.voice.setVoiceTts(!!r.tts) // Render the configured record key (config.yaml ``voice.record_key``) // instead of hardcoded "Ctrl+B" — the gateway response carries the diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 71768bc2b0a..fde5231c278 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -102,6 +102,7 @@ export function useMainApp(gw: GatewayClient) { const [stickyPrompt, setStickyPrompt] = useState('') const [catalog, setCatalog] = useState(null) const [voiceEnabled, setVoiceEnabled] = useState(false) + const [voiceTts, setVoiceTts] = useState(false) const [voiceRecording, setVoiceRecording] = useState(false) const [voiceProcessing, setVoiceProcessing] = useState(false) const [voiceRecordKey, setVoiceRecordKey] = useState(DEFAULT_VOICE_RECORD_KEY) @@ -555,7 +556,8 @@ export function useMainApp(gw: GatewayClient) { recording: voiceRecording, setProcessing: setVoiceProcessing, setRecording: setVoiceRecording, - setVoiceEnabled + setVoiceEnabled, + setVoiceTts }, wheelStep: WHEEL_SCROLL_STEP }) @@ -579,7 +581,8 @@ export function useMainApp(gw: GatewayClient) { voice: { setProcessing: setVoiceProcessing, setRecording: setVoiceRecording, - setVoiceEnabled + setVoiceEnabled, + setVoiceTts } }), [ @@ -830,7 +833,7 @@ export function useMainApp(gw: GatewayClient) { turnStartedAt: ui.sid ? turnStartedAt : null, // CLI parity: the classic prompt_toolkit status bar shows a red dot // on REC (cli.py:_get_voice_status_fragments line 2344). - voiceLabel: voiceRecording ? '● REC' : voiceProcessing ? '◉ STT' : `voice ${voiceEnabled ? 'on' : 'off'}` + voiceLabel: voiceRecording ? '● REC' : voiceProcessing ? '◉ STT' : `voice ${voiceEnabled ? 'on' : 'off'}${voiceTts ? ' [tts]' : ''}` }), [ cwd, @@ -842,7 +845,8 @@ export function useMainApp(gw: GatewayClient) { ui, voiceEnabled, voiceProcessing, - voiceRecording + voiceRecording, + voiceTts ] )