fix(voice): keep the voice preferences in reach from chat Settings

With Voice Preview off the voice panel never opens on its own, and chat
Settings hid the voice preferences, so the Preview toggle, inactivity
timeout and model tier could not be reached by pointer. Chat Settings now
show them below the chat options, still in chat's frame and without the
voice tabs.
This commit is contained in:
Sameh Khamis committed 2026-11-03 15:54:54 -08:00
1 parent 4f4af9c4e0
commit 829db19f60
4 files changed
+71 -8

No files matched your search

+3 -2
View File
@@ -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
+8 -2
View File
@@ -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]
+4 -4
View File
@@ -125,6 +125,10 @@ const frameStyle = (panel) => ({
<UiSelect id="gev-voice-provider" class="gev-field" :aria-label="t('AI Provider')" :title="view.provider.title" :model-value="view.provider.value" @update:model-value="emit('provider-change', $event)"><option v-for="option in view.provider.options" :key="option.value" :value="option.value">{{ option.label }}</option></UiSelect>
</label>
<p class="gev-voice-settings-note">{{ t('The selected AI provider is shared by voice and chat.') }}</p>
<div id="gev-settings-chat">
<label class="gev-voice-preference gev-voice-preference-toggle"><input id="gev-chat-quick" type="checkbox" v-checked="view.chat.quickChecked" @change="emit('chat-quick-change', $event.target.checked)" /><span>{{ t('Show quick commands') }}</span></label>
<p>{{ t('Chat replies are silent. No microphone access is needed.') }}</p>
</div>
<div id="gev-settings-voice">
<label class="gev-voice-preference gev-voice-preference-toggle"><input id="gev-voice-preview-enabled" type="checkbox" aria-describedby="gev-voice-preview-note" v-checked="view.preferences.previewChecked" @change="emit('preview-change', $event.target.checked)" /><span>{{ t('Show Voice Preview') }}</span></label>
<p id="gev-voice-preview-note" class="gev-voice-settings-note">{{ view.preferences.previewNote }}</p>
@@ -152,10 +156,6 @@ const frameStyle = (panel) => ({
</div>
</div>
</div>
<div id="gev-settings-chat">
<label class="gev-voice-preference gev-voice-preference-toggle"><input id="gev-chat-quick" type="checkbox" v-checked="view.chat.quickChecked" @change="emit('chat-quick-change', $event.target.checked)" /><span>{{ t('Show quick commands') }}</span></label>
<p>{{ t('Chat replies are silent. No microphone access is needed.') }}</p>
</div>
</section>
</div>
</section>
+56
View File
@@ -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();