diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx index 8a3ceb589a..f8fbeb511a 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx @@ -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 @@ -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()) }) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts index d3b9ee20a3..f2487e25ca 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.ts +++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts @@ -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