From dc4c2414f94c758da7bf4fdb77339e0cf149a07d Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 27 Jun 2026 00:25:21 -0500 Subject: [PATCH] fix(desktop): re-arm wake detector after a manual voice end Ending a voice conversation manually left the wake detector paused for good, so the wake word couldn't be used again. The composer paused the detector on voice start but only resumed on the voiceConversationActive -> false render; if ending voice tore the composer down first, that render never landed and the resume was skipped. Resume on unmount as well (latched on wakePausedRef so it fires exactly once), and stop early-returning when the $gateway atom is momentarily null. Add wake.pause/resume INFO logs for visibility. --- .../chat/composer/hooks/use-composer-voice.ts | 23 +++++++++++-------- tui_gateway/server.py | 8 +++++-- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/hooks/use-composer-voice.ts b/apps/desktop/src/app/chat/composer/hooks/use-composer-voice.ts index 5817c502ca01..e2bc1b0382c9 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-composer-voice.ts +++ b/apps/desktop/src/app/chat/composer/hooks/use-composer-voice.ts @@ -154,22 +154,25 @@ export function useComposerVoice({ ) const wakePausedRef = useRef(false) - - useEffect(() => { - const gateway = $gateway.get() - - if (!gateway) { + const resumeWakeIfPaused = useCallback(() => { + if (!wakePausedRef.current) { return } + wakePausedRef.current = false + void $gateway.get()?.request('wake.resume', {}).catch(() => undefined) + }, []) + + useEffect(() => { if (voiceConversationActive) { wakePausedRef.current = true - void gateway.request('wake.pause', {}).catch(() => undefined) - } else if (wakePausedRef.current) { - wakePausedRef.current = false - void gateway.request('wake.resume', {}).catch(() => undefined) + void $gateway.get()?.request('wake.pause', {}).catch(() => undefined) + } else { + resumeWakeIfPaused() } - }, [voiceConversationActive]) + }, [resumeWakeIfPaused, voiceConversationActive]) + + useEffect(() => resumeWakeIfPaused, [resumeWakeIfPaused]) // Explicit start/end for the on-screen conversation controls (the hotkey uses // the gated toggle above). diff --git a/tui_gateway/server.py b/tui_gateway/server.py index d679324b1d47..5f637863ab88 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -17712,8 +17712,9 @@ def _(rid, params: dict) -> dict: try: from tools.wake_word import pause_listening pause_listening() - except Exception: - pass + logger.info("wake.pause: detector paused") + except Exception as e: + logger.debug("wake.pause failed: %s", e) return _ok(rid, {"paused": True}) @@ -17723,6 +17724,9 @@ def _(rid, params: dict) -> dict: active = _wake_is_active() if active: _wake_resume_if_active() + logger.info("wake.resume: detector resumed") + else: + logger.info("wake.resume: ignored (listener not armed)") return _ok(rid, {"resumed": active})