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
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe('useModelControls', () => {
})
})

it('routes active-session picker changes through config.set with an explicit session-scoped provider', async () => {
it('persists an active primary-session picker change as the profile default via config.set --global', async () => {
$activeSessionId.set('session-1')
const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never)
let controls!: Controls
Expand All @@ -233,10 +233,13 @@ describe('useModelControls', () => {
})
).resolves.toBe(true)

// The primary main agent's pick IS the profile default, so it persists to
// config.yaml (model.default + model.provider) — which is what lets a
// chosen subscription provider outrank a leftover OPENAI_API_KEY env var.
expect(requestGateway).toHaveBeenCalledWith('config.set', {
session_id: 'session-1',
key: 'model',
value: 'claude-sonnet-4.6 --provider anthropic --session'
value: 'claude-sonnet-4.6 --provider anthropic --global'
})
expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything())
})
Expand Down
18 changes: 17 additions & 1 deletion apps/desktop/src/app/session/hooks/use-model-controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,26 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO
}

try {
// The PRIMARY profile's main agent is the profile's default — its
// model/provider choice IS the default, so persist it to config.yaml
// (model.default + model.provider) via --global. This is what makes
// the selection "stick": a set model.provider outranks a leftover
// OPENAI_API_KEY env var in resolve_provider(), so the main agent
// keeps the chosen (e.g. subscription) provider across restarts
// instead of silently falling back to an env key.
//
// Two things stay --session, deliberately:
// - a SECONDARY chat tile: picking a model there must not rewrite the
// profile default (the cross-session-contamination guard).
// - MoA (mixture-of-agents) presets: a transient orchestration choice
// that must never become the persisted global gateway default.
const isSessionOnlyPreset = (selection.provider || '').toLowerCase() === 'moa'
const persistsAsDefault = touchesPrimary && !isSessionOnlyPreset
const scope = persistsAsDefault ? '--global' : '--session'
const result = await requestGateway<{ deferred?: boolean }>('config.set', {
session_id: liveSessionId,
key: 'model',
value: `${selection.model} --provider ${selection.provider} --session`
value: `${selection.model} --provider ${selection.provider} ${scope}`
})

// A pick made DURING a turn is queued by the gateway and applied at the
Expand Down
Loading