mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Cover the real provider switch and share chat Settings and stop checks
- Host test switches AI Provider from chat Settings through the real provider commands and checks the new mount keeps chat's context, frame, Back to chat and focus on the provider choice. - Drop the open and minimized branches of the carried presentation: the provider choice lives only in Settings, so chat is never open then. - One showChatSettings helper for the gear and the restore, and one isStoppable helper for the live-or-error check in chat and the panel close.
This commit is contained in:
1 parent
46dde909cc
commit
3577d19513
9 files changed
+99
-58
No files matched your search
+23
-33
@@ -1,6 +1,7 @@
|
||||
import { bindChatAttachments } from './chatAttachments.js';
|
||||
import { bindChatPreferences } from './chatPreferences.js';
|
||||
import { readStoredCloudVoiceAuthMode } from './cloudVoiceAuth.js';
|
||||
import { isStoppable } from './session.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
/**
|
||||
@@ -96,8 +97,7 @@ export function bindChatControl({ ui, session, onOpen, dataManager }) {
|
||||
leaveChatSettings();
|
||||
// Chat replaces voice, a stale voice error included, so the dock stops
|
||||
// showing it.
|
||||
if (!isTyped() && (session.isActive() || session.state === 'error'))
|
||||
session.stop();
|
||||
if (!isTyped() && isStoppable(session)) session.stop();
|
||||
onOpen?.();
|
||||
ui.update('chat', { open: true, minimized: false, rows: 2, authNote });
|
||||
sync();
|
||||
@@ -392,15 +392,7 @@ export function bindChatControl({ ui, session, onOpen, dataManager }) {
|
||||
}
|
||||
const frame = ui.element('chatPanel')?.getBoundingClientRect?.();
|
||||
close();
|
||||
ui.update('voicePanel', {
|
||||
context: 'chat',
|
||||
...(frame?.width && {
|
||||
frameWidth: frame.width,
|
||||
frameHeight: frame.height,
|
||||
}),
|
||||
});
|
||||
ui.update('chat', { backVisible: true });
|
||||
sync();
|
||||
showChatSettings(frame);
|
||||
void ui.focus(['chatBack']);
|
||||
}),
|
||||
// The panel clears its context as it closes, so chat tracks its own.
|
||||
@@ -416,32 +408,30 @@ 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.
|
||||
// Settings shown from chat, in chat's frame when it has one.
|
||||
function showChatSettings(frame) {
|
||||
ui.update('voicePanel', {
|
||||
context: 'chat',
|
||||
...(frame?.width && {
|
||||
frameWidth: frame.width,
|
||||
frameHeight: frame.height,
|
||||
}),
|
||||
});
|
||||
ui.update('chat', { backVisible: true });
|
||||
sync();
|
||||
}
|
||||
// What a provider switch carries to the next mount: chat's Settings, never
|
||||
// the conversation. The provider choice lives only in Settings, so chat
|
||||
// itself is never open during a switch.
|
||||
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;
|
||||
if (!inChatSettings()) return null;
|
||||
const { frameWidth: width, frameHeight: height } = ui.state.voicePanel;
|
||||
return { view: 'settings', frame: width ? { width, height } : 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 });
|
||||
if (disposed || state?.view !== 'settings') return;
|
||||
showChatSettings(state.frame);
|
||||
}
|
||||
return {
|
||||
close,
|
||||
|
||||
@@ -365,7 +365,7 @@ test('the chat gear is named for the settings it opens', async (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('a provider switch carries chat Settings or chat itself to the next mount', async (t) => {
|
||||
test('a provider switch carries chat Settings to the next mount', async (t) => {
|
||||
const mount = () => {
|
||||
const ui = createTestVoiceUi({ panel: true });
|
||||
const chat = bindChatControl({ ui, session: typedSession() });
|
||||
@@ -377,21 +377,22 @@ test('a provider switch carries chat Settings or chat itself to the next mount',
|
||||
before.ui.emit('chat-button');
|
||||
before.ui.update('voicePanel', { hidden: false });
|
||||
before.ui.emit('settings-button');
|
||||
before.ui.update('voicePanel', { frameWidth: 320, frameHeight: 480 });
|
||||
const settings = before.chat.presentation();
|
||||
assert.equal(settings.view, 'settings');
|
||||
assert.deepEqual(settings, {
|
||||
view: 'settings',
|
||||
frame: { width: 320, height: 480 },
|
||||
});
|
||||
const after = mount();
|
||||
after.chat.restorePresentation(settings);
|
||||
assert.equal(after.ui.state.voicePanel.context, 'chat');
|
||||
assert.equal(after.ui.state.voicePanel.frameWidth, 320);
|
||||
assert.equal(after.ui.state.voicePanel.frameHeight, 480);
|
||||
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.
|
||||
// Chat itself, or no chat at all, carries nothing.
|
||||
const open = mount();
|
||||
open.ui.emit('chat-button');
|
||||
assert.equal(open.chat.presentation(), null);
|
||||
assert.equal(mount().chat.presentation(), null);
|
||||
});
|
||||
@@ -1319,6 +1319,56 @@ test('opening chat or closing the voice panel clears a stale voice error', async
|
||||
}
|
||||
});
|
||||
|
||||
test('switching AI Provider from chat Settings keeps chat Settings on the new mount', async (t) => {
|
||||
const view = mountVoiceView(t);
|
||||
const host = createVoicePanelHost({ storage: null });
|
||||
const instances = [];
|
||||
const commands = createProviderCommands(
|
||||
{
|
||||
runner: async () => ({}),
|
||||
dataManager: { layers: new Map() },
|
||||
supportedProviders: ['openai', 'gemini'],
|
||||
},
|
||||
{
|
||||
bind: (options) =>
|
||||
bind({
|
||||
...options,
|
||||
panelHost: host,
|
||||
createControl: (control) =>
|
||||
createTestVoiceUi({ ...control, pinia: view.pinia }),
|
||||
}),
|
||||
factories: { openai: factory(instances), gemini: factory(instances) },
|
||||
storage: null,
|
||||
},
|
||||
);
|
||||
t.after(() => {
|
||||
commands.destroy();
|
||||
host.destroy();
|
||||
});
|
||||
await view.settle();
|
||||
$('#gev-chat-button').click();
|
||||
await view.settle();
|
||||
$('#gev-chat-panel').getBoundingClientRect = () => ({
|
||||
width: 320,
|
||||
height: 480,
|
||||
});
|
||||
$('#gev-voice-settings-button').click();
|
||||
await view.settle();
|
||||
const old = commands.ui;
|
||||
assert.equal(old.state.voicePanel.context, 'chat');
|
||||
old.emit('provider-change', 'gemini');
|
||||
await view.settle();
|
||||
const { ui } = commands;
|
||||
assert.notEqual(ui, old);
|
||||
assert.equal(ui.state.voicePanel.context, 'chat');
|
||||
assert.equal(ui.state.voicePanel.frameWidth, 320);
|
||||
assert.equal(ui.state.voicePanel.frameHeight, 480);
|
||||
assert.equal(ui.state.chat.backVisible, true);
|
||||
assert.equal(ui.state.root.assistantMode, 'chat');
|
||||
assert.equal($('#gev-chat-back').hidden, false);
|
||||
assert.equal(active(), 'gev-voice-provider');
|
||||
});
|
||||
|
||||
test('the voice panel close turns voice off', async (t) => {
|
||||
const f = mounted(t, { view: true });
|
||||
await f.commands.session.start();
|
||||
|
||||
@@ -64,7 +64,7 @@ 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.
|
||||
// Settings opened from chat stay chat's Settings.
|
||||
const presentation = current.chatPresentation?.() ?? null;
|
||||
const generation = ++mountGeneration;
|
||||
voiceEnabled = false;
|
||||
|
||||
@@ -857,13 +857,13 @@ for (const action of ['stop', 'destroy']) {
|
||||
}
|
||||
}
|
||||
|
||||
test('a provider switch hands chat Settings or chat to the new mount', async (t) => {
|
||||
test('a provider switch hands chat Settings 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 };
|
||||
const shown = { view: 'settings', frame: { width: 320, height: 480 } };
|
||||
f.mounts[0].controls.chatPresentation = () => shown;
|
||||
f.select(next);
|
||||
await settle();
|
||||
|
||||
@@ -185,6 +185,10 @@ export function createVoiceSession({ createAdapter, runner, signal }) {
|
||||
};
|
||||
}
|
||||
|
||||
// Live, or left in error: stopping either returns the session to idle.
|
||||
export const isStoppable = (session) =>
|
||||
session.isActive() || session.state === 'error';
|
||||
|
||||
function abortError() {
|
||||
return new DOMException('Voice action cancelled', 'AbortError');
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { bindAssistantPanelLayout, bindLayoutDock } from './panelLayout.js';
|
||||
import { bindChatControl } from './chatControl.js';
|
||||
import { bindVoiceMiniPreview } from './voiceMiniPreview.js';
|
||||
import { createVoiceControl, voiceStatusLabel } from './control.js';
|
||||
import { createVoiceSession } from './session.js';
|
||||
import { createVoiceSession, isStoppable } from './session.js';
|
||||
import { VoiceCardControls } from './voiceCard.js';
|
||||
import { PointerReticle } from './pointerReticle.js';
|
||||
import { VOICE_PROVIDERS } from './providerCapabilities.js';
|
||||
@@ -40,7 +40,7 @@ export function createVoiceCommands({
|
||||
createReticle = createDefaultReticle,
|
||||
panelHost = null,
|
||||
nativePanelProvider = null,
|
||||
// Chat's Settings or chat itself, carried across a provider switch.
|
||||
// Chat's Settings, carried across a provider switch.
|
||||
presentation = null,
|
||||
}) {
|
||||
if (resetExisting) window.__gevVoiceCommands?.stop?.({ removeUi: true });
|
||||
@@ -289,11 +289,7 @@ export function createVoiceCommands({
|
||||
// session belongs to chat, which handles its own close.
|
||||
const offClose = ui.on('settings-close', () => {
|
||||
// A voice error the user closes is over too; stopping returns to idle.
|
||||
if (
|
||||
(!session.isActive() && session.state !== 'error') ||
|
||||
adapter.getStartOptions?.().textOnly
|
||||
)
|
||||
return;
|
||||
if (!isStoppable(session) || adapter.getStartOptions?.().textOnly) return;
|
||||
const ticket = panelMount?.stopTicket();
|
||||
session.stop();
|
||||
panelMount?.closeAfterStop(ticket);
|
||||
|
||||
Reference in new issue
Block a user