Skip to content

fix: voice-mode pref toggle-off now stops the recognizer (#1491)#1518

Merged
1 commit merged intonesquena:masterfrom
franksong2702:fix/1491-voice-pref-toggle-deactivate
May 3, 2026
Merged

fix: voice-mode pref toggle-off now stops the recognizer (#1491)#1518
1 commit merged intonesquena:masterfrom
franksong2702:fix/1491-voice-pref-toggle-deactivate

Conversation

@franksong2702
Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI has a "Hands-free voice mode" button (PR fix(composer): distinct voice-mode icon, descriptive labels, opt-in pref (#1488) #1489, v0.50.271) gated behind a Settings → Preferences toggle
  • _applyVoiceModePref() controls button visibility but doesn't stop the SpeechRecognition when toggled off mid-session
  • If a user enables voice mode, starts a conversation, then opens Settings and disables the pref, the button disappears but the recognizer keeps running — the user has no way to stop it
  • The fix: _applyVoiceModePref() now checks if voice mode is active and calls _deactivate() when the pref is toggled off
  • _voiceModeActive must be declared before _applyVoiceModePref() to avoid Temporal Dead Zone (TDZ) — the variable was previously declared after the function

What Changed

static/boot.js — 3 changes:

  1. Move let _voiceModeActive=false above _applyVoiceModePref() (was below, causing TDZ risk)
  2. _applyVoiceModePref() now reads the pref into a local enabled variable and calls _deactivate() when !enabled && _voiceModeActive
  3. Remove duplicate window._applyVoiceModePref = _applyVoiceModePref assignment

Why It Matters

  • A running SpeechRecognition instance consumes microphone access and battery
  • The user can't stop it because the button is hidden — they'd have to reload the page
  • The _deactivate() function (line 706) properly cleans up: stops recognition, clears timers, restores TTS, resets UI state

Verification

  • _deactivate is a function declaration (hoisted) — safe to call from _applyVoiceModePref despite being defined later
  • 21/21 voice-mode-related tests pass (2 pre-existing failures in test_v050255_opus_followups.py are Python 3.9 int | None syntax, unrelated)
  • The pref toggle in Settings → Preferences calls window._applyVoiceModePref() via panels.js onchange handler — no changes needed there

Risks / Follow-ups

  • Risk: None. _deactivate() is already the standard cleanup path for voice mode (called on button click, error, etc.). Calling it from the pref toggle is the same code path.
  • Follow-up: None needed. The fix is complete and scoped to the exact bug.

Model Used

  • xiaomi/mimo-v2.5-pro (via Xiaomi MiMo)

When a user disables 'Hands-free voice mode' in Settings while voice
mode is active, the button hides but the SpeechRecognition keeps
running — the user can't stop it because the button is invisible.

Fix: _applyVoiceModePref() now checks if voice mode is active and
calls _deactivate() when the pref is toggled off. Move
_voiceModeActive declaration above the function to avoid TDZ.

Also removes a duplicate window._applyVoiceModePref assignment.
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Confirmed the TDZ risk in static/boot.js:

  • _applyVoiceModePref() is defined at line 473
  • _applyVoiceModePref() is called at line 476
  • let _voiceModeActive=false is declared at line 480 — after the call

The call at line 476 happens to work today because the current body of _applyVoiceModePref() doesn't read _voiceModeActive — it only reads the pref and toggles button visibility. But the moment we add the deactivate-on-toggle-off logic this PR introduces, that read becomes a TDZ ReferenceError. Reordering the let declaration above the function is the correct prerequisite, not just hygiene.

Behavioral fix is right too: if the pref is toggled off while _voiceModeActive===true, calling _deactivate() is the only way to actually release the SpeechRecognition handle, the timers, and the mic permission. Without it the user has no way out short of a page reload — exactly the state #1491 describes.

Things I'll spot-check on review:

  1. _deactivate() hoisting — line ~706, declared as function _deactivate(){...} (function declaration, not arrow), so calling it from the now-earlier _applyVoiceModePref is safe via hoisting. Confirmed from the grep above (line 232 already calls window._voiceModeActive defensively).
  2. Duplicate window._applyVoiceModePref removal — there's an assignment at line 478. If this PR removes a later duplicate I want to make sure the line-478 export is still there for the Settings panel to bind against.
  3. Pref toggle-back-on path — when the user re-enables the pref, the button reappears but does the recognizer auto-start, or do they need to click it? The latter is more conservative; the former matches "session is still hands-free."

Pulling locally to verify on a live Settings → Preferences toggle. Thanks @franksong2702.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in f8ed6da May 3, 2026
sunnysktsang pushed a commit to sunnysktsang/hermes-webui that referenced this pull request May 3, 2026
sunnysktsang pushed a commit to sunnysktsang/hermes-webui that referenced this pull request May 3, 2026
…sorbed

CHANGELOG, ROADMAP, TESTING bumped (3936 \u2192 3946).

8 constituent PRs:
- nesquena#1523 (@franksong2702) branch indicator codepoint fix
- nesquena#1519 (@franksong2702) onboarding API-key focus loss fix
- nesquena#1518 (@franksong2702) voice-mode toggle-off recognizer stop
- nesquena#1516 (@franksong2702) YAML newline CSS rules
- nesquena#1517 (@franksong2702) __CACHE_VERSION__ \u2192 __WEBUI_VERSION__ rename
- nesquena#1532 (@ai-ag2026) state.db WebUI session recovery
- nesquena#1525 (@ai-ag2026) stale stream state proactive cleanup
- nesquena#1526 (@ai-ag2026) max_tokens forwarding + OpenRouter quota classifier

Opus MUST-FIX absorbed: sw.js conflict-marker cleanup + regression guard.
Opus SHOULD-FIX deferred to follow-up nesquena#1533 (race in _clear_stale_stream_state).

2 closed as duplicates: nesquena#1528 (identical to nesquena#1517), nesquena#1529 (superseded by nesquena#1516).
1 maintainer-review label: nesquena#1531 (Asunfly stowaway change in force-push).
5 stay on hold: nesquena#1418 nesquena#1464 nesquena#1404 nesquena#1353 nesquena#1311.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants