diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index dab34e1d81..b033fff67a 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -138,7 +138,7 @@ export const ChatInput: React.FC = (props) => { const inputRef = useRef(null); const modelSelectorRef = useRef(null); const [mode, setMode] = useMode(); - const { recentModels, addModel, evictModel } = useModelLRU(); + const { recentModels, addModel, evictModel, defaultModel, setDefaultModel } = useModelLRU(); const commandListId = useId(); const telemetry = useTelemetry(); const [vimEnabled, setVimEnabled] = usePersistedState(VIM_ENABLED_KEY, false, { @@ -176,6 +176,15 @@ export const ChatInput: React.FC = (props) => { [storageKeys.modelKey, addModel] ); + // When entering creation mode (or when the default model changes), reset the + // project-scoped model to the explicit default so manual picks don't bleed + // into subsequent creation flows. + useEffect(() => { + if (variant === "creation" && defaultModel) { + updatePersistedState(storageKeys.modelKey, defaultModel); + } + }, [variant, defaultModel, storageKeys.modelKey]); + // Creation-specific state (hook always called, but only used when variant === "creation") // This avoids conditional hook calls which violate React rules const creationState = useCreationWorkspace( @@ -967,6 +976,8 @@ export const ChatInput: React.FC = (props) => { recentModels={recentModels} onRemoveModel={evictModel} onComplete={() => inputRef.current?.focus()} + defaultModel={defaultModel} + onSetDefaultModel={setDefaultModel} /> ? diff --git a/src/browser/components/ModelSelector.stories.tsx b/src/browser/components/ModelSelector.stories.tsx index 201c7efffe..ca236ce235 100644 --- a/src/browser/components/ModelSelector.stories.tsx +++ b/src/browser/components/ModelSelector.stories.tsx @@ -77,3 +77,18 @@ export const WithManyModels: Story = { onComplete: action("onComplete"), }, }; + +export const WithDefaultModel: Story = { + args: { + value: "anthropic:claude-sonnet-4-5", + onChange: action("onChange"), + onRemoveModel: action("onRemoveModel"), + recentModels: ["anthropic:claude-sonnet-4-5", "anthropic:claude-opus-4-1", "openai:gpt-5-pro"], + onComplete: action("onComplete"), + defaultModel: "anthropic:claude-opus-4-1", + onSetDefaultModel: (model) => { + // Mimic the hook behavior - only allow setting, not clearing + if (model) action("onSetDefaultModel")(model); + }, + }, +}; diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 4e5230d1be..b3eb8306a6 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -7,6 +7,8 @@ import React, { forwardRef, } from "react"; import { cn } from "@/common/lib/utils"; +import { Star } from "lucide-react"; +import { TooltipWrapper, Tooltip } from "./Tooltip"; interface ModelSelectorProps { value: string; @@ -14,6 +16,8 @@ interface ModelSelectorProps { recentModels: string[]; onRemoveModel?: (model: string) => void; onComplete?: () => void; + defaultModel?: string | null; + onSetDefaultModel?: (model: string) => void; } export interface ModelSelectorRef { @@ -21,7 +25,10 @@ export interface ModelSelectorRef { } export const ModelSelector = forwardRef( - ({ value, onChange, recentModels, onRemoveModel, onComplete }, ref) => { + ( + { value, onChange, recentModels, onRemoveModel, onComplete, defaultModel, onSetDefaultModel }, + ref + ) => { const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); const [error, setError] = useState(null); @@ -179,6 +186,14 @@ export const ModelSelector = forwardRef( setHighlightedIndex(currentIndex); }, [recentModels, value]); + const handleSetDefault = (e: React.MouseEvent, model: string) => { + e.preventDefault(); + e.stopPropagation(); + if (defaultModel !== model && onSetDefaultModel) { + onSetDefaultModel(model); + } + }; + // Expose open method to parent via ref useImperativeHandle( ref, @@ -241,18 +256,49 @@ export const ModelSelector = forwardRef( )} onClick={() => handleSelectModel(model)} > -
- {model} - {onRemoveModel && ( - - )} +
+ {model} +
+ {onSetDefaultModel && ( + + + + {defaultModel === model + ? "Current default model" + : "Set as default model"} + + + )} + {onRemoveModel && defaultModel !== model && ( + + )} +
))} diff --git a/src/browser/hooks/useAIViewKeybinds.ts b/src/browser/hooks/useAIViewKeybinds.ts index 47e86381eb..c70bb39a61 100644 --- a/src/browser/hooks/useAIViewKeybinds.ts +++ b/src/browser/hooks/useAIViewKeybinds.ts @@ -6,7 +6,7 @@ import { updatePersistedState, readPersistedState } from "@/browser/hooks/usePer import type { ThinkingLevel, ThinkingLevelOn } from "@/common/types/thinking"; import { DEFAULT_THINKING_LEVEL } from "@/common/types/thinking"; import { getThinkingPolicyForModel } from "@/browser/utils/thinking/policy"; -import { getDefaultModelFromLRU } from "@/browser/hooks/useModelLRU"; +import { getDefaultModel } from "@/browser/hooks/useModelLRU"; import type { StreamingMessageAggregator } from "@/browser/utils/messages/StreamingMessageAggregator"; import { isCompactingStream, cancelCompaction } from "@/browser/utils/compaction/handler"; @@ -116,7 +116,7 @@ export function useAIViewKeybinds({ // Fall back to message history model, then to most recent model from LRU // This matches the same logic as useSendMessageOptions const selectedModel = readPersistedState(getModelKey(workspaceId), null); - const modelToUse = selectedModel ?? currentModel ?? getDefaultModelFromLRU(); + const modelToUse = selectedModel ?? currentModel ?? getDefaultModel(); // Storage key for remembering this model's last-used active thinking level const lastThinkingKey = getLastThinkingByModelKey(modelToUse); diff --git a/src/browser/hooks/useModelLRU.ts b/src/browser/hooks/useModelLRU.ts index 67da7897bd..565698eada 100644 --- a/src/browser/hooks/useModelLRU.ts +++ b/src/browser/hooks/useModelLRU.ts @@ -6,6 +6,7 @@ import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; const MAX_LRU_SIZE = 12; const LRU_KEY = "model-lru"; +const DEFAULT_MODEL_KEY = "model-default"; // Ensure defaultModel is first, then fill with other abbreviations (deduplicated) const FALLBACK_MODEL = WORKSPACE_DEFAULTS.model ?? defaultModel; @@ -13,6 +14,7 @@ const DEFAULT_MODELS = [ FALLBACK_MODEL, ...Array.from(new Set(Object.values(MODEL_ABBREVIATIONS))).filter((m) => m !== FALLBACK_MODEL), ].slice(0, MAX_LRU_SIZE); + function persistModels(models: string[]): void { updatePersistedState(LRU_KEY, models.slice(0, MAX_LRU_SIZE)); } @@ -31,15 +33,9 @@ export function evictModelFromLRU(model: string): void { persistModels(nextList); } -/** - * Get the default model from LRU (non-hook version for use outside React) - * This is the ONLY place that reads from LRU outside of the hook. - * - * @returns The most recently used model, or WORKSPACE_DEFAULTS.model if LRU is empty - */ -export function getDefaultModelFromLRU(): string { - const lru = readPersistedState(LRU_KEY, DEFAULT_MODELS.slice(0, MAX_LRU_SIZE)); - return lru[0] ?? FALLBACK_MODEL; +export function getDefaultModel(): string { + const persisted = readPersistedState(DEFAULT_MODEL_KEY, null); + return persisted ?? FALLBACK_MODEL; } /** @@ -54,6 +50,12 @@ export function useModelLRU() { { listener: true } ); + const [defaultModel, setDefaultModel] = usePersistedState( + DEFAULT_MODEL_KEY, + FALLBACK_MODEL, + { listener: true } + ); + // Merge any new defaults from MODEL_ABBREVIATIONS (only once on mount) useEffect(() => { setRecentModels((prev) => { @@ -107,5 +109,7 @@ export function useModelLRU() { evictModel, getRecentModels, recentModels, + defaultModel, + setDefaultModel, }; } diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index 33472a2d1b..576211c96a 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -1,7 +1,7 @@ import { useThinkingLevel } from "./useThinkingLevel"; import { useMode } from "@/browser/contexts/ModeContext"; import { usePersistedState } from "./usePersistedState"; -import { getDefaultModelFromLRU } from "./useModelLRU"; +import { getDefaultModel } from "./useModelLRU"; import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { getModelKey } from "@/common/constants/storage"; import type { SendMessageOptions } from "@/common/types/ipc"; @@ -56,7 +56,7 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptions { const [thinkingLevel] = useThinkingLevel(); const [mode] = useMode(); const { options: providerOptions } = useProviderOptions(); - const defaultModel = getDefaultModelFromLRU(); + const defaultModel = getDefaultModel(); const [preferredModel] = usePersistedState( getModelKey(workspaceId), defaultModel, // Default to most recently used model diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index 4e4acfb681..b18a2c802c 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -1,7 +1,7 @@ import { getModelKey, getThinkingLevelKey, getModeKey } from "@/common/constants/storage"; import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { readPersistedState } from "@/browser/hooks/usePersistedState"; -import { getDefaultModelFromLRU } from "@/browser/hooks/useModelLRU"; +import { getDefaultModel } from "@/browser/hooks/useModelLRU"; import type { SendMessageOptions } from "@/common/types/ipc"; import type { UIMode } from "@/common/types/mode"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -38,7 +38,7 @@ function getProviderOptions(): MuxProviderOptions { */ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptions { // Read model preference (workspace-specific), fallback to LRU default - const model = readPersistedState(getModelKey(workspaceId), getDefaultModelFromLRU()); + const model = readPersistedState(getModelKey(workspaceId), getDefaultModel()); // Read thinking level (workspace-specific) const thinkingLevel = readPersistedState(