mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix(voice): keep chat's view across a provider switch and clear stale errors
Switching AI Provider in Settings opened from chat rebuilt the voice control, so Settings fell back to voice and the dock went idle, widening the wings. The switch now carries chat's Settings, or chat itself, to the new provider as it already carries the input mode; the conversation still starts fresh and focus stays on the provider choice. After a voice error, opening chat or closing the voice panel left the dock red on ERROR, because both stopped only an active session. They now also end a session left in error, which returns the dock to idle.
This commit is contained in:
1 parent
4859ba4c94
commit
53ea76251f
7 files changed
+120
-6
No files matched your search
@@ -766,9 +766,12 @@ 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, 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 is off, with Back to chat; voice Settings show Back to voice. Switching
|
||||
AI Provider keeps that view: chat's Settings, or chat itself, stay as they were
|
||||
for the new provider. 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 panel's close turns voice off. Opening chat or closing the voice panel
|
||||
also clears a voice error left on the dock. 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.
|
||||
|
||||
@@ -94,7 +94,10 @@ export function bindChatControl({ ui, session, onOpen, dataManager }) {
|
||||
: '';
|
||||
function open({ focus = true } = {}) {
|
||||
leaveChatSettings();
|
||||
if (!isTyped() && session.isActive()) session.stop();
|
||||
// Chat replaces voice, a stale voice error included, so the dock stops
|
||||
// showing it.
|
||||
if (!isTyped() && (session.isActive() || session.state === 'error'))
|
||||
session.stop();
|
||||
onOpen?.();
|
||||
ui.update('chat', { open: true, minimized: false, rows: 2, authNote });
|
||||
sync();
|
||||
@@ -413,10 +416,39 @@ export function bindChatControl({ ui, session, onOpen, dataManager }) {
|
||||
}),
|
||||
];
|
||||
sync();
|
||||
// What a provider switch carries to the next mount: chat's Settings or
|
||||
// chat itself, never the conversation.
|
||||
function presentation() {
|
||||
if (inChatSettings()) {
|
||||
const { frameWidth, frameHeight } = ui.state.voicePanel;
|
||||
return { view: 'settings', frameWidth, frameHeight };
|
||||
}
|
||||
if (chat().open) return { view: chat().minimized ? 'minimized' : 'open' };
|
||||
return null;
|
||||
}
|
||||
// Restored without moving focus: the switch keeps it on the provider choice.
|
||||
function restorePresentation(state) {
|
||||
if (disposed || !state) return;
|
||||
if (state.view === 'settings') {
|
||||
ui.update('voicePanel', {
|
||||
context: 'chat',
|
||||
...(state.frameWidth && {
|
||||
frameWidth: state.frameWidth,
|
||||
frameHeight: state.frameHeight,
|
||||
}),
|
||||
});
|
||||
ui.update('chat', { backVisible: true });
|
||||
return sync();
|
||||
}
|
||||
open({ focus: false });
|
||||
if (state.view === 'minimized') minimize({ focus: false });
|
||||
}
|
||||
return {
|
||||
close,
|
||||
minimize,
|
||||
restore: open,
|
||||
presentation,
|
||||
restorePresentation,
|
||||
get minimized() {
|
||||
return chat().open && chat().minimized;
|
||||
},
|
||||
|
||||
@@ -364,3 +364,34 @@ test('the chat gear is named for the settings it opens', async (t) => {
|
||||
'Settings',
|
||||
);
|
||||
});
|
||||
|
||||
test('a provider switch carries chat Settings or chat itself to the next mount', async (t) => {
|
||||
const mount = () => {
|
||||
const ui = createTestVoiceUi({ panel: true });
|
||||
const chat = bindChatControl({ ui, session: typedSession() });
|
||||
t.after(() => chat.destroy());
|
||||
return { ui, chat };
|
||||
};
|
||||
// Chat's Settings: the next mount shows Settings in chat's frame again.
|
||||
const before = mount();
|
||||
before.ui.emit('chat-button');
|
||||
before.ui.update('voicePanel', { hidden: false });
|
||||
before.ui.emit('settings-button');
|
||||
const settings = before.chat.presentation();
|
||||
assert.equal(settings.view, 'settings');
|
||||
const after = mount();
|
||||
after.chat.restorePresentation(settings);
|
||||
assert.equal(after.ui.state.voicePanel.context, 'chat');
|
||||
assert.equal(after.ui.state.chat.backVisible, true);
|
||||
assert.equal(after.ui.state.root.assistantMode, 'chat');
|
||||
// Open and minimized chat come back as they were, without taking focus.
|
||||
for (const view of ['open', 'minimized']) {
|
||||
const next = mount();
|
||||
next.chat.restorePresentation({ view });
|
||||
assert.equal(next.ui.state.chat.open, true);
|
||||
assert.equal(next.ui.state.chat.minimized, view === 'minimized');
|
||||
assert.equal(next.chat.presentation().view, view);
|
||||
}
|
||||
// Idle chat carries nothing.
|
||||
assert.equal(mount().chat.presentation(), null);
|
||||
});
|
||||
@@ -1298,6 +1298,27 @@ test('undocking while a tray is open brings the panel back as its bar', async (t
|
||||
assert.equal($('#gev-chat-panel').dataset.placement, 'compact');
|
||||
});
|
||||
|
||||
test('opening chat or closing the voice panel clears a stale voice error', async (t) => {
|
||||
for (const leave of ['chat', 'close']) {
|
||||
const f = mounted(t, { view: true });
|
||||
await f.commands.session.start();
|
||||
f.commands.voicePanel.select('preview');
|
||||
f.instances[0].hooks.emit({
|
||||
type: 'state',
|
||||
state: 'error',
|
||||
detail: 'Lost',
|
||||
});
|
||||
await f.settle();
|
||||
assert.equal($('#gev-voice-control').dataset.status, 'error');
|
||||
assert.equal(f.commands.session.isActive(), false);
|
||||
if (leave === 'chat') $('#gev-chat-button').click();
|
||||
else $('#gev-voice-settings-close').click();
|
||||
await f.settle();
|
||||
assert.equal(f.commands.session.state, 'idle', leave);
|
||||
assert.notEqual($('#gev-voice-control').dataset.status, 'error', leave);
|
||||
}
|
||||
});
|
||||
|
||||
test('the voice panel close turns voice off', async (t) => {
|
||||
const f = mounted(t, { view: true });
|
||||
await f.commands.session.start();
|
||||
|
||||
@@ -64,6 +64,8 @@ export function createProviderCommands(options, { bind, factories, storage }) {
|
||||
const textOnly = Boolean(
|
||||
current.session.adapter.getStartOptions?.().textOnly,
|
||||
);
|
||||
// The panel stays where the user is: chat's Settings or chat itself.
|
||||
const presentation = current.chatPresentation?.() ?? null;
|
||||
const generation = ++mountGeneration;
|
||||
voiceEnabled = false;
|
||||
current.session.destroy();
|
||||
@@ -80,7 +82,7 @@ export function createProviderCommands(options, { bind, factories, storage }) {
|
||||
} catch {
|
||||
/* Storage is optional. */
|
||||
}
|
||||
mount();
|
||||
mount(presentation);
|
||||
if (resume && !disposed && provider === next)
|
||||
void current.session.start({
|
||||
pushToTalk,
|
||||
@@ -142,7 +144,7 @@ export function createProviderCommands(options, { bind, factories, storage }) {
|
||||
return typeof value === 'function' ? value.bind(current) : value;
|
||||
},
|
||||
});
|
||||
function mount() {
|
||||
function mount(presentation = null) {
|
||||
voiceEnabled = false;
|
||||
const generation = ++mountGeneration;
|
||||
current = bind({
|
||||
@@ -158,6 +160,7 @@ export function createProviderCommands(options, { bind, factories, storage }) {
|
||||
handleSessionEvent(event);
|
||||
},
|
||||
createSession: factories[provider],
|
||||
presentation,
|
||||
});
|
||||
window.__gevVoiceCommands = facade;
|
||||
}
|
||||
|
||||
@@ -856,3 +856,18 @@ for (const action of ['stop', 'destroy']) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('a provider switch hands chat Settings or chat to the new mount', async (t) => {
|
||||
const f = compositionFixture(t);
|
||||
const first = f.facade.provider;
|
||||
const next = first === 'openai' ? 'gemini' : 'openai';
|
||||
// Without chat open there is nothing to carry.
|
||||
assert.equal(f.mounts[0].controls.chatPresentation(), null);
|
||||
const shown = { view: 'settings', frameWidth: 320, frameHeight: 480 };
|
||||
f.mounts[0].controls.chatPresentation = () => shown;
|
||||
f.select(next);
|
||||
await settle();
|
||||
assert.equal(f.mounts.length, 2);
|
||||
assert.deepEqual(f.mounts[1].options.presentation, shown);
|
||||
assert.equal(f.facade.provider, next);
|
||||
});
|
||||
@@ -40,6 +40,8 @@ export function createVoiceCommands({
|
||||
createReticle = createDefaultReticle,
|
||||
panelHost = null,
|
||||
nativePanelProvider = null,
|
||||
// Chat's Settings or chat itself, carried across a provider switch.
|
||||
presentation = null,
|
||||
}) {
|
||||
if (resetExisting) window.__gevVoiceCommands?.stop?.({ removeUi: true });
|
||||
const ui = createControl(panelHost ? { panel: true } : {});
|
||||
@@ -225,6 +227,8 @@ export function createVoiceCommands({
|
||||
onOpen: () => panelMount?.panel.close(),
|
||||
})
|
||||
: null;
|
||||
chat?.restorePresentation(presentation);
|
||||
controls.chatPresentation = () => chat?.presentation() ?? null;
|
||||
function minimizeVoicePanel() {
|
||||
const panel = ui.element('voicePanel');
|
||||
const holdsFocus = panel?.contains?.(globalThis.document?.activeElement);
|
||||
@@ -284,7 +288,12 @@ export function createVoiceCommands({
|
||||
// The panel's close turns voice off, as chat's close ends the chat. A typed
|
||||
// session belongs to chat, which handles its own close.
|
||||
const offClose = ui.on('settings-close', () => {
|
||||
if (!session.isActive() || adapter.getStartOptions?.().textOnly) return;
|
||||
// A voice error the user closes is over too; stopping returns to idle.
|
||||
if (
|
||||
(!session.isActive() && session.state !== 'error') ||
|
||||
adapter.getStartOptions?.().textOnly
|
||||
)
|
||||
return;
|
||||
const ticket = panelMount?.stopTicket();
|
||||
session.stop();
|
||||
panelMount?.closeAfterStop(ticket);
|
||||
|
||||
Reference in new issue
Block a user