fix(voice): show the voice panel that typed chat asks for

set_panel_open for voice-settings or voice-preview reported success while
chat's open state kept the voice panel hidden, minimized chat included. A
successful explicit show now closes chat, which keeps the typed session,
its running turn and its messages; reopening chat restores them.
This commit is contained in:
Sameh Khamis committed 2026-11-03 15:54:54 -08:00
1 parent b6967a3dd9
commit 4f4af9c4e0
3 files changed
+72 -4

No files matched your search

+4 -1
View File
@@ -767,7 +767,10 @@ 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
Preview, and does nothing in chat. Closing chat ends a typed session, and the
voice panel's close turns voice off.
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
voice panel; the typed conversation and its running reply carry on, and the
chat button brings them back.
Contacts nearest-aircraft follow-ups retain the snapshot's distance basis even
after the camera moves. An explicit new scope replaces that basis. Provider
+54 -2
View File
@@ -511,7 +511,7 @@ test('the voice inactivity timeout never ends a typed chat session', async (t) =
async function directActions(
t,
{ storage = null, delayExit = false, exposePanel = true } = {},
{ storage = null, delayExit = false, exposePanel = true, typed = false } = {},
) {
let f, releaseExit;
const state = { active: true, exits: 0, visual: [] };
@@ -544,7 +544,12 @@ async function directActions(
exposePanel ? f?.commands.captureVoicePanelOperation() : undefined,
});
f = mounted(t, { native: true, runner, storage });
await f.commands.session.start();
// A typed chat session starts from chat's first send instead.
if (typed) {
const adapter = f.instances[0];
adapter.getStartOptions = () => ({ textOnly: true });
adapter.sendText = () => true;
} else await f.commands.session.start();
return {
...f,
runner,
@@ -618,6 +623,53 @@ test('named voice settings exits Cockpit directly; Preview stays disabled and vi
);
});
for (const minimized of [false, true])
test(`an explicit voice panel request from ${minimized ? 'minimized ' : ''}typed chat shows the panel and keeps the chat`, async (t) => {
const f = await directActions(t, { typed: true });
const adapter = f.instances[0];
f.ui.emit('chat-button');
f.ui.emit('chat-input', 'Fly to Tokyo');
f.ui.emit('chat-submit');
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(f.ui.state.chat.busy, true, 'the typed turn is running');
if (minimized) f.ui.emit('chat-minimize');
assert.equal(f.ui.state.root.chatOpen, true);
for (const [panelId, view] of [
['voice-settings', 'settings'],
['voice-preview', 'preview'],
]) {
const result = await f.run('set_panel_open', { panelId, open: true });
assert.equal(result.ok, true);
// `[data-chat-open='true'] .gev-voice-panel` is display:none.
assert.equal(f.ui.state.root.chatOpen, false, panelId);
assert.equal(f.ui.state.chat.open, false);
assert.equal(f.ui.state.voicePanel.hidden, false);
assert.equal(f.ui.state.voicePanel.view, view);
}
assert.equal(adapter.stops, 0, 'the typed session continues');
assert.equal(f.commands.session.isActive(), true);
assert.equal(f.ui.state.chat.busy, true, 'the turn is not cancelled');
adapter.hooks.emit({
type: 'transcript',
role: 'assistant',
text: 'Flying to Tokyo.',
final: true,
itemId: 'reply',
});
adapter.hooks.emit({ type: 'completion', status: 'completed' });
assert.equal(f.ui.state.chat.busy, false);
f.ui.emit('chat-button');
assert.equal(f.ui.state.root.chatOpen, true);
assert.equal(f.ui.state.voicePanel.hidden, true);
assert.deepEqual(
f.ui.chatMessages.map(({ role }) => role),
['user', 'assistant'],
'reopened chat shows the whole conversation',
);
assert.equal(f.ui.chatMessages[0].text, 'Fly to Tokyo');
assert.equal(adapter.stops, 0);
});
test('canonical exit reveals requested voice UI; newer manual choice or Stop defeats pending reveal', async (t) => {
const f = await directActions(t, { delayExit: true });
const pending = f.run('control_cockpit', {
+14 -1
View File
@@ -157,7 +157,20 @@ export function createVoiceCommands({
controls.voiceCard = card;
if (panelMount) {
controls.voicePanel = panelMount.panel;
controls.captureVoicePanelOperation = panelMount.captureOperation;
// An explicit, successful show hands the frame from chat to the voice
// panel, which chat's open state hides. Closing chat keeps its typed
// session, running turn and messages; reopening chat restores them.
controls.captureVoicePanelOperation = () => {
const operation = panelMount.captureOperation();
return {
...operation,
show(view, open = true) {
const result = operation.show(view, open);
if (open && result.ok) chat?.close();
return result;
},
};
};
controls.getVoicePanelState = panelMount.snapshot;
controls.getVoicePreferences = panelHost.snapshot;
}