fix(settings): stop defaults from repointing a chat that has its own model

Changing the default model, variant or agent wrote the chat's live
selection unconditionally. For a session carrying its own choice the
effect was mostly transient — ModelControls restores that choice on the
next hydration — but a message sent right after the change went out on
the new model.

The guard is the same pair of signals ModelControls restores from
(shouldPreserveManualModelOverride): a manual selection plus a saved
selection for the current session. Chats following the default still
switch immediately.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 20:31:31 +03:00
parent 0079347edc
commit e2e55756e2
2 changed files with 37 additions and 13 deletions
+1
View File
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Desktop/Remote instances: a managed remote server can now also be published to the remote machine's own network, so other devices there reach it without the SSH tunnel. It requires a UI password, and stays private to the tunnel otherwise. - Desktop/Remote instances: a managed remote server can now also be published to the remote machine's own network, so other devices there reach it without the SSH tunnel. It requires a UI password, and stays private to the tunnel otherwise.
- Desktop/Remote instances: disconnecting from a connection set to not keep the server running now actually stops that remote server. - Desktop/Remote instances: disconnecting from a connection set to not keep the server running now actually stops that remote server.
- Chat: in a chat without a project, the work status card again steps aside when the context panel is open, instead of sitting next to it. - Chat: in a chat without a project, the work status card again steps aside when the context panel is open, instead of sitting next to it.
- Settings/General: changing the default model, variant or agent no longer repoints an open chat that already carries a model you picked for it. Chats following the default still switch immediately.
- **Settings:** the project selector on Providers, Agents, MCP, Commands and Skills now only changes what those pages show. It used to switch the whole app, so opening another project's configuration moved your chat, session list and file tree with it. - **Settings:** the project selector on Providers, Agents, MCP, Commands and Skills now only changes what those pages show. It used to switch the whole app, so opening another project's configuration moved your chat, session list and file tree with it.
- Settings/Providers: the provider you select no longer jumps to a different one on its own. Changing the chat's model or agent, and background provider refreshes, used to move the settings selection with them. - Settings/Providers: the provider you select no longer jumps to a different one on its own. Changing the chat's model or agent, and background provider refreshes, used to move the settings selection with them.
- **Chat sessions:** start chats without choosing a project. They live in their own Chats section, rather than inheriting a project's repository and worktree context. - **Chat sessions:** start chats without choosing a project. They live in their own Chats section, rather than inheriting a project's repository and worktree context.
@@ -17,6 +17,8 @@ import { SettingsInfoHint } from '@/components/sections/shared/SettingsInfoHint'
import { updateDesktopSettings } from '@/lib/persistence'; import { updateDesktopSettings } from '@/lib/persistence';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useSelectionStore } from '@/sync/selection-store';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { parseModelIdentifier } from '@/lib/modelIdentifier'; import { parseModelIdentifier } from '@/lib/modelIdentifier';
@@ -43,6 +45,20 @@ export const DefaultsSettings: React.FC = () => {
const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel); const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel);
const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant); const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant);
const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent); const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent);
// A default describes new sessions. Applying it to the open chat is a
// convenience, not the point, so it stops where the chat carries a choice the
// user made for it — the same pair of signals ModelControls restores from
// (`shouldPreserveManualModelOverride`).
const selectionIsManual = useConfigStore((state) => state.selectionSource === 'manual');
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const getSessionModelSelection = useSelectionStore((state) => state.getSessionModelSelection);
const getSessionAgentSelection = useSelectionStore((state) => state.getSessionAgentSelection);
const chatHasOwnModel = Boolean(
selectionIsManual && currentSessionId && getSessionModelSelection(currentSessionId),
);
const chatHasOwnAgent = Boolean(
selectionIsManual && currentSessionId && getSessionAgentSelection(currentSessionId),
);
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog); const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog); const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
const providers = useConfigStore((state) => state.providers); const providers = useConfigStore((state) => state.providers);
@@ -148,14 +164,17 @@ export const DefaultsSettings: React.FC = () => {
setDefaultModel(newValue); setDefaultModel(newValue);
setDefaultVariant(undefined); setDefaultVariant(undefined);
setSettingsDefaultVariant(undefined); setSettingsDefaultVariant(undefined);
setCurrentVariant(undefined);
setSettingsDefaultModel(newValue); setSettingsDefaultModel(newValue);
if (providerId && modelId) { if (!chatHasOwnModel) {
const provider = providers.find((p) => p.id === providerId); setCurrentVariant(undefined);
if (provider) {
setProvider(providerId); if (providerId && modelId) {
setModel(modelId); const provider = providers.find((p) => p.id === providerId);
if (provider) {
setProvider(providerId);
setModel(modelId);
}
} }
} }
@@ -173,7 +192,7 @@ export const DefaultsSettings: React.FC = () => {
console.warn('Failed to save default model:', error); console.warn('Failed to save default model:', error);
} }
}, },
[providers, setCurrentVariant, setModel, setProvider, setSettingsDefaultModel, setSettingsDefaultVariant] [chatHasOwnModel, providers, setCurrentVariant, setModel, setProvider, setSettingsDefaultModel, setSettingsDefaultVariant]
); );
const DEFAULT_VARIANT_VALUE = '__default__'; const DEFAULT_VARIANT_VALUE = '__default__';
@@ -190,7 +209,9 @@ export const DefaultsSettings: React.FC = () => {
const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : variant || undefined; const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : variant || undefined;
setDefaultVariant(newValue); setDefaultVariant(newValue);
setSettingsDefaultVariant(newValue); setSettingsDefaultVariant(newValue);
setCurrentVariant(newValue); if (!chatHasOwnModel) {
setCurrentVariant(newValue);
}
try { try {
await updateDesktopSettings({ defaultVariant: newValue ?? '' }); await updateDesktopSettings({ defaultVariant: newValue ?? '' });
@@ -198,7 +219,7 @@ export const DefaultsSettings: React.FC = () => {
console.warn('Failed to save default variant:', error); console.warn('Failed to save default variant:', error);
} }
}, },
[setCurrentVariant, setSettingsDefaultVariant] [chatHasOwnModel, setCurrentVariant, setSettingsDefaultVariant]
); );
const handleAgentChange = React.useCallback( const handleAgentChange = React.useCallback(
@@ -207,7 +228,7 @@ export const DefaultsSettings: React.FC = () => {
setDefaultAgent(newValue); setDefaultAgent(newValue);
setSettingsDefaultAgent(newValue); setSettingsDefaultAgent(newValue);
if (agentName) { if (agentName && !chatHasOwnAgent) {
setAgent(agentName); setAgent(agentName);
} }
@@ -217,7 +238,7 @@ export const DefaultsSettings: React.FC = () => {
console.warn('Failed to save default agent:', error); console.warn('Failed to save default agent:', error);
} }
}, },
[setAgent, setSettingsDefaultAgent] [chatHasOwnAgent, setAgent, setSettingsDefaultAgent]
); );
const handleSmallModelUseDefaultChange = React.useCallback( const handleSmallModelUseDefaultChange = React.useCallback(
@@ -316,12 +337,14 @@ export const DefaultsSettings: React.FC = () => {
if (!supportsVariants && defaultVariant) { if (!supportsVariants && defaultVariant) {
setDefaultVariant(undefined); setDefaultVariant(undefined);
setSettingsDefaultVariant(undefined); setSettingsDefaultVariant(undefined);
setCurrentVariant(undefined); if (!chatHasOwnModel) {
setCurrentVariant(undefined);
}
updateDesktopSettings({ defaultVariant: '' }).catch(() => { updateDesktopSettings({ defaultVariant: '' }).catch(() => {
// best effort // best effort
}); });
} }
}, [defaultVariant, setCurrentVariant, setSettingsDefaultVariant, supportsVariants]); }, [chatHasOwnModel, defaultVariant, setCurrentVariant, setSettingsDefaultVariant, supportsVariants]);
if (isLoading) { if (isLoading) {
return null; return null;