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
This commit is contained in:
@@ -641,7 +641,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const prevAgentNameRef = React.useRef<string | undefined>(undefined);
|
const prevAgentNameRef = React.useRef<string | undefined>(undefined);
|
||||||
const explicitAgentSwitchRef = React.useRef<string | null>(null);
|
|
||||||
const latestLoadedUserChoiceRestoreRef = React.useRef<string | null>(null);
|
const latestLoadedUserChoiceRestoreRef = React.useRef<string | null>(null);
|
||||||
|
|
||||||
const currentSessionDirectory = currentSessionId ? getDirectoryForSession(currentSessionId) : undefined;
|
const currentSessionDirectory = currentSessionId ? getDirectoryForSession(currentSessionId) : undefined;
|
||||||
@@ -1032,9 +1031,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
prevAgentNameRef.current = currentAgentName;
|
prevAgentNameRef.current = currentAgentName;
|
||||||
|
|
||||||
if (currentAgentName && currentSessionId) {
|
if (currentAgentName && currentSessionId) {
|
||||||
const shouldPreferAgentModel = explicitAgentSwitchRef.current === currentAgentName;
|
|
||||||
explicitAgentSwitchRef.current = null;
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const timer = setTimeout(resolve, 50);
|
const timer = setTimeout(resolve, 50);
|
||||||
abortController.signal.addEventListener('abort', () => {
|
abortController.signal.addEventListener('abort', () => {
|
||||||
@@ -1047,33 +1043,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
return;
|
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);
|
const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
|
||||||
|
|
||||||
if (persistedChoice) {
|
if (persistedChoice) {
|
||||||
@@ -1099,12 +1068,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
agents,
|
|
||||||
currentAgentName,
|
currentAgentName,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
getAgentModelForSession,
|
getAgentModelForSession,
|
||||||
saveAgentModelForSession,
|
|
||||||
saveSessionModelSelection,
|
|
||||||
tryApplyModelSelection,
|
tryApplyModelSelection,
|
||||||
contextHydrated,
|
contextHydrated,
|
||||||
]);
|
]);
|
||||||
@@ -1185,7 +1151,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
const handleAgentChange = React.useCallback((agentName: string, options?: { closeModelSelector?: boolean }) => {
|
const handleAgentChange = React.useCallback((agentName: string, options?: { closeModelSelector?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
explicitAgentSwitchRef.current = agentName;
|
|
||||||
setAgent(agentName);
|
setAgent(agentName);
|
||||||
addRecentAgent(agentName);
|
addRecentAgent(agentName);
|
||||||
if (options?.closeModelSelector ?? true) {
|
if (options?.closeModelSelector ?? true) {
|
||||||
|
|||||||
@@ -589,6 +589,30 @@ describe('useConfigStore provider persistence', () => {
|
|||||||
expect(state.currentModelId).toBe('model-a');
|
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 () => {
|
test('loadAgents does not fetch OpenCode config directly', async () => {
|
||||||
useConfigStore.setState({
|
useConfigStore.setState({
|
||||||
activeDirectoryKey: DIRECTORY,
|
activeDirectoryKey: DIRECTORY,
|
||||||
|
|||||||
@@ -2387,6 +2387,9 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
currentProviderId,
|
currentProviderId,
|
||||||
currentModelId,
|
currentModelId,
|
||||||
} = get();
|
} = get();
|
||||||
|
// Captured before the first set below, which unconditionally
|
||||||
|
// marks the selection as manual.
|
||||||
|
const hadManualSelection = get().selectionSource === "manual";
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const directoryKey = state.activeDirectoryKey;
|
const directoryKey = state.activeDirectoryKey;
|
||||||
@@ -2508,8 +2511,7 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
// Prefer a session-level manual override for this agent over the
|
// Prefer a session-level manual override for this agent over the
|
||||||
// agent's configured default. Re-applying setAgent after subtask
|
// agent's configured default. Re-applying setAgent after subtask
|
||||||
// completion / rematerialization must not clobber the override
|
// completion / rematerialization must not clobber the override
|
||||||
// (issue #2404). Explicit agent-picker switches still force the
|
// (issue #2404).
|
||||||
// agent default via ModelControls' shouldPreferAgentModel path.
|
|
||||||
if (currentSessionId) {
|
if (currentSessionId) {
|
||||||
const existingAgentModel = useSelectionStore.getState().getAgentModelForSession(currentSessionId, agentName);
|
const existingAgentModel = useSelectionStore.getState().getAgentModelForSession(currentSessionId, agentName);
|
||||||
if (existingAgentModel && hasProviderModel(providers, existingAgentModel.providerId, existingAgentModel.modelId)) {
|
if (existingAgentModel && hasProviderModel(providers, existingAgentModel.providerId, existingAgentModel.modelId)) {
|
||||||
@@ -2538,6 +2540,14 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 the agent has no preferred model, use settings default.
|
||||||
if (settingsDefaultModel) {
|
if (settingsDefaultModel) {
|
||||||
const parsed = parseModelString(settingsDefaultModel);
|
const parsed = parseModelString(settingsDefaultModel);
|
||||||
|
|||||||
Reference in New Issue
Block a user