diff --git a/ui-web/src/features/settings/source.test.ts b/ui-web/src/features/settings/source.test.ts index e6091ae8c..5b2a253ae 100644 --- a/ui-web/src/features/settings/source.test.ts +++ b/ui-web/src/features/settings/source.test.ts @@ -11,18 +11,21 @@ const seen: Array<[string, unknown]> = [] const toasts: string[] = [] const opened: string[] = [] const railReloads: number[] = [] +const chipWrites: string[] = [] async function load(answers: Record = {}): Promise { seen.length = 0 toasts.length = 0 opened.length = 0 railReloads.length = 0 + chipWrites.length = 0 const mod = await loadPart(() => import('./source'), { fakes: { 'src/state/toast': { show: (text: string) => { toasts.push(text) } }, 'src/lib/openUrl': { open: (url: string) => { opened.push(url) } }, 'src/features/rail/source': { loadSessions: async () => { railReloads.push(1) }, SESS_CHANNELS: ['tui', 'cron'] }, 'src/state/banner': { draw: () => {} }, + 'src/features/model/source': { showModel: (m: string) => { chipWrites.push(m) } }, 'src/i18n/t': { t: (key: string, vars?: Record) => (vars ? `${key} ${JSON.stringify(vars)}` : key) }, }, }) as Source @@ -70,6 +73,19 @@ describe('settings source', () => { expect(toasts[1]).toContain('tools.web.proxy is not editable here') }) + it('a settings load moves the default pair and leaves the conversation chip alone', async () => { + /* The chip is the open conversation's model, which is not in this reply. + Painting it from `agents.defaults` here put the default back over a + conversation that had switched -- on every dialog open and on every + settings write, each of which reloads -- and the switch read as lost + until the page was reloaded, though the turns kept running on it. */ + const settings = { agents: { defaults: { model: 'deepseek/pro', provider: 'deepseek' } } } + const mod = await load({ 'settings.get': { settings, config_path: '/c.json' } }) + await mod.loadSettings() + expect(mod.settingsSnapshot().model).toBe('deepseek/pro') + expect(chipWrites).toEqual([]) + }) + it('usage asks for the two dates, inclusive', async () => { const mod = await load({ 'settings.usage': { days: 2 } }) await mod.settingsSource.usage({ from: '2026-09-01', to: '2026-09-02' }) diff --git a/ui-web/src/features/settings/source.ts b/ui-web/src/features/settings/source.ts index a1c536e8e..2a76ca11c 100644 --- a/ui-web/src/features/settings/source.ts +++ b/ui-web/src/features/settings/source.ts @@ -28,7 +28,6 @@ import { persistModel, providers, setDefaultPair, - showModel, } from '../model/source' import { loadSessions, SESS_CHANNELS } from '../rail/source' @@ -124,8 +123,14 @@ export async function loadSettings(): Promise { // provider), while the composer chip shows whatever the open conversation // runs. Sharing one value made the settings control display the session's // model -- and badge the session's provider -- as the default. + // + // Which is why this load stops here. It used to paint the chip from the same + // defaults, so opening the dialog -- or any settings write, each of which + // reloads -- put the default back over a conversation that had switched, and + // the switch read as lost until a reload. The chip belongs to + // `loadProviders`, which asks `model.options` for the visible conversation + // and is run by every path that changes which one that is. setDefaultPair(defaults.model || '', defaults.provider || '') - if (defaults.model) showModel(defaults.model) } /* The settings plus the provider catalogue behind `model.options`. Deliberately