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
This commit is contained in:
chyuwei 2026-05-23 22:36:08 +08:00 committed by Teknium
parent 2c34a7da87
commit 7de8cd4c5f
4 changed files with 15 additions and 4 deletions

View file

@ -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,
{

View file

@ -216,6 +216,7 @@ export interface InputHandlerContext {
setProcessing: StateSetter<boolean>
setRecording: StateSetter<boolean>
setVoiceEnabled: StateSetter<boolean>
setVoiceTts: StateSetter<boolean>
}
wheelStep: number
}
@ -254,6 +255,7 @@ export interface GatewayEventHandlerContext {
setProcessing: StateSetter<boolean>
setRecording: StateSetter<boolean>
setVoiceEnabled: StateSetter<boolean>
setVoiceTts: StateSetter<boolean>
}
}
@ -296,6 +298,7 @@ export interface SlashHandlerContext {
voice: {
setVoiceEnabled: StateSetter<boolean>
setVoiceRecordKey: (v: ParsedVoiceRecordKey) => void
setVoiceTts: StateSetter<boolean>
}
}

View file

@ -232,6 +232,7 @@ export const sessionCommands: SlashCommand[] = [
ctx.gateway.rpc<VoiceToggleResponse>('voice.toggle', { action }).then(
ctx.guarded<VoiceToggleResponse>(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

View file

@ -102,6 +102,7 @@ export function useMainApp(gw: GatewayClient) {
const [stickyPrompt, setStickyPrompt] = useState('')
const [catalog, setCatalog] = useState<null | SlashCatalog>(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<ParsedVoiceRecordKey>(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
]
)