From 6dd2b356230b1167282c6b04460f0e025570c9b0 Mon Sep 17 00:00:00 2001 From: YeLuo Date: Mon, 16 Mar 2026 15:23:30 +0800 Subject: [PATCH] fix: stabilize model selection across session boundaries - NewChatPage: read initial model/provider from localStorage instead of hardcoded 'sonnet'/'' so new chats inherit the last selection - ChatView: persist model changes to localStorage in handleProviderModelChange so mid-session switches carry over to new chats - NewChatPage: persist model changes to localStorage in onProviderModelChange - MessageInput: gate auto-correction effect on modelsLoaded flag to prevent premature model reset while provider models are still being fetched - useProviderModels: add loaded flag to distinguish "not fetched yet" from "provider has no models" Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/chat/page.tsx | 10 ++++++++-- src/components/chat/ChatView.tsx | 3 +++ src/components/chat/MessageInput.tsx | 6 ++++-- src/hooks/useProviderModels.ts | 7 +++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index f0ce6707..4c4b7aa7 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -44,8 +44,12 @@ export default function NewChatPage() { const [recentProjects, setRecentProjects] = useState([]); const [hasProvider, setHasProvider] = useState(true); // assume true until checked const [mode] = useState('code'); - const [currentModel, setCurrentModel] = useState('sonnet'); - const [currentProviderId, setCurrentProviderId] = useState(''); + const [currentModel, setCurrentModel] = useState(() => + (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') : null) || 'sonnet' + ); + const [currentProviderId, setCurrentProviderId] = useState(() => + (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') : null) || '' + ); const [pendingPermission, setPendingPermission] = useState(null); const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null); const [streamingToolOutput, setStreamingToolOutput] = useState(''); @@ -554,6 +558,8 @@ export default function NewChatPage() { onProviderModelChange={(pid, model) => { setCurrentProviderId(pid); setCurrentModel(model); + localStorage.setItem('codepilot:last-model', model); + localStorage.setItem('codepilot:last-provider-id', pid); }} workingDirectory={workingDir} effort={selectedEffort} diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 4870077b..58284848 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -122,6 +122,9 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal const handleProviderModelChange = useCallback((newProviderId: string, model: string) => { setCurrentProviderId(newProviderId); setCurrentModel(model); + // Persist to localStorage so new chats inherit the latest selection + localStorage.setItem('codepilot:last-model', model); + localStorage.setItem('codepilot:last-provider-id', newProviderId); fetch(`/api/chat/sessions/${sessionId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 8c04ca39..0e9a3197 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -81,17 +81,19 @@ export function MessageInput({ // --- Extracted hooks --- const popover = usePopoverState(modelName); - const { providerGroups, currentProviderIdValue, modelOptions, currentModelOption } = useProviderModels(providerId, modelName); + const { providerGroups, currentProviderIdValue, modelOptions, currentModelOption, loaded: modelsLoaded } = useProviderModels(providerId, modelName); // Auto-correct model when it doesn't exist in the current provider's model list. // This prevents sending an unsupported model name (e.g. 'opus' to MiniMax which only has 'sonnet'). + // Gate on `modelsLoaded` to avoid premature correction before the provider models API responds. useEffect(() => { + if (!modelsLoaded) return; if (modelName && modelOptions.length > 0 && !modelOptions.some(m => m.value === modelName)) { const fallback = modelOptions[0].value; onModelChange?.(fallback); onProviderModelChange?.(currentProviderIdValue, fallback); } - }, [modelName, modelOptions, currentProviderIdValue, onModelChange, onProviderModelChange]); + }, [modelsLoaded, modelName, modelOptions, currentProviderIdValue, onModelChange, onProviderModelChange]); const { badge, setBadge, cliBadge, setCliBadge, removeBadge, removeCliBadge, hasBadge } = useCommandBadge(textareaRef); diff --git a/src/hooks/useProviderModels.ts b/src/hooks/useProviderModels.ts index 570d9460..8f664e65 100644 --- a/src/hooks/useProviderModels.ts +++ b/src/hooks/useProviderModels.ts @@ -13,6 +13,8 @@ export interface UseProviderModelsReturn { currentProviderIdValue: string; modelOptions: typeof DEFAULT_MODEL_OPTIONS; currentModelOption: (typeof DEFAULT_MODEL_OPTIONS)[number]; + /** True once the initial fetch has completed (success or error). */ + loaded: boolean; } export function useProviderModels( @@ -21,6 +23,7 @@ export function useProviderModels( ): UseProviderModelsReturn { const [providerGroups, setProviderGroups] = useState([]); const [defaultProviderId, setDefaultProviderId] = useState(''); + const [loaded, setLoaded] = useState(false); const fetchProviderModels = useCallback(() => { fetch('/api/providers/models') @@ -46,6 +49,9 @@ export function useProviderModels( models: DEFAULT_MODEL_OPTIONS, }]); setDefaultProviderId(''); + }) + .finally(() => { + setLoaded(true); }); }, []); @@ -73,5 +79,6 @@ export function useProviderModels( currentProviderIdValue, modelOptions, currentModelOption, + loaded, }; }