diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f5c694b42..17c9bfd9c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -764,8 +764,9 @@ again once the trays close and the pointer leaves the wings. Panel height animat other or the voice view changes. Minimized voice shows the last sentence from each side, and the empty Voice Preview suggests things to say until voice starts. Settings opened from chat keep chat's frame and show the shared AI -provider and the quick-commands option, with Back to chat; voice Settings show -Back to voice. Escape steps back from either Settings view, closes Voice +provider, the quick-commands option and then the voice preferences (Voice +Preview, inactivity timeout and model tier), so they stay in reach while +Preview is off, with Back to chat; voice Settings show Back to voice. Escape steps back from either Settings view, closes Voice Preview, and does nothing in chat. Closing chat ends a typed session, and the voice panel's close turns voice off. Asking the assistant from chat to open Voice Settings or Voice Preview puts chat away, minimized or not, and shows the diff --git a/src/ui/styles/ai-chat.css b/src/ui/styles/ai-chat.css index 990407f51..83dcdefb4 100644 --- a/src/ui/styles/ai-chat.css +++ b/src/ui/styles/ai-chat.css @@ -1242,14 +1242,20 @@ body[data-assistant-docked='true'] #right-context-rail { display: none; } -/* Settings opened from chat: same frame as chat, chat options only. */ +/* Settings opened from chat: same frame as chat, chat options first, then + the voice preferences, the only pointer route to them while Preview is off. */ #command-dock #gev-voice-control #gev-voice-panel[data-context='chat'] - :is(.gev-voice-panel-tabs, .gev-panel-layout, #gev-settings-voice), + :is(.gev-voice-panel-tabs, .gev-panel-layout), #gev-voice-panel:not([data-context='chat']) #gev-settings-chat { display: none; } +#gev-voice-panel[data-context='chat'] #gev-settings-voice { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--glass-border); +} #gev-voice-control #gev-voice-panel[data-context='chat'][data-placement='compact']:not( [data-minimized] diff --git a/src/ui/views/panels/VoiceControl.vue b/src/ui/views/panels/VoiceControl.vue index 43118c5a3..8c1144e52 100644 --- a/src/ui/views/panels/VoiceControl.vue +++ b/src/ui/views/panels/VoiceControl.vue @@ -125,6 +125,10 @@ const frameStyle = (panel) => ({

{{ t('The selected AI provider is shared by voice and chat.') }}

+
+ +

{{ t('Chat replies are silent. No microphone access is needed.') }}

+

{{ view.preferences.previewNote }}

@@ -152,10 +156,6 @@ const frameStyle = (panel) => ({
-
- -

{{ t('Chat replies are silent. No microphone access is needed.') }}

-
diff --git a/src/voice/panel/host.test.mjs b/src/voice/panel/host.test.mjs index 3c7a8bbd5..7b7e33347 100644 --- a/src/voice/panel/host.test.mjs +++ b/src/voice/panel/host.test.mjs @@ -1,6 +1,7 @@ import '../../testSupport/registerDom.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; import { createPinia } from 'pinia'; import { createVoiceControl } from '../control.js'; import { useVoiceStore } from '../../ui/stores/voice.js'; @@ -1007,6 +1008,61 @@ test('chat Settings keep chat context until the panel closes', async (t) => { assert.equal(f.ui.state.chat.backVisible, false); }); +// Shown once neither the element nor an ancestor is hidden by the page's +// chat stylesheet or a hidden attribute. +function shown(selector) { + for (let node = $(selector); node?.nodeType === 1; node = node.parentElement) + if (node.hidden || globalThis.getComputedStyle(node).display === 'none') + return false; + return true; +} + +test('with Preview off, chat Settings reach the voice preferences', async (t) => { + const style = globalThis.document.createElement('style'); + style.textContent = readFileSync( + new URL('../../ui/styles/ai-chat.css', import.meta.url), + 'utf8', + ); + globalThis.document.head.append(style); + t.after(() => style.remove()); + const f = mounted(t, { view: true }); + // The control renders into the command dock, as the page does. + globalThis.document.body.firstElementChild.id = 'command-dock'; + f.ui.emit('preview-change', false); + f.commands.voicePanel.close(); + await f.settle(); + $('#gev-chat-button').click(); + await f.settle(); + $('#gev-voice-settings-button').click(); + await f.settle(); + assert.equal(f.ui.state.voicePanel.context, 'chat'); + for (const selector of [ + '#gev-settings-chat', + '#gev-settings-voice', + '#gev-voice-preview-enabled', + '#gev-voice-timeout-select', + ]) + assert.equal(shown(selector), true, selector); + // Chat's frame keeps the voice tabs and dock layout buttons away. + assert.equal(shown('.gev-voice-panel-tabs'), false); + const preview = $('#gev-voice-preview-enabled'); + preview.checked = true; + preview.dispatchEvent(new Event('change', { bubbles: true })); + assert.equal(f.commands.getVoicePreferences().previewEnabled, true); + const timeout = $('#gev-voice-timeout-select'); + timeout.value = '10'; + timeout.dispatchEvent(new Event('change', { bubbles: true })); + assert.equal(f.commands.getVoicePreferences().minutes, 10); + f.ui.update('tier', { hidden: false }); + await f.settle(); + assert.equal(shown('#gev-voice-tier'), true); + let tiers = 0; + t.after(f.ui.on('tier', () => tiers++)); + $('#gev-voice-tier').click(); + assert.equal(tiers, 1); + assert.equal(f.ui.state.voicePanel.context, 'chat', 'still chat Settings'); +}); + test('chat Settings keep only the quick commands preference', async (t) => { const f = mounted(t, { view: true }); await f.settle();