diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index a3d918b2..d09b0004 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -633,7 +633,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; @@ -1051,9 +1050,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', () => { @@ -1066,33 +1062,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) { @@ -1118,12 +1087,9 @@ export const ModelControls: React.FC = ({ abortController.abort(); }; }, [ - agents, currentAgentName, currentSessionId, getAgentModelForSession, - saveAgentModelForSession, - saveSessionModelSelection, tryApplyModelSelection, contextHydrated, ]); @@ -1212,7 +1178,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 1cd6e4d1..99a8f0d4 100644 --- a/packages/ui/src/stores/useConfigStore.test.ts +++ b/packages/ui/src/stores/useConfigStore.test.ts @@ -698,6 +698,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 46ac3379..d61cc083 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -2435,6 +2435,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; @@ -2554,8 +2557,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)) { @@ -2584,6 +2586,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);