Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions ui-web/src/features/settings/source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {}): Promise<Source> {
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<string, unknown>) => (vars ? `${key} ${JSON.stringify(vars)}` : key) },
},
}) as Source
Expand Down Expand Up @@ -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' })
Expand Down
9 changes: 7 additions & 2 deletions ui-web/src/features/settings/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
persistModel,
providers,
setDefaultPair,
showModel,
} from '../model/source'
import { loadSessions, SESS_CHANNELS } from '../rail/source'

Expand Down Expand Up @@ -124,8 +123,14 @@ export async function loadSettings(): Promise<void> {
// 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
Expand Down
Loading