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
2 changes: 1 addition & 1 deletion ui-web/src/features/extAgents/ExtAgentsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const hostModels = vi.hoisted(() => ({
loads: 0,
}))
vi.mock('../model/source', () => ({
providers: () => hostModels.providers,
defaultProviders: () => hostModels.providers,
loadDefaultProviders: async () => {
hostModels.loads += 1
},
Expand Down
2 changes: 1 addition & 1 deletion ui-web/src/features/extAgents/ExtAgentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { KeyInput } from '../../components/KeyInput'
import { ModelPicker } from '../../components/ModelPicker'
import { t } from '../../i18n/t'
import * as lang from '../../state/lang'
import { loadDefaultProviders, providers as hostProviders } from '../model/source'
import { defaultProviders as hostProviders, loadDefaultProviders } from '../model/source'
import { offered } from '../model/types'
import { byOf, installOf, isOwnRow } from './catalogue'
import { SectionBlock, Spin, Tile, connect, ordered, shownOf } from './Rows'
Expand Down
32 changes: 31 additions & 1 deletion ui-web/src/features/model/source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ async function live({ session = null, answers = null }: Options = {}) {
loadPermMode: settingsModule.loadPermMode,
loadSettings: settingsModule.loadSettings,
settingsSnapshot: settingsModule.settingsSnapshot,
get providersLive() { return model.providers() },
loadDefaultProviders: model.loadDefaultProviders,
get providersLive() { return model.modelSource.providers() },
get defaultProvidersLive() { return model.defaultProviders() },
get defaultModelLive() { return model.defaultModel() },
get defaultProviderLive() { return model.defaultProvider() },
}
Expand Down Expand Up @@ -344,6 +346,34 @@ describe('providers the page does not offer', () => {
})
})

describe('the two provider scopes', () => {
const row = (slug: string, current = false) =>
({ slug, name: slug, authenticated: true, models: [`${slug}/m`], is_current: current })

it('a default-scoped read leaves the conversation rows alone', async () => {
/* `is_current` is the whole difference: the backend marks the
conversation's provider when the read names a session and
agents.defaults' when it does not. Sharing one array let the settings
dialog's read -- which every settings write repeats -- put the default's
answer under the composer's picker, which then opened on the default's
column with the conversation's model prepended and ticked there, under a
vendor whose key does not serve it. */
const h = await live({ session: 'sess-1' })

const perSession = h.settings.loadProviders()
await h.settle(0, { model: 'gemini/g', provider: 'gemini', providers: [row('deepseek'), row('gemini', true)] })
await perSession
expect(h.settings.providersLive.filter((p) => p.current).map((p) => p.id)).toEqual(['gemini'])

const perDefault = h.settings.loadDefaultProviders()
await h.settle(1, { model: 'deepseek/d', provider: 'deepseek', providers: [row('deepseek', true), row('gemini')] })
await perDefault

expect(h.settings.providersLive.filter((p) => p.current).map((p) => p.id)).toEqual(['gemini'])
expect(h.settings.defaultProvidersLive.filter((p) => p.current).map((p) => p.id)).toEqual(['deepseek'])
})
})

describe('the follows-default repaint under navigation', () => {
it('drops the repaint when the reader left during the write', async () => {
const h = await live({ session: 'a' })
Expand Down
14 changes: 12 additions & 2 deletions ui-web/src/features/model/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,23 @@ import type { ApiProtocol, Kind, ModelSource, Offer, Provider } from './types'
type ProviderWire = ResultOf<'model.options'>['providers'][number]

let providersLive: Provider[] = []
let defaultProvidersLive: Provider[] = []
let defaultModelLive = ''
let defaultProviderLive = ''

/* Whether first-run setup reported a configured provider. On an object because
the boot in src/app/boot.ts is what learns the answer. */
export const setupState: { providerConfigured: boolean | null } = { providerConfigured: null }

export const providers = (): Provider[] => providersLive
/* The rows the settings page reads: the default-scoped answer. Apart from the
session-scoped one below because `is_current` is the difference between them
-- the backend marks the conversation's provider when the read names a
session and `agents.defaults`' when it does not. One array held both, so
opening the settings dialog (or any provider write) put the default's answer
under the composer's picker: it then opened on the default's column with the
conversation's model prepended and ticked there, under a vendor whose key
does not serve it. The same split as `defaultModelLive` above, one layer out. */
export const defaultProviders = (): Provider[] => defaultProvidersLive
export const defaultModel = (): string => defaultModelLive
export const defaultProvider = (): string => defaultProviderLive

Expand Down Expand Up @@ -116,7 +125,7 @@ const rowsOf = (list: ProviderWire[]): Provider[] =>
coalesced onto the dropped load and showed no provider to connect. */
export async function loadDefaultProviders(): Promise<void> {
const mo = await gateway().call('model.options', {})
providersLive = rowsOf(mo.providers || [])
defaultProvidersLive = rowsOf(mo.providers || [])
}

export async function loadProviders(sid?: string | null, gen?: number): Promise<void> {
Expand Down Expand Up @@ -265,6 +274,7 @@ export function stagedTier(): string | null {
registered, are both the module's. */
export function _resetForTests(): void {
providersLive = []
defaultProvidersLive = []
defaultModelLive = ''
defaultProviderLive = ''
paintChip = () => {}
Expand Down
6 changes: 3 additions & 3 deletions ui-web/src/features/settings/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
defaultProvider,
loadDefaultProviders,
persistModel,
providers,
defaultProviders,
setDefaultPair,
} from '../model/source'
import { loadSessions, SESS_CHANNELS } from '../rail/source'
Expand Down Expand Up @@ -157,7 +157,7 @@ export const settingsSnapshot = (): SettingsSnapshot => ({
// Both default-scoped on purpose: the settings page describes what new
// conversations start on, so pairing the default model with the visible
// session's provider badged the wrong row whenever the two scopes differ.
providers: providers(), curProvider: defaultProvider(), model: defaultModel(),
providers: defaultProviders(), curProvider: defaultProvider(), model: defaultModel(),
tools: extTools(), skills: extSkillRows(), mcp: extMcpRows(),
}) as SettingsSnapshot

Expand Down Expand Up @@ -384,7 +384,7 @@ export const keySet = (tool: string, vendor: string, raw: Record<string, unknown
already has a connected provider and a chat model, and whether its web
step already has a key on file for either web tool's vendor. */
export function modelStepDone(): boolean {
return providers().some((p) => p.on) && !!defaultModel()
return defaultProviders().some((p) => p.on) && !!defaultModel()
}

export function webStepDone(): boolean {
Expand Down
Loading