diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b341ea..2cc409e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Hermes Web UI -- Changelog +## [v0.50.271] — 2026-05-02 + +### Changed (1 self-built PR) + +- **Composer voice buttons: distinct icon, distinct labels, opt-in voice mode** (#1488, self-built, closes #1488) — the composer footer rendered two near-identical mic icons whose tooltips both said "Voice input": one was push-to-talk dictation (older feature), the other was turn-based hands-free voice mode (newer). After researching how ChatGPT, Claude, and Gemini handle the same problem, this PR adopts the industry convention: **mic = dictation, audio-waveform = voice mode**. (1) Voice-mode button now uses Lucide's `audio-lines` glyph (six vertical bars of varying height — the universal "two-way voice conversation" icon, also registered in `LI_PATHS` for reuse). (2) Distinct, localized tooltips: `voice_dictate: 'Dictate'` (with `voice_dictate_active: 'Stop dictation'` flip-state) and `voice_mode_toggle: 'Voice mode'` (with `voice_mode_toggle_active: 'Exit voice mode'` flip-state). The legacy `voice_toggle` key (which resolved to "Voice input" in every locale and caused the duplicate-tooltip bug) is removed. (3) Voice mode is now **opt-in** via Settings → Preferences → "Hands-free voice mode button" — default off keeps the composer uncluttered for the broad-majority case (plain dictation only). The dictation mic stays visible by default, unchanged. Toggle is `localStorage`-backed (`hermes-voice-mode-button`), and `panels.js`'s onchange handler calls `window._applyVoiceModePref()` so the audio-waveform button appears/disappears immediately with no reload. 17 new regression tests in `tests/test_issue1488_composer_voice_buttons.py` pin: distinct static + i18n titles, audio-lines glyph shape (≥5 vertical-bar paths, no leftover mic-with-sparkles rect), all 4 new keys in all 9 locales, removal of stale `voice_toggle`, English labels match ChatGPT/Gemini convention, pref gating (no unconditional `display=''` left in boot.js), Settings checkbox + i18n, panels.js wiring, and active-state tooltip flips. Browser-verified end-to-end on port 8789 (default 1 mic / pref-on 2 distinct icons / live re-apply via Settings). (`static/index.html`, `static/icons.js`, `static/i18n.js`, `static/boot.js`, `static/panels.js`, `tests/test_issue1488_composer_voice_buttons.py`) + ## [v0.50.270] — 2026-05-02 ### Fixed (1 contributor PR) diff --git a/ROADMAP.md b/ROADMAP.md index 5d699648..72b3ab86 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI. > Everything you can do from the CLI terminal, you can do from this UI. > -> Last updated: v0.50.270 (May 02, 2026) — 3849 tests collected +> Last updated: v0.50.271 (May 02, 2026) — 3866 tests collected > Tests: `pytest tests/ --collect-only -q` > Source: / diff --git a/TESTING.md b/TESTING.md index 27618f6a..cc4366ee 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1835,8 +1835,8 @@ Bridged CLI sessions: --- -*Last updated: v0.50.270, May 02, 2026* -*Total automated tests collected: 3849* +*Last updated: v0.50.271, May 02, 2026* +*Total automated tests collected: 3866* *Regression gate: tests/test_regressions.py* *Run: pytest tests/ -v --timeout=60* *Source: /* diff --git a/static/boot.js b/static/boot.js index 43629350..ff420dd8 100644 --- a/static/boot.js +++ b/static/boot.js @@ -236,6 +236,9 @@ $('btnAttach').onclick=()=>$('fileInput').click(); function _setRecording(on){ window._micActive=on; btn.classList.toggle('recording',on); + // Active-state title flips so the tooltip is honest about what + // pressing the button will do (#1488). + btn.title = on ? t('voice_dictate_active') : t('voice_dictate'); status.style.display=on?'':'none'; if(statusText) statusText.textContent=on?'Listening':'Listening'; if(!on){ _finalText=''; _prefix=''; } @@ -429,8 +432,21 @@ window._micPendingSend=window._micPendingSend||false; if(!modeBtn||!bar||!indicator||!label) return; - // Show the voice mode button — browser supports both STT and TTS - modeBtn.style.display=''; + // Voice-mode button is gated behind a Preferences toggle (#1488). + // Default off — keeps the composer footer uncluttered for users who + // only need plain dictation. The hands-free conversation feature is + // a power-user surface; explicit opt-in avoids the visual confusion + // of two near-identical mic icons. + function _voiceModePrefEnabled(){ + try{ return localStorage.getItem('hermes-voice-mode-button')==='true'; } + catch(_){ return false; } + } + function _applyVoiceModePref(){ + modeBtn.style.display = _voiceModePrefEnabled() ? '' : 'none'; + } + _applyVoiceModePref(); + // Expose so the settings pane can re-apply immediately on toggle. + window._applyVoiceModePref = _applyVoiceModePref; let _voiceModeActive=false; let _voiceModeState='idle'; // idle | listening | thinking | speaking @@ -643,7 +659,7 @@ window._micPendingSend=window._micPendingSend||false; function _activate(){ _voiceModeActive=true; modeBtn.classList.add('active'); - modeBtn.title=t('voice_mode_active'); + modeBtn.title=t('voice_mode_toggle_active'); showToast(t('voice_mode_active'),1500); // If the agent is busy, wait — state will be 'thinking' and we'll detect completion if(typeof S!=='undefined'&&S.busy){ @@ -660,7 +676,7 @@ window._micPendingSend=window._micPendingSend||false; _voiceModeState='idle'; _voiceModeThinkingSid=null; modeBtn.classList.remove('active'); - modeBtn.title=t('voice_toggle'); + modeBtn.title=t('voice_mode_toggle'); bar.style.display='none'; clearTimeout(_silenceTimer); try{ if(_recognition) _recognition.abort(); }catch(_){} diff --git a/static/i18n.js b/static/i18n.js index d2c64725..fdcbc42f 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -15,8 +15,12 @@ const LOCALES = { mic_no_speech: 'No speech detected. Try again.', mic_network: 'Speech recognition unavailable.', mic_error: 'Voice input error: ', + // Composer voice buttons (#1488 — distinct labels for dictation vs voice mode) + voice_dictate: 'Dictate', + voice_dictate_active: 'Stop dictation', + voice_mode_toggle: 'Voice mode', + voice_mode_toggle_active: 'Exit voice mode', // Turn-based voice mode (#1333) - voice_toggle: 'Voice input', voice_listening: 'Listening…', voice_speaking: 'Speaking…', voice_thinking: 'Thinking…', @@ -494,6 +498,9 @@ const LOCALES = { settings_desc_tts: "Show a speaker button on each assistant message to read it aloud using your browser's speech synthesis.", settings_label_tts_auto_read: 'Auto-read responses aloud', settings_desc_tts_auto_read: 'Automatically speak each new assistant response when it finishes. Pauses when you start typing.', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', settings_label_tts_voice: 'Voice', settings_desc_tts_voice: "Preferred voice. Populated from your browser's available voices.", settings_label_tts_rate: 'Speech rate', @@ -887,7 +894,11 @@ const LOCALES = { mic_network: '音声認識を利用できません。', mic_error: '音声入力エラー: ', // Turn-based voice mode (#1333) - voice_toggle: '音声入力', + // Composer voice buttons (#1488) + voice_dictate: 'ディクテーション', + voice_dictate_active: 'ディクテーション停止', + voice_mode_toggle: '音声モード', + voice_mode_toggle_active: '音声モードを終了', voice_listening: '聞き取り中…', voice_speaking: '発話中…', voice_thinking: '考え中…', @@ -1365,6 +1376,9 @@ const LOCALES = { settings_desc_tts: 'アシスタントの各メッセージにスピーカーボタンを表示し、ブラウザの音声合成で読み上げます。', settings_label_tts_auto_read: '応答を自動で読み上げ', settings_desc_tts_auto_read: '新しいアシスタント応答が完了するたびに自動で読み上げます。入力中は一時停止します。', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'ハンズフリー音声モードのボタン', + settings_desc_voice_mode: '音声波形ボタンをディクテーションマイクの隣に表示します。発話の合間に自動送信し、返答を読み上げます。音声認識と TTS の両方をサポートするブラウザが必要です。', settings_label_tts_voice: '声', settings_desc_tts_voice: '優先する声。ブラウザで利用可能な声から選択されます。', settings_label_tts_rate: '読み上げ速度', @@ -1756,7 +1770,11 @@ const LOCALES = { mic_no_speech: 'Речь не распознана. Попробуйте ещё раз.', mic_network: 'Распознавание речи недоступно.', mic_error: 'Ошибка ввода речи: ', - voice_toggle: 'Голосовой ввод', + // Composer voice buttons (#1488) + voice_dictate: 'Диктовка', + voice_dictate_active: 'Остановить диктовку', + voice_mode_toggle: 'Голосовой режим', + voice_mode_toggle_active: 'Выйти из голосового режима', voice_listening: 'Слушаю…', voice_speaking: 'Говорю…', voice_thinking: 'Думаю…', @@ -2517,6 +2535,9 @@ const LOCALES = { settings_desc_tts: 'Показать кнопку динамика на сообщениях ассистента', settings_label_tts_auto_read: 'Авто-чтение ответов', settings_desc_tts_auto_read: 'Автоматически озвучивать ответы ассистента', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Кнопка режима свободных рук', + settings_desc_voice_mode: 'Показывать кнопку голосового режима (аудиоволны) рядом с микрофоном диктовки. Hermes автоматически отправляет реплики после паузы и зачитывает ответы вслух. Требуется браузер с поддержкой распознавания речи и TTS.', settings_label_tts_voice: 'Голос', settings_desc_tts_voice: 'Выберите голос для синтеза речи', settings_label_tts_rate: 'Скорость речи', @@ -3316,6 +3337,9 @@ const LOCALES = { settings_desc_tts: 'Mostrar botón de altavoz en mensajes del asistente', settings_label_tts_auto_read: 'Leer respuestas automáticamente', settings_desc_tts_auto_read: 'Leer en voz alta las respuestas del asistente automáticamente', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: 'Voz', settings_desc_tts_voice: 'Seleccionar voz para síntesis de voz', settings_label_tts_rate: 'Velocidad de voz', @@ -3358,7 +3382,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate }, @@ -4124,6 +4152,9 @@ const LOCALES = { settings_desc_tts: 'Lautsprecher-Symbol auf Assistenten-Nachrichten anzeigen', settings_label_tts_auto_read: 'Antworten automatisch vorlesen', settings_desc_tts_auto_read: 'Assistenten-Antworten automatisch vorlesen', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: 'Stimme', settings_desc_tts_voice: 'Stimme für Sprachsynthese auswählen', settings_label_tts_rate: 'Sprechgeschwindigkeit', @@ -4167,7 +4198,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate }, @@ -4928,6 +4963,9 @@ const LOCALES = { settings_desc_tts: '在助手消息上显示扬声器按钮', settings_label_tts_auto_read: '自动朗读回复', settings_desc_tts_auto_read: '自动朗读助手回复', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: '语音', settings_desc_tts_voice: '选择语音合成声音', settings_label_tts_rate: '语速', @@ -4970,7 +5008,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate }, @@ -5838,6 +5880,9 @@ const LOCALES = { settings_desc_tts: '在助手訊息上顯示喇叭按鈕', settings_label_tts_auto_read: '自動朗讀回覆', settings_desc_tts_auto_read: '自動朗讀助手回覆', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: '語音', settings_desc_tts_voice: '選擇語音合成聲音', settings_label_tts_rate: '語速', @@ -5881,7 +5926,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate }, @@ -6561,6 +6610,9 @@ const LOCALES = { settings_desc_tts: 'Mostrar botão de alto-falante nas mensagens do assistente', settings_label_tts_auto_read: 'Ler respostas automaticamente', settings_desc_tts_auto_read: 'Ler automaticamente as respostas do assistente', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: 'Voz', settings_desc_tts_voice: 'Selecionar voz para síntese de voz', settings_label_tts_rate: 'Velocidade da fala', @@ -6603,7 +6655,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate // login-flow keys (issue #1442) sign_out_failed: 'Falha ao sair: ', @@ -7421,6 +7477,9 @@ const LOCALES = { settings_desc_tts: '도움말 메시지에 스피커 버튼 표시', settings_label_tts_auto_read: '답변 자동 읽기', settings_desc_tts_auto_read: '도움말 답변을 자동으로 읽어줌', + // Composer voice-mode pref (#1488) + settings_label_voice_mode: 'Hands-free voice mode button', // TODO: translate + settings_desc_voice_mode: 'Show the voice-mode button (audio waveform) next to the dictation mic. Lets you speak naturally — Hermes auto-sends after a pause and reads replies aloud. Requires a browser that supports both speech recognition and TTS.', // TODO: translate settings_label_tts_voice: '음성', settings_desc_tts_voice: '음성 합성 음성 선택', settings_label_tts_rate: '말 속도', @@ -7463,7 +7522,11 @@ const LOCALES = { voice_mode_off: 'Voice mode off', // TODO: translate voice_speaking: 'Speaking…', // TODO: translate voice_thinking: 'Thinking…', // TODO: translate - voice_toggle: 'Voice input', // TODO: translate + // Composer voice buttons (#1488) + voice_dictate: 'Dictate', // TODO: translate + voice_dictate_active: 'Stop dictation', // TODO: translate + voice_mode_toggle: 'Voice mode', // TODO: translate + voice_mode_toggle_active: 'Exit voice mode', // TODO: translate subagent_children: 'Subagent sessions', // TODO: translate }, }; diff --git a/static/icons.js b/static/icons.js index e13feace..10e5eb78 100644 --- a/static/icons.js +++ b/static/icons.js @@ -64,6 +64,8 @@ const LI_PATHS = { 'git-branch': '', // Audio / TTS 'volume-2': '', + // Voice-mode button — universal "two-way voice conversation" glyph (matches ChatGPT/Gemini) + 'audio-lines': '', // Queue pill chevron (ui.js queue indicator) 'chevron-up': '', // Insights panel stat cards (panels.js) diff --git a/static/index.html b/static/index.html index 87d1d1e7..08ebb423 100644 --- a/static/index.html +++ b/static/index.html @@ -389,7 +389,7 @@ - -