From 082a3fe36cbc3afacbeda7a76ce16c0e537f0e48 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 10:36:32 +0900 Subject: [PATCH 01/15] feat: add quick model selector button to chat footer Adds ModelSelector next to the mode, API config and auto-approve controls, with chat i18n strings for all locales. --- .../src/components/chat/ChatTextArea.tsx | 8 + .../src/components/chat/ModelSelector.tsx | 305 ++++++++++++++++++ .../chat/__tests__/ModelSelector.spec.tsx | 101 ++++++ webview-ui/src/i18n/locales/ca/chat.json | 8 + webview-ui/src/i18n/locales/de/chat.json | 8 + webview-ui/src/i18n/locales/en/chat.json | 8 + webview-ui/src/i18n/locales/es/chat.json | 8 + webview-ui/src/i18n/locales/fr/chat.json | 8 + webview-ui/src/i18n/locales/hi/chat.json | 8 + webview-ui/src/i18n/locales/id/chat.json | 8 + webview-ui/src/i18n/locales/it/chat.json | 8 + webview-ui/src/i18n/locales/ja/chat.json | 8 + webview-ui/src/i18n/locales/ko/chat.json | 8 + webview-ui/src/i18n/locales/nl/chat.json | 8 + webview-ui/src/i18n/locales/pl/chat.json | 8 + webview-ui/src/i18n/locales/pt-BR/chat.json | 8 + webview-ui/src/i18n/locales/ru/chat.json | 8 + webview-ui/src/i18n/locales/tr/chat.json | 8 + webview-ui/src/i18n/locales/vi/chat.json | 8 + webview-ui/src/i18n/locales/zh-CN/chat.json | 8 + webview-ui/src/i18n/locales/zh-TW/chat.json | 8 + 21 files changed, 558 insertions(+) create mode 100644 webview-ui/src/components/chat/ModelSelector.tsx create mode 100644 webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a78f3f0f8e..61419487cc 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -27,6 +27,7 @@ import { StandardTooltip } from "@src/components/ui" import Thumbnails from "../common/Thumbnails" import { ModeSelector } from "./ModeSelector" import { ApiConfigSelector } from "./ApiConfigSelector" +import { ModelSelector } from "./ModelSelector" import { AutoApproveDropdown } from "./AutoApproveDropdown" import { MAX_IMAGES_PER_MESSAGE } from "./constants" import ContextMenu from "./ContextMenu" @@ -92,6 +93,7 @@ export const ChatTextArea = forwardRef( const { filePaths, openedTabs, + apiConfiguration, currentApiConfigName, listApiConfigMeta, customModes, @@ -1319,6 +1321,12 @@ export const ChatTextArea = forwardRef( lockApiConfigAcrossModes={!!lockApiConfigAcrossModes} onToggleLockApiConfig={handleToggleLockApiConfig} /> +
diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx new file mode 100644 index 0000000000..8b1d69cebf --- /dev/null +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -0,0 +1,305 @@ +import React from "react" +import { Fzf } from "fzf" +import { Check, X } from "lucide-react" + +import { + type ProviderSettings, + type ProviderName, + type ModelInfo, + providerIdentifiers, + isRetiredProvider, +} from "@roo-code/types" + +import { cn } from "@/lib/utils" +import { enabledSelectorTriggerClassName, selectorTriggerClassName } from "@/components/ui/selectorTriggerStyles" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { useRouterModels } from "@/components/ui/hooks/useRouterModels" +import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { vscode } from "@/utils/vscode" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + isStaticModelProvider, + getStaticModelsForProvider, + getProviderModelConfig, + handleModelChangeSideEffects, +} from "@/components/settings/utils/providerModelConfig" +import { filterModels } from "@/components/settings/utils/organizationFilters" + +import { IconButton } from "./IconButton" + +const SEARCH_THRESHOLD = 6 + +// Providers whose model can only be picked via a bespoke Settings UI +// (freeform text, local model discovery, or a compound vendor/family selector) +// rather than a simple id list. +const UNSUPPORTED_QUICK_PICK_PROVIDERS: ProviderName[] = [ + providerIdentifiers.openai, + providerIdentifiers.ollama, + providerIdentifiers.lmstudio, + providerIdentifiers.vscodeLm, +] + +function getModelIdField(provider: ProviderName): string | undefined { + return getProviderModelConfig(provider)?.field +} + +function useModelListForProvider(provider: ProviderName | undefined, apiConfiguration?: ProviderSettings) { + const isStatic = provider ? isStaticModelProvider(provider) : false + const isUnsupported = !provider || UNSUPPORTED_QUICK_PICK_PROVIDERS.includes(provider) + const isRouterProvider = !isStatic && !isUnsupported + + const routerModels = useRouterModels({ provider, enabled: isRouterProvider }) + const routerModelsData = routerModels.data as Record> | undefined + + return React.useMemo(() => { + if (!provider || isUnsupported) { + return { isUnsupported: true as const, field: undefined, models: null } + } + + const field = getModelIdField(provider) + if (!field) { + return { isUnsupported: true as const, field: undefined, models: null } + } + + const models = isStatic + ? getStaticModelsForProvider(provider, undefined, apiConfiguration) + : (routerModelsData?.[provider] ?? null) + + return { isUnsupported: false as const, field, models } + }, [provider, isUnsupported, isStatic, apiConfiguration, routerModelsData]) +} + +interface ModelSelectorProps { + apiConfiguration?: ProviderSettings + disabled?: boolean + title: string + triggerClassName?: string +} + +export const ModelSelector = ({ + apiConfiguration, + disabled = false, + title, + triggerClassName = "", +}: ModelSelectorProps) => { + const [open, setOpen] = React.useState(false) + const [searchValue, setSearchValue] = React.useState("") + const searchInputRef = React.useRef(null) + const portalContainer = useRooPortal("roo-portal") + const { t } = useAppTranslation() + const { organizationAllowList, currentApiConfigName } = useExtensionState() + + const providerRaw = apiConfiguration?.apiProvider || providerIdentifiers.openrouter + const provider: ProviderName | undefined = isRetiredProvider(providerRaw) ? undefined : providerRaw + + const { id: selectedModelId, info: selectedModelInfo } = useSelectedModel(apiConfiguration) + const { isUnsupported, field, models } = useModelListForProvider(provider, apiConfiguration) + + const filteredModelRecord = React.useMemo( + () => filterModels(models, provider, organizationAllowList), + [models, provider, organizationAllowList], + ) + + const modelIds = React.useMemo(() => { + if (!filteredModelRecord) return [] + return Object.keys(filteredModelRecord).sort((a, b) => a.localeCompare(b)) + }, [filteredModelRecord]) + + const searchableItems = React.useMemo( + () => + modelIds.map((id) => ({ + id, + searchStr: [id, filteredModelRecord?.[id]?.displayName].filter(Boolean).join(" "), + })), + [modelIds, filteredModelRecord], + ) + + const fzfInstance = React.useMemo( + () => new Fzf(searchableItems, { selector: (item) => item.searchStr }), + [searchableItems], + ) + + const filteredModelIds = React.useMemo(() => { + if (!searchValue) return modelIds + return fzfInstance.find(searchValue).map((result) => result.item.id) + }, [modelIds, searchValue, fzfInstance]) + + const triggerLabel = selectedModelInfo?.displayName ?? selectedModelId ?? "" + + const openSettings = React.useCallback(() => { + vscode.postMessage({ type: "switchTab", tab: "settings" }) + setOpen(false) + }, []) + + // Reuses the same `upsertApiConfiguration` path Settings' Save button uses, + // so the change persists and activates immediately for the current task. + const handleModelChange = React.useCallback( + (modelIdField: string, modelId: string) => { + if (!apiConfiguration || !provider) return + + const updatedConfig = { ...apiConfiguration } as ProviderSettings + ;(updatedConfig as Record)[modelIdField] = modelId + + handleModelChangeSideEffects(provider, modelId, (f, v) => { + ;(updatedConfig as Record)[f] = v + }) + + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration: updatedConfig, + }) + }, + [apiConfiguration, currentApiConfigName, provider], + ) + + const handleSelect = React.useCallback( + (modelId: string) => { + if (!field) return + handleModelChange(field, modelId) + setOpen(false) + setSearchValue("") + }, + [field, handleModelChange], + ) + + const onClearSearch = React.useCallback(() => { + setSearchValue("") + searchInputRef.current?.focus() + }, []) + + const onOpenChange = React.useCallback((isOpen: boolean) => { + setOpen(isOpen) + if (!isOpen) setSearchValue("") + }, []) + + React.useEffect(() => { + if (open && searchInputRef.current) { + searchInputRef.current.focus() + } + }, [open]) + + const triggerClasses = cn( + "inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs", + selectorTriggerClassName, + disabled ? "opacity-50 cursor-not-allowed" : enabledSelectorTriggerClassName, + triggerClassName, + ) + + if (isUnsupported) { + return ( + + + + ) + } + + const showSearch = modelIds.length > SEARCH_THRESHOLD + + return ( + + + + {triggerLabel} + + + +
+ {showSearch ? ( +
+ setSearchValue(e.target.value)} + placeholder={t("chat:modelSelector.searchPlaceholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="model-search-input" + /> + {searchValue.length > 0 && ( +
+ +
+ )} +
+ ) : ( +
+

+ {t("chat:modelSelector.description")} +

+
+ )} + +
+ {filteredModelIds.length === 0 ? ( +
+ {t("common:ui.no_results")} +
+ ) : ( +
+ {filteredModelIds.map((modelId) => { + const isSelected = modelId === selectedModelId + const modelInfo = filteredModelRecord?.[modelId] + return ( +
handleSelect(modelId)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "hover:bg-vscode-list-hoverBackground", + isSelected + ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" + : "", + )} + data-testid="model-selector-item"> +
+
{modelInfo?.displayName ?? modelId}
+
+ {isSelected && } +
+ ) + })} +
+ )} +
+ +
+
+ +
+
+ {showSearch && ( + + + + )} +

+ {t("chat:modelSelector.title")} +

+
+
+
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx new file mode 100644 index 0000000000..5211dba3b6 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -0,0 +1,101 @@ +import { render, screen, fireEvent } from "@/utils/test-utils" + +import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" + +import { ModelSelector } from "../ModelSelector" + +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + organizationAllowList: { allowAll: true, providers: {} }, + currentApiConfigName: "default", + }), +})) + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@/components/ui/hooks/useRooPortal", () => ({ + useRooPortal: () => document.body, +})) + +import { vscode } from "@/utils/vscode" + +describe("ModelSelector", () => { + const anthropicConfig: ProviderSettings = { + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-sonnet-4-5", + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + test("shows the currently selected model on the trigger for a static-model provider", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toBeInTheDocument() + }) + + test("disables the trigger when disabled", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toBeDisabled() + }) + + test("shows a search bar since anthropic has more than 6 models and filters the list", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByTestId("model-search-input")).toBeInTheDocument() + const allItems = screen.getAllByTestId("model-selector-item") + expect(allItems.length).toBeGreaterThan(6) + + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + + const filteredItems = screen.getAllByTestId("model-selector-item") + expect(filteredItems.length).toBeLessThan(allItems.length) + }) + + test("selecting a model persists it via upsertApiConfiguration and closes the popover", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + + fireEvent.click(screen.getAllByTestId("model-selector-item")[0]) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "upsertApiConfiguration", + text: "default", + apiConfiguration: expect.objectContaining({ apiModelId: "claude-opus-4-6" }), + }) + expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() + }) + + test("falls back to a degraded trigger for providers without a simple model list, and jumps to Settings on click", () => { + render( + , + ) + + expect(screen.queryByTestId("model-selector-trigger")).not.toBeInTheDocument() + const unsupportedTrigger = screen.getByTestId("model-selector-unsupported-trigger") + expect(unsupportedTrigger).toBeInTheDocument() + + fireEvent.click(unsupportedTrigger) + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "switchTab", tab: "settings" }) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 7779a0a5cb..4d7f5018b0 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Selecciona el mode d'interacció", "selectApiConfig": "Seleccioneu la configuració de l'API", + "selectModel": "Selecciona el model", + "modelSelector": { + "title": "Models", + "settings": "Configuració del model", + "description": "Tria el model que s'utilitzarà per a aquesta configuració d'API.", + "searchPlaceholder": "Cerca models...", + "unsupportedTooltip": "El model d'aquest proveïdor es configura a Configuració" + }, "lockApiConfigAcrossModes": "Bloqueja la configuració de l'API a tots els modes en aquest espai de treball", "unlockApiConfigAcrossModes": "La configuració de l'API està bloquejada a tots els modes en aquest espai de treball (fes clic per desbloquejar)", "enhancePrompt": "Millora la sol·licitud amb context addicional", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 45f9d34c3b..48b9ab59d2 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Interaktionsmodus auswählen", "selectApiConfig": "API-Konfiguration auswählen", + "selectModel": "Modell auswählen", + "modelSelector": { + "title": "Modelle", + "settings": "Modelleinstellungen", + "description": "Wähle das Modell für diese API-Konfiguration aus.", + "searchPlaceholder": "Modelle suchen...", + "unsupportedTooltip": "Das Modell dieses Anbieters wird in den Einstellungen konfiguriert" + }, "lockApiConfigAcrossModes": "API-Konfiguration für alle Modi in diesem Arbeitsbereich sperren", "unlockApiConfigAcrossModes": "API-Konfiguration ist für alle Modi in diesem Arbeitsbereich gesperrt (klicke zum Entsperren)", "enhancePrompt": "Prompt mit zusätzlichem Kontext verbessern", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 460e94413e..4e779be6ac 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -140,6 +140,7 @@ }, "selectMode": "Select mode for interaction", "selectApiConfig": "Select API configuration", + "selectModel": "Select model", "lockApiConfigAcrossModes": "Lock API configuration across all modes in this workspace", "unlockApiConfigAcrossModes": "API configuration is locked across all modes in this workspace (click to unlock)", "enhancePrompt": "Enhance prompt with additional context", @@ -151,6 +152,13 @@ "searchPlaceholder": "Search modes...", "noResults": "No results found" }, + "modelSelector": { + "title": "Models", + "settings": "Model Settings", + "description": "Choose the model used for this API configuration.", + "searchPlaceholder": "Search models...", + "unsupportedTooltip": "This provider's model is configured in Settings" + }, "enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", "addImages": "Add images to message", "sendMessage": "Send message", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 9f2727493f..47b68a379b 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Seleccionar modo de interacción", "selectApiConfig": "Seleccionar configuración de API", + "selectModel": "Seleccionar modelo", + "modelSelector": { + "title": "Modelos", + "settings": "Configuración del modelo", + "description": "Elige el modelo que se usará para esta configuración de API.", + "searchPlaceholder": "Buscar modelos...", + "unsupportedTooltip": "El modelo de este proveedor se configura en Ajustes" + }, "lockApiConfigAcrossModes": "Bloquear la configuración de API en todos los modos de este espacio de trabajo", "unlockApiConfigAcrossModes": "La configuración de API está bloqueada en todos los modos de este espacio de trabajo (clic para desbloquear)", "enhancePrompt": "Mejorar el mensaje con contexto adicional", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 2b4cf8c557..f188846302 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Sélectionner le mode d'interaction", "selectApiConfig": "Sélectionner la configuration de l'API", + "selectModel": "Sélectionner le modèle", + "modelSelector": { + "title": "Modèles", + "settings": "Paramètres du modèle", + "description": "Choisissez le modèle utilisé pour cette configuration d'API.", + "searchPlaceholder": "Rechercher des modèles...", + "unsupportedTooltip": "Le modèle de ce fournisseur se configure dans les Paramètres" + }, "lockApiConfigAcrossModes": "Verrouiller la configuration API pour tous les modes dans cet espace de travail", "unlockApiConfigAcrossModes": "La configuration API est verrouillée pour tous les modes dans cet espace de travail (cliquer pour déverrouiller)", "enhancePrompt": "Améliorer la requête avec un contexte supplémentaire", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index ebbd679434..e411da4d39 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "इंटरैक्शन मोड चुनें", "selectApiConfig": "एपीआई कॉन्फ़िगरेशन का चयन करें", + "selectModel": "मॉडल चुनें", + "modelSelector": { + "title": "मॉडल", + "settings": "मॉडल सेटिंग्स", + "description": "इस API कॉन्फ़िगरेशन के लिए उपयोग किया जाने वाला मॉडल चुनें।", + "searchPlaceholder": "मॉडल खोजें...", + "unsupportedTooltip": "इस प्रदाता का मॉडल सेटिंग्स में कॉन्फ़िगर किया जाता है" + }, "lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें", "unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)", "enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index d097894162..a7add7f3a8 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -143,6 +143,14 @@ }, "selectMode": "Pilih mode untuk interaksi", "selectApiConfig": "Pilih konfigurasi API", + "selectModel": "Pilih model", + "modelSelector": { + "title": "Model", + "settings": "Pengaturan Model", + "description": "Pilih model yang digunakan untuk konfigurasi API ini.", + "searchPlaceholder": "Cari model...", + "unsupportedTooltip": "Model penyedia ini dikonfigurasi di Pengaturan" + }, "lockApiConfigAcrossModes": "Kunci konfigurasi API di semua mode dalam workspace ini", "unlockApiConfigAcrossModes": "Konfigurasi API terkunci di semua mode dalam workspace ini (klik untuk membuka kunci)", "enhancePrompt": "Tingkatkan prompt dengan konteks tambahan", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index e99c76e702..fc77cc634f 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Seleziona modalità di interazione", "selectApiConfig": "Seleziona la configurazione API", + "selectModel": "Seleziona modello", + "modelSelector": { + "title": "Modelli", + "settings": "Impostazioni del modello", + "description": "Scegli il modello da usare per questa configurazione API.", + "searchPlaceholder": "Cerca modelli...", + "unsupportedTooltip": "Il modello di questo provider si configura nelle Impostazioni" + }, "lockApiConfigAcrossModes": "Blocca la configurazione API per tutte le modalità in questo workspace", "unlockApiConfigAcrossModes": "La configurazione API è bloccata per tutte le modalità in questo workspace (clicca per sbloccare)", "enhancePrompt": "Migliora prompt con contesto aggiuntivo", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 5ffd5c873c..75eb5396bb 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "対話モードを選択", "selectApiConfig": "API構成を選択", + "selectModel": "モデルを選択", + "modelSelector": { + "title": "モデル", + "settings": "モデル設定", + "description": "この API 設定で使用するモデルを選択します。", + "searchPlaceholder": "モデルを検索...", + "unsupportedTooltip": "このプロバイダーのモデルは設定で構成します" + }, "lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック", "unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)", "enhancePrompt": "追加コンテキストでプロンプトを強化", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 313d3b4722..4293463b6c 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -113,6 +113,7 @@ }, "selectMode": "상호작용 모드 선택", "selectApiConfig": "API 구성 선택", + "selectModel": "모델 선택", "lockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성 잠금", "unlockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성이 잠겨 있습니다 (클릭하여 해제)", "enhancePrompt": "추가 컨텍스트로 프롬프트 향상", @@ -134,6 +135,13 @@ "searchPlaceholder": "모드 검색...", "noResults": "결과를 찾을 수 없습니다" }, + "modelSelector": { + "title": "모델", + "settings": "모델 설정", + "description": "이 API 구성에서 사용할 모델을 선택하세요.", + "searchPlaceholder": "모델 검색...", + "unsupportedTooltip": "이 공급자의 모델은 설정에서 구성합니다" + }, "errorReadingFile": "파일 읽기 오류", "noValidImages": "처리된 유효한 이미지가 없습니다", "separator": "구분자", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 1e091996c6..a36783301d 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Selecteer modus voor interactie", "selectApiConfig": "Selecteer API-configuratie", + "selectModel": "Model selecteren", + "modelSelector": { + "title": "Modellen", + "settings": "Modelinstellingen", + "description": "Kies het model dat voor deze API-configuratie wordt gebruikt.", + "searchPlaceholder": "Modellen zoeken...", + "unsupportedTooltip": "Het model van deze provider wordt in de Instellingen geconfigureerd" + }, "lockApiConfigAcrossModes": "API-configuratie vergrendelen voor alle modi in deze werkruimte", "unlockApiConfigAcrossModes": "API-configuratie is vergrendeld voor alle modi in deze werkruimte (klik om te ontgrendelen)", "enhancePrompt": "Prompt verbeteren met extra context", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 76ff83065b..9581584289 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Wybierz tryb interakcji", "selectApiConfig": "Wybierz konfigurację API", + "selectModel": "Wybierz model", + "modelSelector": { + "title": "Modele", + "settings": "Ustawienia modelu", + "description": "Wybierz model używany w tej konfiguracji API.", + "searchPlaceholder": "Szukaj modeli...", + "unsupportedTooltip": "Model tego dostawcy jest konfigurowany w Ustawieniach" + }, "lockApiConfigAcrossModes": "Zablokuj konfigurację API dla wszystkich trybów w tym obszarze roboczym", "unlockApiConfigAcrossModes": "Konfiguracja API jest zablokowana dla wszystkich trybów w tym obszarze roboczym (kliknij, aby odblokować)", "enhancePrompt": "Ulepsz podpowiedź dodatkowym kontekstem", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 5884e3424b..b4d67f529a 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Selecionar modo de interação", "selectApiConfig": "Selecionar configuração da API", + "selectModel": "Selecionar modelo", + "modelSelector": { + "title": "Modelos", + "settings": "Configurações do modelo", + "description": "Escolha o modelo usado nesta configuração de API.", + "searchPlaceholder": "Pesquisar modelos...", + "unsupportedTooltip": "O modelo deste provedor é configurado nas Configurações" + }, "lockApiConfigAcrossModes": "Bloquear configuração da API em todos os modos neste workspace", "unlockApiConfigAcrossModes": "A configuração da API está bloqueada em todos os modos neste workspace (clique para desbloquear)", "enhancePrompt": "Aprimorar prompt com contexto adicional", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 8de32b56fe..9ba08d8fec 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Выберите режим взаимодействия", "selectApiConfig": "Выберите конфигурацию API", + "selectModel": "Выбрать модель", + "modelSelector": { + "title": "Модели", + "settings": "Настройки модели", + "description": "Выберите модель для этой конфигурации API.", + "searchPlaceholder": "Поиск моделей...", + "unsupportedTooltip": "Модель этого провайдера настраивается в Настройках" + }, "lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве", "unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)", "enhancePrompt": "Улучшить запрос с дополнительным контекстом", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 0cb4576ee0..1d39b29d1c 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Etkileşim modunu seçin", "selectApiConfig": "API yapılandırmasını seçin", + "selectModel": "Model seç", + "modelSelector": { + "title": "Modeller", + "settings": "Model Ayarları", + "description": "Bu API yapılandırması için kullanılacak modeli seçin.", + "searchPlaceholder": "Model ara...", + "unsupportedTooltip": "Bu sağlayıcının modeli Ayarlar'da yapılandırılır" + }, "lockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırmasını kilitle", "unlockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırması kilitli (kilidi açmak için tıkla)", "enhancePrompt": "Ek bağlamla istemi geliştir", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 5a3965a5c1..10185f2dc8 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "Chọn chế độ tương tác", "selectApiConfig": "Chọn cấu hình API", + "selectModel": "Chọn mô hình", + "modelSelector": { + "title": "Mô hình", + "settings": "Cài đặt mô hình", + "description": "Chọn mô hình được dùng cho cấu hình API này.", + "searchPlaceholder": "Tìm kiếm mô hình...", + "unsupportedTooltip": "Mô hình của nhà cung cấp này được cấu hình trong Cài đặt" + }, "lockApiConfigAcrossModes": "Khóa cấu hình API cho tất cả chế độ trong workspace này", "unlockApiConfigAcrossModes": "Cấu hình API đã bị khóa cho tất cả chế độ trong workspace này (nhấn để mở khóa)", "enhancePrompt": "Nâng cao yêu cầu với ngữ cảnh bổ sung", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 982220c2ea..e64a671203 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -113,6 +113,14 @@ }, "selectMode": "选择交互模式", "selectApiConfig": "选择 API 配置", + "selectModel": "选择模型", + "modelSelector": { + "title": "模型", + "settings": "模型设置", + "description": "选择此 API 配置使用的模型。", + "searchPlaceholder": "搜索模型...", + "unsupportedTooltip": "此提供商的模型需在设置中配置" + }, "lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置", "unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)", "enhancePrompt": "增强提示词", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 65fbc49176..501be13b2c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -140,6 +140,14 @@ }, "selectMode": "選擇互動模式", "selectApiConfig": "選取 API 設定", + "selectModel": "選擇模型", + "modelSelector": { + "title": "模型", + "settings": "模型設定", + "description": "選擇此 API 設定使用的模型。", + "searchPlaceholder": "搜尋模型...", + "unsupportedTooltip": "此供應商的模型需在設定中配置" + }, "lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定", "unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)", "enhancePrompt": "使用額外內容強化提示詞", From 81cb7e566de2bb49fcffd46cdb771acc6415267b Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 10:36:32 +0900 Subject: [PATCH 02/15] test: update ChatTextArea visual baselines for model selector The footer now includes the ModelSelector button, so the resting/focus composer snapshots change and the editor needs one more Tab stop to reach. --- .../chat/__tests__/ChatTextArea.visual.tsx | 2 +- .../chat-composer-focus-dark.png | Bin 9166 -> 9801 bytes ...hat-composer-focus-high-contrast-light.png | Bin 9606 -> 10292 bytes .../chat-composer-focus-high-contrast.png | Bin 9624 -> 10313 bytes .../chat-composer-focus-light.png | Bin 9326 -> 10017 bytes .../chat-composer-resting-dark.png | Bin 9162 -> 9801 bytes ...t-composer-resting-high-contrast-light.png | Bin 9606 -> 10292 bytes .../chat-composer-resting-high-contrast.png | Bin 9624 -> 10313 bytes .../chat-composer-resting-light.png | Bin 9326 -> 10017 bytes 9 files changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx index 9c447fe011..cebbf7493d 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx @@ -17,7 +17,7 @@ for (const theme of visualThemes) { await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()) for ( let index = 0; - index < 10 && !(await editor.evaluate((element) => element === document.activeElement)); + index < 12 && !(await editor.evaluate((element) => element === document.activeElement)); index++ ) { await page.keyboard.press("Tab") diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-dark.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-dark.png index 4de566f736bcf2ebeb288f4d8eaea9b509255c29..b829f280789e74525907c49023a86903986e7b0a 100644 GIT binary patch literal 9801 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 diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-high-contrast-light.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-high-contrast-light.png index f5c759fad8546da967cdbb07f93b034d78d293be..4d815fe6eb3e9a9dbf68b69fd2f5634be458616d 100644 GIT binary patch literal 10292 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 G2L3Of2;d|D diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-high-contrast.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-high-contrast.png index 465ffc0263d816e953f37275f9ceb514223f33bf..dbe2d65f2dfbf0d17887c5fd7ef74b5b17cc4623 100644 GIT binary patch literal 10313 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�T)Xjeo%zI=0Jw}Nu$swu0L3TL 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 L@}d>O`houi$=aJZ diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-light.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-focus-light.png index 1f238a4619657763f15f05e20656ec0f08a03eb9..1cb9115d042991a9037e9d68a43d80aa5209255c 100644 GIT binary patch literal 10017 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 diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-dark.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-dark.png index fad9aa30b923ada5cbd0e6c1f1a5df83319b1d39..b829f280789e74525907c49023a86903986e7b0a 100644 GIT binary patch literal 9801 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 CivmCZ diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast-light.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast-light.png index f5c759fad8546da967cdbb07f93b034d78d293be..4d815fe6eb3e9a9dbf68b69fd2f5634be458616d 100644 GIT binary patch literal 10292 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 G2L3Of2;d|D diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast.png index 465ffc0263d816e953f37275f9ceb514223f33bf..dbe2d65f2dfbf0d17887c5fd7ef74b5b17cc4623 100644 GIT binary patch literal 10313 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�T)Xjeo%zI=0Jw}Nu$swu0L3TL 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 L@}d>O`houi$=aJZ diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-light.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-light.png index 1f238a4619657763f15f05e20656ec0f08a03eb9..1cb9115d042991a9037e9d68a43d80aa5209255c 100644 GIT binary patch literal 10017 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 From 7049752362b36b3a78f6ffd733cb4c7513e42776 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 10:41:54 +0900 Subject: [PATCH 03/15] fix: skip no-op model reselect and hide deprecated models in ModelSelector Re-selecting the active model reset reasoning settings via side effects, and deprecated models were newly selectable. Match ModelPicker behavior. Co-Authored-By: Claude Code --- .../src/components/chat/ModelSelector.tsx | 11 +++-- .../chat/__tests__/ModelSelector.spec.tsx | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index 8b1d69cebf..4e6c416262 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -104,8 +104,11 @@ export const ModelSelector = ({ const modelIds = React.useMemo(() => { if (!filteredModelRecord) return [] - return Object.keys(filteredModelRecord).sort((a, b) => a.localeCompare(b)) - }, [filteredModelRecord]) + return Object.entries(filteredModelRecord) + .filter(([id, info]) => id === selectedModelId || !info.deprecated) + .map(([id]) => id) + .sort((a, b) => a.localeCompare(b)) + }, [filteredModelRecord, selectedModelId]) const searchableItems = React.useMemo( () => @@ -158,11 +161,11 @@ export const ModelSelector = ({ const handleSelect = React.useCallback( (modelId: string) => { if (!field) return - handleModelChange(field, modelId) + if (modelId !== selectedModelId) handleModelChange(field, modelId) setOpen(false) setSearchValue("") }, - [field, handleModelChange], + [field, handleModelChange, selectedModelId], ) const onClearSearch = React.useCallback(() => { diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index 5211dba3b6..16197accd2 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -27,6 +27,17 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({ useRooPortal: () => document.body, })) +const staticModelsOverride = vi.hoisted(() => ({ value: undefined as Record | undefined })) + +vi.mock("@/components/settings/utils/providerModelConfig", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getStaticModelsForProvider: (...args: Parameters) => + staticModelsOverride.value ?? actual.getStaticModelsForProvider(...args), + } +}) + import { vscode } from "@/utils/vscode" describe("ModelSelector", () => { @@ -37,6 +48,41 @@ describe("ModelSelector", () => { beforeEach(() => { vi.clearAllMocks() + staticModelsOverride.value = undefined + }) + + test("re-selecting the active model does not post a config update", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-sonnet-4-5" } }) + const items = screen.getAllByTestId("model-selector-item") + const active = items.find((el) => el.querySelector(".lucide-check"))! + fireEvent.click(active) + + expect(vscode.postMessage).not.toHaveBeenCalled() + expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() + }) + + test("hides deprecated models except the currently selected one", () => { + const base = { contextWindow: 1000, supportsPromptCache: false } + staticModelsOverride.value = { + "model-active": { ...base, deprecated: true }, + "model-old": { ...base, deprecated: true }, + "model-new": base, + } + + render( + , + ) + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const labels = screen.getAllByTestId("model-selector-item").map((el) => el.textContent) + expect(labels).toEqual(["model-active", "model-new"]) }) test("shows the currently selected model on the trigger for a static-model provider", () => { From d140398cbef9774631e7fb038f8dcf500e8717e7 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 10:53:50 +0900 Subject: [PATCH 04/15] test: update Electron chat-dark baseline for model selector --- .../electron-chat-dark-sidebar.png | Bin 30321 -> 29896 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/vscode-e2e/src/visual/__screenshots__/electron-chat-dark-sidebar.png b/apps/vscode-e2e/src/visual/__screenshots__/electron-chat-dark-sidebar.png index e8497070ef3440fe4a3bbb3ef49f9a1de9d4c4d9..3791a71175cfa767cb683a6453d6c2e0bebf05c0 100644 GIT binary patch delta 10390 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 bdqHF#blkAdi?)Pc0xvb?`${E>=D7a>ov=Hq delta 10818 zcmcI~XH-;8*QG)OO_CN65fK_}v55kbl#FD_IY|=9Ip+&a5)_b}BnwCucu0~3B;-lsY@Ztrj3{uV9=JtwhUdP(L@yN)|p z^K2GLM+y@UTcnF7W$db2)~Yi0NZoGRXV@wu%i~!}l`H0rd+;<)X3Tc%dg-HToNT*8 zZuB;4KW@~N%&xndyWF}f`dz9(J2uP?(~OJo3=a*ab0K%myk;_mMfLf(UTIXcdT{_dsm#kmUZ2fxC@gBRGMlS3}?U3 z6W*4zZ7O4rN$kvx`Vj-?2^Uw%@sM8gS&x^o7u)^TMVLbVkUk5=v6!81n3=QqY%)xk zjtBCG_ewj|a0ZH86C}anU}pPgggN?J<%lde{fmpoFSIgDy{p90OmWl%%k0W9aPF zLOGTzyk@Zb{B8fCwUPmMYEw;;rqLjUxD6mRDcTUsDQ&pLw zM6Mpu3QY+?11CPjG}nZ+WSv5nC+9uC+H~*nU-A+*mhjJTA=Ro|-u|Jhi&ui<}m}uup&I5%6WFSlw zO&&J<8pnTH8%<6S`X%s%DZUswp=aSG0&9|-T!OIx3gpYo2@lj|B8542l&L;8k~s+y z*)?&Mg>w+?ZofiGC=ai`pq@s4YVtGEa&f8DDV~+SqJxW}NcY_ZHk>CTP$2;iVF&j= zN@h=5v$1cqI!s&hJPiNIDMTI-i0Cv>Vv6$@Y2I+A=zLqLqs|mJvBf&^s^@W_R0MU` za96}f=wobIETYp4J1h%>M8dO1HhN8;V~{;PJuUMF?GIrOx}`k=|HLQ?i6XvKRaMp2 zo-MY2XfCK^acgX+lNmOx=ZYsby_rL zS*f%0;|Er!Vf9pAUf#*hRCP^_dG!gwv@;o+pAChz{e zaEh9Ab#(rE3Jx5CDbPgXX+7rR%q-n*ARE5Zn z=j`mrIN7_f2h`MZ3^AIe`ou6SY;0`p6J@42K|w($C#Q7pgRb*)PXhx3TilespNE&7 zozfsec6QHN*;`v%iHV6TD=Xm;Mom+*y0TJULE#QDu{KNm_wO%ZqQ8Itj+I>*92}gU z)-yAE$`@SU)Fez3X;A0dw{}QOM0D%6koW1I^YinuLQOMM)9-b4IV$UP76?W1oeN zTo;w9@RSsP|M@R=^?bGeOa$|ex2GJAkyBK3b8|}<@QBW}D4pPc@ghkfPfAJ(Udu02x4*1snM`&DmCOIll}xvb3`DXJFk^Mb_Oy`h!3ihPE7f z`#?TE9xl)M(ScT0Rv?fd!PX8#L$l!iWkE|dN@(Ym?znJUW1bXZ+}03UUwFw&mrb>N z$z|#eko5lkuLE;8B89IhI~-CKCT3@CD|H^-37ObB@@zoKP=)`zx;T-dj}oUJ8X9ug zoqmCw%BYae8|gAB&&k0NMS$%iSr64K`cLnI9OUKZ4kd+~>gp6%)hkZ6^m(SO`3Mz$3KP;T z91Zb`)`*&L0+dqp9YI;@|jiUvOE#)IKs*yt$r3rDcL7v4h2 zxlc&(TiM$aAR|Y|^6Ki7)BPop&ytdo&E>^)byK$0gcv0JNBBctz6bzPQjs+BC{mbc zV5^Z~^76iW#3O#(-!m(YQK$z*xHdex5Y!}2PY1{3ZZK<>Ec=A%vDsN!5yG$let~q@ zIi8%RMF8|MIC5@5a=GeL{h zgjxm$9S+lm>?qRE`lehY93=GC*48`kb%k(H^~@`OE1_ex$%4f0erR{~_i7w6S1TN& z49-zujOD{Cpl!HdnMDAhX!>-|L9gRj^630gOHjjS zYaAXk4;h#B_V%){KrFE;zjPlFwTfts@qa#m8@+zrGdPGa9|vPEEG*db)w}KK2ehyY z3qOmJwwvcsTUP@h?5-~9;Q#|nV6UWg;Jy=PovO*x->t(*89wHoS2mYB8*rf zBqT&0JeP^qC{!=j=vdm{*susTf@8jEu>kb8tQ_VNk^)w$jaCc+tC-YNRecq0f+9^yOEcBxslF{5MfLC@fOh~zd>V$7 z2KxH_;Yv_L9b+q101^&}=WNR6<|e4rM7VX|zf%U+I&Va`8`HsmtR24V zGfhZH!1nhWSi>$=HZ#kU%zp6Tfwqp$OZPt+dTaxpvYMLrBbIn=rzxqZ3Q9{$3k$I| zjVN8O&W`{v{bcV;N$m2+#zVlsT}Ikfg@uHjDSf6XpFe+w5);2oS|jZX6!Ny#*Wb4r zFD)&Bg)t1PDK96D?8wW_RmoQaWI$JUYI%8iaPU*RF-S`?n7>hCM8q9(iIcs#y_1WI zf?7rpy^m~ci;lIVp@9=yF132HIgW;ghJcj0yK5C|>`hgZ$XS4TJ!J08N1b%PU~CiM zP7;)ql%JpPb8!p^Ah`qytn1x z@KLj}vLc=MsP%PqKeQXOCA=;ibEHm?V*oqNLI(k>n-SvRxC;<8Kelz@R%z_Wh8mhY zx`^~w!%UR?{%Cw5|^DS&_Y5 ze)Z~=_K}UPZ5ouGP7>0(VDD~hj78LgL4xv;r;-l}7GM~XkF|ad^!B1+y4u?%28Fsz z96da=i!}mt%UB4{u@ZWoL`gTBRZkIf78Mog)w!}J$r)1ue90ZNJmf)QkY2~zw@C?B8)%tmLr4-Ys~M!zQVkN0K?0E!mIY$r*JsfMut zjV79WzT#Ic9P_EJ3h252P&j%xCJVj(4yai^>V}4fB&R)k5SG-$LkMgK@T3!z{@9 zvonv#4yV&U3x@1ar^EF8ydjG8cBz*>WpI~wJ3glKUK%#u9XSOBjAlV)B@k_n)K8-> zb1vuac#u6`@JK_+>>D)bgIH1q-z}w=QBe~%Z6v7mZ+sE)@ZrNAokjx8EiPWL=s;yW z*RO3^K$b`Ev|2cErBcM5e5(9!lG-h|M ziT2AH53<8yagSN!|KOMvt7CO3oN`ic%M$sV0Visj1D7yAzt_fa4#@1owE=_g&Wxq7 zau!1UyWb@7tZ<210sC#gehAiBjsbgyNfd!JW1)DfJ#GSV`m@o>6&^5{Vlh}FXAIf@ zsjhwuFR!Y?-NmP172${hux7&f;I#gKp_c43VdC^abb;ME&uGlu^4hLq(owTM!;H~% zX5F=JYjd+XICIik-@xF09$l@Ur@v2jNeNq=?8?5o^UFu44FEgj7^b9s)JdVmQta`N zpdONBst$*;zKj3KSn+xu3^GqzSM4P1jOP(Orjs85egNN{D)i5e4pxeu=H^>HaXM$27vepV!U+kZ^K>3YU_c%%)eTYJX2Gl~LwtU3Wwrkzo z-hd80P$c9MkE1$si+5b>fSPDHnI$xxsfAJ=B(bouVS3@1Q*WWjj)1InaDj}ZkNPBy z0uwRRcD<XVhC|{kAaxs+=fO9LT+3^Sguh-%A2<3LFPJr-O~Zw{B73$%u=p zSz8y?2VM4d^50s9jM*u>-{Nu$3#T*p617^N$NUjUuf3e&f@X<`T9*lQXWQf)C&XBK zDDp`}20z-)@j^dgBe6WNp646xDi$TV|!TNA^b_*TGNfh(M zKSL$SR2`6oX`ShrSTj^F6*cmSa&PQy$0_AYX)T%$N%c_5?N9Xni*6y<#Rqhs2(W_? zSo!RAN^rNNkw@&p1RzPnCSZUHcL@W;|Mwck%iW|9AJ!-b%Olp8?M=jdj8?V z7SC38V8-X0Nfm~KXd+Mk{#~BE-LYT4(SGaW$JY&C0ME_(+S>-KjkB{eP{F!PB(wLo zCo6y{v8WE#{r5_otd$k>ul@$#D^G25G00;}>lV3$kI-D2pjXw+`kPmi|72TMQaP91 z3BgWOT|F;;w>mevrI|s)fi!B-p0CY^bv^=+V8BPa%7L-RktCO?B<=iNwQvk3 znt}G5Z}wkVc{ijLlJynTqu#$ip1_Xdo4e)y=C-pJRaTB|9qrE4xtW-VBLboLc#0gA zuER^98SMj~C&@Jjkw^@+x3?D&%jBrYAjAb|<~nB-iWeWo(=-;l zpYo3mIaBWev=Pfh*lXzN=#)?GPzHm|nT2NZ#f^;VS&pZBL*{$3CZUsn2M2O!3 zOS%&IOoiD%Ukb0LrX~asM?|({PKP0=;aWOczs?OhawX^`fV32b1^6z&2fZ@^mSN=7;ccfl_4{$SuJnU@2U?X5HfugtJ%s?92 z8bSitph1JDi>YaDU7apdT!C6aS{hAAD+dpcWHhP=D%W1~1jQ>=p*pHARhorDS6BBf zrKy8MRIbH7Q^mc#cE{rZp zdiiDdbQy5}9H`y+sJZ0p^w{#%WD#wWzE*zn4c91A{9q#t@{3A7&@Dq^DhL=f4+Bgy z`iMD9!7WM}=Dsz^m1@Blt&sP8gfIb7zml(JX=B40Ckuiawm~i-FDD0JN)*ASUk}#{ zY<2<#K%&r&ToU(G<%SDrsX zD?b7duBnN(^_xB4wmn~S5jVw$P#TUYE`ByWu}^e|Px_s|lCc(tOJ?_(0=@v|oUdj~ zxq5N|z*ZseEoCmqLqFEj-0QHQuB|?;W{y(73gg@@3Ui1oBeI>HB~{_Rc1Lt~tHbAY zZ7jw}#OmfC;YG2{<`JIWDDa~sl);7qM9*`t;p9NbyvKS|8&6=a4Qj{wP+Fc)D&AnOBoRx43eqNdRigyp z3DD+|89U`28iDsll=34YDVoE(v|7O{Y0Gg*jrnG&6Iqzq?8-N$%Na~!m}{6`P}H@0Y|*HImW)2rt-ZYjX%tL!kjn@7WU1>%P7M!u9ebH) zwK)y0s=-xMzT~L!RADfGCIDsCtN-CarN*zJJK~yDy>7bEG>p zx@E_2YxgiUEjp>u0>9$(wBzlk-Jv&<*~)b@jpgM(%2_0oaRZE`QWp;>gesbK?KOIH zPG8#U(AYy*(*D>XyCSZxsQTv|RN(;nTED3=0iwcG=>Ln(tbJ!RD(h?XB78cRT$of0 z7$UBL1Ly99a9H`f|Gn*zYx0~N{Gag&{%d@(8nUPAV#vDa=)fEp9n1AU+VJH&c_<@B zQMg5Nu-SzeGKJnV@SY50VfBNb)$=i|94BjNU7;A?pODlUJ3xe>nO$yNStLaz8B~GI zlZ&@ylmhrKrI%K*RPKTX2b5|@RfbYLWhd0EL#L2I&E=kfUr++$yMIB9*PTH zswsXE3i>oG8SO;>YiFjaoZ$;vHk11kOccsU-SICaoWx1ExrX`yyg$|#0UNL=9Nb~#c-b5|ylU!IS8eCx=uR*#6l z{@;5IN5+|HFi0I;-IkA!Ap{)wFtMB1YU=8smjuYZYz~&!(vUe9#RrduS(|v)1X4*c z8}-zV10`8gv=?2KA2RAe)`&CL%~a9I2GB|p(?TH4^VJ@*+A(Q;o5K7k))WBbe}2$8 zR6I*1A7Cvdot>&Z_1^9-?po2gcO&r16MEw081ONmIRpX6dFv?Nfk5@QwDclceufFJ zq&;7Uu`JUv0o5ftS;I-BiX4@}A#~MMsyRBqEY)Zf;=GmNq9P7y6&QF(1aYgGT13~T<|Tf_rTAk06GB#{Pw5*Z2N=12`)S;{{XEW4Lxl-E^Ik$fkw zTQ5Rv74m;P1}o2$eMKLoCc6NPPC~Q*^M6evA++82h3mE!0BhSnFVY99%gYyC>t4H@ zgLaTc;nC&UA5m`fMc<0lE9>y>#O3L-!-L$kc+={{QEKxJe z65!HiM)>5lo=2NyZ#(+ag$?4_Dy>tF3<&gR$PzEeCQc1{6@81(81dBI{2W|@&3L<^ zboH~ofbZdr#mbA3E_-SSz$DIU2YlG}G|^!~=j+)we@JeWyb;IU!?V!r?95CG= zMm_-MdwrO0rhR;MGMAp7e&Q|wD&kiy&^ejfNlQubI_%?>iMF=3h8gLL3RL~@(>C9K zNBQW{&i3}SzMxQ!_2Y$`p(BH6Zer%-OBHSmGUmYZ`A%jH@tEG+_Qa(8a^WkZlxb&8w8L#FK-Pzap0<;un-sy*PEJ6-z79`K{rBMIft|MVz;j8PtY^H zMsE*ycXvYGrkiVk2ES^Z)~Ymr5+I0hftaWm5aJgU1hwu}CPM4m#*t^ShOVY2@CKfc zhITB~0&|&zx2T{X41DgX2VH^dYhRdXi9u5%=s$cuQ%54$ft3=I?;bZG87mKeN*+!Y zCi$n9V}+kG{gALchj=w>Ojh9MpS8866Yczor*Gky>35lpzjH(p%Zf`;CuTQ`WQ++J zkZ=Zcy>u&OwnRvlAx7|^^D)>>+kfB~_w~#W;6ipRdF)h9uu${>iye-s*=xL*^@T2W zY=mr6GSKH+1~=zjPt&K-tv)9+c@tZ6?n~i-8c*BZBWMRV$T6sLCV5{SO#rIe74uXW zETg-CEj#5)e*S=#dC*kqDTj%Ee&O`fv?7(y0fT&BJ?qKL9Bt&|a|ME{s2n@D2Z5(k z278)&5jwz3tDCNN_Mn>! zJ20YU<;z12gX@EYIq$>1`}*#5Npfj?4p^d}9!o`VadBN>A`^0a+|!zzH=yC3XHA#WP6Jnm!Z(K_io#caM-*@7@r2!X zYuEIB&sGXGOXElHepM@&&d6#0`Ug0Q3C|1|WJud$`ati)?@gPHiYc9%(|*U*eY}8} zCe@S1VS9Sf&I8lBx!lW>q|=a!{T4~t;A}aa`QaZBzx8@y!p*#(BKy9@;ng1#7wEH$ zffh@oj1e$cE3GDmH~zMNxG&lb@~OiCw4EfBfgh5v<`HZ}8GKvvA?U5LB%#Bw?pn44 z=56=1g9e{j&gC%(&G|&rou939m#e%UW?zB~$|v-_!gDvk-W@QlSlJ|LiJ%@hM6PPv z+3g)|O)Tx#fSu@ZxayA$;Q=cXT%z&@?h8S{dBvk~7Xh6(<@+7`ZnGXwqonJ>Aiy&7 zP$ee_0@y#mNlBFPHiy?Y!U*LX&J_*lab&?dS6!CM(p=s`BR8`SuOLy+z%)2&3 zLp*y(R~f#QH25YnW#Z3f`u3TMbU}ZL)U%Fa+ygU%uUF7Y!X~9Y-*}b?Vmb4ELOf>s z{?r)l)r6pzJPLz6*;dxm%jnZ7*U+XJ@EJ12^8O^71N_ysLmaHzr4tuD2G^X-H-`hl zBn+l8F)=sir8fkm2A&%^Rud(0IRf-Rbv|qO8-2R>;aMXb#cn0Q5(*mEe*u#OEn;Ag zA%~$FKGRjEx18ba#$}WdOQ1myF675Ex2B63;3emcYyh@@eH&g!s6P^}5*!i&U2Pcz zcwH~vZvtHj5rm16(ZW;Vv#w*e?|}9LT9|F6zwPSv_HDLYJVbtu;*Q@_CO_@r9(bE)OV_@tFh@o zhx}!#t&h(ez+S2G^hYKqC$ps@(}aAeL=h={|H2UHq52C@0gW?MzyI^0p%@`>!anQi;ewei(((OuN^9rAN4*T@EhWH+5y=Ee7_Y(4>UQ`Y#Y%>3*&90Z!kV^bYWv8 z@BaP!cVQNgwY9*1tWvOi#x#{WsrZ&| z>-+9)aetexrkVtgMpePFfb5~mUALyG)vM|*h)hvn{|ewr=+iWig6l)sb7%c-1h3}- zR)I4rEpny6YO%E3Om&;R+XM=OTe|Q01`uP0>~O9Iv;NO#KY1G$4$s8tl0JX-KHtcb zXBc5Ft#|x0-?Hbk=Y>HMk&t{Kg>Qp1BWqxn+5{U3(2&4ibeC^~k^vr}1-*H5b$K|j zCO@hCpH_2e!*TMMC5UIAO7CZWH(N)?17?FW+5K0xB?uZm`}w~cBgWzjp`rLht)5<9 z&x6?aP_Bfr2-K)?ncj=jXF-R5+t-@be65JMFTZWPCQ^u3BgYSxc3?@f>!5#zSL$tN zMA!6+Y(36t|VBeeZgA4<>p#R_a^XCqNHcbUMX6PW=0za4b$% zm^~*^0lkzH0Iyr6O>DWpc8*1m-{g19&)$x2O`4lo`?o+bS{sMHY~l9&*28hUx2|lD zp5RFNy~|zJ5RQ`#RnZagEgwBW;@X&APsctATJyjl3D&wcZ3&i+Jb~}x{4&)%^mtA9 z<^{7NA9a7MNI(lRrmLz+SlPF|wa<|bH6&0ze zsh}7cz5($c4^82VNa+l+nlG#2N#J0OD;>?W((Om#lJ^KCBxYTL#FUZbqJY&y|I&KL zP;vSd!N8Z4L}|iMV?_n7ju%2`<4^RWNd>mFy*jkx=aAa ztF5ig%oJsaDPwsUD!#f3t#)7Vg=NY9%DQE~S!_Mln32z`Fy?6Do09lQ?1wn{-{clA zoI`mDy?*&a&`&0put>hPR?t-iJswz|Zk}N^PEOS&W?fgL1C zmYLlZ=z8n?EB*a)dg&T4$p3R#Z)U?u3Y;+$pn2w7y;Q;Yi_fNoLdTA`Oadrt5bV^GK~aLGt0h?j^NV*Uq2x$2An From ab52cfc599ae86bf950fd631a2e1f093724e724b Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 11:27:39 +0900 Subject: [PATCH 05/15] fix: address model selector review findings Persist model changes through a host-side updateProfileModel that merges a patch into the latest stored profile inside the mutation queue, validates the expected provider, patch keys and organization allow-list. Gate router model requests on a real model field, abort them on unmount, show a loading state, make model items keyboard-selectable, and extend test coverage. --- packages/types/src/vscode-extension-host.ts | 1 + src/core/webview/ClineProvider.ts | 158 ++++++++++---- .../ClineProvider.apiHandlerRebuild.spec.ts | 91 ++++++++ .../__tests__/webviewMessageHandler.spec.ts | 41 ++++ src/core/webview/webviewMessageHandler.ts | 14 ++ .../src/components/chat/ModelSelector.tsx | 52 ++--- .../chat/__tests__/ChatTextArea.visual.tsx | 9 +- .../chat/__tests__/ModelSelector.spec.tsx | 203 +++++++++++++++--- .../hooks/__tests__/useRouterModels.spec.ts | 86 ++++++++ .../components/ui/hooks/useRouterModels.ts | 17 +- webview-ui/src/i18n/locales/ca/chat.json | 3 +- webview-ui/src/i18n/locales/de/chat.json | 3 +- webview-ui/src/i18n/locales/en/chat.json | 3 +- webview-ui/src/i18n/locales/es/chat.json | 3 +- webview-ui/src/i18n/locales/fr/chat.json | 3 +- webview-ui/src/i18n/locales/hi/chat.json | 3 +- webview-ui/src/i18n/locales/id/chat.json | 3 +- webview-ui/src/i18n/locales/it/chat.json | 3 +- webview-ui/src/i18n/locales/ja/chat.json | 3 +- webview-ui/src/i18n/locales/ko/chat.json | 3 +- webview-ui/src/i18n/locales/nl/chat.json | 3 +- webview-ui/src/i18n/locales/pl/chat.json | 3 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 3 +- webview-ui/src/i18n/locales/ru/chat.json | 3 +- webview-ui/src/i18n/locales/tr/chat.json | 3 +- webview-ui/src/i18n/locales/vi/chat.json | 3 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 3 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 3 +- 28 files changed, 599 insertions(+), 127 deletions(-) create mode 100644 webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c0e8509105..698cc68969 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -469,6 +469,7 @@ export interface WebviewMessage { | "currentApiConfigName" | "saveApiConfiguration" | "upsertApiConfiguration" + | "updateProfileModel" | "deleteApiConfiguration" | "loadApiConfiguration" | "loadApiConfigurationById" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b2f0ea8386..dd8fbb2328 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -56,6 +56,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, getModelId, isRetiredProvider, + modelIdKeys, providerIdentifiers, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" @@ -191,6 +192,17 @@ function scheduleTask( }) } +// Keys a model-selector patch may touch: model IDs plus settings reset on model change. +const PROFILE_MODEL_PATCH_KEYS: ReadonlySet = new Set([ + ...modelIdKeys, + "reasoningEffort", + "modelMaxTokens", + "modelMaxThinkingTokens", + "awsCustomArn", + "awsCustomArnBaseModelId", + "awsModelContextWindow", +]) + type GetStateOptions = { includeTaskHistory?: boolean } @@ -1880,49 +1892,9 @@ export class ClineProvider activate: boolean = true, ): Promise { try { - return await this.enqueueProviderProfileMutation(async (signal) => { - // TODO: Do we need to be calling `activateProfile`? It's not - // clear to me what the source of truth should be; in some cases - // we rely on the `ContextProxy`'s data store and in other cases - // we rely on the `ProviderSettingsManager`'s data store. It might - // be simpler to unify these two. - const id = await this.providerSettingsManager.saveConfig(name, providerSettings) - - if (signal.aborted) return id - - if (activate) { - const { mode } = await this.getState() - - // These promises do the following: - // 1. Adds or updates the list of provider profiles. - // 2. Sets the current provider profile. - // 3. Sets the current mode's provider profile. - // 4. Copies the provider settings to the context. - // - // Note: 1, 2, and 4 can be done in one `ContextProxy` call: - // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... }) - // We should probably switch to that and verify that it works. - // I left the original implementation in just to be safe. - await Promise.all([ - this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()), - this.updateGlobalState("currentApiConfigName", name), - this.providerSettingsManager.setModeConfig(mode, id), - this.contextProxy.setProviderSettings(providerSettings), - ]) - - // Change the provider for the current task. - // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). - this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true }) - - // Keep the current task's sticky provider profile in sync with the newly-activated profile. - await this.persistStickyProviderProfileToCurrentTask(name) - } else { - await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) - } - - await this.postStateToWebview() - return id - }) + return await this.enqueueProviderProfileMutation((signal) => + this.persistProviderProfile(name, providerSettings, activate, signal), + ) } catch (error) { this.log( `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -1933,6 +1905,106 @@ export class ClineProvider } } + /** + * Applies a model change to the latest stored profile inside the mutation queue, + * so a stale webview snapshot cannot overwrite newer profile settings. + * A `null` patch value clears the key. + */ + async updateProfileModel(name: string, expectedProvider: string, patch: Record): Promise { + try { + await this.enqueueProviderProfileMutation(async (signal) => { + const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) + + if (stored.apiProvider !== expectedProvider) { + throw new Error( + `Profile "${name}" provider changed from "${expectedProvider}" to "${String(stored.apiProvider)}"`, + ) + } + + const merged: Record = { ...stored } + + for (const [key, value] of Object.entries(patch)) { + if (!PROFILE_MODEL_PATCH_KEYS.has(key)) { + throw new Error(`Key "${key}" is not allowed in a model patch`) + } + + if (value === null || value === undefined) { + delete merged[key] + } else { + merged[key] = value + } + } + + const { organizationAllowList } = await this.getState() + + if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { + throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) + } + + if (signal.aborted) return + + await this.persistProviderProfile(name, merged as ProviderSettings, true, signal) + }) + } catch (error) { + this.log(`Error update profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + + vscode.window.showErrorMessage( + error instanceof OrganizationAllowListViolationError + ? error.message + : t("common:errors.create_api_config"), + ) + } + } + + private async persistProviderProfile( + name: string, + providerSettings: ProviderSettings, + activate: boolean, + signal: AbortSignal, + ): Promise { + // TODO: Do we need to be calling `activateProfile`? It's not + // clear to me what the source of truth should be; in some cases + // we rely on the `ContextProxy`'s data store and in other cases + // we rely on the `ProviderSettingsManager`'s data store. It might + // be simpler to unify these two. + const id = await this.providerSettingsManager.saveConfig(name, providerSettings) + + if (signal.aborted) return id + + if (activate) { + const { mode } = await this.getState() + + // These promises do the following: + // 1. Adds or updates the list of provider profiles. + // 2. Sets the current provider profile. + // 3. Sets the current mode's provider profile. + // 4. Copies the provider settings to the context. + // + // Note: 1, 2, and 4 can be done in one `ContextProxy` call: + // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... }) + // We should probably switch to that and verify that it works. + // I left the original implementation in just to be safe. + await Promise.all([ + this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()), + this.updateGlobalState("currentApiConfigName", name), + this.providerSettingsManager.setModeConfig(mode, id), + this.contextProxy.setProviderSettings(providerSettings), + ]) + + // Change the provider for the current task. + // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). + this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true }) + + // Keep the current task's sticky provider profile in sync with the newly-activated profile. + await this.persistStickyProviderProfileToCurrentTask(name) + } else { + await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) + } + + await this.postStateToWebview() + return id + } + async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) { const globalSettings = this.contextProxy.getValues() let profileToActivate: string | undefined = globalSettings.currentApiConfigName diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 4561d688fe..641ff48574 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -428,6 +428,97 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }) }) + describe("updateProfileModel", () => { + const storedProfile = { + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + rateLimitSeconds: 5, + reasoningEffort: "high", + } + + beforeEach(() => { + provider["providerSettingsManager"].getProfile = vi.fn().mockResolvedValue(storedProfile) + vi.mocked(vscode.window.showErrorMessage).mockClear() + }) + + test("merges the patch into the latest stored profile, keeping newer settings and clearing null keys", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + reasoningEffort: null, + }) + + const saveConfig = vi.mocked(provider["providerSettingsManager"].saveConfig) + expect(saveConfig).toHaveBeenCalledTimes(1) + const [name, saved] = saveConfig.mock.calls[0] + expect(name).toBe("test-config") + expect(saved).toMatchObject({ + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "anthropic/claude", + rateLimitSeconds: 5, + }) + expect(saved).not.toHaveProperty("reasoningEffort") + expect(saved).not.toHaveProperty("name") + }) + + test("rebuilds the current task API handler with the new model", async () => { + const mockTask = new Task({ ...defaultTaskOptions }) + mockTask.api = { + getModel: vi.fn().mockReturnValue({ id: "openai/gpt-4", info: { contextWindow: 128000 } }), + } as unknown as typeof mockTask.api + await provider.addClineToStack(mockTask) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + + expect(mockTask.updateApiConfiguration).toHaveBeenCalledWith( + expect.objectContaining({ openRouterModelId: "anthropic/claude" }), + ) + }) + + test("rejects the update when the stored provider differs from the expected provider", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.anthropic, { + apiModelId: "claude-sonnet-4-5", + }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) + + test("rejects patch keys outside the model and reset allow-list", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + openRouterApiKey: "stolen", + }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) + + test("rejects a model that the organization allow-list forbids", async () => { + const realGetState = provider.getState.bind(provider) + vi.spyOn(provider, "getState").mockImplementation(async () => ({ + ...(await realGetState()), + organizationAllowList: { + allowAll: false, + providers: { + [providerIdentifiers.openrouter]: { allowAll: false, models: ["openai/gpt-4"] }, + }, + }, + })) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) + }) + describe("activateProviderProfile", () => { test("serializes provider profile mutations without interleaving", async () => { const events: string[] = [] diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index c57e9f3ac3..1b7f046853 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -2368,3 +2368,44 @@ describe("webviewMessageHandler - telemetrySetting", () => { expect(TelemetryService.instance.updateTelemetryState).not.toHaveBeenCalled() }) }) + +describe("updateProfileModel", () => { + let updateProfileModel: Mock + + beforeEach(() => { + vi.clearAllMocks() + updateProfileModel = vi.fn().mockResolvedValue(undefined) + mockClineProvider.updateProfileModel = updateProfileModel + }) + + it("delegates a valid message to the provider", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "updateProfileModel", + text: "my-profile", + values: { + expectedProvider: providerIdentifiers.openrouter, + patch: { openRouterModelId: "a/b", reasoningEffort: null }, + }, + }) + + expect(updateProfileModel).toHaveBeenCalledWith("my-profile", providerIdentifiers.openrouter, { + openRouterModelId: "a/b", + reasoningEffort: null, + }) + }) + + it.each([ + ["missing profile name", { values: { expectedProvider: providerIdentifiers.openrouter, patch: {} } }], + ["missing expectedProvider", { text: "p", values: { patch: {} } }], + ["non-object patch", { text: "p", values: { expectedProvider: providerIdentifiers.openrouter, patch: "x" } }], + ["array patch", { text: "p", values: { expectedProvider: providerIdentifiers.openrouter, patch: [] } }], + ["missing values", { text: "p" }], + ])("ignores a malformed message: %s", async (_label, partial) => { + await webviewMessageHandler(mockClineProvider, { + type: "updateProfileModel", + ...partial, + } as unknown as Parameters[1]) + + expect(updateProfileModel).not.toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a5e11931c8..735371a436 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2336,6 +2336,20 @@ export const webviewMessageHandler = async ( await provider.upsertProviderProfile(message.text, message.apiConfiguration) } break + case "updateProfileModel": { + const { expectedProvider, patch } = message.values ?? {} + + if ( + message.text && + typeof expectedProvider === "string" && + patch && + typeof patch === "object" && + !Array.isArray(patch) + ) { + await provider.updateProfileModel(message.text, expectedProvider, patch) + } + break + } case "renameApiConfiguration": if (message.values && message.apiConfiguration) { try { diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index 4e6c416262..9b016f8d6f 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -47,28 +47,25 @@ function getModelIdField(provider: ProviderName): string | undefined { function useModelListForProvider(provider: ProviderName | undefined, apiConfiguration?: ProviderSettings) { const isStatic = provider ? isStaticModelProvider(provider) : false - const isUnsupported = !provider || UNSUPPORTED_QUICK_PICK_PROVIDERS.includes(provider) + const field = provider ? getModelIdField(provider) : undefined + const isUnsupported = !provider || !field || UNSUPPORTED_QUICK_PICK_PROVIDERS.includes(provider) const isRouterProvider = !isStatic && !isUnsupported const routerModels = useRouterModels({ provider, enabled: isRouterProvider }) const routerModelsData = routerModels.data as Record> | undefined + const isLoading = isRouterProvider && routerModels.isLoading return React.useMemo(() => { - if (!provider || isUnsupported) { - return { isUnsupported: true as const, field: undefined, models: null } - } - - const field = getModelIdField(provider) - if (!field) { - return { isUnsupported: true as const, field: undefined, models: null } + if (!provider || !field || isUnsupported) { + return { isUnsupported: true as const, field: undefined, models: null, isLoading: false } } const models = isStatic ? getStaticModelsForProvider(provider, undefined, apiConfiguration) : (routerModelsData?.[provider] ?? null) - return { isUnsupported: false as const, field, models } - }, [provider, isUnsupported, isStatic, apiConfiguration, routerModelsData]) + return { isUnsupported: false as const, field, models, isLoading } + }, [provider, field, isUnsupported, isStatic, apiConfiguration, routerModelsData, isLoading]) } interface ModelSelectorProps { @@ -95,7 +92,7 @@ export const ModelSelector = ({ const provider: ProviderName | undefined = isRetiredProvider(providerRaw) ? undefined : providerRaw const { id: selectedModelId, info: selectedModelInfo } = useSelectedModel(apiConfiguration) - const { isUnsupported, field, models } = useModelListForProvider(provider, apiConfiguration) + const { isUnsupported, field, models, isLoading } = useModelListForProvider(provider, apiConfiguration) const filteredModelRecord = React.useMemo( () => filterModels(models, provider, organizationAllowList), @@ -136,26 +133,25 @@ export const ModelSelector = ({ setOpen(false) }, []) - // Reuses the same `upsertApiConfiguration` path Settings' Save button uses, - // so the change persists and activates immediately for the current task. + // The host merges this patch into the latest stored profile, so a stale + // snapshot cannot overwrite newer settings. `null` clears a key. const handleModelChange = React.useCallback( (modelIdField: string, modelId: string) => { - if (!apiConfiguration || !provider) return + if (!provider) return - const updatedConfig = { ...apiConfiguration } as ProviderSettings - ;(updatedConfig as Record)[modelIdField] = modelId + const patch: Record = { [modelIdField]: modelId } handleModelChangeSideEffects(provider, modelId, (f, v) => { - ;(updatedConfig as Record)[f] = v + patch[f] = v ?? null }) vscode.postMessage({ - type: "upsertApiConfiguration", + type: "updateProfileModel", text: currentApiConfigName, - apiConfiguration: updatedConfig, + values: { expectedProvider: provider, patch }, }) }, - [apiConfiguration, currentApiConfigName, provider], + [currentApiConfigName, provider], ) const handleSelect = React.useCallback( @@ -250,7 +246,11 @@ export const ModelSelector = ({ )}
- {filteredModelIds.length === 0 ? ( + {isLoading ? ( +
+ {t("chat:modelSelector.loading")} +
+ ) : filteredModelIds.length === 0 ? (
{t("common:ui.no_results")}
@@ -260,11 +260,15 @@ export const ModelSelector = ({ const isSelected = modelId === selectedModelId const modelInfo = filteredModelRecord?.[modelId] return ( -
handleSelect(modelId)} + aria-current={isSelected ? "true" : undefined} className={cn( - "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "w-full px-3 py-1.5 text-sm text-left cursor-pointer flex items-center", + "bg-transparent border-0 text-vscode-foreground", + "focus-visible:outline focus-visible:outline-1 focus-visible:outline-vscode-focusBorder", "hover:bg-vscode-list-hoverBackground", isSelected ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" @@ -275,7 +279,7 @@ export const ModelSelector = ({
{modelInfo?.displayName ?? modelId}
{isSelected && } -
+ ) })}
diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx index cebbf7493d..69ad3e675c 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx @@ -14,14 +14,7 @@ for (const theme of visualThemes) { await expect(editor).toBeVisible() await expect(story).toHaveScreenshot(`chat-composer-resting-${theme.name}.png`) - await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()) - for ( - let index = 0; - index < 12 && !(await editor.evaluate((element) => element === document.activeElement)); - index++ - ) { - await page.keyboard.press("Tab") - } + await editor.focus() await expect(editor).toBeFocused() await expect(story).toHaveScreenshot(`chat-composer-focus-${theme.name}.png`) await expectBoundedLayout(page, story, { diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index 16197accd2..e89724b0d3 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -10,11 +10,13 @@ vi.mock("@/utils/vscode", () => ({ }, })) +const extensionState = vi.hoisted(() => ({ + organizationAllowList: { allowAll: true, providers: {} } as Record, + currentApiConfigName: "default", +})) + vi.mock("@/context/ExtensionStateContext", () => ({ - useExtensionState: () => ({ - organizationAllowList: { allowAll: true, providers: {} }, - currentApiConfigName: "default", - }), + useExtensionState: () => extensionState, })) vi.mock("@/i18n/TranslationContext", () => ({ @@ -27,6 +29,18 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({ useRooPortal: () => document.body, })) +const routerModelsResult = vi.hoisted(() => ({ + value: { data: undefined, isLoading: false } as { data: unknown; isLoading: boolean }, + calls: [] as unknown[], +})) + +vi.mock("@/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: (opts: unknown) => { + routerModelsResult.calls.push(opts) + return routerModelsResult.value + }, +})) + const staticModelsOverride = vi.hoisted(() => ({ value: undefined as Record | undefined })) vi.mock("@/components/settings/utils/providerModelConfig", async (importOriginal) => { @@ -40,6 +54,8 @@ vi.mock("@/components/settings/utils/providerModelConfig", async (importOriginal import { vscode } from "@/utils/vscode" +const base = { contextWindow: 1000, supportsPromptCache: false } + describe("ModelSelector", () => { const anthropicConfig: ProviderSettings = { apiProvider: providerIdentifiers.anthropic, @@ -49,6 +65,82 @@ describe("ModelSelector", () => { beforeEach(() => { vi.clearAllMocks() staticModelsOverride.value = undefined + routerModelsResult.value = { data: undefined, isLoading: false } + routerModelsResult.calls = [] + extensionState.organizationAllowList = { allowAll: true, providers: {} } + }) + + test("shows the currently selected model on the trigger for a static-model provider", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toHaveTextContent("claude-sonnet-4-5") + }) + + test("disables the trigger when disabled", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toBeDisabled() + }) + + test("shows a search bar since anthropic has more than 6 models and filters the list", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByTestId("model-search-input")).toBeInTheDocument() + const allItems = screen.getAllByTestId("model-selector-item") + expect(allItems.length).toBeGreaterThan(6) + + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + + const filteredItems = screen.getAllByTestId("model-selector-item") + expect(filteredItems.length).toBeLessThan(allItems.length) + }) + + test("shows the description instead of a search bar when there are 6 or fewer models", () => { + staticModelsOverride.value = { "model-a": base, "model-b": base } + + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() + expect(screen.getByText("chat:modelSelector.description")).toBeInTheDocument() + expect(screen.getAllByTestId("model-selector-item")).toHaveLength(2) + }) + + test("selecting a model patches the latest profile via updateProfileModel and closes the popover", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + + fireEvent.click(screen.getAllByTestId("model-selector-item")[0]) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateProfileModel", + text: "default", + values: { + expectedProvider: providerIdentifiers.anthropic, + patch: expect.objectContaining({ + apiModelId: "claude-opus-4-6", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }), + }, + }) + expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() + }) + + test("model items are real buttons so keyboard users can select them", () => { + staticModelsOverride.value = { "model-a": base, "model-b": base } + + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + for (const item of screen.getAllByTestId("model-selector-item")) { + expect(item.tagName).toBe("BUTTON") + } }) test("re-selecting the active model does not post a config update", () => { @@ -65,7 +157,6 @@ describe("ModelSelector", () => { }) test("hides deprecated models except the currently selected one", () => { - const base = { contextWindow: 1000, supportsPromptCache: false } staticModelsOverride.value = { "model-active": { ...base, deprecated: true }, "model-old": { ...base, deprecated: true }, @@ -85,47 +176,95 @@ describe("ModelSelector", () => { expect(labels).toEqual(["model-active", "model-new"]) }) - test("shows the currently selected model on the trigger for a static-model provider", () => { - render() + test("hides models excluded by the organization allow-list", () => { + staticModelsOverride.value = { "model-a": base, "model-b": base, "model-c": base } + extensionState.organizationAllowList = { + allowAll: false, + providers: { [providerIdentifiers.anthropic]: { allowAll: false, models: ["model-a", "model-c"] } }, + } + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) - expect(screen.getByTestId("model-selector-trigger")).toBeInTheDocument() + const labels = screen.getAllByTestId("model-selector-item").map((el) => el.textContent) + expect(labels).toEqual(["model-a", "model-c"]) }) - test("disables the trigger when disabled", () => { - render() + describe("dynamic-model providers", () => { + const openRouterConfig: ProviderSettings = { + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "vendor/model-one", + } - expect(screen.getByTestId("model-selector-trigger")).toBeDisabled() - }) + test("lists router models and selects through openRouterModelId", () => { + routerModelsResult.value = { + isLoading: false, + data: { openrouter: { "vendor/model-one": base, "vendor/model-two": base } }, + } - test("shows a search bar since anthropic has more than 6 models and filters the list", () => { - render() + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) - fireEvent.click(screen.getByTestId("model-selector-trigger")) + const items = screen.getAllByTestId("model-selector-item") + expect(items.map((el) => el.textContent)).toEqual(["vendor/model-one", "vendor/model-two"]) - expect(screen.getByTestId("model-search-input")).toBeInTheDocument() - const allItems = screen.getAllByTestId("model-selector-item") - expect(allItems.length).toBeGreaterThan(6) + fireEvent.click(items[1]) - fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateProfileModel", + text: "default", + values: { + expectedProvider: providerIdentifiers.openrouter, + patch: expect.objectContaining({ openRouterModelId: "vendor/model-two" }), + }, + }) + }) - const filteredItems = screen.getAllByTestId("model-selector-item") - expect(filteredItems.length).toBeLessThan(allItems.length) - }) + test("shows a loading state, not 'no results', while router models load", () => { + routerModelsResult.value = { data: undefined, isLoading: true } - test("selecting a model persists it via upsertApiConfiguration and closes the popover", () => { - render() + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) - fireEvent.click(screen.getByTestId("model-selector-trigger")) - fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "claude-opus-4-6" } }) + expect(screen.getByText("chat:modelSelector.loading")).toBeInTheDocument() + expect(screen.queryByText("common:ui.no_results")).not.toBeInTheDocument() + }) - fireEvent.click(screen.getAllByTestId("model-selector-item")[0]) + test("shows 'no results' when the router returns no models", () => { + routerModelsResult.value = { data: { openrouter: {} }, isLoading: false } - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "upsertApiConfiguration", - text: "default", - apiConfiguration: expect.objectContaining({ apiModelId: "claude-opus-4-6" }), + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + expect(screen.queryByText("chat:modelSelector.loading")).not.toBeInTheDocument() + }) + + test("shows 'no results' when the router request failed", () => { + routerModelsResult.value = { data: undefined, isLoading: false } + + render() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + }) + + test("does not request router models for providers without a model field", () => { + render( + , + ) + + expect(routerModelsResult.calls.length).toBeGreaterThan(0) + expect(routerModelsResult.calls.every((opts) => (opts as { enabled: boolean }).enabled === false)).toBe( + true, + ) + expect(screen.getByTestId("model-selector-unsupported-trigger")).toBeInTheDocument() }) - expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() }) test("falls back to a degraded trigger for providers without a simple model list, and jumps to Settings on click", () => { diff --git a/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts new file mode 100644 index 0000000000..3744d9bc28 --- /dev/null +++ b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts @@ -0,0 +1,86 @@ +import { RouterModelsMessageType, providerIdentifiers } from "@roo-code/types" + +import { fetchRouterModels } from "../useRouterModels" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +describe("fetchRouterModels", () => { + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + }) + + const handlerOf = (spy: { mock: { calls: unknown[][] } }) => + spy.mock.calls.find((args) => args[0] === "message")?.[1] as EventListener + + test("resolves with the router models of the matching provider and removes its listener", async () => { + const addSpy = vi.spyOn(window, "addEventListener") + const removeSpy = vi.spyOn(window, "removeEventListener") + + const promise = fetchRouterModels(providerIdentifiers.openrouter) + const handler = handlerOf(addSpy) + + handler( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: { [providerIdentifiers.openrouter]: {} }, + }, + }), + ) + + await expect(promise).resolves.toEqual({ [providerIdentifiers.openrouter]: {} }) + expect(removeSpy).toHaveBeenCalledWith("message", handler) + }) + + test("on abort, rejects, removes the same message listener, and clears the timeout", async () => { + vi.useFakeTimers() + const addSpy = vi.spyOn(window, "addEventListener") + const removeSpy = vi.spyOn(window, "removeEventListener") + const controller = new AbortController() + + const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + const handler = handlerOf(addSpy) + expect(typeof handler).toBe("function") + expect(vi.getTimerCount()).toBe(1) + + controller.abort() + + await expect(promise).rejects.toMatchObject({ name: "AbortError" }) + expect(removeSpy).toHaveBeenCalledWith("message", handler) + expect(vi.getTimerCount()).toBe(0) + }) + + test("rejects at once without adding a listener when the signal is already aborted", async () => { + vi.useFakeTimers() + const addSpy = vi.spyOn(window, "addEventListener") + const controller = new AbortController() + controller.abort() + + await expect(fetchRouterModels(providerIdentifiers.openrouter, controller.signal)).rejects.toMatchObject({ + name: "AbortError", + }) + expect(handlerOf(addSpy)).toBeUndefined() + expect(vi.getTimerCount()).toBe(0) + }) + + test("on timeout, rejects and removes the same message listener", async () => { + vi.useFakeTimers() + const addSpy = vi.spyOn(window, "addEventListener") + const removeSpy = vi.spyOn(window, "removeEventListener") + + const promise = fetchRouterModels(providerIdentifiers.openrouter) + const handler = handlerOf(addSpy) + const assertion = expect(promise).rejects.toThrow("timed out") + + await vi.advanceTimersByTimeAsync(10_000) + + await assertion + expect(removeSpy).toHaveBeenCalledWith("message", handler) + }) +}) diff --git a/webview-ui/src/components/ui/hooks/useRouterModels.ts b/webview-ui/src/components/ui/hooks/useRouterModels.ts index a7d6b36725..7557562583 100644 --- a/webview-ui/src/components/ui/hooks/useRouterModels.ts +++ b/webview-ui/src/components/ui/hooks/useRouterModels.ts @@ -14,14 +14,21 @@ type UseRouterModelsOptions = { enabled?: boolean // gate fetching entirely } -export const fetchRouterModels = async (provider?: string) => +export const fetchRouterModels = async (provider?: string, signal?: AbortSignal) => new Promise((resolve, reject) => { const cleanup = () => { + signal?.removeEventListener("abort", onAbort) if (typeof window !== "undefined") { window.removeEventListener("message", handler) } } + const onAbort = () => { + clearTimeout(timeout) + cleanup() + reject(new DOMException("Router models request aborted", "AbortError")) + } + const timeout = setTimeout(() => { cleanup() reject(new Error("Router models request timed out")) @@ -50,6 +57,12 @@ export const fetchRouterModels = async (provider?: string) => } } + if (signal?.aborted) { + onAbort() + return + } + + signal?.addEventListener("abort", onAbort, { once: true }) window.addEventListener("message", handler) if (provider) { vscode.postMessage({ type: RouterModelsMessageType.requestRouterModels, values: { provider } }) @@ -62,7 +75,7 @@ export const useRouterModels = (opts: UseRouterModelsOptions = {}) => { const provider = opts.provider || undefined return useQuery({ queryKey: [RouterModelsMessageType.routerModels, provider || allRouterModelsProvider], - queryFn: () => fetchRouterModels(provider), + queryFn: ({ signal }) => fetchRouterModels(provider, signal), enabled: opts.enabled !== false, }) } diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 4d7f5018b0..5ee7ecad14 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -119,7 +119,8 @@ "settings": "Configuració del model", "description": "Tria el model que s'utilitzarà per a aquesta configuració d'API.", "searchPlaceholder": "Cerca models...", - "unsupportedTooltip": "El model d'aquest proveïdor es configura a Configuració" + "unsupportedTooltip": "El model d'aquest proveïdor es configura a Configuració", + "loading": "Carregant models..." }, "lockApiConfigAcrossModes": "Bloqueja la configuració de l'API a tots els modes en aquest espai de treball", "unlockApiConfigAcrossModes": "La configuració de l'API està bloquejada a tots els modes en aquest espai de treball (fes clic per desbloquejar)", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 48b9ab59d2..48112f26c3 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -119,7 +119,8 @@ "settings": "Modelleinstellungen", "description": "Wähle das Modell für diese API-Konfiguration aus.", "searchPlaceholder": "Modelle suchen...", - "unsupportedTooltip": "Das Modell dieses Anbieters wird in den Einstellungen konfiguriert" + "unsupportedTooltip": "Das Modell dieses Anbieters wird in den Einstellungen konfiguriert", + "loading": "Modelle werden geladen..." }, "lockApiConfigAcrossModes": "API-Konfiguration für alle Modi in diesem Arbeitsbereich sperren", "unlockApiConfigAcrossModes": "API-Konfiguration ist für alle Modi in diesem Arbeitsbereich gesperrt (klicke zum Entsperren)", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 4e779be6ac..d9a200e932 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -157,7 +157,8 @@ "settings": "Model Settings", "description": "Choose the model used for this API configuration.", "searchPlaceholder": "Search models...", - "unsupportedTooltip": "This provider's model is configured in Settings" + "unsupportedTooltip": "This provider's model is configured in Settings", + "loading": "Loading models..." }, "enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", "addImages": "Add images to message", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 47b68a379b..819c686912 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -119,7 +119,8 @@ "settings": "Configuración del modelo", "description": "Elige el modelo que se usará para esta configuración de API.", "searchPlaceholder": "Buscar modelos...", - "unsupportedTooltip": "El modelo de este proveedor se configura en Ajustes" + "unsupportedTooltip": "El modelo de este proveedor se configura en Ajustes", + "loading": "Cargando modelos..." }, "lockApiConfigAcrossModes": "Bloquear la configuración de API en todos los modos de este espacio de trabajo", "unlockApiConfigAcrossModes": "La configuración de API está bloqueada en todos los modos de este espacio de trabajo (clic para desbloquear)", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index f188846302..1ebdc00c3b 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -119,7 +119,8 @@ "settings": "Paramètres du modèle", "description": "Choisissez le modèle utilisé pour cette configuration d'API.", "searchPlaceholder": "Rechercher des modèles...", - "unsupportedTooltip": "Le modèle de ce fournisseur se configure dans les Paramètres" + "unsupportedTooltip": "Le modèle de ce fournisseur se configure dans les Paramètres", + "loading": "Chargement des modèles..." }, "lockApiConfigAcrossModes": "Verrouiller la configuration API pour tous les modes dans cet espace de travail", "unlockApiConfigAcrossModes": "La configuration API est verrouillée pour tous les modes dans cet espace de travail (cliquer pour déverrouiller)", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index e411da4d39..d9365a194f 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -119,7 +119,8 @@ "settings": "मॉडल सेटिंग्स", "description": "इस API कॉन्फ़िगरेशन के लिए उपयोग किया जाने वाला मॉडल चुनें।", "searchPlaceholder": "मॉडल खोजें...", - "unsupportedTooltip": "इस प्रदाता का मॉडल सेटिंग्स में कॉन्फ़िगर किया जाता है" + "unsupportedTooltip": "इस प्रदाता का मॉडल सेटिंग्स में कॉन्फ़िगर किया जाता है", + "loading": "मॉडल लोड हो रहे हैं..." }, "lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें", "unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index a7add7f3a8..197f036bfe 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -149,7 +149,8 @@ "settings": "Pengaturan Model", "description": "Pilih model yang digunakan untuk konfigurasi API ini.", "searchPlaceholder": "Cari model...", - "unsupportedTooltip": "Model penyedia ini dikonfigurasi di Pengaturan" + "unsupportedTooltip": "Model penyedia ini dikonfigurasi di Pengaturan", + "loading": "Memuat model..." }, "lockApiConfigAcrossModes": "Kunci konfigurasi API di semua mode dalam workspace ini", "unlockApiConfigAcrossModes": "Konfigurasi API terkunci di semua mode dalam workspace ini (klik untuk membuka kunci)", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index fc77cc634f..81ddc6f641 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -119,7 +119,8 @@ "settings": "Impostazioni del modello", "description": "Scegli il modello da usare per questa configurazione API.", "searchPlaceholder": "Cerca modelli...", - "unsupportedTooltip": "Il modello di questo provider si configura nelle Impostazioni" + "unsupportedTooltip": "Il modello di questo provider si configura nelle Impostazioni", + "loading": "Caricamento dei modelli..." }, "lockApiConfigAcrossModes": "Blocca la configurazione API per tutte le modalità in questo workspace", "unlockApiConfigAcrossModes": "La configurazione API è bloccata per tutte le modalità in questo workspace (clicca per sbloccare)", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 75eb5396bb..e6f5ccc3ee 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -119,7 +119,8 @@ "settings": "モデル設定", "description": "この API 設定で使用するモデルを選択します。", "searchPlaceholder": "モデルを検索...", - "unsupportedTooltip": "このプロバイダーのモデルは設定で構成します" + "unsupportedTooltip": "このプロバイダーのモデルは設定で構成します", + "loading": "モデルを読み込み中..." }, "lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック", "unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 4293463b6c..cfce8ee08d 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -140,7 +140,8 @@ "settings": "모델 설정", "description": "이 API 구성에서 사용할 모델을 선택하세요.", "searchPlaceholder": "모델 검색...", - "unsupportedTooltip": "이 공급자의 모델은 설정에서 구성합니다" + "unsupportedTooltip": "이 공급자의 모델은 설정에서 구성합니다", + "loading": "모델 로드 중..." }, "errorReadingFile": "파일 읽기 오류", "noValidImages": "처리된 유효한 이미지가 없습니다", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index a36783301d..6a224c52b1 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -119,7 +119,8 @@ "settings": "Modelinstellingen", "description": "Kies het model dat voor deze API-configuratie wordt gebruikt.", "searchPlaceholder": "Modellen zoeken...", - "unsupportedTooltip": "Het model van deze provider wordt in de Instellingen geconfigureerd" + "unsupportedTooltip": "Het model van deze provider wordt in de Instellingen geconfigureerd", + "loading": "Modellen laden..." }, "lockApiConfigAcrossModes": "API-configuratie vergrendelen voor alle modi in deze werkruimte", "unlockApiConfigAcrossModes": "API-configuratie is vergrendeld voor alle modi in deze werkruimte (klik om te ontgrendelen)", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 9581584289..411625c874 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -119,7 +119,8 @@ "settings": "Ustawienia modelu", "description": "Wybierz model używany w tej konfiguracji API.", "searchPlaceholder": "Szukaj modeli...", - "unsupportedTooltip": "Model tego dostawcy jest konfigurowany w Ustawieniach" + "unsupportedTooltip": "Model tego dostawcy jest konfigurowany w Ustawieniach", + "loading": "Ładowanie modeli..." }, "lockApiConfigAcrossModes": "Zablokuj konfigurację API dla wszystkich trybów w tym obszarze roboczym", "unlockApiConfigAcrossModes": "Konfiguracja API jest zablokowana dla wszystkich trybów w tym obszarze roboczym (kliknij, aby odblokować)", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index b4d67f529a..1532578fc9 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -119,7 +119,8 @@ "settings": "Configurações do modelo", "description": "Escolha o modelo usado nesta configuração de API.", "searchPlaceholder": "Pesquisar modelos...", - "unsupportedTooltip": "O modelo deste provedor é configurado nas Configurações" + "unsupportedTooltip": "O modelo deste provedor é configurado nas Configurações", + "loading": "Carregando modelos..." }, "lockApiConfigAcrossModes": "Bloquear configuração da API em todos os modos neste workspace", "unlockApiConfigAcrossModes": "A configuração da API está bloqueada em todos os modos neste workspace (clique para desbloquear)", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 9ba08d8fec..66b5fafea8 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -119,7 +119,8 @@ "settings": "Настройки модели", "description": "Выберите модель для этой конфигурации API.", "searchPlaceholder": "Поиск моделей...", - "unsupportedTooltip": "Модель этого провайдера настраивается в Настройках" + "unsupportedTooltip": "Модель этого провайдера настраивается в Настройках", + "loading": "Загрузка моделей..." }, "lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве", "unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 1d39b29d1c..b989735f97 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -119,7 +119,8 @@ "settings": "Model Ayarları", "description": "Bu API yapılandırması için kullanılacak modeli seçin.", "searchPlaceholder": "Model ara...", - "unsupportedTooltip": "Bu sağlayıcının modeli Ayarlar'da yapılandırılır" + "unsupportedTooltip": "Bu sağlayıcının modeli Ayarlar'da yapılandırılır", + "loading": "Modeller yükleniyor..." }, "lockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırmasını kilitle", "unlockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırması kilitli (kilidi açmak için tıkla)", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 10185f2dc8..6b82f12c27 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -119,7 +119,8 @@ "settings": "Cài đặt mô hình", "description": "Chọn mô hình được dùng cho cấu hình API này.", "searchPlaceholder": "Tìm kiếm mô hình...", - "unsupportedTooltip": "Mô hình của nhà cung cấp này được cấu hình trong Cài đặt" + "unsupportedTooltip": "Mô hình của nhà cung cấp này được cấu hình trong Cài đặt", + "loading": "Đang tải mô hình..." }, "lockApiConfigAcrossModes": "Khóa cấu hình API cho tất cả chế độ trong workspace này", "unlockApiConfigAcrossModes": "Cấu hình API đã bị khóa cho tất cả chế độ trong workspace này (nhấn để mở khóa)", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index e64a671203..68df9b91cf 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -119,7 +119,8 @@ "settings": "模型设置", "description": "选择此 API 配置使用的模型。", "searchPlaceholder": "搜索模型...", - "unsupportedTooltip": "此提供商的模型需在设置中配置" + "unsupportedTooltip": "此提供商的模型需在设置中配置", + "loading": "正在加载模型..." }, "lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置", "unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 501be13b2c..917c1ed5b8 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -146,7 +146,8 @@ "settings": "模型設定", "description": "選擇此 API 設定使用的模型。", "searchPlaceholder": "搜尋模型...", - "unsupportedTooltip": "此供應商的模型需在設定中配置" + "unsupportedTooltip": "此供應商的模型需在設定中配置", + "loading": "正在載入模型..." }, "lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定", "unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)", From 93c8335b58088e0c9d8f079bec9705427fa152a1 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 12:55:15 +0900 Subject: [PATCH 06/15] fix: harden updateProfileModel against partial writes and allow-list bypass Allow reset keys such as awsCustomArn to be cleared only, so a patch cannot point Bedrock at a model the allow-list never checked. Restore the previous profile when activation fails after saving and report an unrecoverable partial state. Check the abort signal after each async boundary. --- src/core/webview/ClineProvider.ts | 61 +++++++++++++--- .../ClineProvider.apiHandlerRebuild.spec.ts | 73 +++++++++++++++++++ src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/id/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/nl/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + 20 files changed, 141 insertions(+), 11 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index dd8fbb2328..ed40721fb9 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -193,8 +193,11 @@ function scheduleTask( } // Keys a model-selector patch may touch: model IDs plus settings reset on model change. -const PROFILE_MODEL_PATCH_KEYS: ReadonlySet = new Set([ - ...modelIdKeys, +const PROFILE_MODEL_ID_PATCH_KEYS: ReadonlySet = new Set(modelIdKeys) + +// These keys can only be cleared. Setting `awsCustomArn` would let a patch invoke a Bedrock +// resource that the organization allow-list never checked, because the allow-list sees `apiModelId`. +const PROFILE_MODEL_RESET_ONLY_KEYS: ReadonlySet = new Set([ "reasoningEffort", "modelMaxTokens", "modelMaxThinkingTokens", @@ -1911,6 +1914,8 @@ export class ClineProvider * A `null` patch value clears the key. */ async updateProfileModel(name: string, expectedProvider: string, patch: Record): Promise { + let partialState = false + try { await this.enqueueProviderProfileMutation(async (signal) => { const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) @@ -1924,14 +1929,20 @@ export class ClineProvider const merged: Record = { ...stored } for (const [key, value] of Object.entries(patch)) { - if (!PROFILE_MODEL_PATCH_KEYS.has(key)) { - throw new Error(`Key "${key}" is not allowed in a model patch`) - } + if (PROFILE_MODEL_ID_PATCH_KEYS.has(key)) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`Key "${key}" must be a non-empty string in a model patch`) + } + + merged[key] = value + } else if (PROFILE_MODEL_RESET_ONLY_KEYS.has(key)) { + if (value !== null && value !== undefined) { + throw new Error(`Key "${key}" can only be cleared in a model patch`) + } - if (value === null || value === undefined) { delete merged[key] } else { - merged[key] = value + throw new Error(`Key "${key}" is not allowed in a model patch`) } } @@ -1943,15 +1954,37 @@ export class ClineProvider if (signal.aborted) return - await this.persistProviderProfile(name, merged as ProviderSettings, true, signal) + try { + await this.persistProviderProfile(name, merged as ProviderSettings, true, signal) + } catch (persistError) { + // Saving and activating are separate writes. Put the previous profile back so + // storage, context settings, and the task do not disagree about the model. + try { + await this.persistProviderProfile( + name, + stored as ProviderSettings, + true, + new AbortController().signal, + ) + } catch (restoreError) { + partialState = true + this.log( + `Error restoring profile after failed model update: ${JSON.stringify(restoreError, Object.getOwnPropertyNames(restoreError), 2)}`, + ) + } + + throw persistError + } }) } catch (error) { this.log(`Error update profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) vscode.window.showErrorMessage( - error instanceof OrganizationAllowListViolationError - ? error.message - : t("common:errors.create_api_config"), + partialState + ? t("common:errors.update_profile_model_partial") + : error instanceof OrganizationAllowListViolationError + ? error.message + : t("common:errors.create_api_config"), ) } } @@ -1974,6 +2007,8 @@ export class ClineProvider if (activate) { const { mode } = await this.getState() + if (signal.aborted) return id + // These promises do the following: // 1. Adds or updates the list of provider profiles. // 2. Sets the current provider profile. @@ -1991,6 +2026,8 @@ export class ClineProvider this.contextProxy.setProviderSettings(providerSettings), ]) + if (signal.aborted) return id + // Change the provider for the current task. // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true }) @@ -2001,6 +2038,8 @@ export class ClineProvider await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) } + if (signal.aborted) return id + await this.postStateToWebview() return id } diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 641ff48574..273039b263 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -498,6 +498,79 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalled() }) + test("only allows reset keys to be cleared, so a patch cannot set a Bedrock custom ARN", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + awsCustomArn: "arn:aws:bedrock:us-east-1:123456789012:inference-profile/other-model", + }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) + + test("rejects an empty or non-string model id", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: "" }) + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: 42 }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + }) + + test("restores the previous profile when activation fails after the new profile was saved", async () => { + const manager = provider["providerSettingsManager"] + vi.mocked(manager.setModeConfig).mockRejectedValueOnce(new Error("activation failed")) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + + const saved = vi.mocked(manager.saveConfig).mock.calls.map(([, config]) => config.openRouterModelId) + expect(saved).toEqual(["anthropic/claude", "openai/gpt-4"]) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config") + }) + + test("reports a partial state when the previous profile cannot be restored", async () => { + const manager = provider["providerSettingsManager"] + vi.mocked(manager.setModeConfig).mockRejectedValue(new Error("activation failed")) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.update_profile_model_partial") + }) + + test("does not activate the profile after the mutation timed out while waiting for state", async () => { + vi.useFakeTimers() + + try { + const realGetState = provider.getState.bind(provider) + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let calls = 0 + vi.spyOn(provider, "getState").mockImplementation(async () => { + // 1st call: allow-list check. 2nd call: activation inside persistProviderProfile. + if (++calls === 2) await gate + return realGetState() + }) + + const update = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + + await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS + 1) + await update + + release() + await vi.advanceTimersByTimeAsync(0) + + expect(provider["providerSettingsManager"].setModeConfig).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + test("rejects a model that the organization allow-list forbids", async () => { const realGetState = provider.getState.bind(provider) vi.spyOn(provider, "getState").mockImplementation(async () => ({ diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index a85c19cec4..579cd6b7d7 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -48,6 +48,7 @@ "search_commits": "Ha fallat la cerca de commits", "save_api_config": "Ha fallat el desament de la configuració de l'API", "create_api_config": "Ha fallat la creació de la configuració de l'API", + "update_profile_model_partial": "No s'ha pogut actualitzar el model ni restaurar la configuració d'API anterior. Comprova la configuració d'API a Configuració.", "rename_api_config": "Ha fallat el canvi de nom de la configuració de l'API", "load_api_config": "Ha fallat la càrrega de la configuració de l'API", "delete_api_config": "Ha fallat l'eliminació de la configuració de l'API", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 9f2b6d2056..3d8175a77e 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -44,6 +44,7 @@ "search_commits": "Fehler beim Suchen von Commits", "save_api_config": "Fehler beim Speichern der API-Konfiguration", "create_api_config": "Fehler beim Erstellen der API-Konfiguration", + "update_profile_model_partial": "Das Modell konnte nicht aktualisiert und die vorherige API-Konfiguration nicht wiederhergestellt werden. Überprüfe deine API-Konfiguration in den Einstellungen.", "rename_api_config": "Fehler beim Umbenennen der API-Konfiguration", "load_api_config": "Fehler beim Laden der API-Konfiguration", "delete_api_config": "Fehler beim Löschen der API-Konfiguration", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 507780366a..a7d616028f 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -44,6 +44,7 @@ "search_commits": "Failed to search commits", "save_api_config": "Failed to save api configuration", "create_api_config": "Failed to create api configuration", + "update_profile_model_partial": "Failed to update the model and could not restore the previous API configuration. Check your API configuration in Settings.", "rename_api_config": "Failed to rename api configuration", "load_api_config": "Failed to load api configuration", "delete_api_config": "Failed to delete api configuration", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index f646f5c4f3..2eb6848f11 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -44,6 +44,7 @@ "search_commits": "Error al buscar commits", "save_api_config": "Error al guardar la configuración de API", "create_api_config": "Error al crear la configuración de API", + "update_profile_model_partial": "No se pudo actualizar el modelo ni restaurar la configuración de API anterior. Revisa tu configuración de API en Ajustes.", "rename_api_config": "Error al renombrar la configuración de API", "load_api_config": "Error al cargar la configuración de API", "delete_api_config": "Error al eliminar la configuración de API", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 0e5f59ca1e..b6b65a88de 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -44,6 +44,7 @@ "search_commits": "Erreur lors de la recherche des commits", "save_api_config": "Erreur lors de l'enregistrement de la configuration API", "create_api_config": "Erreur lors de la création de la configuration API", + "update_profile_model_partial": "Impossible de mettre à jour le modèle et de restaurer la configuration d'API précédente. Vérifie ta configuration d'API dans les paramètres.", "rename_api_config": "Erreur lors du renommage de la configuration API", "load_api_config": "Erreur lors du chargement de la configuration API", "delete_api_config": "Erreur lors de la suppression de la configuration API", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 59cf0f0657..f69b5adccf 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -44,6 +44,7 @@ "search_commits": "कमिट्स खोजने में विफल", "save_api_config": "API कॉन्फ़िगरेशन सहेजने में विफल", "create_api_config": "API कॉन्फ़िगरेशन बनाने में विफल", + "update_profile_model_partial": "मॉडल अपडेट नहीं हो सका और पिछला API कॉन्फ़िगरेशन भी बहाल नहीं हो सका। सेटिंग्स में अपना API कॉन्फ़िगरेशन जाँचें।", "rename_api_config": "API कॉन्फ़िगरेशन का नाम बदलने में विफल", "load_api_config": "API कॉन्फ़िगरेशन लोड करने में विफल", "delete_api_config": "API कॉन्फ़िगरेशन हटाने में विफल", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 87ce78d209..e401612bf3 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -44,6 +44,7 @@ "search_commits": "Gagal mencari commit", "save_api_config": "Gagal menyimpan konfigurasi api", "create_api_config": "Gagal membuat konfigurasi api", + "update_profile_model_partial": "Gagal memperbarui model dan tidak dapat memulihkan konfigurasi API sebelumnya. Periksa konfigurasi API Anda di Pengaturan.", "rename_api_config": "Gagal mengganti nama konfigurasi api", "load_api_config": "Gagal memuat konfigurasi api", "delete_api_config": "Gagal menghapus konfigurasi api", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index ce7ffce090..aeb984940a 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -44,6 +44,7 @@ "search_commits": "Errore durante la ricerca dei commit", "save_api_config": "Errore durante il salvataggio della configurazione API", "create_api_config": "Errore durante la creazione della configurazione API", + "update_profile_model_partial": "Impossibile aggiornare il modello e ripristinare la configurazione API precedente. Controlla la configurazione API nelle impostazioni.", "rename_api_config": "Errore durante la ridenominazione della configurazione API", "load_api_config": "Errore durante il caricamento della configurazione API", "delete_api_config": "Errore durante l'eliminazione della configurazione API", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index d412e8f8e0..a7ec0c4de3 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -44,6 +44,7 @@ "search_commits": "コミットの検索に失敗しました", "save_api_config": "API設定の保存に失敗しました", "create_api_config": "API設定の作成に失敗しました", + "update_profile_model_partial": "モデルを更新できず、以前の API 設定も復元できませんでした。設定で API 設定を確認してください。", "rename_api_config": "API設定の名前変更に失敗しました", "load_api_config": "API設定の読み込みに失敗しました", "delete_api_config": "API設定の削除に失敗しました", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index ff2fcb10d9..906652ac14 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -44,6 +44,7 @@ "search_commits": "커밋 검색에 실패했습니다", "save_api_config": "API 구성 저장에 실패했습니다", "create_api_config": "API 구성 생성에 실패했습니다", + "update_profile_model_partial": "모델을 업데이트하지 못했고 이전 API 구성도 복원하지 못했습니다. 설정에서 API 구성을 확인하세요.", "rename_api_config": "API 구성 이름 변경에 실패했습니다", "load_api_config": "API 구성 로드에 실패했습니다", "delete_api_config": "API 구성 삭제에 실패했습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index e8b639af5d..34cdbbbe89 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -44,6 +44,7 @@ "search_commits": "Zoeken naar commits mislukt", "save_api_config": "Opslaan van API-configuratie mislukt", "create_api_config": "Aanmaken van API-configuratie mislukt", + "update_profile_model_partial": "Het model kon niet worden bijgewerkt en de vorige API-configuratie kon niet worden hersteld. Controleer je API-configuratie in de instellingen.", "rename_api_config": "Hernoemen van API-configuratie mislukt", "load_api_config": "Laden van API-configuratie mislukt", "delete_api_config": "Verwijderen van API-configuratie mislukt", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 73e2293a10..2a743fc009 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -44,6 +44,7 @@ "search_commits": "Nie udało się wyszukać commitów", "save_api_config": "Nie udało się zapisać konfiguracji API", "create_api_config": "Nie udało się utworzyć konfiguracji API", + "update_profile_model_partial": "Nie udało się zaktualizować modelu ani przywrócić poprzedniej konfiguracji API. Sprawdź konfigurację API w ustawieniach.", "rename_api_config": "Nie udało się zmienić nazwy konfiguracji API", "load_api_config": "Nie udało się załadować konfiguracji API", "delete_api_config": "Nie udało się usunąć konfiguracji API", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 82654929b4..eaf1cae06b 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -48,6 +48,7 @@ "search_commits": "Falha ao pesquisar commits", "save_api_config": "Falha ao salvar a configuração da API", "create_api_config": "Falha ao criar a configuração da API", + "update_profile_model_partial": "Não foi possível atualizar o modelo nem restaurar a configuração de API anterior. Verifique sua configuração de API nas configurações.", "rename_api_config": "Falha ao renomear a configuração da API", "load_api_config": "Falha ao carregar a configuração da API", "delete_api_config": "Falha ao excluir a configuração da API", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 5857b1290f..6f48365ec1 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -44,6 +44,7 @@ "search_commits": "Не удалось выполнить поиск коммитов", "save_api_config": "Не удалось сохранить конфигурацию API", "create_api_config": "Не удалось создать конфигурацию API", + "update_profile_model_partial": "Не удалось обновить модель и восстановить предыдущую конфигурацию API. Проверьте конфигурацию API в настройках.", "rename_api_config": "Не удалось переименовать конфигурацию API", "load_api_config": "Не удалось загрузить конфигурацию API", "delete_api_config": "Не удалось удалить конфигурацию API", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 48faf7d03c..b5c543c405 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -44,6 +44,7 @@ "search_commits": "Taahhütler aranamadı", "save_api_config": "API yapılandırması kaydedilemedi", "create_api_config": "API yapılandırması oluşturulamadı", + "update_profile_model_partial": "Model güncellenemedi ve önceki API yapılandırması geri yüklenemedi. Ayarlar'da API yapılandırmanı kontrol et.", "rename_api_config": "API yapılandırmasının adı değiştirilemedi", "load_api_config": "API yapılandırması yüklenemedi", "delete_api_config": "API yapılandırması silinemedi", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 5651e0eb3b..cb2ed8e9b8 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -44,6 +44,7 @@ "search_commits": "Không thể tìm kiếm các commit", "save_api_config": "Không thể lưu cấu hình API", "create_api_config": "Không thể tạo cấu hình API", + "update_profile_model_partial": "Không thể cập nhật mô hình và không thể khôi phục cấu hình API trước đó. Hãy kiểm tra cấu hình API trong Cài đặt.", "rename_api_config": "Không thể đổi tên cấu hình API", "load_api_config": "Không thể tải cấu hình API", "delete_api_config": "Không thể xóa cấu hình API", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index e5ecf86da2..08cfe736b5 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -49,6 +49,7 @@ "search_commits": "搜索提交失败", "save_api_config": "保存API配置失败", "create_api_config": "创建API配置失败", + "update_profile_model_partial": "无法更新模型,也无法恢复之前的 API 配置。请在设置中检查 API 配置。", "rename_api_config": "重命名API配置失败", "load_api_config": "加载API配置失败", "delete_api_config": "删除API配置失败", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index e70c464da0..92fdbaeb96 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -44,6 +44,7 @@ "search_commits": "搜尋提交失敗", "save_api_config": "儲存 API 設定失敗", "create_api_config": "建立 API 設定失敗", + "update_profile_model_partial": "無法更新模型,也無法還原先前的 API 設定。請在設定中檢查 API 設定。", "rename_api_config": "重新命名 API 設定失敗", "load_api_config": "載入 API 設定失敗", "delete_api_config": "刪除 API 設定失敗", From 9e60e5535a68e9d975e035ee423a8ccaf02228fb Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 13:25:20 +0900 Subject: [PATCH 07/15] chore: retrigger CI From 9444b2fbf7b1868ac9de0660f7682dd431c8b394 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 13:51:35 +0900 Subject: [PATCH 08/15] fix: keep model updates atomic across timeouts and clear Bedrock fields with null Let updateProfileModel run to completion after the caller times out and hold the profile mutation queue until it settles, so a saved profile is never left unactivated and a later mutation cannot overlap its writes. Wait for all activation writes before rolling back, and send null (not "") for Bedrock reset fields so the host accepts the patch. --- src/core/webview/ClineProvider.ts | 119 ++++++++++-------- .../ClineProvider.apiHandlerRebuild.spec.ts | 90 ++++++++++++- .../src/components/chat/ModelSelector.tsx | 3 +- .../chat/__tests__/ModelSelector.spec.tsx | 23 ++++ 4 files changed, 182 insertions(+), 53 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ed40721fb9..52f5ea3756 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -273,7 +273,15 @@ export class ClineProvider return runDelegationTransition(ClineProvider.delegationTransitionLocks, parentTaskId, fn) } - private enqueueProviderProfileMutation(fn: (signal: AbortSignal) => Promise): Promise { + /** + * With `runToCompletion`, a timeout only rejects the caller: the mutation is not aborted and the + * queue advances once it settles. Use it for multi-write mutations that must not stop halfway + * (storage saved but not activated) or overlap with the next mutation. + */ + private enqueueProviderProfileMutation( + fn: (signal: AbortSignal) => Promise, + options: { runToCompletion?: boolean } = {}, + ): Promise { const controller = new AbortController() // Run fn after either outcome so a rejected mutation never poisons the queue. const run = this.providerProfileMutationQueue.then( @@ -281,6 +289,11 @@ export class ClineProvider () => fn(controller.signal), ) const callerResult = this.withProviderProfileMutationTimeout(run, () => { + if (options.runToCompletion) { + this.log("Provider profile mutation timed out; waiting for it to finish before the next mutation") + return + } + controller.abort() this.log("Provider profile mutation timed out; aborting in-flight mutation") }) @@ -304,7 +317,7 @@ export class ClineProvider // Advance from the timeout-bounded result. Each fn checks its AbortSignal before // writing state, so advancing the queue on timeout cannot produce stale overwrites. - this.providerProfileMutationQueue = callerResult.then( + this.providerProfileMutationQueue = (options.runToCompletion ? run : callerResult).then( () => undefined, () => undefined, ) @@ -1917,65 +1930,65 @@ export class ClineProvider let partialState = false try { - await this.enqueueProviderProfileMutation(async (signal) => { - const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) + await this.enqueueProviderProfileMutation( + async (signal) => { + const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) - if (stored.apiProvider !== expectedProvider) { - throw new Error( - `Profile "${name}" provider changed from "${expectedProvider}" to "${String(stored.apiProvider)}"`, - ) - } + if (stored.apiProvider !== expectedProvider) { + throw new Error( + `Profile "${name}" provider changed from "${expectedProvider}" to "${String(stored.apiProvider)}"`, + ) + } - const merged: Record = { ...stored } + const merged: Record = { ...stored } - for (const [key, value] of Object.entries(patch)) { - if (PROFILE_MODEL_ID_PATCH_KEYS.has(key)) { - if (typeof value !== "string" || value.length === 0) { - throw new Error(`Key "${key}" must be a non-empty string in a model patch`) - } + for (const [key, value] of Object.entries(patch)) { + if (PROFILE_MODEL_ID_PATCH_KEYS.has(key)) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`Key "${key}" must be a non-empty string in a model patch`) + } - merged[key] = value - } else if (PROFILE_MODEL_RESET_ONLY_KEYS.has(key)) { - if (value !== null && value !== undefined) { - throw new Error(`Key "${key}" can only be cleared in a model patch`) - } + merged[key] = value + } else if (PROFILE_MODEL_RESET_ONLY_KEYS.has(key)) { + if (value !== null && value !== undefined) { + throw new Error(`Key "${key}" can only be cleared in a model patch`) + } - delete merged[key] - } else { - throw new Error(`Key "${key}" is not allowed in a model patch`) + delete merged[key] + } else { + throw new Error(`Key "${key}" is not allowed in a model patch`) + } } - } - const { organizationAllowList } = await this.getState() + const { organizationAllowList } = await this.getState() - if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { - throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) - } + if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { + throw new OrganizationAllowListViolationError( + t("common:errors.violated_organization_allowlist"), + ) + } - if (signal.aborted) return + if (signal.aborted) return - try { - await this.persistProviderProfile(name, merged as ProviderSettings, true, signal) - } catch (persistError) { - // Saving and activating are separate writes. Put the previous profile back so - // storage, context settings, and the task do not disagree about the model. try { - await this.persistProviderProfile( - name, - stored as ProviderSettings, - true, - new AbortController().signal, - ) - } catch (restoreError) { - partialState = true - this.log( - `Error restoring profile after failed model update: ${JSON.stringify(restoreError, Object.getOwnPropertyNames(restoreError), 2)}`, - ) - } + await this.persistProviderProfile(name, merged as ProviderSettings, true, signal) + } catch (persistError) { + // Saving and activating are separate writes. Put the previous profile back so + // storage, context settings, and the task do not disagree about the model. + try { + await this.persistProviderProfile(name, stored as ProviderSettings, true, signal) + } catch (restoreError) { + partialState = true + this.log( + `Error restoring profile after failed model update: ${JSON.stringify(restoreError, Object.getOwnPropertyNames(restoreError), 2)}`, + ) + } - throw persistError - } - }) + throw persistError + } + }, + { runToCompletion: true }, + ) } catch (error) { this.log(`Error update profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) @@ -2019,12 +2032,18 @@ export class ClineProvider // this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... }) // We should probably switch to that and verify that it works. // I left the original implementation in just to be safe. - await Promise.all([ + // `allSettled` so a caller that rolls back after a failure never races a write still in flight. + const activationResults = await Promise.allSettled([ this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()), this.updateGlobalState("currentApiConfigName", name), this.providerSettingsManager.setModeConfig(mode, id), this.contextProxy.setProviderSettings(providerSettings), ]) + const failedActivation = activationResults.find( + (result): result is PromiseRejectedResult => result.status === "rejected", + ) + + if (failedActivation) throw failedActivation.reason if (signal.aborted) return id diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 273039b263..de57823154 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -539,7 +539,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.update_profile_model_partial") }) - test("does not activate the profile after the mutation timed out while waiting for state", async () => { + test("finishes activation after the caller timed out instead of leaving a saved but inactive profile", async () => { vi.useFakeTimers() try { @@ -561,16 +561,102 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS + 1) await update + expect(provider["providerSettingsManager"].setModeConfig).not.toHaveBeenCalled() release() await vi.advanceTimersByTimeAsync(0) - expect(provider["providerSettingsManager"].setModeConfig).not.toHaveBeenCalled() + const manager = provider["providerSettingsManager"] + expect(manager.setModeConfig).toHaveBeenCalledTimes(1) + expect(vi.mocked(manager.saveConfig).mock.calls.map(([, config]) => config.openRouterModelId)).toEqual([ + "anthropic/claude", + ]) + } finally { + vi.useRealTimers() + } + }) + + test("holds the next profile mutation until a timed-out model update settles", async () => { + vi.useFakeTimers() + + try { + const realGetState = provider.getState.bind(provider) + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let calls = 0 + vi.spyOn(provider, "getState").mockImplementation(async () => { + if (++calls === 2) await gate + return realGetState() + }) + const manager = provider["providerSettingsManager"] + manager.activateProfile = vi.fn().mockResolvedValue({ + name: "other-profile", + id: "other-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4.1-mini", + }) + + const update = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS + 1) + await update + + const next = provider.activateProviderProfile({ name: "other-profile" }) + await vi.advanceTimersByTimeAsync(0) + expect(manager.activateProfile).not.toHaveBeenCalled() + + release() + await vi.advanceTimersByTimeAsync(0) + await next + + expect(manager.activateProfile).toHaveBeenCalledTimes(1) + // One write set from the model update, one from the queued activation. + expect(manager.setModeConfig).toHaveBeenCalledTimes(2) } finally { vi.useRealTimers() } }) + test("waits for every activation write before restoring the previous profile", async () => { + const manager = provider["providerSettingsManager"] + const events: string[] = [] + let releaseContext!: () => void + const contextWrite = new Promise((resolve) => { + releaseContext = resolve + }) + vi.mocked(manager.setModeConfig).mockImplementationOnce(async () => { + events.push("activation:failed") + throw new Error("activation failed") + }) + vi.spyOn(provider.contextProxy, "setProviderSettings").mockImplementationOnce(async () => { + await contextWrite + events.push("context:written") + }) + vi.mocked(manager.saveConfig).mockImplementation(async (_name, config) => { + events.push(`save:${config.openRouterModelId}`) + return "test-id" + }) + + const update = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + }) + await vi.waitFor(() => expect(events).toContain("activation:failed")) + expect(events).not.toContain("save:openai/gpt-4") + + releaseContext() + await update + + expect(events).toEqual([ + "save:anthropic/claude", + "activation:failed", + "context:written", + "save:openai/gpt-4", + ]) + }) + test("rejects a model that the organization allow-list forbids", async () => { const realGetState = provider.getState.bind(provider) vi.spyOn(provider, "getState").mockImplementation(async () => ({ diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index 9b016f8d6f..1c1393eff0 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -142,7 +142,8 @@ export const ModelSelector = ({ const patch: Record = { [modelIdField]: modelId } handleModelChangeSideEffects(provider, modelId, (f, v) => { - patch[f] = v ?? null + // The host accepts only `null` to clear a key, but the shared helper clears Bedrock fields with "". + patch[f] = v === undefined || v === "" ? null : v }) vscode.postMessage({ diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index e89724b0d3..e933c159e1 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -132,6 +132,29 @@ describe("ModelSelector", () => { expect(screen.queryByTestId("model-search-input")).not.toBeInTheDocument() }) + test("selecting a Bedrock model clears the ARN fields with null, never an empty string", () => { + staticModelsOverride.value = { "model-a": base, "model-b": base } + const bedrockConfig: ProviderSettings = { + apiProvider: providerIdentifiers.bedrock, + apiModelId: "model-a", + awsCustomArn: "arn:aws:bedrock:us-east-1:123456789012:inference-profile/old", + } + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.click(screen.getAllByTestId("model-selector-item")[1]) + + const [message] = vi.mocked(vscode.postMessage).mock.calls[0] + const patch: Record = message.values?.patch ?? {} + expect(patch).toMatchObject({ + apiModelId: "model-b", + awsCustomArn: null, + awsCustomArnBaseModelId: null, + awsModelContextWindow: null, + }) + expect(Object.values(patch)).not.toContain("") + }) + test("model items are real buttons so keyboard users can select them", () => { staticModelsOverride.value = { "model-a": base, "model-b": base } From 0e30619bf246fb7af51a4dcd782ae1429706facb Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 14:05:40 +0900 Subject: [PATCH 09/15] chore: retrigger CI From c833fa367fc6b3793ac5adfa5b3e54a74ef3c675 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 13 Oct 2026 14:29:17 +0900 Subject: [PATCH 10/15] fix: run upsertProviderProfile to completion so timeouts cannot leave a saved but inactive profile --- src/core/webview/ClineProvider.ts | 5 ++- .../ClineProvider.apiHandlerRebuild.spec.ts | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 52f5ea3756..dc8b8ddcaa 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1908,8 +1908,9 @@ export class ClineProvider activate: boolean = true, ): Promise { try { - return await this.enqueueProviderProfileMutation((signal) => - this.persistProviderProfile(name, providerSettings, activate, signal), + return await this.enqueueProviderProfileMutation( + (signal) => this.persistProviderProfile(name, providerSettings, activate, signal), + { runToCompletion: true }, ) } catch (error) { this.log( diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index de57823154..0405257ab6 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -576,6 +576,44 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { } }) + test("upsertProviderProfile finishes activation after the caller timed out", async () => { + vi.useFakeTimers() + + try { + const realGetState = provider.getState.bind(provider) + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let calls = 0 + vi.spyOn(provider, "getState").mockImplementation(async () => { + // 1st call: activation inside persistProviderProfile. + if (++calls === 1) await gate + return realGetState() + }) + + const upsert = provider.upsertProviderProfile( + "test-config", + { apiProvider: providerIdentifiers.openrouter, openRouterModelId: "anthropic/claude" }, + true, + ) + + await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS + 1) + await expect(upsert).resolves.toBeUndefined() + + const manager = provider["providerSettingsManager"] + expect(manager.setModeConfig).not.toHaveBeenCalled() + + release() + await vi.advanceTimersByTimeAsync(0) + + expect(manager.saveConfig).toHaveBeenCalledTimes(1) + expect(manager.setModeConfig).toHaveBeenCalledTimes(1) + } finally { + vi.useRealTimers() + } + }) + test("holds the next profile mutation until a timed-out model update settles", async () => { vi.useFakeTimers() From eb8b9e28010167a35bcca9176a4840f0673ae47e Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Fri, 16 Oct 2026 15:13:11 +0900 Subject: [PATCH 11/15] fix: treat unset stored apiProvider as anthropic in updateProfileModel The seeded default profile has no apiProvider, so the provider check threw even though getState() reports anthropic to the webview. Resolve the default the same way and persist it with the model patch. --- src/core/webview/ClineProvider.ts | 9 ++++++--- .../ClineProvider.apiHandlerRebuild.spec.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index dc8b8ddcaa..4fb202ca2e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1935,13 +1935,16 @@ export class ClineProvider async (signal) => { const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) - if (stored.apiProvider !== expectedProvider) { + // Unset provider (e.g. the seeded default profile) resolves to anthropic, matching getState(). + const storedProvider = stored.apiProvider ?? providerIdentifiers.anthropic + + if (storedProvider !== expectedProvider) { throw new Error( - `Profile "${name}" provider changed from "${expectedProvider}" to "${String(stored.apiProvider)}"`, + `Profile "${name}" provider changed from "${expectedProvider}" to "${storedProvider}"`, ) } - const merged: Record = { ...stored } + const merged: Record = { ...stored, apiProvider: storedProvider } for (const [key, value] of Object.entries(patch)) { if (PROFILE_MODEL_ID_PATCH_KEYS.has(key)) { diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 0405257ab6..2fa34bcc31 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -488,6 +488,22 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalled() }) + test("treats a stored profile without apiProvider as anthropic and persists that provider", async () => { + const { apiProvider: _apiProvider, ...unsetProfile } = storedProfile + provider["providerSettingsManager"].getProfile = vi.fn().mockResolvedValue(unsetProfile) + + await provider.updateProfileModel("test-config", providerIdentifiers.anthropic, { + apiModelId: "claude-sonnet-4-5", + }) + + const saveConfig = vi.mocked(provider["providerSettingsManager"].saveConfig) + expect(saveConfig).toHaveBeenCalledTimes(1) + expect(saveConfig.mock.calls[0][1]).toMatchObject({ + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-sonnet-4-5", + }) + }) + test("rejects patch keys outside the model and reset allow-list", async () => { await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: "anthropic/claude", From 7612ef0b462f9ca2d0e5b06a44e1ea5cbaa7c371 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Fri, 16 Oct 2026 15:20:10 +0900 Subject: [PATCH 12/15] fix: hide Bedrock custom-arn pseudo-model in chat model selector The host cannot set awsCustomArn, so picking custom-arn from the quick selector saved a model id with no ARN. Only show it when already selected. --- .../src/components/chat/ModelSelector.tsx | 9 ++++++- .../chat/__tests__/ModelSelector.spec.tsx | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index 1c1393eff0..a42e4c9d2a 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -31,6 +31,8 @@ import { IconButton } from "./IconButton" const SEARCH_THRESHOLD = 6 +const BEDROCK_CUSTOM_ARN_MODEL_ID = "custom-arn" + // Providers whose model can only be picked via a bespoke Settings UI // (freeform text, local model discovery, or a compound vendor/family selector) // rather than a simple id list. @@ -102,7 +104,12 @@ export const ModelSelector = ({ const modelIds = React.useMemo(() => { if (!filteredModelRecord) return [] return Object.entries(filteredModelRecord) - .filter(([id, info]) => id === selectedModelId || !info.deprecated) + .filter(([id, info]) => { + if (id === selectedModelId) return true + // The host cannot set awsCustomArn, so picking this here would save a model id without an ARN. + if (id === BEDROCK_CUSTOM_ARN_MODEL_ID) return false + return !info.deprecated + }) .map(([id]) => id) .sort((a, b) => a.localeCompare(b)) }, [filteredModelRecord, selectedModelId]) diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index e933c159e1..85821da767 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -155,6 +155,30 @@ describe("ModelSelector", () => { expect(Object.values(patch)).not.toContain("") }) + test("hides the Bedrock custom-arn pseudo-model unless it is already selected", () => { + staticModelsOverride.value = { "model-a": base, "custom-arn": base } + const bedrockConfig: ProviderSettings = { apiProvider: providerIdentifiers.bedrock, apiModelId: "model-a" } + const { unmount } = render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getAllByTestId("model-selector-item")).toHaveLength(1) + expect(screen.queryByText("custom-arn")).not.toBeInTheDocument() + unmount() + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getAllByTestId("model-selector-item")).toHaveLength(2) + }) + test("model items are real buttons so keyboard users can select them", () => { staticModelsOverride.value = { "model-a": base, "model-b": base } From d1bbaf497cfe9fc8a012fcfc0ad3ab11581332db Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Fri, 16 Oct 2026 15:22:48 +0900 Subject: [PATCH 13/15] test: cover every reset-only key in updateProfileModel Only reasoningEffort was verified to clear on null, so dropping another key from PROFILE_MODEL_RESET_ONLY_KEYS would have gone unnoticed. --- .../ClineProvider.apiHandlerRebuild.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 2fa34bcc31..f9330b4984 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -463,6 +463,31 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(saved).not.toHaveProperty("name") }) + test.each([ + ["reasoningEffort", "high"], + ["modelMaxTokens", 4096], + ["modelMaxThinkingTokens", 2048], + ["awsCustomArn", "arn:aws:bedrock:us-east-1:123456789012:inference-profile/old"], + ["awsCustomArnBaseModelId", "anthropic.claude-3-sonnet-20240229-v1:0"], + ["awsModelContextWindow", 200000], + ])("clears the reset-only key %s when the patch sets it to null", async (key, storedValue) => { + provider["providerSettingsManager"].getProfile = vi + .fn() + .mockResolvedValue({ ...storedProfile, [key]: storedValue }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "anthropic/claude", + [key]: null, + }) + + const saveConfig = vi.mocked(provider["providerSettingsManager"].saveConfig) + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + expect(saveConfig).toHaveBeenCalledTimes(1) + const saved = saveConfig.mock.calls[0][1] + expect(saved).toMatchObject({ openRouterModelId: "anthropic/claude" }) + expect(saved).not.toHaveProperty(key) + }) + test("rebuilds the current task API handler with the new model", async () => { const mockTask = new Task({ ...defaultTaskOptions }) mockTask.api = { From 21d363b0e6d26b5ea34d5b15fa3c6fa31605d3e7 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Fri, 16 Oct 2026 16:59:25 +0900 Subject: [PATCH 14/15] fix: enforce Bedrock custom ARN invariant in updateProfileModel A retained awsCustomArn survived a model patch that omitted null, so the ARN resource bypassed the organization allow-list that only checks apiModelId. Drop the ARN fields unless custom-arn stays selected, and reject selecting custom-arn without an existing ARN. --- src/core/webview/ClineProvider.ts | 18 ++++++ .../ClineProvider.apiHandlerRebuild.spec.ts | 62 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6e3dfd4993..3c68944687 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -195,6 +195,8 @@ function scheduleTask( // Keys a model-selector patch may touch: model IDs plus settings reset on model change. const PROFILE_MODEL_ID_PATCH_KEYS: ReadonlySet = new Set(modelIdKeys) +const BEDROCK_CUSTOM_ARN_MODEL_ID = "custom-arn" + // These keys can only be cleared. Setting `awsCustomArn` would let a patch invoke a Bedrock // resource that the organization allow-list never checked, because the allow-list sees `apiModelId`. const PROFILE_MODEL_RESET_ONLY_KEYS: ReadonlySet = new Set([ @@ -1943,6 +1945,22 @@ export class ClineProvider } } + if (storedProvider === providerIdentifiers.bedrock) { + if (merged.apiModelId === BEDROCK_CUSTOM_ARN_MODEL_ID) { + // The allow-list only sees apiModelId, so a patch must not select an ARN it cannot supply. + if (!merged.awsCustomArn) { + throw new Error( + `Model "${BEDROCK_CUSTOM_ARN_MODEL_ID}" requires an existing custom ARN`, + ) + } + } else { + // A retained ARN would override the allow-listed model, so drop it with its context fields. + for (const key of PROFILE_MODEL_RESET_ONLY_KEYS) { + if (key.startsWith("aws")) delete merged[key] + } + } + } + const { organizationAllowList } = await this.getState() if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index f9330b4984..3121fe1046 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -549,6 +549,68 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalled() }) + describe("Bedrock custom ARN invariant", () => { + const arn = "arn:aws:bedrock:us-east-1:123456789012:inference-profile/unvetted" + const bedrockWithArn = { + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.bedrock, + apiModelId: "custom-arn", + awsCustomArn: arn, + awsCustomArnBaseModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + awsModelContextWindow: 200000, + awsRegion: "us-east-1", + } + + beforeEach(() => { + provider["providerSettingsManager"].getProfile = vi.fn().mockResolvedValue(bedrockWithArn) + }) + + test("drops the retained ARN and its context fields when the patch omits them", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.bedrock, { + apiModelId: "anthropic.claude-3-haiku-20240307-v1:0", + }) + + const saveConfig = vi.mocked(provider["providerSettingsManager"].saveConfig) + expect(saveConfig).toHaveBeenCalledTimes(1) + const saved = saveConfig.mock.calls[0][1] + expect(saved).toMatchObject({ + apiModelId: "anthropic.claude-3-haiku-20240307-v1:0", + awsRegion: "us-east-1", + }) + expect(saved).not.toHaveProperty("awsCustomArn") + expect(saved).not.toHaveProperty("awsCustomArnBaseModelId") + expect(saved).not.toHaveProperty("awsModelContextWindow") + }) + + test("keeps the ARN while custom-arn stays selected", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.bedrock, { + apiModelId: "custom-arn", + }) + + expect(vi.mocked(provider["providerSettingsManager"].saveConfig).mock.calls[0][1]).toMatchObject({ + apiModelId: "custom-arn", + awsCustomArn: arn, + }) + }) + + test("rejects selecting custom-arn when the profile has no ARN", async () => { + provider["providerSettingsManager"].getProfile = vi.fn().mockResolvedValue({ + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.bedrock, + apiModelId: "anthropic.claude-3-haiku-20240307-v1:0", + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.bedrock, { + apiModelId: "custom-arn", + }) + + expect(provider["providerSettingsManager"].saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) + }) + test("rejects an empty or non-string model id", async () => { await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: "" }) await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: 42 }) From ce8aa6d21379b554e9f7180c3564dc5bddd372d9 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Fri, 16 Oct 2026 17:00:14 +0900 Subject: [PATCH 15/15] test: cover ModelSelector display name rendering and search --- .../chat/__tests__/ModelSelector.spec.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index 85821da767..d4dd381015 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -52,6 +52,19 @@ vi.mock("@/components/settings/utils/providerModelConfig", async (importOriginal } }) +const selectedModelOverride = vi.hoisted(() => ({ + value: undefined as { id: string; info?: { displayName?: string } } | undefined, +})) + +vi.mock("@/components/ui/hooks/useSelectedModel", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useSelectedModel: (...args: Parameters) => + selectedModelOverride.value ?? actual.useSelectedModel(...args), + } +}) + import { vscode } from "@/utils/vscode" const base = { contextWindow: 1000, supportsPromptCache: false } @@ -65,6 +78,7 @@ describe("ModelSelector", () => { beforeEach(() => { vi.clearAllMocks() staticModelsOverride.value = undefined + selectedModelOverride.value = undefined routerModelsResult.value = { data: undefined, isLoading: false } routerModelsResult.calls = [] extensionState.organizationAllowList = { allowAll: true, providers: {} } @@ -155,6 +169,51 @@ describe("ModelSelector", () => { expect(Object.values(patch)).not.toContain("") }) + describe("display names", () => { + const named = { + "model-a": { ...base, displayName: "Alpha Fast" }, + "model-b": { ...base, displayName: "Beta Deep" }, + } + + test("shows the selected model's display name instead of its id on the trigger", () => { + selectedModelOverride.value = { id: "model-a", info: { displayName: "Alpha Fast" } } + + render() + + const trigger = screen.getByTestId("model-selector-trigger") + expect(trigger).toHaveTextContent("Alpha Fast") + expect(trigger).not.toHaveTextContent("model-a") + }) + + test("lists display names and finds a model by searching its display name", () => { + staticModelsOverride.value = { + ...named, + "model-c": { ...base, displayName: "Gamma" }, + "model-d": { ...base, displayName: "Delta" }, + "model-e": { ...base, displayName: "Epsilon" }, + "model-f": { ...base, displayName: "Zeta" }, + "model-g": { ...base, displayName: "Eta" }, + } + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getByText("Beta Deep")).toBeInTheDocument() + + fireEvent.change(screen.getByTestId("model-search-input"), { target: { value: "Beta Deep" } }) + + const items = screen.getAllByTestId("model-selector-item") + expect(items).toHaveLength(1) + expect(items[0]).toHaveTextContent("Beta Deep") + + fireEvent.click(items[0]) + expect(vi.mocked(vscode.postMessage).mock.calls[0][0].values?.patch).toMatchObject({ + apiModelId: "model-b", + }) + }) + }) + test("hides the Bedrock custom-arn pseudo-model unless it is already selected", () => { staticModelsOverride.value = { "model-a": base, "custom-arn": base } const bedrockConfig: ProviderSettings = { apiProvider: providerIdentifiers.bedrock, apiModelId: "model-a" }