From 264fc16f2c079d4d4ab0bb3739784dd2ddeb0cb7 Mon Sep 17 00:00:00 2001 From: Serhii Dziupin Date: Wed, 5 Aug 2026 13:50:23 +0300 Subject: [PATCH] fix(ui): keep the selected model when switching agent modes Switching between Build and Plan modes reset the model selector to the settings default because setAgent fell through to the settings-default fallback whenever the target agent had no saved override, and the explicit-switch path in ModelControls force-applied the agent's default model, overwriting any per-agent override. setAgent now keeps the current model selection when the user has a live manual selection and the target agent configures no model of its own, and the explicit-switch handler no longer clobbers saved per-agent overrides with the agent default. Startup and pin behavior are unchanged: the settings-default and agent-pin cascade still applies when no manual selection exists yet. Fixes #2531 --- .../ui/src/components/chat/ModelControls.tsx | 35 ------------------- packages/ui/src/stores/useConfigStore.test.ts | 24 +++++++++++++ packages/ui/src/stores/useConfigStore.ts | 14 ++++++-- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 262664b9..a5df7d0d 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -641,7 +641,6 @@ export const ModelControls: React.FC = ({ ]; const prevAgentNameRef = React.useRef(undefined); - const explicitAgentSwitchRef = React.useRef(null); const latestLoadedUserChoiceRestoreRef = React.useRef(null); const currentSessionDirectory = currentSessionId ? getDirectoryForSession(currentSessionId) : undefined; @@ -1032,9 +1031,6 @@ export const ModelControls: React.FC = ({ prevAgentNameRef.current = currentAgentName; if (currentAgentName && currentSessionId) { - const shouldPreferAgentModel = explicitAgentSwitchRef.current === currentAgentName; - explicitAgentSwitchRef.current = null; - await new Promise((resolve) => { const timer = setTimeout(resolve, 50); abortController.signal.addEventListener('abort', () => { @@ -1047,33 +1043,6 @@ export const ModelControls: React.FC = ({ return; } - const selectedAgent = shouldPreferAgentModel - ? agents.find((agent) => agent.name === currentAgentName) - : undefined; - if (selectedAgent?.model?.providerID && selectedAgent.model.modelID) { - const result = tryApplyModelSelection( - selectedAgent.model.providerID, - selectedAgent.model.modelID, - currentAgentName, - ); - if (result === 'applied' || result === 'provider-missing') { - if (result === 'applied') { - saveSessionModelSelection( - currentSessionId, - selectedAgent.model.providerID, - selectedAgent.model.modelID, - ); - saveAgentModelForSession( - currentSessionId, - currentAgentName, - selectedAgent.model.providerID, - selectedAgent.model.modelID, - ); - } - return; - } - } - const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName); if (persistedChoice) { @@ -1099,12 +1068,9 @@ export const ModelControls: React.FC = ({ abortController.abort(); }; }, [ - agents, currentAgentName, currentSessionId, getAgentModelForSession, - saveAgentModelForSession, - saveSessionModelSelection, tryApplyModelSelection, contextHydrated, ]); @@ -1185,7 +1151,6 @@ export const ModelControls: React.FC = ({ const handleAgentChange = React.useCallback((agentName: string, options?: { closeModelSelector?: boolean }) => { try { - explicitAgentSwitchRef.current = agentName; setAgent(agentName); addRecentAgent(agentName); if (options?.closeModelSelector ?? true) { diff --git a/packages/ui/src/stores/useConfigStore.test.ts b/packages/ui/src/stores/useConfigStore.test.ts index 52dbe3e6..72857f4d 100644 --- a/packages/ui/src/stores/useConfigStore.test.ts +++ b/packages/ui/src/stores/useConfigStore.test.ts @@ -589,6 +589,30 @@ describe('useConfigStore provider persistence', () => { expect(state.currentModelId).toBe('model-a'); }); + test('[issue-2531] setAgent keeps the manual model when switching to an agent without an override', () => { + const sessionId = 'ses_2531_mode_switch'; + useSessionUIStore.setState({ currentSessionId: sessionId }); + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + providers: [provider('deepseek', 'deepseek-v4-pro'), provider('kimi', 'kimi-k3')], + agents: [testAgent('build'), testAgent('plan')], + settingsDefaultModel: 'deepseek/deepseek-v4-pro', + currentProviderId: 'kimi', + currentModelId: 'kimi-k3', + currentAgentName: 'build', + selectionSource: 'manual', + currentVariant: undefined, + directoryScoped: {}, + }); + + useConfigStore.getState().setAgent('plan'); + + const state = useConfigStore.getState(); + expect(state.currentAgentName).toBe('plan'); + expect(state.currentProviderId).toBe('kimi'); + expect(state.currentModelId).toBe('kimi-k3'); + }); + test('loadAgents does not fetch OpenCode config directly', async () => { useConfigStore.setState({ activeDirectoryKey: DIRECTORY, diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 4a7dfd2e..9714b497 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -2387,6 +2387,9 @@ export const useConfigStore = create()( currentProviderId, currentModelId, } = get(); + // Captured before the first set below, which unconditionally + // marks the selection as manual. + const hadManualSelection = get().selectionSource === "manual"; set((state) => { const directoryKey = state.activeDirectoryKey; @@ -2508,8 +2511,7 @@ export const useConfigStore = create()( // Prefer a session-level manual override for this agent over the // agent's configured default. Re-applying setAgent after subtask // completion / rematerialization must not clobber the override - // (issue #2404). Explicit agent-picker switches still force the - // agent default via ModelControls' shouldPreferAgentModel path. + // (issue #2404). if (currentSessionId) { const existingAgentModel = useSelectionStore.getState().getAgentModelForSession(currentSessionId, agentName); if (existingAgentModel && hasProviderModel(providers, existingAgentModel.providerId, existingAgentModel.modelId)) { @@ -2538,6 +2540,14 @@ export const useConfigStore = create()( } } + // The user has a live manual model selection and the target + // agent configures no model of its own. Switching modes or + // agents must not reset the selection to the settings default + // (issue #2531) — mode switches are not model changes. + if (hadManualSelection && currentProviderId && currentModelId) { + return; + } + // If the agent has no preferred model, use settings default. if (settingsDefaultModel) { const parsed = parseModelString(settingsDefaultModel);