From 83ad90882f522e59240cbae401b567bd6b2e1021 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 29 Sep 2026 17:07:24 +0900 Subject: [PATCH 01/11] feat: add model selector UI to chat Adds a model selector dropdown to the chat composer, letting users switch models per-task without leaving the chat view. - Filters selectable models by organization allow list - Excludes deprecated models and disables selection for unsaved tasks or when selectApiConfigDisabled is set - Preserves static router provider models and resets search state when the popover closes without a selection - Adds unit, mutation, and visual regression coverage for the new component and updated composer baselines --- .../electron-chat-dark-sidebar.png | Bin 30321 -> 29896 bytes .../src/components/chat/ChatTextArea.tsx | 26 +- .../src/components/chat/ModeSelector.tsx | 3 +- .../src/components/chat/ModelSelector.tsx | 301 ++++ .../chat/__tests__/ChatTextArea.spec.tsx | 86 ++ .../chat/__tests__/ChatTextArea.visual.tsx | 2 +- .../chat/__tests__/ModelSelector.spec.tsx | 1266 +++++++++++++++++ .../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 .../src/components/chat/selectorConstants.ts | 1 + webview-ui/src/i18n/locales/ca/chat.json | 2 + webview-ui/src/i18n/locales/ca/common.json | 2 + webview-ui/src/i18n/locales/de/chat.json | 2 + webview-ui/src/i18n/locales/de/common.json | 2 + webview-ui/src/i18n/locales/en/chat.json | 2 + webview-ui/src/i18n/locales/en/common.json | 2 + webview-ui/src/i18n/locales/es/chat.json | 2 + webview-ui/src/i18n/locales/es/common.json | 2 + webview-ui/src/i18n/locales/fr/chat.json | 2 + webview-ui/src/i18n/locales/fr/common.json | 2 + webview-ui/src/i18n/locales/hi/chat.json | 2 + webview-ui/src/i18n/locales/hi/common.json | 2 + webview-ui/src/i18n/locales/id/chat.json | 2 + webview-ui/src/i18n/locales/id/common.json | 2 + webview-ui/src/i18n/locales/it/chat.json | 2 + webview-ui/src/i18n/locales/it/common.json | 2 + webview-ui/src/i18n/locales/ja/chat.json | 2 + webview-ui/src/i18n/locales/ja/common.json | 2 + webview-ui/src/i18n/locales/ko/chat.json | 2 + webview-ui/src/i18n/locales/ko/common.json | 2 + webview-ui/src/i18n/locales/nl/chat.json | 2 + webview-ui/src/i18n/locales/nl/common.json | 2 + webview-ui/src/i18n/locales/pl/chat.json | 2 + webview-ui/src/i18n/locales/pl/common.json | 2 + webview-ui/src/i18n/locales/pt-BR/chat.json | 2 + webview-ui/src/i18n/locales/pt-BR/common.json | 2 + webview-ui/src/i18n/locales/ru/chat.json | 2 + webview-ui/src/i18n/locales/ru/common.json | 2 + webview-ui/src/i18n/locales/tr/chat.json | 2 + webview-ui/src/i18n/locales/tr/common.json | 2 + webview-ui/src/i18n/locales/vi/chat.json | 2 + webview-ui/src/i18n/locales/vi/common.json | 2 + webview-ui/src/i18n/locales/zh-CN/chat.json | 2 + webview-ui/src/i18n/locales/zh-CN/common.json | 2 + webview-ui/src/i18n/locales/zh-TW/chat.json | 2 + webview-ui/src/i18n/locales/zh-TW/common.json | 2 + 52 files changed, 1753 insertions(+), 4 deletions(-) create mode 100644 webview-ui/src/components/chat/ModelSelector.tsx create mode 100644 webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx create mode 100644 webview-ui/src/components/chat/selectorConstants.ts 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 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 diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 2c539766fe..0f10c48f31 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -3,7 +3,7 @@ import { useEvent } from "react-use" import DynamicTextArea from "react-textarea-autosize" import { VolumeX, Image, WandSparkles, SendHorizontal, X, ListEnd, Square } from "lucide-react" -import type { ExtensionMessage } from "@roo-code/types" +import type { ExtensionMessage, ProviderSettings } from "@roo-code/types" import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions" import { WebviewMessage } from "@roo/WebviewMessage" @@ -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, @@ -104,6 +106,7 @@ export const ChatTextArea = forwardRef( commands, enterBehavior, lockApiConfigAcrossModes, + organizationAllowList, } = useExtensionState() // Find the ID and display text for the currently selected API configuration. @@ -114,6 +117,7 @@ export const ChatTextArea = forwardRef( displayName: currentApiConfigName || "", // Use the name directly for display. } }, [listApiConfigMeta, currentApiConfigName]) + const hasPersistedApiConfiguration = !!currentApiConfigName const [gitCommits, setGitCommits] = useState([]) const [showDropdown, setShowDropdown] = useState(false) @@ -944,6 +948,18 @@ export const ChatTextArea = forwardRef( [setMode], ) + // Common model selector handler + const handleModelChange = useCallback( + (value: ProviderSettings) => { + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration: value, + }) + }, + [currentApiConfigName], + ) + // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { vscode.postMessage({ type: "loadApiConfigurationById", text: value }) @@ -1319,6 +1335,14 @@ export const ChatTextArea = forwardRef( lockApiConfigAcrossModes={!!lockApiConfigAcrossModes} onToggleLockApiConfig={handleToggleLockApiConfig} /> +
diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 9e6a5a3a76..20804c7f1f 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -16,8 +16,7 @@ import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" import { IconButton } from "./IconButton" - -const SEARCH_THRESHOLD = 6 +import { SEARCH_THRESHOLD } from "./selectorConstants" interface ModeSelectorProps { value: Mode diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx new file mode 100644 index 0000000000..b06c14d2f8 --- /dev/null +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -0,0 +1,301 @@ +import { useState, useMemo, useCallback } from "react" +import { Fzf } from "fzf" + +import { + type ModelInfo, + type ModelRecord, + type OrganizationAllowList, + type ProviderSettings, + isDynamicProvider, + isRetiredProvider, + providerIdentifiers, +} 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 { + getProviderModelConfig, + getStaticModelsForProvider, + isStaticModelProvider, +} from "../settings/utils/providerModelConfig" +import { filterModels } from "../settings/utils/organizationFilters" +import { SEARCH_THRESHOLD } from "./selectorConstants" + +interface ModelSelectorProps { + apiConfiguration: ProviderSettings + onChange: (apiConfiguration: ProviderSettings) => void + disabled?: boolean + title: string + triggerClassName?: string + organizationAllowList?: OrganizationAllowList +} + +export const ModelSelector = ({ + apiConfiguration, + onChange, + disabled = false, + title, + triggerClassName = "", + organizationAllowList, +}: ModelSelectorProps) => { + const { t } = useAppTranslation() + const [open, setOpen] = useState(false) + const [searchValue, setSearchValue] = useState("") + const portalContainer = useRooPortal("roo-portal") + + const rawProvider = apiConfiguration?.apiProvider || providerIdentifiers.openrouter + const retired = isRetiredProvider(rawProvider) + const provider = retired ? providerIdentifiers.openrouter : rawProvider + const dynamicProvider = !retired && isDynamicProvider(provider) ? provider : undefined + const modelConfig = retired ? undefined : getProviderModelConfig(provider, apiConfiguration) + + const routerModels = useRouterModels({ provider: dynamicProvider, enabled: !!dynamicProvider }) + const { id: selectedModelId, info: selectedModelInfo, isLoading } = useSelectedModel(apiConfiguration) + + const models: ModelRecord = useMemo(() => { + // Stryker disable next-line ConditionalExpression,BlockStatement: every provider that is + // dynamic or has static models also has an entry in PROVIDER_MODEL_CONFIG, so `modelConfig` + // is only ever undefined for providers that would fall through to `{}` below anyway. + if (!modelConfig) { + return {} + } + + const staticModels = isStaticModelProvider(provider) + ? getStaticModelsForProvider(provider, undefined, apiConfiguration) + : {} + const { "custom-arn": _customArn, ...modelsWithoutCustomArn } = staticModels + + const allowedModels = dynamicProvider + ? filterModels( + { ...modelsWithoutCustomArn, ...(routerModels.data?.[dynamicProvider] ?? {}) }, + provider, + organizationAllowList, + ) + : filterModels(modelsWithoutCustomArn, provider, organizationAllowList) + + // Exclude deprecated models from being newly selectable, but keep the currently + // selected model visible even if it's deprecated (mirrors ModelPicker.tsx). + return Object.fromEntries( + Object.entries(allowedModels ?? {}).filter( + ([modelId, modelInfo]) => modelId === selectedModelId || !modelInfo.deprecated, + ), + ) + }, [ + modelConfig, + dynamicProvider, + routerModels.data, + provider, + apiConfiguration, + organizationAllowList, + selectedModelId, + ]) + + const modelIds = useMemo(() => Object.keys(models), [models]) + + const isModelListLoading = !!dynamicProvider && routerModels.isLoading + const isSupported = !!modelConfig && (isModelListLoading || modelIds.length > 0) + const isDisabled = disabled || modelIds.length === 0 + + // Label shown for a model — prefers `ModelInfo.displayName` when present, falling back to + // the raw model id (mirrors ModelPicker.tsx's trigger/list label logic). + // Stryker disable next-line ArrayDeclaration: this callback closes over no props or state, so + // its identity across renders isn't observable — only its (unmutated) body behavior is. + const getModelLabel = useCallback((modelId: string, info?: ModelInfo) => info?.displayName ?? modelId, []) + + const selectedModelLabel = getModelLabel(selectedModelId, selectedModelInfo) + + // Create searchable items for fuzzy search. + const searchableItems = useMemo( + () => + modelIds.map((id) => { + const label = getModelLabel(id, models[id]) + return { original: id, searchStr: label === id ? id : `${label} ${id}` } + }), + [modelIds, models, getModelLabel], + ) + + const fzfInstance = useMemo( + () => new Fzf(searchableItems, { selector: (item) => item.searchStr }), + [searchableItems], + ) + + const filteredModelIds = useMemo(() => { + // Stryker disable next-line ConditionalExpression,BlockStatement: fzf's `find("")` already + // returns every item in its original order, so skipping this shortcut is unobservable. + if (!searchValue) { + return modelIds + } + + return fzfInstance.find(searchValue).map((result) => result.item.original) + }, [modelIds, searchValue, fzfInstance]) + + const handleOpenChange = useCallback((next: boolean) => { + setOpen(next) + if (!next) { + setSearchValue("") + } + }, []) + + const handleEditClick = useCallback( + () => { + vscode.postMessage({ type: "switchTab", tab: "settings" }) + // Stryker disable next-line BooleanLiteral,CallExpression: this button only renders + // while the popover (and its `open` state) doesn't exist, so this call has no + // observable effect either way. + setOpen(false) + }, + // Stryker disable next-line ArrayDeclaration: this callback closes over no props or state. + [], + ) + + const handleSelect = useCallback( + (modelId: string) => { + // Stryker disable next-line ConditionalExpression,BlockStatement: handleSelect is only + // ever invoked from a rendered model-list item, which requires a non-empty `models` + // map, which in turn requires `modelConfig` to be defined — this guard can't be hit. + if (!modelConfig) { + return + } + + const updated: ProviderSettings = { + ...apiConfiguration, + reasoningEffort: undefined, + modelMaxTokens: undefined, + modelMaxThinkingTokens: undefined, + } + if (provider === providerIdentifiers.bedrock && modelId !== "custom-arn") { + ;(updated as Record)["awsCustomArn"] = undefined + } + ;(updated as Record)[modelConfig.field] = modelId + + onChange(updated) + + setOpen(false) + setSearchValue("") + }, + [apiConfiguration, modelConfig, provider, onChange], + ) + + const renderModelItem = useCallback( + (modelId: string) => { + const isCurrentModel = modelId === selectedModelId + const label = getModelLabel(modelId, models[modelId]) + + return ( + + ) + }, + [selectedModelId, models, getModelLabel, handleSelect, isDisabled], + ) + + if (!isSupported) { + return ( + + + + ) + } + + return ( + + + + + {isLoading || isModelListLoading ? t("common:ui.loading") : selectedModelLabel} + + + + +
+ {modelIds.length > SEARCH_THRESHOLD && ( +
+ setSearchValue(e.target.value)} + placeholder={t("common:ui.search_placeholder")} + 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" + autoFocus + /> + {searchValue.length > 0 && ( +
+
+ )} +
+ )} + + {filteredModelIds.length === 0 ? ( +
+ {t("common:ui.no_results")} +
+ ) : ( +
+ {filteredModelIds.map(renderModelItem)} +
+ )} + +
+

+ {t("chat:selectModel")} +

+
+
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index aae9ac3cc6..efcb448971 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1,3 +1,5 @@ +import type { ReactNode } from "react" + import { providerIdentifiers } from "@roo-code/types" import { defaultModeSlug } from "@roo/modes" @@ -34,6 +36,16 @@ const mockConvertToMentionPath = pathMentions.convertToMentionPath as ReturnType // Mock ExtensionStateContext vi.mock("@src/context/ExtensionStateContext") +vi.mock("@src/components/ui", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + StandardTooltip: ({ children, content }: { children: ReactNode; content?: ReactNode }) => ( +
{children}
+ ), + } +}) + // Custom query function to get the enhance prompt button const getEnhancePromptButton = () => { return screen.getByRole("button", { @@ -71,6 +83,7 @@ describe("ChatTextArea", () => { apiConfiguration: { apiProvider: providerIdentifiers.anthropic, }, + currentApiConfigName: "default", taskHistory: [], cwd: "/test/workspace", }) @@ -1207,6 +1220,79 @@ describe("ChatTextArea", () => { }) }) + describe("model selector", () => { + it("passes the responsive trigger class name to the model selector", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toHaveClass("min-w-[28px]") + }) + + it("passes the selectModel translation as the trigger tooltip", () => { + render() + + expect(screen.getByTestId("model-selector-trigger").closest("[data-tooltip-content]")).toHaveAttribute( + "data-tooltip-content", + "chat:selectModel", + ) + }) + + it("disables the model selector trigger when selectApiConfigDisabled is true", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).toBeDisabled() + }) + + it("enables the model selector trigger when selectApiConfigDisabled is false", () => { + render() + + expect(screen.getByTestId("model-selector-trigger")).not.toBeDisabled() + }) + + it("disables model selection without a persisted API configuration", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + apiConfiguration: { apiProvider: providerIdentifiers.anthropic }, + currentTaskId: "task-local-context", + currentApiConfigName: undefined, + taskHistory: [], + cwd: "/test/workspace", + }) + + render() + + const trigger = screen.getByTestId("model-selector-trigger") + expect(trigger).toBeDisabled() + expect(trigger.closest("[data-tooltip-content]")).toHaveAttribute( + "data-tooltip-content", + "chat:selectApiConfig", + ) + + mockPostMessage.mockClear() + fireEvent.click(trigger) + expect(mockPostMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ type: "upsertApiConfiguration" }), + ) + }) + + it("posts upsertApiConfiguration with the current config name when a model is selected", () => { + render() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) + + expect(mockPostMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "upsertApiConfiguration", + text: "default", + apiConfiguration: expect.objectContaining({ + apiModelId: expect.stringContaining("claude-3-5-haiku"), + }), + }), + ) + }) + }) + describe("blank suggestion copy crash (issue #1226)", () => { const getSendButton = (container: HTMLElement) => { const buttons = container.querySelectorAll("button") diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.visual.tsx index 9c447fe011..e1beb9ce9e 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 < 15 && !(await editor.evaluate((element) => element === document.activeElement)); index++ ) { await page.keyboard.press("Tab") 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..808483fcfb --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -0,0 +1,1266 @@ +import type { ComponentProps } from "react" +import userEvent from "@testing-library/user-event" +import type { Popover, PopoverTrigger, PopoverContent, StandardTooltip } from "@/components/ui" +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ModelInfo, + type RouterModels, + type ProviderSettings, + type OrganizationAllowList, +} from "@roo-code/types" + +import { render, screen, fireEvent, within } from "@/utils/test-utils" +import { vscode } from "@/utils/vscode" + +import { ModelSelector } from "../ModelSelector" + +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +const { useRooPortalMock, popoverContentContainer } = vi.hoisted(() => ({ + useRooPortalMock: vi.fn(() => document.body), + popoverContentContainer: { current: undefined as Element | DocumentFragment | null | undefined }, +})) + +vi.mock("@/components/ui/hooks/useRooPortal", () => ({ + useRooPortal: useRooPortalMock, +})) + +const { useRouterModelsMock, useSelectedModelMock } = vi.hoisted(() => ({ + useRouterModelsMock: vi.fn((): { data: Partial | undefined; isLoading: boolean } => ({ + data: {}, + isLoading: false, + })), + useSelectedModelMock: vi.fn((): { id: string; info?: { displayName?: string }; isLoading: boolean } => ({ + id: "claude-sonnet-4-5", + isLoading: false, + })), +})) + +vi.mock("@/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: useRouterModelsMock, +})) + +vi.mock("@/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: useSelectedModelMock, +})) + +vi.mock("@/components/ui", async () => { + const { createContext, useContext } = await import("react") + const PopoverContext = createContext, "open" | "onOpenChange">>({}) + return { + Popover: ({ children, open, onOpenChange }: ComponentProps) => ( + +
+ {children} +
+
+ ), + PopoverTrigger: ({ children, ...props }: ComponentProps) => { + const { open, onOpenChange } = useContext(PopoverContext) + return ( + + ) + }, + PopoverContent: ({ children, container }: ComponentProps) => { + const { open } = useContext(PopoverContext) + popoverContentContainer.current = container + return open ?
{children}
: null + }, + StandardTooltip: ({ children, content }: ComponentProps) => ( +
{children}
+ ), + } +}) + +const makeModelInfo = (overrides: Partial = {}): ModelInfo => ({ + contextWindow: 128000, + supportsPromptCache: false, + ...overrides, +}) + +const manyDynamicModels = Object.fromEntries( + Array.from({ length: 8 }, (_, index) => [`openrouter/model-${index}`, makeModelInfo()]), +) + +describe("ModelSelector", () => { + const onChangeMock = vi.fn() + const focusDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "focus") + beforeAll(() => { + // Global setup replaces focus with a no-op for FAST; keyboard tests need native DOM focus. + const frame = document.createElement("iframe") + document.body.appendChild(frame) + const nativeFocus = frame.contentDocument?.createElement("button").focus + if (!nativeFocus) throw new Error("Could not initialize native DOM focus") + Object.defineProperty(HTMLElement.prototype, "focus", { + configurable: true, + writable: true, + value: nativeFocus, + }) + frame.remove() + }) + afterAll(() => { + if (focusDescriptor) Object.defineProperty(HTMLElement.prototype, "focus", focusDescriptor) + }) + + beforeEach(() => { + vi.clearAllMocks() + useRouterModelsMock.mockReturnValue({ data: {}, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "claude-sonnet-4-5", isLoading: false }) + }) + + it.each([true, false])("distinguishes a pending dynamic list from a settled list (has models: %s)", (hasModels) => { + useRouterModelsMock.mockReturnValue({ data: undefined, isLoading: true }) + const props = { + apiConfiguration: { apiProvider: providerIdentifiers.openrouter }, + title: "Select model", + onChange: onChangeMock, + } + const { rerender } = render() + expect(screen.getByTestId("model-selector-trigger")).toHaveTextContent("common:ui.loading") + expect(screen.getByTestId("model-selector-trigger")).toBeDisabled() + expect(screen.queryByTestId("model-selector-disabled")).not.toBeInTheDocument() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(onChangeMock).not.toHaveBeenCalled() + + useRouterModelsMock.mockReturnValue({ + data: { openrouter: hasModels ? { "model-a": makeModelInfo() } : {} }, + isLoading: false, + }) + rerender() + if (hasModels) { + expect(screen.getByTestId("model-selector-trigger")).not.toBeDisabled() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getByRole("button", { name: "model-a" })).toBeInTheDocument() + } else { + expect(screen.getByTestId("model-selector-disabled")).toBeInTheDocument() + } + }) + + it("supports keyboard search clearing and model selection", async () => { + const user = userEvent.setup() + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + render( + , + ) + await user.click(screen.getByTestId("model-selector-trigger")) + const search = screen.getByRole("textbox") + await user.type(search, "model-3") + await user.tab() + expect(screen.getByRole("button", { name: "common:ui.clear_search" })).toHaveFocus() + await user.keyboard("{Enter}") + expect(search).toHaveValue("") + expect(screen.queryByRole("button", { name: "common:ui.clear_search" })).not.toBeInTheDocument() + search.focus() + await user.tab() + expect(screen.getByRole("button", { name: "openrouter/model-0" })).toHaveFocus() + await user.keyboard(" ") + expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() + expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-0" })) + }) + + it("blocks model updates when disabled after the model list is rendered", async () => { + const user = userEvent.setup() + useRouterModelsMock.mockReturnValue({ data: { openrouter: { "model-a": makeModelInfo() } }, isLoading: false }) + const props = { + apiConfiguration: { apiProvider: providerIdentifiers.openrouter }, + title: "Select model", + onChange: onChangeMock, + } + const { rerender } = render() + await user.click(screen.getByTestId("model-selector-trigger")) + const model = screen.getByRole("button", { name: "model-a" }) + model.focus() + rerender() + expect(model).toBeDisabled() + await user.keyboard("{Enter}") + await user.click(model) + expect(onChangeMock).not.toHaveBeenCalled() + }) + + it("renders the static model list for a static provider and calls onChange on select", () => { + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByTestId("model-selector-trigger")).not.toBeDisabled() + + const anotherModel = screen.getAllByText(/claude-3-5-haiku/i)[0] + fireEvent.click(anotherModel) + + expect(onChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ apiModelId: expect.stringContaining("claude-3-5-haiku") }), + ) + }) + + it("resets reasoning/thinking-token overrides and closes the popover after selecting a model", () => { + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) + + expect(onChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ + reasoningEffort: undefined, + modelMaxTokens: undefined, + modelMaxThinkingTokens: undefined, + }), + ) + expect(screen.getByTestId("popover-root")).toHaveAttribute("data-open", "false") + expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() + }) + + it("prefers a model's displayName over its raw id when present", () => { + useRouterModelsMock.mockReturnValue({ + data: { + openrouter: { + "openrouter/model-a": makeModelInfo({ displayName: "Model A (friendly)" }), + "openrouter/model-b": makeModelInfo(), + }, + }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ + id: "openrouter/model-a", + info: { displayName: "Model A (friendly)" }, + isLoading: false, + }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + // Trigger shows the displayName, not the raw id. + expect(screen.getByTestId("model-selector-trigger")).toHaveTextContent("Model A (friendly)") + expect(screen.queryByText("openrouter/model-a")).not.toBeInTheDocument() + + // List item for the model without a displayName still falls back to its raw id. + expect(screen.getByText("openrouter/model-b")).toBeInTheDocument() + }) + + it("renders the dynamic router model list for a dynamic provider", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.getByText("openrouter/model-b")).toBeInTheDocument() + const list = within(screen.getByTestId("popover-content")) + expect(list.getByRole("button", { name: "openrouter/model-a" })).toHaveAttribute("aria-pressed", "true") + expect(list.getByRole("button", { name: "openrouter/model-b" })).toHaveAttribute("aria-pressed", "false") + + fireEvent.click(screen.getByText("openrouter/model-b")) + + expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-b" })) + }) + + it.each([ + { provider: providerIdentifiers.deepseek, staticModel: "deepseek-v4-flash" }, + { provider: providerIdentifiers.moonshot, staticModel: "kimi-k3" }, + ])("merges the static catalog with router models for $provider", ({ provider, staticModel }) => { + useRouterModelsMock.mockReturnValue({ + data: { [provider]: { "router-only-model": makeModelInfo() } }, + isLoading: false, + }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + expect(list.getByText(staticModel)).toBeInTheDocument() + expect(list.getByText("router-only-model")).toBeInTheDocument() + }) + + it.each([ + { provider: providerIdentifiers.deepseek, staticModel: "deepseek-v4-flash" }, + { provider: providerIdentifiers.moonshot, staticModel: "kimi-k3" }, + ])( + "falls back to the static catalog when router models are unavailable for $provider", + ({ provider, staticModel }) => { + useRouterModelsMock.mockReturnValue({ data: undefined, isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(within(screen.getByTestId("popover-content")).getByText(staticModel)).toBeInTheDocument() + }, + ) + + it("requests router models for a dynamic provider with fetching enabled", () => { + render( + , + ) + + expect(useRouterModelsMock).toHaveBeenCalledWith({ + provider: providerIdentifiers.openrouter, + enabled: true, + }) + }) + + it("requests router models with fetching disabled for a static provider", () => { + render( + , + ) + + expect(useRouterModelsMock).toHaveBeenCalledWith({ + provider: undefined, + enabled: false, + }) + }) + + it("falls back to the openrouter dynamic provider when apiProvider is unset", () => { + render( + , + ) + + expect(useRouterModelsMock).toHaveBeenCalledWith({ + provider: providerIdentifiers.openrouter, + enabled: true, + }) + // No router models are loaded/mocked here, so the empty dynamic model list renders the + // unsupported/disabled fallback rather than an interactive trigger. + expect(screen.getByTestId("model-selector-disabled")).toBeInTheDocument() + }) + + it("mounts the popover content into the roo portal container", () => { + render( + , + ) + + expect(useRooPortalMock).toHaveBeenCalledWith("roo-portal") + expect(popoverContentContainer.current).toBe(useRooPortalMock.mock.results[0]?.value) + expect(popoverContentContainer.current).toBe(document.body) + }) + + it("shows the disabled view with a raw provider fallback label for a retired provider", () => { + useSelectedModelMock.mockReturnValue({ id: "", isLoading: false }) + + render( + , + ) + + // Retired providers have no model config of their own, so they never fetch router + // models and always render the unsupported/disabled view. With no selected model label, + // it falls back to the actual configured (retired) provider rather than openrouter. + expect(useRouterModelsMock).toHaveBeenCalledWith({ provider: undefined, enabled: false }) + expect(screen.getByTestId("model-selector-disabled")).toHaveTextContent(retiredProviderIdentifiers.groq) + }) + + it("preserves the selected model label for a retired provider when one is available", () => { + useSelectedModelMock.mockReturnValue({ + id: "llama-3-groq", + info: { displayName: "Llama 3 (Groq)" }, + isLoading: false, + }) + + render( + , + ) + + expect(screen.getByTestId("model-selector-disabled")).toHaveTextContent("Llama 3 (Groq)") + expect(screen.getByTestId("model-selector-disabled")).not.toHaveTextContent(retiredProviderIdentifiers.groq) + }) + + it("disables the selector for a provider outside the supported scope", () => { + useSelectedModelMock.mockReturnValue({ id: "", isLoading: false }) + + render( + , + ) + + expect(screen.queryByTestId("model-selector-trigger")).not.toBeInTheDocument() + expect(screen.getByTestId("model-selector-disabled")).toBeInTheDocument() + + fireEvent.click(screen.getByTestId("model-selector-disabled")) + + expect(vscode.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "switchTab", tab: "settings" })) + }) + + it("falls back to the provider name in the unsupported trigger when there is no selected model label", () => { + useSelectedModelMock.mockReturnValue({ id: "", isLoading: false }) + + render( + , + ) + + expect(screen.getByTestId("model-selector-disabled")).toHaveTextContent(providerIdentifiers.ollama) + }) + + it("shows the unsupported tooltip content and base classes on the disabled view", () => { + useSelectedModelMock.mockReturnValue({ id: "", isLoading: false }) + + render( + , + ) + + const disabledButton = screen.getByTestId("model-selector-disabled") + expect(disabledButton.closest("[data-tooltip-content]")).toHaveAttribute( + "data-tooltip-content", + "chat:selectModelUnsupported", + ) + expect(disabledButton).toHaveClass("min-w-0") + expect(disabledButton).toHaveClass("opacity-50") + }) + + it("disables the enabled selector's trigger when the disabled prop is set", () => { + render( + , + ) + + const trigger = screen.getByTestId("model-selector-trigger") + expect(trigger).toBeDisabled() + expect(trigger).toHaveClass("cursor-not-allowed") + expect(trigger).not.toHaveClass("opacity-100") + }) + + it("shows the loading label instead of the selected model while the selection is loading", () => { + useSelectedModelMock.mockReturnValue({ id: "claude-sonnet-4-5", isLoading: true }) + + render( + , + ) + + const trigger = screen.getByTestId("model-selector-trigger") + expect(trigger).toHaveTextContent("common:ui.loading") + expect(trigger).not.toHaveTextContent("claude-sonnet-4-5") + }) + + it("shows the title as the enabled trigger's tooltip content and applies the base trigger classes", () => { + render( + , + ) + + const trigger = screen.getByTestId("model-selector-trigger") + expect(trigger.closest("[data-tooltip-content]")).toHaveAttribute("data-tooltip-content", "Select model") + expect(trigger).toHaveClass("min-w-0") + }) + + it("does not open the popover until the user interacts with the trigger", () => { + render( + , + ) + + expect(screen.getByTestId("popover-root")).toHaveAttribute("data-open", "false") + expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() + }) + + it("does not append anything to the trigger class name by default", () => { + render( + , + ) + + expect(screen.getByTestId("model-selector-trigger")).not.toHaveClass("Stryker") + }) + + it("applies a custom trigger class name when provided", () => { + render( + , + ) + + expect(screen.getByTestId("model-selector-trigger")).toHaveClass("my-custom-trigger") + }) + + it("highlights the currently selected model with a check mark and not other models", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + const currentItem = list.getByText("openrouter/model-a").parentElement + const otherItem = list.getByText("openrouter/model-b").parentElement + + expect(currentItem).toHaveClass("bg-vscode-list-activeSelectionBackground") + expect(currentItem?.querySelector(".codicon-check")).toBeInTheDocument() + + expect(otherItem).not.toHaveClass("bg-vscode-list-activeSelectionBackground") + expect(otherItem?.querySelector(".codicon-check")).not.toBeInTheDocument() + expect(otherItem).toHaveClass("px-3") + expect(otherItem).toHaveClass("hover:bg-vscode-list-hoverBackground") + }) + + it("does not show a search box when there are few models", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.queryByLabelText("common:ui.search_placeholder")).not.toBeInTheDocument() + }) + + it("does not show a search box at exactly the search threshold, but does show it just above it", () => { + const atThreshold = Object.fromEntries( + Array.from({ length: 6 }, (_, index) => [`openrouter/model-${index}`, makeModelInfo()]), + ) + useRouterModelsMock.mockReturnValue({ data: { openrouter: atThreshold }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + const { rerender } = render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(screen.queryByLabelText("common:ui.search_placeholder")).not.toBeInTheDocument() + + const aboveThreshold = { ...atThreshold, "openrouter/model-6": makeModelInfo() } + useRouterModelsMock.mockReturnValue({ data: { openrouter: aboveThreshold }, isLoading: false }) + + rerender( + , + ) + + expect(screen.getByLabelText("common:ui.search_placeholder")).toBeInTheDocument() + }) + + it("shows an empty search box above the search threshold and filters the model list as the user types", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + expect(searchInput).toHaveValue("") + expect(searchInput).toHaveAttribute("placeholder", "common:ui.search_placeholder") + const list = within(screen.getByTestId("popover-content")) + expect(Object.keys(manyDynamicModels).every((id) => list.getByText(id) !== null)).toBe(true) + + fireEvent.change(searchInput, { target: { value: "model-3" } }) + + expect(list.getByText("openrouter/model-3")).toBeInTheDocument() + expect(list.queryByText("openrouter/model-0")).not.toBeInTheDocument() + }) + + it("clears the search value after selecting a filtered model", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "model-3" } }) + fireEvent.click(within(screen.getByTestId("popover-content")).getByText("openrouter/model-3")) + expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getByLabelText("common:ui.search_placeholder")).toHaveValue("") + }) + + it("clears the search value when the popover closes without a selection", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "model-3" } }) + expect(searchInput).toHaveValue("model-3") + + // Close the popover via the trigger without selecting a model. + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId("model-selector-trigger")) + expect(screen.getByLabelText("common:ui.search_placeholder")).toHaveValue("") + }) + + it("matches a model by its displayName, and by its raw id when it has no displayName", () => { + useRouterModelsMock.mockReturnValue({ + data: { + openrouter: Object.fromEntries([ + ["openrouter/model-a", makeModelInfo({ displayName: "Zebra Special" })], + ...Array.from({ length: 7 }, (_, index) => [`openrouter/model-${index}`, makeModelInfo()]), + ]), + }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + + fireEvent.change(searchInput, { target: { value: "Zebra" } }) + expect(list.getByText("Zebra Special")).toBeInTheDocument() + + fireEvent.change(searchInput, { target: { value: "model-3" } }) + expect(list.getByText("openrouter/model-3")).toBeInTheDocument() + expect(list.queryByText("Zebra Special")).not.toBeInTheDocument() + }) + + it("does not search a model's id against itself twice when it has no displayName", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + // A search term that only fuzzy-matches if the id were searched as "id id" (i.e. searched + // against itself twice) must not match, since a model without a displayName is only + // searched against its raw id once. + fireEvent.change(screen.getByLabelText("common:ui.search_placeholder"), { + target: { value: "3 openrouter" }, + }) + + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + }) + + it("shows a no-results message when the search does not match any model", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + fireEvent.change(screen.getByLabelText("common:ui.search_placeholder"), { + target: { value: "no-such-model" }, + }) + + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + }) + + it("shows a clear icon only once the user has typed a search value, and clears the search when clicked", () => { + useRouterModelsMock.mockReturnValue({ data: { openrouter: manyDynamicModels }, isLoading: false }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-0", isLoading: false }) + + const { container } = render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(container.querySelector(".codicon-close")).not.toBeInTheDocument() + + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "model-3" } }) + + const clearIcon = container.querySelector(".codicon-close") + expect(clearIcon).toBeInTheDocument() + + fireEvent.click(clearIcon as Element) + + expect(searchInput).toHaveValue("") + expect(container.querySelector(".codicon-close")).not.toBeInTheDocument() + expect(within(screen.getByTestId("popover-content")).getByText("openrouter/model-0")).toBeInTheDocument() + }) + + it("shows the selectModel footer heading in the popover", () => { + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + expect(within(screen.getByTestId("popover-content")).getByText("chat:selectModel")).toBeInTheDocument() + }) + + it("re-derives the search index when the model list changes on rerender", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), ...manyDynamicModels } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + const { rerender } = render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const searchInput = screen.getByLabelText("common:ui.search_placeholder") + fireEvent.change(searchInput, { target: { value: "brand-new-model" } }) + expect(screen.getByText("common:ui.no_results")).toBeInTheDocument() + + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/brand-new-model": makeModelInfo(), ...manyDynamicModels } }, + isLoading: false, + }) + + rerender( + , + ) + + // The search index must be rebuilt from the new model list, not reused from the first + // render, for the newly-added model to be findable and the removed one to disappear. + expect( + within(screen.getByTestId("popover-content")).getByText("openrouter/brand-new-model"), + ).toBeInTheDocument() + }) + + it("re-renders the model list with fresh click handlers and highlighting when the selection changes", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + const { rerender } = render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-b", isLoading: false }) + rerender( + , + ) + + const list = within(screen.getByTestId("popover-content")) + expect(list.getByText("openrouter/model-a").parentElement).not.toHaveClass( + "bg-vscode-list-activeSelectionBackground", + ) + expect(list.getByText("openrouter/model-b").parentElement).toHaveClass( + "bg-vscode-list-activeSelectionBackground", + ) + + fireEvent.click(list.getByText("openrouter/model-a")) + + expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-a" })) + }) + + it("hides the custom-arn pseudo-model for a Bedrock provider and allows selecting a normal model", () => { + useSelectedModelMock.mockReturnValue({ id: "anthropic.claude-sonnet-4-5-20250929-v1:0", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + // A normal Bedrock model is present in the list. + expect(list.getByText("anthropic.claude-3-5-haiku-20241022-v1:0")).toBeInTheDocument() + + // The custom-arn pseudo-model is hidden from the chat selector. + expect(list.queryByText("Use Custom ARN")).not.toBeInTheDocument() + expect(list.queryByText("custom-arn")).not.toBeInTheDocument() + + // Selecting a normal Bedrock model sends the expected apiModelId update and clears any + // stale awsCustomArn left over from a previous "Use Custom ARN" selection in Settings, + // so the extension doesn't keep reading the old ARN instead of the newly chosen model. + fireEvent.click(list.getByText("anthropic.claude-3-5-haiku-20241022-v1:0")) + + expect(onChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiModelId: "anthropic.claude-3-5-haiku-20241022-v1:0", + awsCustomArn: undefined, + }), + ) + }) + + it("filters static models by the organization allow list when not all models are allowed", () => { + useSelectedModelMock.mockReturnValue({ id: "claude-3-5-haiku-20241022", isLoading: false }) + + const allowList: OrganizationAllowList = { + allowAll: false, + providers: { + anthropic: { + allowAll: false, + models: ["claude-3-5-haiku-20241022"], + }, + }, + } + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + // Only the allowed model is present. + expect(list.getByText("claude-3-5-haiku-20241022")).toBeInTheDocument() + + // Other models are filtered out by the allow list. + expect(list.queryByText("claude-sonnet-4-5")).not.toBeInTheDocument() + }) + + it("shows the disabled view when the provider is entirely absent from the organization allow list", () => { + useSelectedModelMock.mockReturnValue({ id: "claude-3-5-haiku-20241022", isLoading: false }) + + // allowAll is false and `providers` has no entry at all for the current provider, so + // filterModels returns {} for it rather than an allowed subset. + const allowList: OrganizationAllowList = { + allowAll: false, + providers: {}, + } + + render( + , + ) + + expect(screen.queryByTestId("model-selector-trigger")).not.toBeInTheDocument() + expect(screen.getByTestId("model-selector-disabled")).toBeInTheDocument() + }) + + it("filters dynamic router models by the organization allow list", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + const allowList: OrganizationAllowList = { + allowAll: false, + providers: { + openrouter: { + allowAll: false, + models: ["openrouter/model-a"], + }, + }, + } + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + // Only the allowed model is present. + expect(list.getByText("openrouter/model-a")).toBeInTheDocument() + + // The blocked model is filtered out. + expect(list.queryByText("openrouter/model-b")).not.toBeInTheDocument() + }) + + it("hides deprecated models but keeps a deprecated selected model visible", () => { + useRouterModelsMock.mockReturnValue({ + data: { + openrouter: { + "openrouter/model-a": makeModelInfo({ deprecated: true }), + "openrouter/model-b": makeModelInfo(), + }, + }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + // The currently selected model stays visible even though it's deprecated. + expect(list.getByText("openrouter/model-a")).toBeInTheDocument() + expect(list.getByText("openrouter/model-b")).toBeInTheDocument() + }) + + it("excludes a non-selected deprecated model from the list entirely", () => { + useRouterModelsMock.mockReturnValue({ + data: { + openrouter: { + "openrouter/model-a": makeModelInfo(), + "openrouter/model-deprecated": makeModelInfo({ deprecated: true }), + }, + }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + expect(list.getByText("openrouter/model-a")).toBeInTheDocument() + expect(list.queryByText("openrouter/model-deprecated")).not.toBeInTheDocument() + }) + + it("does not filter models when the organization allow list allows all", () => { + useRouterModelsMock.mockReturnValue({ + data: { openrouter: { "openrouter/model-a": makeModelInfo(), "openrouter/model-b": makeModelInfo() } }, + isLoading: false, + }) + useSelectedModelMock.mockReturnValue({ id: "openrouter/model-a", isLoading: false }) + + const allowList: OrganizationAllowList = { + allowAll: true, + providers: {}, + } + + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + const list = within(screen.getByTestId("popover-content")) + + // All models are present when allowAll is true. + expect(list.getByText("openrouter/model-a")).toBeInTheDocument() + expect(list.getByText("openrouter/model-b")).toBeInTheDocument() + }) +}) 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..8155d693380379b12a5846709d853211b3e03b48 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 zcmbulRal%&(5{PXaCZw5EWv}j%i!)BAh;9U2?+#u3+^t1ySuwXkl^m?CEr^AwGYdC->=VpWlnZR6*ofUNJtKDYCr*FrlyUYE8 z%Q|l_UXy&qi2bG???@} zH#@Fjf~uP6g%a*uU=row+B4A?J4a>*V1ac3r>mK&-V|Wcy;5R6aS;)mbH9=cUsBi0Xt)oy5!7YDqf04pz9aCE3+Z zW#@jpj2OTh) zBQuoB`>wmZuT*_vi_PN65xUGk(?Qjtju-JI4z2chbb=liAYfq##*@=NIzRI=o_Iq_ z0%nusc01Aa^z>9$SI;Kdy}rCG(X2B5Nyq^O&Cis3G1Lqg9)*>awcd7BTT#)lqT~MZ zPs7(m5tdzt_glCy!j+wqIg;Dk+Z8PrGVo^V<+{>UhqEONIPC8c9*(}DM=G$NLz7PK7 zTGM>><#s>IYSL6M)8;Vj@G~DxS@k?B6MVVLiIqHFZurQ-p$3Mua(udT=r>DX(wVC= zk)+mDK(~H;G*BEE85t2oS*WBFtk=j-{Sxiah3rn&>ocX-6aEvADp4%>Ys zJkDVd#M$hb1sl)#3h6v{YkZ|%^LlR7H2Us1VTQ-4c5RC_7NG6EpQo+2Ykq!yHp}(r z=L=OP?Jp13fp93KK6ge#Emu>r_xJZ{u}##})W>VByzK08Qm?CS>!V|1G@Pc|s^z-e zZ8kH77zKG98V26MSmhcOYu>l377iihdd(^8909!q17THh!kZVn!!=I5-*LqDJv=<- z4Se}G-#3l#F4fySzC50)}Hr;{*fhi9VF{Q7XV{pkX^|1fGU zq^@DQr=QGsC0{<3Hp4x@M*AsRl9WKuhi|N7&3`!YWB(BhEUe4v+Nv`qeBEk`2ixO; z!q@3WvFQ=l0~{nq7|il2h3&yGWZ*EOIQSU^IC-ITIq6N;Ez$T@6H2Mc{oj*WqH;$Yx{6Ry8Em zVp9#To1|0|ap&2RCN7cW?phM5{#`;!tNbOX|5#!SqaX26_2soQ&Jiv_K&p9X5p0B%C58CZ;-0}$}BUu z{T){XT9TTf?+ARX{QWXCUKHTPSV9!a8a`!aR>?PLM&-VQ!0`rz9rKUdi z5!vk@uKGM4GwIZOo%iCgOt!y1+ojrsj=LWg6`+4E?~+UnsRK_FyHW<^N2T++?Ij$O zNP#w7N!>RfU-Dv;ejH@gf1&1>S!S=gVX;P8$l|3at!R5VPDGc4aie$y|h6}~Y0`s;t=Q}()^uC-z~+@Sx7>Fujf#INes!${WVLW=LqNum+rM6n)UqEQs0H}Q5% zg=c-uijixt{KJ2{k|WDgZZlh~W{I$b+8##q>EHuL|0-o$A!qEna`9V3p5IYj-j1BM zKgR|skTx8>&*zEAg`qtM;Z(S`v@m&!YLu|m8kBetQf zM$yoy8VBAXf(;Wu+tvPAB{3b-MVLJ)h^+p!QzI=zy^{o($o6Inpv8?&CUlP{33uy!>O{bSVEu^Hvf*Zd~@J??hzZ0JciUtj{E-o3=?+0$aK` zsO8fO^l{s9{qlD;tWS&Z&r>my5Rt3)w4Q=B-?aO?_M|Wg=%?QgW*FI=%(&%d%hyFz zckKB$vh1jxvYX9Dlpfd3>qoR~37^-I`P@;6BH#Dr87UJ&)p5kPJRFu>he#V$9Zu!R z9$@Mwrkc6->b0MBA&-R0Y4odXIb2IV?S^B~;!g#=vNL1JObO^lelt}l3Yvm#ON*5B zog?@p!Em7R%|pClOKDRg=}d#ii)MpbTnaU)GLgj`DJ7}>@}vLzY0c^4@==J16d~vk z7U4WrM~tA7cOoIN%tE$e1h#%*KO%5FI9vS#J~1XkQB|u(2kC@=hxMwm@UREDxJj-4TgEbVnQ z)>evm-6|H4J(ew-R}~^Ba9sape>yRv~ySEqhY6%pjJWO5STlc zEP>5DX>fUw7L@051gmy&ky|fSw9O$sU}|P|eK0MFn1?_t$fml*&EvP$9g>mYK^pad z)OCDR-h8s@yT9B}F_!Sl$Pj`-)Jht!+8;!D4B+mKM3G1C~NODFk zkXEe#nflY(Y`+%uhZDvk^3kNSdKJ+zooU!{lBkb7dG)qC!MjFLwdXuXDLl zI?3J1;mmbzVposXM;1yOix!#G;r{f=r?r*~lx6Zc_J|4a*FS5%W)+ZdqV14YK`2C| zm!k7%=}!7LYsB8+-6uc5tBwrpCB<2ClHLA7b<2Y*49)+O9n9Jg(h_w5l-vk2Kp>U={5ypI+fD6f9G|GK&|rl=CYEMPLD>{Bo}h+z zOsq4+!xP3oBaC}$&LAYHd;(xMbCTMa;X6I znw$InZ(jzGBoK-BoOEagsMiOz2_-HBb^TD1^1gE}YsMtaSFYViO$hgkLD~Yj zO?5C&GZ=+YK3idsxl4CsEIzin>Th7$5WU!rs!`s3YT$${*r<*}#`?^-o($SS^hgR{ zU{84Xo5} zR9L36w<&Df$9*#9OH|=WPK(r=qH2MErcx1FF>xwB+=-F{i#F~y$N(DWs~_<^i{e`7 z6HgW(2cjst-qfE-m_2d;`5ivx3ZOv*LJ%7Mzg`lV+}YbpdChh+dqQFV+lgx5`UiPAIn@mDH_ey*1qlgBL{#*;Ui~;OF0Q4e z1t>sz`uYGZpzeV#9S>;Bg%G&`piqfF57qg0wV(a!MK1Hxipy%I5J#LlrKmcZSinVFf0N2l4uNU=Aw))xU{ES1wH4#PWV35AfoP^)GS zs9&C+pHn%k)s>6O{?lOa(l*M=g`Bc+fC>?>lXu`+27|%b+1WlmK1175J#VAM+p<>W z<>m&v9xv@Va2YUZ*U~XD$;ZQBIsl8f05_}O?!zCWSD-+0+4y*OzFl|&2%YMhn)qlG zjSO+hHP%%z z+MV*z9N^Ra{VxJE@864wiP1$-{j*!@uV3hwjmuj)F{{Nv=$mgQ$<+lrGD_L5wIth+ zxG~s?{L}Mrj8c7leTRvw@rnN&&MB4|c=5aZk$(6xKpUna6Js3dB?Od8+(T?F!G z>SqT^*tq`aur~<8>EPhdvzj9rMYB?WSFD^TLqI?P=$|{_9c%8}D7c^E;W7T1JR&e} zARqwh<@)v(4HZ@Wzx~w7*)s#^M3BI}sj_m6y6b1PAFbCqx7urjBWcxeN&cfOrE94w+^k|?!ufVT6+r>7$4y;;3)wH$TyGRT zoMQ~lHVSO{l*^88wtxMPzkD2*^UcRA2IGScXs%MIp}EJ5YG-rP>$RieV-VY~&#KV} z!MF0dfjreuvSaUIgMpc5V`FXn!Be3rH1C~2geJPSIZV*h*rTuTKtt4%~n4QmAB6Hd-etUm(XSo|FCK)nchC5y09cWvY_Dq#c?J_K&s`gQm z@c@#!Yajsj1oH&1JxxPX(_W(?zgDrg(0(_Ncu=6#<91VPxS>K}*!|>YhH^M@gXvLG zt(CYc7oM&>8rJ%Wviax8i%X;9hpRm#v+j^9%RCv_Q#oy~!;!^5r;UBW1Rud>ImI61 z>(Pf&x!Ffeof@`P6H5TLJnoXnnkf&g?Vo#ci!eZQn z;yDK_V`((S+-SWnAcZKc)xdo{o4hv}2VC3vKv_(Xj;;`JN*k1g4HI?sIp7DzG6gGS zpyrWpXJAvytrH4m@Y1zs_S*h>AE`BX>0R~|A#*N2-iWAGFxr%D)AYoUc2q@2$olxa zbOyq|?G~3Kb^qOD2fux;@v9wg+OCUr+7xH`L;rd=o1Jb>&GXob0Bb``v?$F9up^Vc zA3)9`{%qf|RU#8lFD@xLU!s9lr`8KOOVbJH7d&_~V-ph~Gd=vt^yRdg2`iyP+w$Br zw^==>9|ccKK8sL7LqWxzqGRDW^t`Y^i?3DFf)!`K!mEwObFkB5$a+HbOSpyrNGghe zZLfaW7GPRB$*g9Sl#~Mkc-Yu!?4N;n2@-r}Hyerrg2Q`iYQ4taSV#&Xrhq1mR9hCa zvZA-m0W^HZ4Et@{g)L;!b6|6WssN{jU2+L>xA7+yYk)3IJp!;)ibg&Ko0xb7ATw7z zUcRuzQqe!DdJQVO`tF#tFO5Dd1UE|3OUjg^#?I3srV z_Apt|3HhF{rg789Q)+8#xBH_EJa*%sWShzwkG2&8SXk2Aea{nwN>e*bc6N7-Isnk7 z9@2xj6s@ zR`ntOG*{hL+O4-m2|R?othf96t_}h}hqJ3ZVK%ksw&p>bybqwdTmptFbE(5*@!r*D zw+g*x-0LjVD@Y3ksq}D~GH53SfL8fdO*Xk$kC`ilKEnZ2M4hx##ZDoEp8)*L@w$eg zD-a3U@dkzjggmB=Rfal)EN_ROYH;FZ{xWonTwR})irb&P1fZC0-`B@=%8E}ehpOKU zPzYGXxZxSbGQBRs9WmYzpRvE`bQK&baoFBc^F9FNGQORFDb{ z-Vq)@b}*lw1~sK)RW9wb5*od=0Ptp5WZX}LOgFDL%XaLeIB;WPMq`0i0H__i!oV=% zJl3q+T()Z`;dN!?@BGg!9d|ZMwFdwQF*)2Bn*C%IW^&SeDsb`s3;Sd@fcMscEXjF? zPe5>g){Umpai8q1pb+U|LSZ<3zSLXAN zIkB6Y3q@x4?k%T9DN9PSURel?5XuC6U%Vs|S>bfNyqGYoefLf^2VnsEezAODe)V}cD6nh2#${ODbJ>`B zzcH43BKZG(}p=(amT02oSTzXoje(wrjMzI=tm>D%TSQ)fOv_4siqk=mnaD z9mxj~Z+teef47F)Os)c-^qolr&xSd|grX}CJ0z6b?EqC2-&qHAj>OO3pS}U8{_%sx z)v9y-8v+m>#&-?3ie;(%l;4;9kOgLJLnGpRcR=y6hqQ%}^^*Dx!f}q5P$FS#*b_9bNfbpXJIaw63{z$TiggGkl7uR6HzsD(6w#rwL0=_ z{s^9d7Yl%@Q{6^C@673cJFe5Q1qXwp9XYg{)0J$5w!?-F4mG2qob9Gzpd!pHEK=RJ zfd)ZZ+t9Iz{g(6Pr6w1_aWMGd>Od6t2w_?LTTNBdeFAIUs(a1b< zy1Hq>Rt%zz*GJqLouXaGWa#KPsfyWF#^9g@M--yOBCS?ON2_E<2cPWd^U3J*-6yDS zs^R4Hh1?f%_AQ2tsYyk z6QH?aG1NWC4KpcRhA@YovL*0IRrkx+zEp9N5oB~S7I#f4VU&BRr$WAfI+pYbv<4wv zxdw>{mo5?v(=s^P?^ao#?&M;yfIbKk#ZDrRi>Flin%;z)ZSJ?V6|}s4eUsep(Sb zfg)sk=RMGCqUHs~+rK9chBxy{=kti^qdcRHqqSNY-Q71auri~B){#RYLA z#8MtpU_@h~qpMi#T(!7oJStMz{l@*|(uTEgkgdG~0 zod|!2J1bfzPuq_|?!~OD#XA0LKP^14%h`@5apXtZ51$2MU843oALH@jl{;zP+T(V%|xCloRiS7$stpwF@*V z^!HA^bjV8hM1b~!{rjMM7>SvFRd}-zOa$^IW%M}3kwGkDFYO+_A34YvE9fbo#y6-| z!~Fu|crwOS;aI97r&mKQeCMv8zfYlwLxBr zy|Z{>n)LT^!AYib>=hL>kS5E^VVizW*U|Hy+1X-tnm_;h-|SrN6NjiFozomB5o>3t-E$MSB!N_=4AIA=17j05fkBoU#F^5gm zXyErgdM)$KK^y)GK_T+Fp4BL%uOjhy_gsrZG7t$Ky`8XAva#i! z@A0oS0eP#l=u$lO$>(_N4>=owx#uNr~ET8D8`GS4&qaO z3T&nW=np~Sd{S!=7JlJL93`^d{}v@DSeJehH#<|mOGJ#W119Y(!RE376I}%}XQeU2 zL$yR5rIRwUTe=1-m6yAz)Mh}F5;0OWnY1;Gb7DDnG8*Ud>=V%3@?!9y(1m5A2x;i0 zJk0B8)!C_wHaCq#T4s!TsU|INkd`-eN`U;LwZ|}Geq~KHR7zBnnM!Rv60Glit#+xR z#g)vuwa#MFw?Uq4W$2_^+DkgU8IQ1_^M}C9QYs(L1>32>VpP7Ri6#uZ3kSU?e2*O1*M}EPSy_f+tJ_p#j~{Dvk#Gq;mTE5&6nF$ zt#^8JH<`Ir+pP;2OCL!lwp13A3ZZe%w|V@vNpNl>N+m~B;2B;a(>eX1#XK2Pio3OT zW{Ntr;U0#V;ju@sOAG_Elb+^lm2pprSVEb3=ZvGYD(S(#C6Jl!!)zk1I2w8Xh>J7x z7k`bBTEORvq3{W~%dq{@0tuQjZh7L;%_7UcOl_IB$~|X?wZB^sSBdiT`cL_dC8j-= zxo=*C7w;L!e~U@ zE_o-{7NM#f9Xv>~$@cv*<(wK&gOQQ%1Y>L-B@>~xDmDZGeLp6k#xix1s)1^|c&S@x z6&F|>;sN?k-P?aPwJ_8xu7FJTKDspt@4Wt)fQ?6E4}h~2788lfz0qUpiosv#);UXf zZsD?>*IjxIOTh+0HbiQA@sym$0=gAVtMTv^O}ofyW#(L58uU8-@?q5cekN3S$kP=v zV5Hh+Ko&_knVZv;EIveIn(+xil}c1Y$&sIK0+8cD_QrPIaPNX$ttqL>StoM_?w>-= z05g_>idPNR_NUuAOO^jeB!hb>ET#Y<4^)=8*w~d%jT3$-0@cpaJb7ZOIsguDZYahg zCA=HUHGcZ+m&o?;*#4VRq`9`TnR$o5;c4&IS6m|KbD>p(y0j5RUa`wckmeLLwx#LE zY{%W9!NI|S+7}dD&b$35BK{L{@>+#Zi-!g^<~+S^LMQd(q*h}|AwOTSFN$J?pmKp2 zZym79Yrh~98Y&V#XIA=l7xlN{{NfPB*%y7i|A5PPGts=Z)SYFvD*$Cyz7% z2*`f&|0U1gBqPEB&X7s}cY+H(GUEpW*=A-8euHgj^GhxGjVg)E*$PfHy0-WNhthai zBk2Lc)OjSmoakcah||vIagXS|CU7-9{qIPZE&Y|(@mUn;F%$;Y=T0-~g({!!?fwYx z?yhtuS}O&4$K550w+hi!3;jVoMl5i`DPSW$Ysp)gXl0?EbQ$tv>lNm!wI+kYhxQv$ z0W@SpKA#J@c={~+b zHOJystkh=)4~s4qyiqYl$Da78FS@)Lvjembdg38HwXd(hk-yj1h5FMEz!$i;f-+K-nN?qESh2C^S5LhJ}nK7<(I1{_rEYe67yc`zqmy7nb5LZ-O~cXmTA#U&3*IQDFj$TH@#ML5+<*n_cshf2W?YcDCY<|MATtWPrVB}&JuEO$4vIZ3pj zhg2DwM0`D%_2X$iQS_*f5D0rzp7EajyDY3dN!#G28!_8`*^qtt+gk9j)Y&4?R7SX; z0=U&%cI983ct$S?c+(Ri?%X$v^Ibg(8q4kOI22g;sGH$%_bxh zw^^;JLaoF&m+vr%YRlDok!gCgl{2wJF!fD6+e)P5& z{sm%3*oL1%$pr0(M@KnW-g^cG23k)Ssdo52jH?);6}O*p1Y>i|w|~D&=eI#cLsJ-Z z-0TUTDgBZ#0w))?rwMlK1pXHvA3rL7ttaS7$fotK-s|!o$(kBwOvZ~6?HY3`pOg0s zSf22WACPD=NZ+?P{p$+9he!+<=TxDxAZ5s@%RT%QhAwo@>+uTGoX%r5xc2nu?h>Kr zd$T?ADB0q6$Wu7f6@pgL1WT{CTY9_%o`z*O40=ohfAR3}L=olEBVm#|=2#ppH|e*y z@o(C`te_zGQKPA;u70!&!YCAzR)~lg+^|R;eBbDIyH{9STRWAo@`2KGD=(76*kmA{ z!O-ua?({7rhJsAOr&0LT=so)C`tZIr&{Ni@p=iNXj|H<72F7eyPRmlQ)l@8v)Tr=I zCY9g)X*g3h{G=O8@^+UFdd+7wxi#~}kbkp`o|l(bKtO!NN(` z`|2PZ3})IsACv63m{dSHuMxMJq!;;^FB=_0E_^n?l*6XqTx$Y}xiTqkJUKa$e1w}^ z>`t_}ANTpwCB~fN%XXmPF%GA42kEqLgp#SLDftp0*Iext=GZS(7igA#_PbkJ1A#!Z z<@!TMj}Lc#z^KDUGS)nIvbH>do};0n3Ne`}C@QYBdb*gJ(kndyo$DVM=p^G|FVU%E z$*H$ph@XnzfPy8>vF#UY_t+bjH(f)s#l1yV`2%}N!osQ&-@~kDO5%k49(~BYe0?7c zAJ2#m=$ps3MZW#JnALSq{k64adSnJ0PE%D?&5Z4GL7q8#ygSD8f=>OE`T+#i;_>lq z{X_HG_v|AIq8E{|50d~pQ}^y{n% z_QaHIry3ir%v;0=O!}faVN)U+aEFeE&js>HtQD=*iWve|WS$!#L@|`&gQ?O|>nwTs z`G1hWCdEqzZC*~W*+04oT_)WN9z0ertwm{Q0+8wqJA7;RR@=NS#|t!4}5`a&_t!bB#b_hsdYs|Vo;G7N`tfOtWw-Z>;rEbVJSKT#yj$&OgRGg! z?ed)}x`>Fs$NELOL~Q4|3VDT+t$|bzLc%(lEI^CUYjQSY)e6Ao3TV8X)m2qqCO1!> zRC@#h$kKju&F{P=XS*kbB`!^Z&GRcK|EOt)k|j9O?P!^VeDV+ z;QZ{&z-y18)ML)T+YT6lY_EUJCA(Bl@E=3BYQfFooAg=w3?8Yx3rt;nI*32K_nAXLf&^76L&xDC0g@7ZWDDs zs&zgiKB-kf2z1_+Sef+Ik&W3NO5r`@)*#m9i&`{S`T5U--V&1%Y&Mkqa5lv0a|q+<>02mNy0( zL}%CA%*CiCy3CeoC!jZ)@Fkvq-Cgy)vuo7?l5Y2;QD>R(Dmj4;k3MfM_#iT;6LU4$ z$bSQMNA48+6(#M-Izr6Pz)@4+41zUXrTPlw7jWFcK#dGk!~I9Tg{E!GFU zR7>4ta`&V@$3h*rh`epx`<+pid8D9Qs6;^;SZ|_dN?f_m%wW+d_(w5%X(ChFON42*#=$tsf+cmZQjdMCziyyLOl>4m`%|||xOKr2xiHYqM zmjNjb`~*JHqs1U_qTK(c#)mWo-NR>IAEEAh$R zC!I*)(c}_7sbTCKlc?=2uZv|ugi#PlOq583x`8=9+}EGoN%~}xu1J&4(!^L5_qc8piWZk`sJi3Ub4-1-8*{mQc`M}U+3fQ!G|pOq z15|}SqiMW9J_PPN*9@08?la~1#NWg|{EJGAYMkYOZg&PD?M=@6k*1(97*TOR#CZz& z@{BvRMuQ;dwlaTr{{$;5M6h}h~;qSfF##fQ+f<4Q^9c__+Oc45R3$wC+mE z6<%p&s*t&wd@88wrL8P=1vMum1{(EJkmXj1vE@9N z$UMAlF_Nx1SsaGucX!xO#7-H@LtQZ|zG!G(Y4E0(Ws!4`{e)R?lpvb@cPN2bem=%z zS{I%bO&EBUfHOSfqu1$96i%QedU?$+ska09V2N9xAgiTaNv9C(^Guv-94<95X$64m z-H15!`fur_;)Y%o)86f~4yz7o=?qT{adr$RZkLpn=Vfr4@<>++bq4T>v?v+5n?%0U z<_Lg{k5De*<3qu|&Z^6sfHG$i4v*Af9quW*Z^V)o=w=f>I$U z?(kt5b0yy1Tu~_1vP7Z;jT2Nl-b>|T`W?*e&%iyu(&MtD^2H5uG&=vvjWjw3|f@6_L*j{ zY!)}p-W|n}ejTTcBk1BuCbaO^6clg=6T#lupms@xq%<&ML}7S1dRLAvQVWa}en!rI zSvfQvOXe;n{IjMvl1TRz`)VMArH$p!``|rd^1OneWrh zu8CbCzj--l2tHL-R4iDPvzq#ifeWjq=hz?ImI~Ft@yX@V5WNnYa=pi2sip0GopKnS z{{igwpMlKltEO~$(+BFKu_kcPoebb(NKT8_H8Kk=Qd6+8RYzqdx3WF9G+8>Vq9a5& z>UerQg+lV1YV$9R;ebxE_(Mr|EoL?A6PDmY6N*u|5uqZ4#XdBI%vU!UU$H&zNm+T5 zIp~G>HkDr!HvkPQ{<7}lrpbJr`AB*dxu%$Iv5UI{HvW{2^e7gXbPVU>+Aw0Zy(d#d z`c}K$C9~heGc~-ijO~2wID^(mNjzAEu01tif}0{uiZz@wWAg8MzQ3zjp(`j&E_JGh z>gTWEI@?4q#6H~VK~>GBH`5W>4s8^>xS)U2{?P8u)@%m(i~dth-V~q?#|||+2e(L# zDqV_x1vdQYMf10L$zkV0Ns#RTblcCl&|w9e0Pnoy%+>9@D>)|}O*eJl#gMZUg+hY% zfDeJh2#UkxL>7nD3C8tWL{3lS+4c0v(zeGt4|0tm*j;{=zNV-r;v9t~X7_Q-4#*9@ zrP9bWdj8~s^+xwt4fFD5SqF)9f<9vO7iuP};BN~mg|Nm>#&`}!xhX?ky zwzq{Jf4ZC85g2oA2l!H`Ak)Yj-CZ4aJl@(E8ylx^=$9oXv3{|iuk4(ERI-wnk2qz2 ze)-$1uCA{BemR>ZwZn^xi~IX~z^O3N(`yu~^}?}3ZaRQF z9Q5Dv#jv{V792~ro3|`1B~m{yXG%2b%+*@l z?4T}N&-s!D$jiJ%3^IXQjU_e5mKw$@3L+wiy|c5kt84A4@bhFjDoRTS=O4S>CVp*T zW_r$=jZZ)`+Oc|oa$ib57I+MPw)A>`15H7oz)LfCsKtL$k)NBJyN`21m*M|6&Hm54 zW;3fkM=|5t7R!m>z!Ou=rt!mvZP z!TyhK112ge&vx{G!X3|(i7YC5+v+x!Esm1$DM#V~@P|==>ni|!osp5z@B8OP5sU_s zlET?tsIj11R7lr|_^RjO?)vHee5}TN z1XPWLMY&vOGuPNC1l9e|M3GLDsMePXh>UJ-0o~O~H~^N9Fo%--^VGkAfT?ms-R*%v zi#pUzER!uaXcLi;@C9V8*Yirgj|0YUdwZK^oK80CugzSA;S(SqW{Dhu)#;}VScxa4 zOd)P@af*~b6|KNXbxZ+9A6VCv;$HNOjHHtP!95(6?d(?fxZhpW0O<2S?tVAI;au5$=Z1f+lJV>8m9wA8 zPBwoFrl3NE0B}9#?o@$H?Dj$AKlhJ)T7a8YAOax9ppjE9-1B&l5goGET-G^3wY4mpZv$}K4zTK&KHnzFFbTkhU`H2V2_ za)np+LeVN7e}%D0UQN^RNn3y>_=G_l=}bFXuVueqYRx=`JkibW?$Mq6 z`T-OwcxPOY>HprMyCb=ejx8$Egj}}&{qe5BBIkY~r}s%Cq51Tch~V)z1MMd{OV1T& z1Ok3X;b?inE(H&T#+=)osJy%`GOxM~?Ft++vzM`L!XG}{t@3_bZI($}qUsRHpT6Jr z7G{dZe~RU7#j=fBG%IFLi5Ld|O4|NK+P`ejuO7M1u_&o`o$0Vq?0Evxk--ke$E zfO@A6la?Os<~(5R%_LC==3jl$r%WWy&_Ff8TsbRO!ANPFS5 zJfUxI1Ocncs)@k-*M;b#Y@#C+ z-Y;$r8OAJxD~p1g?ee$N74}WuEs4P8q>H_QylmYGAo#`#Q&j_eeEfsOI-ARXGd8nj zMnU?x*9QyGREX6a##W}pcQ1K)c_470SNK#^R7ODUTu(dlm zIRThM#%`q8-x}|06M&sowm!b$J4_xQ2Lw+pi%%gTtJFxG&~mZH8oU`{v;$PZCeJ+CG8gaQ58Q1}fr>b%uoTG8J8rNRK1mYA3r##=ot zEsy>_V2A1eq$HrL-1jCK`1zCW%~Ty}>CuQdTq_pc0A@!l;B~RPp`dtv^kp@Ry0*jS z{GZNUp~D{^Bh>G45ZcD~J9 zFo9GZQLE7;LZffEfX8XG2S_0FW^& z+VYP8P|GIRFpvXpwEgjRKU^9R40I!64rL^`UgoM!=dcllWVZL>mLtz=VLP~1!oIqZ zdq)G%vZ7=ZqHbHZN@nv$`Jeyj^fAHzWh6i_du=Jd9M9huRpxaqvl}7tbk`6;(y|ok z1ncGA7J1Q@e(lr^Sty-QEQG2YI<()wB%f}Si@&6D8G$}h0Y!W`5#w{_n%~oRC{6p* z;~jwds29Yvq(af(yC^j8PUL@hW}#}DWV1j)^f)yZ2*skZZ`;^nRymC1Xzp(x${!;i zZ?o>+$?<#K_(7%i7|)Z~cJ_dj-7H|Yxa>Nmc=emR%kbyix`}<$HsL0{B_E=FPZ$no z+XXJ!L0?bLGN8*%-`)*N2nsfMzyMWuz3L4P2%r=PNcH9iCuAg2!wotJ1wL-Nw^$k1 zwbp#7WMJtuN_nCOpSwd(l@!i5-mCH~O?>jtZAvYH!yj?DFF(ycmGYodJnR7VQ+y}W zW&Yt5LX{97-xo!4elew7;})e=!{`W&7Q~&|oC3-xwB@*6K|w)^VF9Dc>A3CMIM1q; zDcd7Yr_P$ZOPGYqp>YFoSjax5#%{jyC!iY3a5_%=$ci=+lqL3ecPXnNns4!@i>Sg;pO_Qn9gU^)x*}a$|nnF z{rU6fifv+|(bd7i@8R4IF(sw&sI?2dp@pvu1L_6x1Q7HnY%x&Z7kIxDQK+}`@LGqrpWG#s-2u7y`)XY1gFECKhSj)u>)|0|Z9G)zA_q9zA&ys@)EAj?wauuWD9&?+*K^tasnf)K}KFu$5p~l9y*98k?G>$X3DP`)1XI zg@t31o+ds}sdg_5ZP58NLp>wbgxnQnFo?PQPv&^X#9X%e<2IRxQaCNxOu0;yx~$2_2MyK*{dufH)_sV;Uc%$N=-U;S; zFfPtcw~NdyHqhUT#@QO1n!5XIv5#?icFx5o2plXbL{Be3)8v(vSTca3saspJoNMr> zHfm54FNoG8e$zO<1TjRm_jSkz(cZ?1H(ogWpbt9r2V;`KegD`w$n^iFSHb;HHgu!y zXi_;8hlwhJquC7*r89dCGw+(e?WdkOI%8@#IWtUxxjXKTT9hwCeyWG`$u?yUuBW~z z3`ehuJp=S^CBP$np)S~pww&KIlZWvMk}N(MoAr5F%wjB0Yktv}LHcWtCk+x0r?6XY zoJe$pMt%Im4}Z(gKNFEepxilKGb;@b8^A*y3JUlMqX+B|t%!0Qv78X<(6GAG?l?^- zVU(oAqCv?fgcL_*ym0q8YLmf?x>gRcI*k`WN~(j}wQm@Vk*%p-4rkH8#g|TD6>+0> zILB1aHyUprmOr&(eV8u8p^7w3|;r-j7pXkazs%R&4HuD_{f8F26;IXRSt}P9r16uJsiT@vwd@282fl)=B0Ie7-@KEJ6<@ zTH1v(cSoJ_#Ngnvnr|}lMt3EKEnqEa&{bi6$l08Jy^MAVmabO(8?lFjN4F_cOFE^N zBB&&)1HomZHQ3k4^`#mn?M9TNHuBM#epx*GWQ5VNMi;ai6DLIzOx zB=KonMp2g9-ZrF5K9Sl&3B2T#3xggYN|S864`dK@%EIUGR@^2gDMtR5C^X;1g2l0n z)s_prvDO~T_QRnxD1X2XnK9&^FYU z=~U1ctzND8)j&rwyioN;gF67NbV(P_f^p^7_?43L9*&3WGvWSy(>t`D8m*t#Fg#8Y zQSY#(gjz1f$~rVp$d!fK=K?kVc*5rxVWZ29CwMc!<+`g<8erdc`I$bxvYP+QspBe1 ztMm*d#Kh{4v@`DQ2Q8ounX_6O-uHE$sa%M+*6_;_DOY{Xco$L71gMJ){#f_p)vR5C zq`q(57#DW`GMg|HUO_6DTf{!U#un27=ph^q1*>YdQaGXS+p!AFGZe|`VK!0gk|>8E z=XS*~vu)%qiivRpTCe#SUvLLof~UQ!@Yu;j0PVUQz1Vvq{DA*}Qa4Gv(dEO%eK8Hq z21KBovqY~{ZRhEHh@sFT;-!>8~XZ=0LPCj+kZVsU60I4 z7WTGe=zf>8v~gD8CyM-IKOID1Kghe6#-9{>1HWEuRI`RdD`|^8Qo)Milbf`;gy3e0buGjn|m2xUGOca}e z(IpQTh+_JyD*Qc?qNdjbn%d=Yd=XD<-n&%w`GeTgzQ5I6YZL4ocNJl1DW${oBK$j9 z5HtfwbLW%0wX4Um-IKr-&zlLHB&FbK2lczQlxX+dQiBtoA>*;>xm67jF zEoKhYc1r><|6W7=vxdCnwzEU**A{kDPI4UCYVC<%^3j;I*n8ftqbzS3Aw!%b?@zja z+kPzxTTgnpLn`yZy=A>z(N@M;y7SBtIl>br13TnU&F0#4)@z4gG|Sd0D$9Fu)p8ws zsCtZt=gZJc^x0@ROeeh|ZEUo@V;Tm1qE@*YZ0Ppi;%i{frhSdgL;^$x)X4!`Fr}(k z-^fdT(5K;F2Ro+81A)(8e<(F4+=`@w>X_MwWts7V^ zLaO)tAHW^(w=9VLtdZggjEc8_*w8np#`c<06wHdR#Fg%qz!Ro_`^DNk&oPQ*mJCUv z`@D;S2(L~MatY=Lq0j2}V?@w88W2$Phf?pT^gwOBjWH`{3Qpv$T zj%Y*w8Y@epH%PZb%ekbod=@B@0|`tGfHL{D&X0rJDp2y>D@6zoko}7PvhUwaD^*>7 zdSko|6Ia|%YCK#2(%@5F^i3gZM6>^YTZ=@rE5oESd`ovQrFbk1kgoDcEZP-`#7k)O zf34|l_J}>HyzS7&46FTiVCpcl^oU--V6)|LA5^`=W6FGBVUM+~WOamgu22q{QnOTj(dL}d%7s}1K}-5|;)80~f1&~@^m zOmuEh&_i=kz!0-$HTEjBm(%xr(9pAix|PcaR-&ovhP1neYIGScWd1Zd;^p?Xw1K5U z7c(~xk)u#`0OS!gzdGiv3IGw12YGOd5A8m1fJTYzf8?J3Cy;s}>j<*F01oB=r#_H7 zEOy$Z!e{b=rqpp$%CBd01c(4y_CC36X`w__%`szIXTRZ?_xXfQ_ORW;&hHsak#xgY zvGux@H3{JO3kWVabJt$Elkp>vt_!r*R5-T20gVRQm4ua8^!Z+_o6*qJORjNo<$JE3 zt5;A;i~@Y%L_y{71<45K@pR&0Y+;UX`cs`{s^uq@9V05u2xM``UCZsFlfk5myIX)Z zWFIWMTh3OqtM8Z_+^$=I4W^o&Wc(gGTXXaBXuW^m2jV$IWwm`h#3bZ!-ys(OA{_+x zSWo;RWvC+cT+2NVkDZ@-B0@lTdMd6#JOEBpAwF-HuMmoNE1AAZ7P0_W5I)K%Ntc7Z 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 zic&UZ{qWGrlCR}7H;!Mc!}nQT&u1n_3_%$iuSKD!yW5>rW%uS_Mh0#M%4xVotz3)E zYE~5#mK%as{fkyC)(`xA8%F3UNohFDYB?=Q#A1-riO)!LFk2qwxk;Qh;*^*OHxjK| zV=SetjJ9Lr=vd=%U)gqpi+-^YMkM~&{&>Qp8ciO+6&i6nKo*zfyhpH5(edJG$QFK7 z-ncq6BGDR0$Twt7OHG|K5`)L#n2ajEGnys=Ev-M;r=F#8B*$xI${ZL~Ky0k3shOvp zsQ;NpTI6c5C&%S{Q;fPYI`qse(?`A@MDEua@Ma^XS*%#?E5%~tA z`ZFZR1u#VA>}4~eQ3dOovP2VWzcYA}T?}qdR&bzMVQZq`4`r<9zO}^b5jl0ApkSf#LL2T+Hy#bq>8a|a}?UE z;1x#w7=Lg3u48T}S}Yciqe3VMdW`>(RtULHL_zCgoJEzz6h&-(bu=|O1qE6MhKGZEU^N?`g=mYe^8m)2fFJxrAN;rpoCGSBu4Gk?X2Cydx%N1{}Te(XdpMbsDU|B-PN_B*dd(CN&<2Q zkH?i3e7e+#T;5gmv+Vn)E#_imbC5TZ34WADVz06@w_z|bso#$Jirbn`6YQsCsy@pe zEjd=K69g=J;`V$ilWXBn7|T+0xe8EoeIKEjobd9*vifI_5R!nrNzvXYNmdNld58yp z3{@gvt%YGARhr*lY}2~uT~5dGKYRioBG$cWWLe2bzYDozO`udTb)PO(M@IHH@vrZ_ z;uNkA;PX(8DsJ?;wdLR0-DT0bf-VShD6(hS6cG_Y#WJb;nG$)s<|j}=KKy-9x=#iK zUfJu1o3U$9XG{-<-0ERA`<*FJ8C?zAn=GpA46TdwHO5Zc)+smp!M~Z?Vlod)&)Z>V zBsIL)VzS-pBPS;|4Ppl#a!XlG6^=s=(@@nwQP546CdL=L3?Ye93-qb9r)5}@EVeqj zh|St68lxgeNwYYw#X5^m2-2!86x`npFtnCPp^*LO`Eb=|$(`|UNiU-0Q_=n=jmw;~ zQVl50=4*)~^KY!mWuGO{*(@eSDJS1zl+i8C^@f;BmE9E;eR7<{orh5UA#bM2xJFAq zAydPQB;X_H9$9a{)L^q$>EU}48>7eVK6)`oY%n3#>!O#4gbqjl{)C|it~78)x6nST zT(Ey@9GjyW)i7!doD9?w%xjVi0^#EUmf||}NKWRda8vu}e(9^uoKJ75G&nFLWMu+B zTANYjrUgv|-#JR0Z2{!Koo4yf0)%tNH0diO5d-QO9!xxU)5ttd-ESyWu)e(m>9>d_GD8{tU}ex8Erh5qNVS(ZLQGLvnz4n9=gu4-EV57 zd-B)!&+H^w+srg=Z6X@><aHFeE*a_y8P|BwhI~PO`zlpT=l=j#DPXAM#I~%h}G@ zNcByrsT3**u>AV(4bo7CK86l|=%bkIAqePE5?f#*i5Iq(jmlAUW#@88fb&S}2nd_a zHN3kv$3NGRr4!D9#7U`}Z@%MP?h}81o)M4uac0?j2fHv0sb{epQiL=77g|06yY)OE z0w>-Y$Gb>s=N6z{J;LG23T*xm9b{o@WTkx!Fwq?_H*kY6`j#Iq6ln0-)+ULGF~t7>D(J?5 ze5w+fYKw;&k^1U;K|xpba?%8xe%*MjwFTp(T7^z=G#ScLs-*_4`W4nAQE#q2{Qg^Z z;j6Ux4`SYWYvC?|g`e7_h?_8@A>6hYw$b{hT6IQRCl{n!tB^V;(h}J94~g1X8ZoQ= z89P&z)GnPcjqsC17e=QXyfpEYXfB<|14aNfrl*>z>WtCslLpi~coJyn76(-L{O_h-zCT7*Ah|56;vg05c%t zkAKRNla74vDhOCSERk0m(bb2tfe9EzCebD+Aru`%RNMdGL%|p4H#h9yY`3$^%ggI) z5GcT#CyN+Xm@7Cq7!jBC6FGU`6;E}1eEb8YhTF-q#kI^C4aV#3f-jh`5exL#CQAtL z@U#K(86F-!Jv|NN?@utK*=WG?9HPKQL`H6|&sQ5=?vClTy3+Ln!c(-q(RRi2&j4!h z5~=Sd_TK@Nt?%zI{Peg4q&y@PRQB)v{e37XsFhZ?xY*d(s3;ylRlnz0_7AjMsOjt4 zX+V1&dzJLz?7QL$=Kp2^_ zf6Xh#&B;ou&@cIzVQ8MTYocO%9x zbMRv|UX8QKWQ~c8s@K>LzQ@~Bj>c9lmnk`pdb_pHASgozz))(y%m7;Hvfbx#b8~a# z_yo}U0q+PIO8&#$Bpjg063Grv%47Lodqf`U4X z>~AOXmzS5K8{q6ZODiZ2&Dq}esmaN^S3O@=M!RkQZ&(RwX;!vw+Uex; znOX5*U_de!uxlDx^`)49n`!h-GI>3e!PTvf%>PbxI$7r0P8`rC|G#-axc#d)#{Pey zg8$(mweaX@@10aLTn^i^{|jWTOUIqV2X;0F8kcBZ2gLnk&`0_)M`OimLQ1W-^^zN zvL=8+wfnq6xAj*X5g2et)ZkPo96NhDKCO>>Lc(JX}0H9;Y4I!iNO6f9F~+H%;g>(&{uZl9P*Z z;rtB*wRsCsF7k`yg#TH*0O*9Cn!4hY<}d(=c+2(O&l;*_M={SB>}<77Mr0m*IshXR zW4+)_)y#WszUkcWt`ZD-Wj#973qxN6ijEB%bIinz3$nCUU%PWB1V^kHvYIk5i_2m$1aMNF*Ei~)l(gTd2qBc#tE z$guR7y%ty@oaMSq(^Ycr<5Y*tHq=EwP z2ck=r&4Lv8&&r^Zbeo+RQA^*+Km$?N7|jIRg=r#1U!ie4)V>!pI;y#Na#eo zMCa_hp1&;+C8c%16D@?49JJ`zk|79h|1THV8c6@=^T7Q1vn+@%9G87^YS?Y~XpsGa zbC68pJ>K^Iwu&vti9UVp{+X6s2NlD@xXYsmRH|ldY@90|!znK(x6}DGFvJD!kB5t9kMa*Dh`i~ghxOqH3@#lulA&+pfI+#w+CPyj1M1r`}@KDurS_b zdL1t@9IJa9N^EPvmVO%)j^97D%7YkUZ8Wh~!Lc>Jxo5~IZ8X|N$@SnkiW^vB(2pD9- zp8!gUciFZ->d>iL6Z@kIV)#u?0T~*a>@r(S$i>IB>I#~QBT(y}F=&m4f^mAjo)frR z4+5%_W-x%S$;_)A+c+Dg4oG9BE`2@-de9>eQZIvh(@N%0OwKD;Yv8WbPf{ zbMKF8#{fRkIx;d6g~fp8&yI~Kn*aRtWI0#yJT8J9gUw_(_?7(e=9tV6;E~NHh9v-d zSZc6L)2KG=Yr9=-HJ`}IA$3TjX1rwd6!EuYuI0$BSp&d$xn=uF+At^ZDu(i7`R}Gm|!WWMsef z02m6{6h&oa!$(X2Lp}8q`C0$Cn3Q3VOJk1-2_T37s9p)Ypa}p{X;PYX0L_bzk3WJ< zO4lA2oO8H)HcYr%gPEFY)F94OLvs2Jj(taDeI0bu1{%M=RPqV|fO^meX zym8I9)?sFpV=dZ`+e$69H3P%Wdp8?^t#&`pp}iD|e6He8aLd@qImvrzV6`g&J~#Z0 zF1l{2P$73(V_6pE23fnodZApKqh=xvyAOkk@#9Cxv>IpgkR02VeF`K)L&I=e?dj}P zR8p2963#saN*}8tm_-ve1kP;u+DZi0U>FQBHu|#D)YWdjt}PuM^wYBBhx-N_{cV^r z#bd9NX0!FqfbG>lVs*_zOj?aBYUs`Dhr>!5o`82rDJcUK(33X+dCh88tG(Wv=oubH z=*iZZQ4^OV5rE0`d%1U+(=9xLDY0p;ZV`51$*&yz(-{*RgH@(IW!2 z>U}AvGWV%Vv&+>Wu^qs=kWwwK$I`0C0MsmNiW7L@BpS=$;qZNOXU^)wphXcWao+jP zW>xd8mXR@6&*yq(Qr#hh-%_MM z>}5pDbp~zodEKp~ZEd`C40-!N&WO}36ySe4>j8Kw*^O$WtuET`SSA>tNi`J*wW6(G z;2xy>dZ+usv|5}k6cte%K0Qm1M=`Vy_;$CRwhvb`yG||vp7r^54H=I;x-GnM)f2%3 zKzVja8S{Yn8%m;afZDqTz%A95cK(iwATFWDpLjrp0lv!V<~Psj(*-bxcbf==32|i_ zb^i2`rLukP;Fm|6rG{7DIzO#;6BOfKzEZ^AAhKUhh`?xhadBa2WMtQR%x=OerBQlC z~Co++JKu=HMktk-r4Wyl5x_7goFfF>3JQqJ4oQ3<~;lXPCET>Z*6XvvgdamJ7b`sgV6@{2`^DyRwACyab^7U&|@E?I2g5MXPxYO0$d3Pt5#X5-i zJr4{kh9qUEhPYiw3eS|=w?S^p4lsv!T@=gcHpW!BZ*z9sR$Q##17`z@AAvooF&W+?!^DdGi7YXg(FF2YCcTwrYcu>@`ucg}saE`dP^cf*E-$w z9WyY6MA!AA$Syji&XKDaohicJ*Hp?WImUvUns8@rXITz%05Zs9(U&ITf9l{^kkKoJ zOb$ei;ztoQqGgJb3lhayJ0SD!*B4=>Znp_yj zk4e4AlFUE88tTc`I~-D7jZYV-k}J270zYb|*FA|>54uQOq0#-`nJ*U)M@#8m#x-qX zpRM}6$CB3g%mP^uJ@vIBAIwBhAd+vO;mFOZM3wTiv{j(nv98sa%lxT<`;hkVL1W^w2GpG zQck88oVRk*dS$OJ(O}kU&5$85H(zwjue$;m^O=8f1N&+PYn^l%Ib9UQAIp1U_xO#1 z-swP)u9QJ=DjS9A% zmVifg*s0lkgI%FZ%*cCl`OuDP;|*C)x9$$wvC3&Uq<`i_-hawMz2)aPQ5GHDb>{Z|%A zO{@kwUN*}2VbZ?3Z$@U-sKTYp#VF=&`*E`9@gF&Rn3zyzF)sA)&^st54O{AGte}#~ z`oe2qQq&4=30UXx3(m{BRw#!oe+25QDqFx)@Ei7ir?5ZPWYW$5FRyC zlKb79F7P3E6P0Yu6}c*j7lfIWcdzVDTX7{_o8``XVyj@>t1<4de|`eIE#0IIZ*q)E zk~E;;aA@-9Jj_Zh`|#Ev&zWRXBG4%pC5yF6N~WL?G}0Wz`=fD;$dDTddd8mGXqVQ# ziFn<|d9@7jWYd8iKVaifo-0@yKv{UK$TxeKYDQ*`zPDB1M-M!EF}CFgdBLuWAyQal zKF%kbardr|+FvL2H`Ek4?GI#18=TOvuy!R+k0OiRx6+6|Sr~LA%A-OO%TG4i>3MVJ zsrZ#9!ttZUnTEY1X)lwC;gRvr-@G5xInm>t#|jITj+B`O>KEaOZ7K2#&K7?*yuybM z)NRR%S&&_#`*ZlmFPpiFeEN;4HpmTKDCVU&Ks*BUB@YnbLLh8*Q^w4NpPJQ%V$G>Q zL2m#|Ip^JQ@)5%_7tTIlqtrT_Exa5dz=;Um=(^l`JDF^Y?#BIFpvLr)*%EZ(H5Qj7 zz*ls|SA|`L#Ajt?0ZA(gfS)MYYRSligyP(I`x_SinJwmx_+m^dEM~$LQXF5RbYyiUa^!yx=o#xwu2xZp44eT(ML{@d!Q`$xr zuiZQp8IU!OrDc~uWrW849&XX!;;>EMjpyOJ`T*inyRlowU;0@f)EZu7klQepRuN2- zvn-BTf-@+d$f86gfQh&w{?oi@Bel+ikI>||$J|rWZ3~2ZuR$C19~0Bw->J&BFVx<~ zSS2Ngj?FQ&ut|_Px|-eZMUlH7SAJ`_O{~Ei+`{ShkX1CD)Jbp-Jk+sgTN`3O3-6nFz2nx zquV0~oYKFxMvJ|{XPyv=9`Q}O14F!d0|w(w*ogw)S3$of(06aBT|z;+$RD_XmN!yj 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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 diff --git a/webview-ui/src/components/chat/selectorConstants.ts b/webview-ui/src/components/chat/selectorConstants.ts new file mode 100644 index 0000000000..af82a9426c --- /dev/null +++ b/webview-ui/src/components/chat/selectorConstants.ts @@ -0,0 +1 @@ +export const SEARCH_THRESHOLD = 6 diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 687d9962a3..9c26d8ff58 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Selecciona el mode d'interacció", "selectApiConfig": "Seleccioneu la configuració de l'API", + "selectModel": "Seleccioneu el model", + "selectModelUnsupported": "La selecció de model no està disponible per a aquest proveïdor aquí. Feu clic per obrir la 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/ca/common.json b/webview-ui/src/i18n/locales/ca/common.json index 56e9a3745e..64e4dad906 100644 --- a/webview-ui/src/i18n/locales/ca/common.json +++ b/webview-ui/src/i18n/locales/ca/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "S’està carregant...", + "clear_search": "Esborra la cerca", "search_placeholder": "Cerca...", "no_results": "No s'han trobat resultats" }, diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index e862fa9564..48ba0aae8e 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Interaktionsmodus auswählen", "selectApiConfig": "API-Konfiguration auswählen", + "selectModel": "Modell auswählen", + "selectModelUnsupported": "Die Modellauswahl ist für diesen Anbieter hier nicht verfügbar. Klicken Sie, um die Einstellungen zu öffnen.", "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/de/common.json b/webview-ui/src/i18n/locales/de/common.json index ab8bd6d240..a1b382c1d2 100644 --- a/webview-ui/src/i18n/locales/de/common.json +++ b/webview-ui/src/i18n/locales/de/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Wird geladen...", + "clear_search": "Suche löschen", "search_placeholder": "Suchen...", "no_results": "Keine Ergebnisse gefunden" }, diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index cc02656a30..59d3286268 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -140,6 +140,8 @@ }, "selectMode": "Select mode for interaction", "selectApiConfig": "Select API configuration", + "selectModel": "Select model", + "selectModelUnsupported": "Model selection isn't available for this provider here. Click to open settings.", "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", diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index 981eaeec75..f3179c47b9 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Loading...", + "clear_search": "Clear search", "search_placeholder": "Search...", "no_results": "No results found" }, diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 937cb0cf56..ee75465116 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Seleccionar modo de interacción", "selectApiConfig": "Seleccionar configuración de API", + "selectModel": "Seleccionar modelo", + "selectModelUnsupported": "La selección de modelo no está disponible para este proveedor aquí. Haga clic para abrir la configuración.", "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/es/common.json b/webview-ui/src/i18n/locales/es/common.json index 03455b7cad..ea27b02733 100644 --- a/webview-ui/src/i18n/locales/es/common.json +++ b/webview-ui/src/i18n/locales/es/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Cargando...", + "clear_search": "Borrar búsqueda", "search_placeholder": "Buscar...", "no_results": "No se encontraron resultados" }, diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 4656d618a7..efee60fe65 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Sélectionner le mode d'interaction", "selectApiConfig": "Sélectionner la configuration de l'API", + "selectModel": "Sélectionner le modèle", + "selectModelUnsupported": "La sélection du modèle n'est pas disponible pour ce fournisseur ici. Cliquez pour ouvrir 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/fr/common.json b/webview-ui/src/i18n/locales/fr/common.json index def93ad6c5..5df1530142 100644 --- a/webview-ui/src/i18n/locales/fr/common.json +++ b/webview-ui/src/i18n/locales/fr/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Chargement...", + "clear_search": "Effacer la recherche", "search_placeholder": "Rechercher...", "no_results": "Aucun résultat trouvé" }, diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index c36bca4e0f..20d07c20ab 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "इंटरैक्शन मोड चुनें", "selectApiConfig": "एपीआई कॉन्फ़िगरेशन का चयन करें", + "selectModel": "मॉडल चुनें", + "selectModelUnsupported": "इस प्रदाता के लिए यहां मॉडल चयन उपलब्ध नहीं है। सेटिंग्स खोलने के लिए क्लिक करें।", "lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें", "unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)", "enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ", diff --git a/webview-ui/src/i18n/locales/hi/common.json b/webview-ui/src/i18n/locales/hi/common.json index 076530e6b0..c531a42d25 100644 --- a/webview-ui/src/i18n/locales/hi/common.json +++ b/webview-ui/src/i18n/locales/hi/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "लोड हो रहा है...", + "clear_search": "खोज साफ़ करें", "search_placeholder": "खोजें...", "no_results": "कोई परिणाम नहीं मिला" }, diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index d8a642b8ee..bba99250dd 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -143,6 +143,8 @@ }, "selectMode": "Pilih mode untuk interaksi", "selectApiConfig": "Pilih konfigurasi API", + "selectModel": "Pilih model", + "selectModelUnsupported": "Pemilihan model tidak tersedia untuk penyedia ini di sini. Klik untuk membuka 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/id/common.json b/webview-ui/src/i18n/locales/id/common.json index a65295f28d..cb5db3153b 100644 --- a/webview-ui/src/i18n/locales/id/common.json +++ b/webview-ui/src/i18n/locales/id/common.json @@ -20,6 +20,8 @@ "billion_suffix": "m" }, "ui": { + "loading": "Memuat...", + "clear_search": "Hapus pencarian", "search_placeholder": "Cari...", "no_results": "Tidak ada hasil yang ditemukan" }, diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 6ce80b2316..02560afdd9 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Seleziona modalità di interazione", "selectApiConfig": "Seleziona la configurazione API", + "selectModel": "Seleziona modello", + "selectModelUnsupported": "La selezione del modello non è disponibile per questo provider qui. Fai clic per aprire le 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/it/common.json b/webview-ui/src/i18n/locales/it/common.json index 9b801628f4..f6768c9835 100644 --- a/webview-ui/src/i18n/locales/it/common.json +++ b/webview-ui/src/i18n/locales/it/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Caricamento...", + "clear_search": "Cancella ricerca", "search_placeholder": "Cerca...", "no_results": "Nessun risultato trovato" }, diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index f9e535ee89..428d63407d 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "対話モードを選択", "selectApiConfig": "API構成を選択", + "selectModel": "モデルを選択", + "selectModelUnsupported": "このプロバイダーではここでモデルを選択できません。クリックして設定を開きます。", "lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック", "unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)", "enhancePrompt": "追加コンテキストでプロンプトを強化", diff --git a/webview-ui/src/i18n/locales/ja/common.json b/webview-ui/src/i18n/locales/ja/common.json index b3b9d462e0..35b1837e75 100644 --- a/webview-ui/src/i18n/locales/ja/common.json +++ b/webview-ui/src/i18n/locales/ja/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "読み込み中...", + "clear_search": "検索をクリア", "search_placeholder": "検索...", "no_results": "結果が見つかりません" }, diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index a633602e26..358444b9c5 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "상호작용 모드 선택", "selectApiConfig": "API 구성 선택", + "selectModel": "모델 선택", + "selectModelUnsupported": "이 공급자는 여기서 모델을 선택할 수 없습니다. 클릭하면 설정으로 이동합니다.", "lockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성 잠금", "unlockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성이 잠겨 있습니다 (클릭하여 해제)", "enhancePrompt": "추가 컨텍스트로 프롬프트 향상", diff --git a/webview-ui/src/i18n/locales/ko/common.json b/webview-ui/src/i18n/locales/ko/common.json index d7120e2520..1e05c74c76 100644 --- a/webview-ui/src/i18n/locales/ko/common.json +++ b/webview-ui/src/i18n/locales/ko/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "불러오는 중...", + "clear_search": "검색어 지우기", "search_placeholder": "검색...", "no_results": "결과를 찾을 수 없습니다" }, diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 67407b6b7b..6c7605665c 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Selecteer modus voor interactie", "selectApiConfig": "Selecteer API-configuratie", + "selectModel": "Selecteer model", + "selectModelUnsupported": "Modelselectie is hier niet beschikbaar voor deze provider. Klik om instellingen te openen.", "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/nl/common.json b/webview-ui/src/i18n/locales/nl/common.json index ec6cf89ccb..3e814618b1 100644 --- a/webview-ui/src/i18n/locales/nl/common.json +++ b/webview-ui/src/i18n/locales/nl/common.json @@ -20,6 +20,8 @@ "billion_suffix": "mrd" }, "ui": { + "loading": "Laden...", + "clear_search": "Zoekopdracht wissen", "search_placeholder": "Zoeken...", "no_results": "Geen resultaten gevonden" }, diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index f7f88dc87a..15ca28827d 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Wybierz tryb interakcji", "selectApiConfig": "Wybierz konfigurację API", + "selectModel": "Wybierz model", + "selectModelUnsupported": "Wybór modelu nie jest tutaj dostępny dla tego dostawcy. Kliknij, aby otworzyć ustawienia.", "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/pl/common.json b/webview-ui/src/i18n/locales/pl/common.json index 419aa83af1..4d0e56ed78 100644 --- a/webview-ui/src/i18n/locales/pl/common.json +++ b/webview-ui/src/i18n/locales/pl/common.json @@ -20,6 +20,8 @@ "keep": "Zachowaj" }, "ui": { + "loading": "Ładowanie...", + "clear_search": "Wyczyść wyszukiwanie", "search_placeholder": "Szukaj...", "no_results": "Nie znaleziono wyników" }, diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 1adf5870a8..48c66c17bb 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Selecionar modo de interação", "selectApiConfig": "Selecionar configuração da API", + "selectModel": "Selecionar modelo", + "selectModelUnsupported": "A seleção de modelo não está disponível para este provedor aqui. Clique para abrir as 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/pt-BR/common.json b/webview-ui/src/i18n/locales/pt-BR/common.json index 4990796976..ca5dd63dd3 100644 --- a/webview-ui/src/i18n/locales/pt-BR/common.json +++ b/webview-ui/src/i18n/locales/pt-BR/common.json @@ -20,6 +20,8 @@ "keep": "Manter" }, "ui": { + "loading": "Carregando...", + "clear_search": "Limpar pesquisa", "search_placeholder": "Pesquisar...", "no_results": "Nenhum resultado encontrado" }, diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index f42363c5f4..8f028b76f9 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Выберите режим взаимодействия", "selectApiConfig": "Выберите конфигурацию API", + "selectModel": "Выберите модель", + "selectModelUnsupported": "Выбор модели недоступен для этого провайдера здесь. Нажмите, чтобы открыть настройки.", "lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве", "unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)", "enhancePrompt": "Улучшить запрос с дополнительным контекстом", diff --git a/webview-ui/src/i18n/locales/ru/common.json b/webview-ui/src/i18n/locales/ru/common.json index f66384a693..8869b03f7b 100644 --- a/webview-ui/src/i18n/locales/ru/common.json +++ b/webview-ui/src/i18n/locales/ru/common.json @@ -20,6 +20,8 @@ "keep": "Оставить" }, "ui": { + "loading": "Загрузка...", + "clear_search": "Очистить поиск", "search_placeholder": "Поиск...", "no_results": "Результатов не найдено" }, diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 4d8ba6fead..112863bcb0 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Etkileşim modunu seçin", "selectApiConfig": "API yapılandırmasını seçin", + "selectModel": "Model seçin", + "selectModelUnsupported": "Bu sağlayıcı için model seçimi burada kullanılamıyor. Ayarları açmak için tıklayın.", "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/tr/common.json b/webview-ui/src/i18n/locales/tr/common.json index db9e991cd5..9db0b37727 100644 --- a/webview-ui/src/i18n/locales/tr/common.json +++ b/webview-ui/src/i18n/locales/tr/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Yükleniyor...", + "clear_search": "Aramayı temizle", "search_placeholder": "Ara...", "no_results": "Sonuç bulunamadı" }, diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index d48351deac..8fa720008d 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "Chọn chế độ tương tác", "selectApiConfig": "Chọn cấu hình API", + "selectModel": "Chọn mô hình", + "selectModelUnsupported": "Không thể chọn mô hình cho nhà cung cấp này ở đây. Nhấn để mở 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/vi/common.json b/webview-ui/src/i18n/locales/vi/common.json index 57eb31fafa..f9155d48ab 100644 --- a/webview-ui/src/i18n/locales/vi/common.json +++ b/webview-ui/src/i18n/locales/vi/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "Đang tải...", + "clear_search": "Xóa tìm kiếm", "search_placeholder": "Tìm kiếm...", "no_results": "Không tìm thấy kết quả nào" }, diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 00b0e3a85b..c71ee5abab 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -113,6 +113,8 @@ }, "selectMode": "选择交互模式", "selectApiConfig": "选择 API 配置", + "selectModel": "选择模型", + "selectModelUnsupported": "此提供商在此处不支持模型选择。点击以打开设置。", "lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置", "unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)", "enhancePrompt": "增强提示词", diff --git a/webview-ui/src/i18n/locales/zh-CN/common.json b/webview-ui/src/i18n/locales/zh-CN/common.json index 10df089333..8087de8eeb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/common.json +++ b/webview-ui/src/i18n/locales/zh-CN/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "加载中...", + "clear_search": "清除搜索", "search_placeholder": "搜索...", "no_results": "未找到结果" }, diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index d04b1fb08b..f21db946da 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -140,6 +140,8 @@ }, "selectMode": "選擇互動模式", "selectApiConfig": "選取 API 設定", + "selectModel": "選擇模型", + "selectModelUnsupported": "此提供者在此處無法選取模型。按一下以開啟設定。", "lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定", "unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)", "enhancePrompt": "使用額外內容強化提示詞", diff --git a/webview-ui/src/i18n/locales/zh-TW/common.json b/webview-ui/src/i18n/locales/zh-TW/common.json index 558609c157..469d119b64 100644 --- a/webview-ui/src/i18n/locales/zh-TW/common.json +++ b/webview-ui/src/i18n/locales/zh-TW/common.json @@ -20,6 +20,8 @@ "billion_suffix": "b" }, "ui": { + "loading": "載入中...", + "clear_search": "清除搜尋", "search_placeholder": "搜尋...", "no_results": "找不到結果" }, From f2f4cf6aca5658872e5598b5aabba0f9f867964f Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 10:06:40 +0900 Subject: [PATCH 02/11] fix(chat): merge model selection onto the stored profile host-side The chat ModelSelector used to send the webview's full apiConfiguration, which can be the active task's config rather than the profile's, so a model pick could overwrite the profile. Send only a model patch via the new updateProfileModel message; the host merges it onto the stored profile and rejects the update if the provider no longer matches. Reuse handleModelChangeSideEffects for the reset logic and tighten the tests. --- packages/types/src/vscode-extension-host.ts | 1 + ...wMessageHandler.updateProfileModel.spec.ts | 116 ++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 48 ++++++++ .../src/components/chat/ChatTextArea.tsx | 10 +- .../src/components/chat/ModelSelector.tsx | 29 +++-- .../chat/__tests__/ChatTextArea.spec.tsx | 26 ++-- .../chat/__tests__/ModelSelector.spec.tsx | 75 ++++++++--- 7 files changed, 261 insertions(+), 44 deletions(-) create mode 100644 src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 2f1222f8ee..72b7adce52 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -466,6 +466,7 @@ export interface WebviewMessage { | "currentApiConfigName" | "saveApiConfiguration" | "upsertApiConfiguration" + | "updateProfileModel" | "deleteApiConfiguration" | "loadApiConfiguration" | "loadApiConfigurationById" diff --git a/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts new file mode 100644 index 0000000000..76acc1bcd8 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts @@ -0,0 +1,116 @@ +// npx vitest run core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts + +import * as vscode from "vscode" + +vi.mock("vscode", () => ({ window: { showErrorMessage: vi.fn() } })) + +import { providerIdentifiers } from "@roo-code/types" + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +describe("webviewMessageHandler - updateProfileModel", () => { + let mockProvider: { + log: ReturnType + upsertProviderProfile: ReturnType + providerSettingsManager: { getProfile: ReturnType } + } + + const storedProfile = { + name: "default", + id: "profile-1", + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-sonnet-4-5", + apiKey: "stored-key", + reasoningEffort: "high", + modelMaxTokens: 4096, + } + + const send = (values: Record, text: string = "default") => + webviewMessageHandler(mockProvider as unknown as ClineProvider, { type: "updateProfileModel", text, values }) + + beforeEach(() => { + vi.clearAllMocks() + mockProvider = { + log: vi.fn(), + upsertProviderProfile: vi.fn().mockResolvedValue("profile-1"), + providerSettingsManager: { getProfile: vi.fn().mockResolvedValue(storedProfile) }, + } + }) + + it("merges the patch onto the stored profile, clearing null fields", async () => { + await send({ + expectedProvider: providerIdentifiers.anthropic, + patch: { apiModelId: "claude-3-5-haiku", reasoningEffort: null, modelMaxTokens: null }, + }) + + expect(mockProvider.providerSettingsManager.getProfile).toHaveBeenCalledWith({ name: "default" }) + expect(mockProvider.upsertProviderProfile).toHaveBeenCalledTimes(1) + expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith("default", { + id: "profile-1", + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-3-5-haiku", + apiKey: "stored-key", + reasoningEffort: undefined, + modelMaxTokens: undefined, + }) + }) + + it("rejects the update when the stored provider differs from the expected provider", async () => { + await send({ expectedProvider: providerIdentifiers.openrouter, patch: { openRouterModelId: "x/y" } }) + + expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() + expect(mockProvider.log).toHaveBeenCalledWith( + expect.stringContaining(`expected '${providerIdentifiers.openrouter}'`), + ) + }) + + it("ignores non-setting keys, apiProvider, own __proto__ keys and non-primitive values", async () => { + const patch = JSON.parse( + '{"apiModelId":"claude-3-5-haiku","notASetting":"evil","apiProvider":"openrouter","__proto__":{"polluted":true},"apiKey":{"nested":1}}', + ) + + await send({ expectedProvider: providerIdentifiers.anthropic, patch }) + + const saved = mockProvider.upsertProviderProfile.mock.calls[0][1] + expect(saved).not.toHaveProperty("notASetting") + expect(saved).not.toHaveProperty("polluted") + expect(Object.getPrototypeOf(saved)).toBe(Object.prototype) + expect(saved.apiProvider).toBe(providerIdentifiers.anthropic) + expect(saved.apiKey).toBe("stored-key") + expect(saved.apiModelId).toBe("claude-3-5-haiku") + }) + + it("treats a profile without apiProvider as OpenRouter", async () => { + const { apiProvider: _apiProvider, ...withoutProvider } = storedProfile + mockProvider.providerSettingsManager.getProfile.mockResolvedValue(withoutProvider) + + await send({ expectedProvider: providerIdentifiers.openrouter, patch: { openRouterModelId: "x/y" } }) + + expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith( + "default", + expect.objectContaining({ openRouterModelId: "x/y" }), + ) + }) + + it.each([ + ["missing profile name", { expectedProvider: providerIdentifiers.anthropic, patch: {} }, ""], + ["missing expectedProvider", { patch: { apiModelId: "x" } }, "default"], + ["missing patch", { expectedProvider: providerIdentifiers.anthropic }, "default"], + ["null patch", { expectedProvider: providerIdentifiers.anthropic, patch: null }, "default"], + ])("does nothing for %s", async (_label, values, text) => { + await send(values, text) + + expect(mockProvider.providerSettingsManager.getProfile).not.toHaveBeenCalled() + expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() + }) + + it("reports an error without saving when the profile cannot be loaded", async () => { + mockProvider.providerSettingsManager.getProfile.mockRejectedValue(new Error("not found")) + + await send({ expectedProvider: providerIdentifiers.anthropic, patch: { apiModelId: "x" } }) + + expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4ba94d454c..1d901692b2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -30,6 +30,8 @@ import { RouterModelsMessageType, VsCodeLmModelsMessageType, isTelemetryOptedIn, + PROVIDER_SETTINGS_KEYS, + type ProviderSettings, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -2283,6 +2285,52 @@ export const webviewMessageHandler = async ( await provider.upsertProviderProfile(message.text, message.apiConfiguration) } break + case "updateProfileModel": { + // values: { expectedProvider, patch }. A `null` patch value clears the field, since + // `undefined` is dropped when the webview serializes the message. + const expectedProvider = message.values?.expectedProvider + const patch: unknown = message.values?.patch + if (!message.text || !expectedProvider || typeof patch !== "object" || patch === null) { + break + } + + try { + const { name, id, ...stored } = await provider.providerSettingsManager.getProfile({ + name: message.text, + }) + + // A profile without an explicit provider is treated as OpenRouter, matching the chat ModelSelector. + const storedProvider = stored.apiProvider ?? providerIdentifiers.openrouter + + if (storedProvider !== expectedProvider) { + provider.log( + `Ignoring model update for profile '${name}': provider is '${storedProvider}', expected '${expectedProvider}'`, + ) + break + } + + const allowedKeys: ReadonlySet = new Set(PROVIDER_SETTINGS_KEYS) + const merged: Record = { ...stored, id } + for (const [key, value] of Object.entries(patch)) { + // The provider is never patchable, otherwise the expectedProvider guard could be bypassed. + if (key === "apiProvider" || !allowedKeys.has(key)) { + continue + } + if (value !== null && typeof value !== "string" && typeof value !== "number") { + continue + } + merged[key] = value === null ? undefined : value + } + + await provider.upsertProviderProfile(name, merged as ProviderSettings) + } catch (error) { + provider.log( + `Error updating profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.save_api_config")) + } + break + } case "renameApiConfiguration": if (message.values && message.apiConfiguration) { try { diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 0f10c48f31..e36915d2a3 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -3,7 +3,7 @@ import { useEvent } from "react-use" import DynamicTextArea from "react-textarea-autosize" import { VolumeX, Image, WandSparkles, SendHorizontal, X, ListEnd, Square } from "lucide-react" -import type { ExtensionMessage, ProviderSettings } from "@roo-code/types" +import type { ExtensionMessage } from "@roo-code/types" import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions" import { WebviewMessage } from "@roo/WebviewMessage" @@ -27,7 +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 { ModelSelector, type ModelSelection } from "./ModelSelector" import { AutoApproveDropdown } from "./AutoApproveDropdown" import { MAX_IMAGES_PER_MESSAGE } from "./constants" import ContextMenu from "./ContextMenu" @@ -950,11 +950,11 @@ export const ChatTextArea = forwardRef( // Common model selector handler const handleModelChange = useCallback( - (value: ProviderSettings) => { + (selection: ModelSelection) => { vscode.postMessage({ - type: "upsertApiConfiguration", + type: "updateProfileModel", text: currentApiConfigName, - apiConfiguration: value, + values: { expectedProvider: selection.expectedProvider, patch: selection.patch }, }) }, [currentApiConfigName], diff --git a/webview-ui/src/components/chat/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx index b06c14d2f8..36a13261f6 100644 --- a/webview-ui/src/components/chat/ModelSelector.tsx +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -5,6 +5,7 @@ import { type ModelInfo, type ModelRecord, type OrganizationAllowList, + type ProviderName, type ProviderSettings, isDynamicProvider, isRetiredProvider, @@ -22,15 +23,23 @@ import { vscode } from "@/utils/vscode" import { getProviderModelConfig, + handleModelChangeSideEffects, getStaticModelsForProvider, isStaticModelProvider, } from "../settings/utils/providerModelConfig" import { filterModels } from "../settings/utils/organizationFilters" import { SEARCH_THRESHOLD } from "./selectorConstants" +export type ModelSelectionPatch = Record + +export interface ModelSelection { + expectedProvider: ProviderName + patch: ModelSelectionPatch +} + interface ModelSelectorProps { apiConfiguration: ProviderSettings - onChange: (apiConfiguration: ProviderSettings) => void + onChange: (selection: ModelSelection) => void disabled?: boolean title: string triggerClassName?: string @@ -164,23 +173,19 @@ export const ModelSelector = ({ return } - const updated: ProviderSettings = { - ...apiConfiguration, - reasoningEffort: undefined, - modelMaxTokens: undefined, - modelMaxThinkingTokens: undefined, - } - if (provider === providerIdentifiers.bedrock && modelId !== "custom-arn") { - ;(updated as Record)["awsCustomArn"] = undefined + const patch: ModelSelectionPatch = {} + const setField = (field: K, value: ProviderSettings[K]) => { + patch[field] = typeof value === "string" || typeof value === "number" ? value : null } - ;(updated as Record)[modelConfig.field] = modelId + handleModelChangeSideEffects(provider, modelId, setField) + patch[modelConfig.field] = modelId - onChange(updated) + onChange({ expectedProvider: provider, patch }) setOpen(false) setSearchValue("") }, - [apiConfiguration, modelConfig, provider, onChange], + [modelConfig, provider, onChange], ) const renderModelItem = useCallback( diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index efcb448971..084935c9b7 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1275,21 +1275,29 @@ describe("ChatTextArea", () => { ) }) - it("posts upsertApiConfiguration with the current config name when a model is selected", () => { + it("posts only the model patch for the current profile and never a full apiConfiguration", () => { render() + mockPostMessage.mockClear() fireEvent.click(screen.getByTestId("model-selector-trigger")) fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) - expect(mockPostMessage).toHaveBeenCalledWith( - expect.objectContaining({ - type: "upsertApiConfiguration", - text: "default", - apiConfiguration: expect.objectContaining({ + expect(mockPostMessage).toHaveBeenCalledTimes(1) + const message = mockPostMessage.mock.calls[0][0] + expect(message).toEqual({ + type: "updateProfileModel", + text: "default", + values: { + expectedProvider: providerIdentifiers.anthropic, + patch: { apiModelId: expect.stringContaining("claude-3-5-haiku"), - }), - }), - ) + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }, + }) + expect(message).not.toHaveProperty("apiConfiguration") }) }) diff --git a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx index 808483fcfb..5abdff19d8 100644 --- a/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -172,7 +172,15 @@ describe("ModelSelector", () => { expect(screen.getByRole("button", { name: "openrouter/model-0" })).toHaveFocus() await user.keyboard(" ") expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() - expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-0" })) + expect(onChangeMock).toHaveBeenCalledWith({ + expectedProvider: providerIdentifiers.openrouter, + patch: { + openRouterModelId: "openrouter/model-0", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) }) it("blocks model updates when disabled after the model list is rendered", async () => { @@ -214,9 +222,18 @@ describe("ModelSelector", () => { const anotherModel = screen.getAllByText(/claude-3-5-haiku/i)[0] fireEvent.click(anotherModel) - expect(onChangeMock).toHaveBeenCalledWith( - expect.objectContaining({ apiModelId: expect.stringContaining("claude-3-5-haiku") }), - ) + expect(onChangeMock).toHaveBeenCalledTimes(1) + const [selection] = onChangeMock.mock.calls[0] + expect(selection).toEqual({ + expectedProvider: providerIdentifiers.anthropic, + patch: { + apiModelId: expect.stringContaining("claude-3-5-haiku"), + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) + expect(selection.patch).not.toHaveProperty("apiProvider") }) it("resets reasoning/thinking-token overrides and closes the popover after selecting a model", () => { @@ -239,13 +256,15 @@ describe("ModelSelector", () => { fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) - expect(onChangeMock).toHaveBeenCalledWith( - expect.objectContaining({ - reasoningEffort: undefined, - modelMaxTokens: undefined, - modelMaxThinkingTokens: undefined, - }), - ) + expect(onChangeMock).toHaveBeenCalledWith({ + expectedProvider: providerIdentifiers.anthropic, + patch: { + apiModelId: expect.stringContaining("claude-3-5-haiku"), + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) expect(screen.getByTestId("popover-root")).toHaveAttribute("data-open", "false") expect(screen.queryByTestId("popover-content")).not.toBeInTheDocument() }) @@ -316,7 +335,15 @@ describe("ModelSelector", () => { fireEvent.click(screen.getByText("openrouter/model-b")) - expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-b" })) + expect(onChangeMock).toHaveBeenCalledWith({ + expectedProvider: providerIdentifiers.openrouter, + patch: { + openRouterModelId: "openrouter/model-b", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) }) it.each([ @@ -1016,7 +1043,15 @@ describe("ModelSelector", () => { fireEvent.click(list.getByText("openrouter/model-a")) - expect(onChangeMock).toHaveBeenCalledWith(expect.objectContaining({ openRouterModelId: "openrouter/model-a" })) + expect(onChangeMock).toHaveBeenCalledWith({ + expectedProvider: providerIdentifiers.openrouter, + patch: { + openRouterModelId: "openrouter/model-a", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) }) it("hides the custom-arn pseudo-model for a Bedrock provider and allows selecting a normal model", () => { @@ -1051,12 +1086,16 @@ describe("ModelSelector", () => { // so the extension doesn't keep reading the old ARN instead of the newly chosen model. fireEvent.click(list.getByText("anthropic.claude-3-5-haiku-20241022-v1:0")) - expect(onChangeMock).toHaveBeenCalledWith( - expect.objectContaining({ + expect(onChangeMock).toHaveBeenCalledWith({ + expectedProvider: providerIdentifiers.bedrock, + patch: { apiModelId: "anthropic.claude-3-5-haiku-20241022-v1:0", - awsCustomArn: undefined, - }), - ) + awsCustomArn: "", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) }) it("filters static models by the organization allow list when not all models are allowed", () => { From bbb1a8bae8f44796bbef50c6ede116dd7b141c8e Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 13:13:47 +0900 Subject: [PATCH 03/11] fix(chat): apply model selection inside the profile mutation queue Reading and merging the stored profile outside the queue let a model pick made against a profile that was just switched away from be saved and reactivated afterwards, undoing the switch. Move the merge into ClineProvider.updateProfileModel, which checks the profile the webview is shown (task sticky name, else global) before saving. Also assert that a disabled selector posts nothing and that the error key is shown. --- src/core/webview/ClineProvider.ts | 149 +++++++++++++----- .../ClineProvider.apiHandlerRebuild.spec.ts | 148 ++++++++++++++++- ...wMessageHandler.updateProfileModel.spec.ts | 97 ++---------- src/core/webview/webviewMessageHandler.ts | 42 +---- .../chat/__tests__/ChatTextArea.spec.tsx | 4 +- 5 files changed, 268 insertions(+), 172 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 874e2f8f08..bd5bed47b1 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -57,6 +57,7 @@ import { getModelId, isRetiredProvider, providerIdentifiers, + PROVIDER_SETTINGS_KEYS, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" import { TaskRegistry } from "../task/TaskRegistry" @@ -1876,49 +1877,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.upsertProviderProfileUnlocked(name, providerSettings, activate, signal), + ) } catch (error) { this.log( `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -1929,6 +1890,108 @@ export class ClineProvider } } + private async upsertProviderProfileUnlocked( + 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 + } + + /** + * Applies a model selection to a stored profile. Everything runs inside one queued + * mutation so a selection made against a profile that has since been switched away + * from is dropped instead of saved and reactivated. A `null` patch value clears the field. + */ + async updateProfileModel(name: string, expectedProvider: string, patch: object): Promise { + try { + await this.enqueueProviderProfileMutation(async (signal) => { + // Mirrors the profile name the webview is shown (see getStateToPostToWebview). + const task = this.getCurrentTask() + const { currentApiConfigName } = await this.getState() + const visibleProfileName = task ? task.taskApiConfigName : currentApiConfigName + + if (visibleProfileName !== name) { + this.log(`Ignoring model update for profile '${name}': active profile is '${visibleProfileName}'`) + return + } + + const { name: _name, id, ...stored } = await this.providerSettingsManager.getProfile({ name }) + + if (signal.aborted) return + + // A profile without an explicit provider is treated as OpenRouter, matching the chat ModelSelector. + const storedProvider = stored.apiProvider ?? providerIdentifiers.openrouter + + if (storedProvider !== expectedProvider) { + this.log( + `Ignoring model update for profile '${name}': provider is '${storedProvider}', expected '${expectedProvider}'`, + ) + return + } + + const allowedKeys: ReadonlySet = new Set(PROVIDER_SETTINGS_KEYS) + const merged: Record = { ...stored, id } + for (const [key, value] of Object.entries(patch)) { + // The provider is never patchable, otherwise the expectedProvider guard could be bypassed. + if (key === "apiProvider" || !allowedKeys.has(key)) { + continue + } + if (value !== null && typeof value !== "string" && typeof value !== "number") { + continue + } + merged[key] = value === null ? undefined : value + } + + await this.upsertProviderProfileUnlocked(name, merged as ProviderSettings, true, signal) + }) + } catch (error) { + this.log(`Error updating profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + vscode.window.showErrorMessage(t("common:errors.save_api_config")) + } + } + 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 99d254cb9a..01c9f9ba8d 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -3,7 +3,7 @@ import * as vscode from "vscode" import { TelemetryService } from "@roo-code/telemetry" -import { getModelId, RooCodeEventName } from "@roo-code/types" +import { getModelId, RooCodeEventName, type ProviderSettings } from "@roo-code/types" import { ContextProxy } from "../../config/ContextProxy" import type { Mode } from "../../../shared/modes" @@ -130,6 +130,11 @@ vi.mock("../../task/Task", () => ({ }), })) +vi.mock("../../../i18n", async (importOriginal) => ({ + ...(await importOriginal()), + t: (key: string) => key, +})) + vi.mock("@roo-code/cloud", () => ({ CloudService: { hasInstance: vi.fn().mockReturnValue(true), @@ -419,6 +424,147 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }) }) + describe("updateProfileModel", () => { + const manager = () => { + const settingsManager = provider["providerSettingsManager"] + return { + getProfile: vi.mocked(settingsManager.getProfile), + saveConfig: vi.mocked(settingsManager.saveConfig), + activateProfile: vi.mocked(settingsManager.activateProfile), + } + } + + beforeEach(async () => { + await provider.contextProxy.setValue("currentApiConfigName", "test-config") + }) + + const mockStoredProfile = (profile: ProviderSettings & { name?: string }) => + manager().getProfile.mockResolvedValue({ name: "test-config", id: "test-id", ...profile }) + + it("merges the patch onto the stored profile, clearing null fields", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + openRouterApiKey: "stored-key", + reasoningEffort: "high", + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + reasoningEffort: null, + }) + + expect(manager().getProfile).toHaveBeenCalledWith({ name: "test-config" }) + expect(manager().saveConfig).toHaveBeenCalledTimes(1) + expect(manager().saveConfig).toHaveBeenCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "x/y", + openRouterApiKey: "stored-key", + reasoningEffort: undefined, + }) + }) + + it("rejects the update when the stored provider differs from the expected provider", async () => { + await provider.updateProfileModel("test-config", providerIdentifiers.anthropic, { apiModelId: "x" }) + + expect(manager().saveConfig).not.toHaveBeenCalled() + }) + + it("treats a profile without apiProvider as OpenRouter", async () => { + mockStoredProfile({}) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(manager().saveConfig).toHaveBeenCalledWith( + "test-config", + expect.objectContaining({ openRouterModelId: "x/y" }), + ) + }) + + it("ignores non-setting keys, apiProvider, own __proto__ keys and non-primitive values", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "stored-key" }) + const patch = JSON.parse( + '{"openRouterModelId":"x/y","notASetting":"evil","apiProvider":"anthropic","__proto__":{"polluted":true},"openRouterApiKey":{"nested":1}}', + ) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, patch) + + const saved = manager().saveConfig.mock.calls[0][1] + expect(saved).not.toHaveProperty("notASetting") + expect(saved).not.toHaveProperty("polluted") + expect(Object.getPrototypeOf(saved)).toBe(Object.prototype) + expect(saved.apiProvider).toBe(providerIdentifiers.openrouter) + expect(saved.openRouterApiKey).toBe("stored-key") + expect(saved.openRouterModelId).toBe("x/y") + }) + + it("drops an update for a profile that is not the visible profile without reading or saving it", async () => { + await provider.updateProfileModel("other-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(manager().getProfile).not.toHaveBeenCalled() + expect(manager().saveConfig).not.toHaveBeenCalled() + }) + + it("drops a stale update that was queued behind a profile switch", async () => { + manager().activateProfile.mockResolvedValue({ + name: "other-config", + id: "other-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "other/model", + }) + + // The webview still shows "test-config" when the model is picked, before the switch lands. + const switching = provider.activateProviderProfile({ name: "other-config" }) + const updating = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + await Promise.all([switching, updating]) + + expect(manager().saveConfig).not.toHaveBeenCalled() + expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") + }) + + it("uses the task's sticky profile name as the visible profile when a task is active", async () => { + const mockTask = new Task({ ...defaultTaskOptions }) + Object.defineProperty(mockTask, "taskApiConfigName", { value: "sticky-config" }) + await provider.addClineToStack(mockTask) + mockStoredProfile({ name: "sticky-config", apiProvider: providerIdentifiers.openrouter }) + + await provider.updateProfileModel("sticky-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + expect(manager().saveConfig).toHaveBeenCalledWith( + "sticky-config", + expect.objectContaining({ openRouterModelId: "x/y" }), + ) + + manager().saveConfig.mockClear() + // The global profile name is not what the webview is shown while a task is active. + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/z", + }) + expect(manager().saveConfig).not.toHaveBeenCalled() + }) + + it("does not save and does not throw when the profile cannot be loaded", async () => { + manager().getProfile.mockRejectedValue(new Error("not found")) + + await expect( + provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }), + ).resolves.toBeUndefined() + + expect(manager().saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + }) + describe("activateProviderProfile", () => { test("serializes provider profile mutations without interleaving", async () => { const events: string[] = [] diff --git a/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts index 76acc1bcd8..86d5ce34e2 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts @@ -1,116 +1,41 @@ // npx vitest run core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts -import * as vscode from "vscode" - -vi.mock("vscode", () => ({ window: { showErrorMessage: vi.fn() } })) - import { providerIdentifiers } from "@roo-code/types" import { webviewMessageHandler } from "../webviewMessageHandler" import type { ClineProvider } from "../ClineProvider" describe("webviewMessageHandler - updateProfileModel", () => { - let mockProvider: { - log: ReturnType - upsertProviderProfile: ReturnType - providerSettingsManager: { getProfile: ReturnType } - } - - const storedProfile = { - name: "default", - id: "profile-1", - apiProvider: providerIdentifiers.anthropic, - apiModelId: "claude-sonnet-4-5", - apiKey: "stored-key", - reasoningEffort: "high", - modelMaxTokens: 4096, - } + let mockProvider: { updateProfileModel: ReturnType } - const send = (values: Record, text: string = "default") => + const send = (values: Record | undefined, text: string = "default") => webviewMessageHandler(mockProvider as unknown as ClineProvider, { type: "updateProfileModel", text, values }) beforeEach(() => { vi.clearAllMocks() - mockProvider = { - log: vi.fn(), - upsertProviderProfile: vi.fn().mockResolvedValue("profile-1"), - providerSettingsManager: { getProfile: vi.fn().mockResolvedValue(storedProfile) }, - } - }) - - it("merges the patch onto the stored profile, clearing null fields", async () => { - await send({ - expectedProvider: providerIdentifiers.anthropic, - patch: { apiModelId: "claude-3-5-haiku", reasoningEffort: null, modelMaxTokens: null }, - }) - - expect(mockProvider.providerSettingsManager.getProfile).toHaveBeenCalledWith({ name: "default" }) - expect(mockProvider.upsertProviderProfile).toHaveBeenCalledTimes(1) - expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith("default", { - id: "profile-1", - apiProvider: providerIdentifiers.anthropic, - apiModelId: "claude-3-5-haiku", - apiKey: "stored-key", - reasoningEffort: undefined, - modelMaxTokens: undefined, - }) - }) - - it("rejects the update when the stored provider differs from the expected provider", async () => { - await send({ expectedProvider: providerIdentifiers.openrouter, patch: { openRouterModelId: "x/y" } }) - - expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() - expect(mockProvider.log).toHaveBeenCalledWith( - expect.stringContaining(`expected '${providerIdentifiers.openrouter}'`), - ) + mockProvider = { updateProfileModel: vi.fn().mockResolvedValue(undefined) } }) - it("ignores non-setting keys, apiProvider, own __proto__ keys and non-primitive values", async () => { - const patch = JSON.parse( - '{"apiModelId":"claude-3-5-haiku","notASetting":"evil","apiProvider":"openrouter","__proto__":{"polluted":true},"apiKey":{"nested":1}}', - ) + it("delegates the profile name, expected provider and patch to the provider", async () => { + const patch = { apiModelId: "claude-3-5-haiku", reasoningEffort: null } await send({ expectedProvider: providerIdentifiers.anthropic, patch }) - const saved = mockProvider.upsertProviderProfile.mock.calls[0][1] - expect(saved).not.toHaveProperty("notASetting") - expect(saved).not.toHaveProperty("polluted") - expect(Object.getPrototypeOf(saved)).toBe(Object.prototype) - expect(saved.apiProvider).toBe(providerIdentifiers.anthropic) - expect(saved.apiKey).toBe("stored-key") - expect(saved.apiModelId).toBe("claude-3-5-haiku") - }) - - it("treats a profile without apiProvider as OpenRouter", async () => { - const { apiProvider: _apiProvider, ...withoutProvider } = storedProfile - mockProvider.providerSettingsManager.getProfile.mockResolvedValue(withoutProvider) - - await send({ expectedProvider: providerIdentifiers.openrouter, patch: { openRouterModelId: "x/y" } }) - - expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith( - "default", - expect.objectContaining({ openRouterModelId: "x/y" }), - ) + expect(mockProvider.updateProfileModel).toHaveBeenCalledTimes(1) + expect(mockProvider.updateProfileModel).toHaveBeenCalledWith("default", providerIdentifiers.anthropic, patch) }) it.each([ ["missing profile name", { expectedProvider: providerIdentifiers.anthropic, patch: {} }, ""], + ["missing values", undefined, "default"], ["missing expectedProvider", { patch: { apiModelId: "x" } }, "default"], + ["non-string expectedProvider", { expectedProvider: 1, patch: { apiModelId: "x" } }, "default"], ["missing patch", { expectedProvider: providerIdentifiers.anthropic }, "default"], ["null patch", { expectedProvider: providerIdentifiers.anthropic, patch: null }, "default"], + ["non-object patch", { expectedProvider: providerIdentifiers.anthropic, patch: "x" }, "default"], ])("does nothing for %s", async (_label, values, text) => { await send(values, text) - expect(mockProvider.providerSettingsManager.getProfile).not.toHaveBeenCalled() - expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() - }) - - it("reports an error without saving when the profile cannot be loaded", async () => { - mockProvider.providerSettingsManager.getProfile.mockRejectedValue(new Error("not found")) - - await send({ expectedProvider: providerIdentifiers.anthropic, patch: { apiModelId: "x" } }) - - expect(mockProvider.upsertProviderProfile).not.toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalled() + expect(mockProvider.updateProfileModel).not.toHaveBeenCalled() }) }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1d901692b2..69de93aa01 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -30,8 +30,6 @@ import { RouterModelsMessageType, VsCodeLmModelsMessageType, isTelemetryOptedIn, - PROVIDER_SETTINGS_KEYS, - type ProviderSettings, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -2288,47 +2286,13 @@ export const webviewMessageHandler = async ( case "updateProfileModel": { // values: { expectedProvider, patch }. A `null` patch value clears the field, since // `undefined` is dropped when the webview serializes the message. - const expectedProvider = message.values?.expectedProvider + const expectedProvider: unknown = message.values?.expectedProvider const patch: unknown = message.values?.patch - if (!message.text || !expectedProvider || typeof patch !== "object" || patch === null) { + if (!message.text || typeof expectedProvider !== "string" || typeof patch !== "object" || patch === null) { break } - try { - const { name, id, ...stored } = await provider.providerSettingsManager.getProfile({ - name: message.text, - }) - - // A profile without an explicit provider is treated as OpenRouter, matching the chat ModelSelector. - const storedProvider = stored.apiProvider ?? providerIdentifiers.openrouter - - if (storedProvider !== expectedProvider) { - provider.log( - `Ignoring model update for profile '${name}': provider is '${storedProvider}', expected '${expectedProvider}'`, - ) - break - } - - const allowedKeys: ReadonlySet = new Set(PROVIDER_SETTINGS_KEYS) - const merged: Record = { ...stored, id } - for (const [key, value] of Object.entries(patch)) { - // The provider is never patchable, otherwise the expectedProvider guard could be bypassed. - if (key === "apiProvider" || !allowedKeys.has(key)) { - continue - } - if (value !== null && typeof value !== "string" && typeof value !== "number") { - continue - } - merged[key] = value === null ? undefined : value - } - - await provider.upsertProviderProfile(name, merged as ProviderSettings) - } catch (error) { - provider.log( - `Error updating profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.save_api_config")) - } + await provider.updateProfileModel(message.text, expectedProvider, patch) break } case "renameApiConfiguration": diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index 084935c9b7..846768fa19 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1270,9 +1270,7 @@ describe("ChatTextArea", () => { mockPostMessage.mockClear() fireEvent.click(trigger) - expect(mockPostMessage).not.toHaveBeenCalledWith( - expect.objectContaining({ type: "upsertApiConfiguration" }), - ) + expect(mockPostMessage).not.toHaveBeenCalled() }) it("posts only the model patch for the current profile and never a full apiConfiguration", () => { From a479b31c7103ab9c5c5cfe5676d3ad3dc753b5b3 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 17:28:11 +0900 Subject: [PATCH 04/11] fix(chat): stop a timed-out model update before it activates a profile Resolve getState() and listConfig() before the activation writes and re-check the abort signal, so a mutation that timed out while awaiting them cannot restore currentApiConfigName after a later profile switch. --- src/core/webview/ClineProvider.ts | 6 +++- .../ClineProvider.apiHandlerRebuild.spec.ts | 34 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bd5bed47b1..e1a095d574 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1907,6 +1907,10 @@ export class ClineProvider if (activate) { const { mode } = await this.getState() + const listApiConfigMeta = await this.providerSettingsManager.listConfig() + + // A timed-out mutation must not activate its profile after a later switch has run. + if (signal.aborted) return id // These promises do the following: // 1. Adds or updates the list of provider profiles. @@ -1919,7 +1923,7 @@ export class ClineProvider // 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("listApiConfigMeta", listApiConfigMeta), this.updateGlobalState("currentApiConfigName", name), this.providerSettingsManager.setModeConfig(mode, id), this.contextProxy.setProviderSettings(providerSettings), diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 01c9f9ba8d..775981c2a0 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -3,7 +3,7 @@ import * as vscode from "vscode" import { TelemetryService } from "@roo-code/telemetry" -import { getModelId, RooCodeEventName, type ProviderSettings } from "@roo-code/types" +import { getModelId, RooCodeEventName, type ProviderSettings, type ProviderSettingsEntry } from "@roo-code/types" import { ContextProxy } from "../../config/ContextProxy" import type { Mode } from "../../../shared/modes" @@ -551,6 +551,38 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(manager().saveConfig).not.toHaveBeenCalled() }) + it("does not activate its profile when it times out while listing profiles and a later switch has run", async () => { + vi.useFakeTimers() + try { + let resolveList!: (entries: ProviderSettingsEntry[]) => void + const listing = new Promise((resolve) => { + resolveList = resolve + }) + vi.mocked(provider["providerSettingsManager"].listConfig).mockImplementationOnce(() => listing) + manager().activateProfile.mockResolvedValue({ + name: "other-config", + id: "other-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "other/model", + }) + const setValueSpy = vi.spyOn(provider.contextProxy, "setValue") + + const updating = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS) + const switching = provider.activateProviderProfile({ name: "other-config" }) + + resolveList([]) + await Promise.all([updating, switching]) + + expect(setValueSpy).not.toHaveBeenCalledWith("currentApiConfigName", "test-config") + expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") + } finally { + vi.useRealTimers() + } + }) + it("does not save and does not throw when the profile cannot be loaded", async () => { manager().getProfile.mockRejectedValue(new Error("not found")) From 69b0791f59b0e9a1e1e154b738f191650917db7d Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 18:09:01 +0900 Subject: [PATCH 05/11] fix(chat): enforce allow-list and roll back aborted host-side model updates updateProfileModel now only accepts model id and model-selection reset keys, validates the merged profile against the organization allow-list, restores the previous stored profile when the mutation is aborted after saving, and skips handler rebuild/sticky persistence/state posting once aborted after the activation writes. --- src/core/webview/ClineProvider.ts | 44 ++++++++++++++++--- .../ClineProvider.apiHandlerRebuild.spec.ts | 43 ++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e1a095d574..98584f4c1a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1895,6 +1895,7 @@ export class ClineProvider providerSettings: ProviderSettings, activate: boolean, signal: AbortSignal, + rollbackTo?: ProviderSettings, ): 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 @@ -1903,14 +1904,21 @@ export class ClineProvider // be simpler to unify these two. const id = await this.providerSettingsManager.saveConfig(name, providerSettings) - if (signal.aborted) return id + // A timed-out mutation must leave neither a half-applied profile nor stale activation behind. + const abandon = async (): Promise => { + if (rollbackTo) { + await this.providerSettingsManager.saveConfig(name, rollbackTo) + } + return id + } + + if (signal.aborted) return abandon() if (activate) { const { mode } = await this.getState() const listApiConfigMeta = await this.providerSettingsManager.listConfig() - // A timed-out mutation must not activate its profile after a later switch has run. - if (signal.aborted) return id + if (signal.aborted) return abandon() // These promises do the following: // 1. Adds or updates the list of provider profiles. @@ -1929,6 +1937,9 @@ export class ClineProvider this.contextProxy.setProviderSettings(providerSettings), ]) + // The writes above are committed, so there is nothing to roll back; just skip the follow-up work. + 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 }) @@ -1953,7 +1964,7 @@ export class ClineProvider await this.enqueueProviderProfileMutation(async (signal) => { // Mirrors the profile name the webview is shown (see getStateToPostToWebview). const task = this.getCurrentTask() - const { currentApiConfigName } = await this.getState() + const { currentApiConfigName, organizationAllowList } = await this.getState() const visibleProfileName = task ? task.taskApiConfigName : currentApiConfigName if (visibleProfileName !== name) { @@ -1975,7 +1986,14 @@ export class ClineProvider return } - const allowedKeys: ReadonlySet = new Set(PROVIDER_SETTINGS_KEYS) + // Only model selection and its side-effect resets may be patched (see handleModelChangeSideEffects). + const allowedKeys: ReadonlySet = new Set([ + ...PROVIDER_SETTINGS_KEYS.filter((key) => key.endsWith("ModelId")), + "awsCustomArn", + "reasoningEffort", + "modelMaxTokens", + "modelMaxThinkingTokens", + ]) const merged: Record = { ...stored, id } for (const [key, value] of Object.entries(patch)) { // The provider is never patchable, otherwise the expectedProvider guard could be bypassed. @@ -1988,7 +2006,21 @@ export class ClineProvider merged[key] = value === null ? undefined : value } - await this.upsertProviderProfileUnlocked(name, merged as ProviderSettings, true, signal) + // The webview filters models, but this message can be sent by anything; enforce the allow-list here. + if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { + this.log(`Ignoring model update for profile '${name}': violates the organization allow-list`) + vscode.window.showErrorMessage(t("common:errors.violated_organization_allowlist")) + return + } + + const previous: Record = { ...stored, id } + await this.upsertProviderProfileUnlocked( + name, + merged as ProviderSettings, + true, + signal, + previous as ProviderSettings, + ) }) } catch (error) { this.log(`Error updating profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 775981c2a0..cf30456a60 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -501,6 +501,41 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(saved.openRouterModelId).toBe("x/y") }) + it("ignores patch keys other than model ids and model-selection resets", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterBaseUrl: "https://stored" }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + openRouterBaseUrl: "https://evil", + reasoningEffort: null, + }) + + const saved = manager().saveConfig.mock.calls[0][1] + expect(saved.openRouterBaseUrl).toBe("https://stored") + expect(saved.openRouterModelId).toBe("x/y") + }) + + it("rejects a model outside the organization allow-list without saving", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "allowed/model" }) + vi.spyOn(provider, "getState").mockResolvedValue({ + ...(await provider.getState()), + organizationAllowList: { + allowAll: false, + providers: { [providerIdentifiers.openrouter]: { allowAll: false, models: ["allowed/model"] } }, + }, + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "blocked/model", + }) + expect(manager().saveConfig).not.toHaveBeenCalled() + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "allowed/model", + }) + expect(manager().saveConfig).toHaveBeenCalledTimes(1) + }) + it("drops an update for a profile that is not the visible profile without reading or saving it", async () => { await provider.updateProfileModel("other-config", providerIdentifiers.openrouter, { openRouterModelId: "x/y", @@ -552,6 +587,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }) it("does not activate its profile when it times out while listing profiles and a later switch has run", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) vi.useFakeTimers() try { let resolveList!: (entries: ProviderSettingsEntry[]) => void @@ -578,6 +614,13 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(setValueSpy).not.toHaveBeenCalledWith("currentApiConfigName", "test-config") expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") + // The saved model change is rolled back so the stored profile matches the unchanged active state. + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenLastCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) } finally { vi.useRealTimers() } From 6dfafacb028fdfa1d4f2f07f3fb1b72be3239256 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 18:26:33 +0900 Subject: [PATCH 06/11] fix(chat): harden host-side model update rollback and Bedrock ARN handling Only allow awsCustomArn to be reset (never set) since the allow-list does not cover it, settle all activation writes and restore the stored profile if any fails, and skip a late rollback when a newer selection has already changed the saved values. --- src/core/webview/ClineProvider.ts | 44 +++++++--- .../ClineProvider.apiHandlerRebuild.spec.ts | 82 +++++++++++++++++-- 2 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 98584f4c1a..ea72edabba 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1895,7 +1895,7 @@ export class ClineProvider providerSettings: ProviderSettings, activate: boolean, signal: AbortSignal, - rollbackTo?: ProviderSettings, + rollback?: { previous: ProviderSettings; isStillApplied: () => Promise }, ): 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 @@ -1905,10 +1905,15 @@ export class ClineProvider const id = await this.providerSettingsManager.saveConfig(name, providerSettings) // A timed-out mutation must leave neither a half-applied profile nor stale activation behind. - const abandon = async (): Promise => { - if (rollbackTo) { - await this.providerSettingsManager.saveConfig(name, rollbackTo) + // The restore is skipped when a newer mutation has since changed the saved values, so a late + // rollback can never overwrite it. + const restore = async (): Promise => { + if (rollback && (await rollback.isStillApplied())) { + await this.providerSettingsManager.saveConfig(name, rollback.previous) } + } + const abandon = async (): Promise => { + await restore() return id } @@ -1930,12 +1935,22 @@ 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 every started write has finished before the queue can advance. + const writes = await Promise.allSettled([ this.updateGlobalState("listApiConfigMeta", listApiConfigMeta), this.updateGlobalState("currentApiConfigName", name), this.providerSettingsManager.setModeConfig(mode, id), this.contextProxy.setProviderSettings(providerSettings), ]) + const failed = writes.find((write): write is PromiseRejectedResult => write.status === "rejected") + + if (failed) { + await restore() + if (rollback) { + await this.contextProxy.setProviderSettings(rollback.previous) + } + throw failed.reason + } // The writes above are committed, so there is nothing to roll back; just skip the follow-up work. if (signal.aborted) return id @@ -2003,6 +2018,10 @@ export class ClineProvider if (value !== null && typeof value !== "string" && typeof value !== "number") { continue } + // The allow-list does not cover the ARN Bedrock calls, so it may only be reset, never set. + if (key === "awsCustomArn" && value !== null && value !== "") { + continue + } merged[key] = value === null ? undefined : value } @@ -2014,13 +2033,14 @@ export class ClineProvider } const previous: Record = { ...stored, id } - await this.upsertProviderProfileUnlocked( - name, - merged as ProviderSettings, - true, - signal, - previous as ProviderSettings, - ) + const isStillApplied = async (): Promise => { + const current: Record = await this.providerSettingsManager.getProfile({ name }) + return Object.keys(merged).every((key) => key === "id" || current[key] === merged[key]) + } + await this.upsertProviderProfileUnlocked(name, merged as ProviderSettings, true, signal, { + previous: previous as ProviderSettings, + isStillApplied, + }) }) } catch (error) { this.log(`Error updating profile model: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index cf30456a60..07cf27fd98 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -586,10 +586,14 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(manager().saveConfig).not.toHaveBeenCalled() }) - it("does not activate its profile when it times out while listing profiles and a later switch has run", async () => { - mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) - vi.useFakeTimers() - try { + describe("when the mutation times out while listing profiles", () => { + const runTimedOutUpdate = async (newerModel?: string) => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + // Like the real store, getProfile reflects what saveConfig last wrote. + manager().saveConfig.mockImplementation(async (profileName, settings) => { + manager().getProfile.mockResolvedValue({ name: profileName, ...settings }) + return "test-id" + }) let resolveList!: (entries: ProviderSettingsEntry[]) => void const listing = new Promise((resolve) => { resolveList = resolve @@ -609,21 +613,83 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS) const switching = provider.activateProviderProfile({ name: "other-config" }) + if (newerModel) { + // A newer selection lands while the timed-out update is still waiting on the listing. + manager().getProfile.mockResolvedValue({ + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: newerModel, + }) + } + resolveList([]) await Promise.all([updating, switching]) + return setValueSpy + } + + beforeEach(() => vi.useFakeTimers()) + afterEach(() => vi.useRealTimers()) + + it("does not activate its profile and rolls back the saved model", async () => { + const setValueSpy = await runTimedOutUpdate() expect(setValueSpy).not.toHaveBeenCalledWith("currentApiConfigName", "test-config") expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") - // The saved model change is rolled back so the stored profile matches the unchanged active state. expect(manager().saveConfig).toHaveBeenCalledTimes(2) expect(manager().saveConfig).toHaveBeenLastCalledWith("test-config", { id: "test-id", apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4", }) - } finally { - vi.useRealTimers() - } + }) + + it("does not roll back over a newer selection saved in the meantime", async () => { + await runTimedOutUpdate("newer/model") + + expect(manager().saveConfig).toHaveBeenCalledTimes(1) + }) + }) + + it("rolls back the saved model when an activation write fails", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + manager().saveConfig.mockImplementation(async (profileName, settings) => { + manager().getProfile.mockResolvedValue({ name: profileName, ...settings }) + return "test-id" + }) + vi.mocked(provider["providerSettingsManager"].setModeConfig).mockRejectedValueOnce(new Error("boom")) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenLastCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + + it("only allows awsCustomArn to be reset, never set", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.bedrock, + apiModelId: "allowed-model", + awsCustomArn: "arn:stored", + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.bedrock, { + apiModelId: "allowed-model", + awsCustomArn: "arn:attacker", + }) + expect(manager().saveConfig.mock.calls[0][1].awsCustomArn).toBe("arn:stored") + + await provider.updateProfileModel("test-config", providerIdentifiers.bedrock, { + apiModelId: "other-model", + awsCustomArn: "", + }) + expect(manager().saveConfig.mock.calls[1][1].awsCustomArn).toBe("") }) it("does not save and does not throw when the profile cannot be loaded", async () => { From 7091f820e87f495b5930e22a78389d754e35c388 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 18:49:13 +0900 Subject: [PATCH 07/11] fix(chat): make model-update rollback robust and reset-only fields strict Rollback steps now run independently and never mask the original write error, restore listApiConfigMeta, currentApiConfigName and the mode mapping, and reasoning/token-limit/ARN fields may only be cleared by the host-side model update. --- src/core/webview/ClineProvider.ts | 44 +++++++++--- .../ClineProvider.apiHandlerRebuild.spec.ts | 72 +++++++++++++++++++ 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ea72edabba..9e6972ec31 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -194,6 +194,13 @@ type GetStateOptions = { includeTaskHistory?: boolean } +const RESET_ONLY_KEYS: readonly string[] = [ + "awsCustomArn", + "reasoningEffort", + "modelMaxTokens", + "modelMaxThinkingTokens", +] + export class ClineProvider extends EventEmitter implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike @@ -1935,6 +1942,12 @@ 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. + const previousActivation = rollback && { + listApiConfigMeta: this.contextProxy.getValues().listApiConfigMeta, + currentApiConfigName: this.contextProxy.getValues().currentApiConfigName, + modeConfigId: await this.providerSettingsManager.getModeConfigId(mode), + } + // allSettled so every started write has finished before the queue can advance. const writes = await Promise.allSettled([ this.updateGlobalState("listApiConfigMeta", listApiConfigMeta), @@ -1945,9 +1958,24 @@ export class ClineProvider const failed = writes.find((write): write is PromiseRejectedResult => write.status === "rejected") if (failed) { - await restore() - if (rollback) { - await this.contextProxy.setProviderSettings(rollback.previous) + // Each rollback step runs even if the other fails, and neither may mask the original error. + const logRollbackError = (error: unknown) => + this.log(`Profile rollback failed: ${error instanceof Error ? error.message : String(error)}`) + await restore().catch(logRollbackError) + if (rollback && previousActivation) { + const { + listApiConfigMeta: prevMeta, + currentApiConfigName: prevName, + modeConfigId, + } = previousActivation + await Promise.allSettled([ + this.contextProxy.setProviderSettings(rollback.previous), + this.updateGlobalState("listApiConfigMeta", prevMeta), + this.updateGlobalState("currentApiConfigName", prevName), + modeConfigId ? this.providerSettingsManager.setModeConfig(mode, modeConfigId) : undefined, + ]).then((results) => + results.forEach((result) => result.status === "rejected" && logRollbackError(result.reason)), + ) } throw failed.reason } @@ -2004,10 +2032,7 @@ export class ClineProvider // Only model selection and its side-effect resets may be patched (see handleModelChangeSideEffects). const allowedKeys: ReadonlySet = new Set([ ...PROVIDER_SETTINGS_KEYS.filter((key) => key.endsWith("ModelId")), - "awsCustomArn", - "reasoningEffort", - "modelMaxTokens", - "modelMaxThinkingTokens", + ...RESET_ONLY_KEYS, ]) const merged: Record = { ...stored, id } for (const [key, value] of Object.entries(patch)) { @@ -2018,8 +2043,9 @@ export class ClineProvider if (value !== null && typeof value !== "string" && typeof value !== "number") { continue } - // The allow-list does not cover the ARN Bedrock calls, so it may only be reset, never set. - if (key === "awsCustomArn" && value !== null && value !== "") { + // These are only ever cleared when the model changes. Setting them would bypass the + // allow-list (the Bedrock ARN) or accept unvalidated token limits and reasoning effort. + if (RESET_ONLY_KEYS.includes(key) && value !== null && !(key === "awsCustomArn" && value === "")) { continue } merged[key] = value === null ? undefined : value diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 07cf27fd98..1ded732a5f 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -672,6 +672,78 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") }) + it("still restores the context settings and logs when restoring the saved profile also fails", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + manager() + .saveConfig.mockImplementationOnce(async (profileName, settings) => { + manager().getProfile.mockResolvedValue({ name: profileName, ...settings }) + return "test-id" + }) + .mockRejectedValueOnce(new Error("restore failed")) + vi.mocked(provider["providerSettingsManager"].setModeConfig).mockRejectedValueOnce(new Error("boom")) + const logSpy = vi.spyOn(provider, "log") + const setProviderSettingsSpy = vi.spyOn(provider.contextProxy, "setProviderSettings") + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(logSpy).toHaveBeenCalledWith("Profile rollback failed: restore failed") + expect(setProviderSettingsSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ openRouterModelId: "openai/gpt-4" }), + ) + // The original activation error is what gets reported, not the rollback error. + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Error updating profile model")) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + + it("accepts reasoning and token-limit fields only as resets", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + reasoningEffort: "high", + modelMaxTokens: 1000, + modelMaxThinkingTokens: 500, + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + reasoningEffort: "low", + modelMaxTokens: 999999999, + modelMaxThinkingTokens: -1, + }) + const kept = manager().saveConfig.mock.calls[0][1] + expect(kept).toMatchObject({ reasoningEffort: "high", modelMaxTokens: 1000, modelMaxThinkingTokens: 500 }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/z", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }) + const cleared = manager().saveConfig.mock.calls[1][1] + expect(cleared.reasoningEffort).toBeUndefined() + expect(cleared.modelMaxTokens).toBeUndefined() + expect(cleared.modelMaxThinkingTokens).toBeUndefined() + }) + + it("restores the activation state when an activation write fails", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + vi.mocked(provider["providerSettingsManager"].getModeConfigId).mockResolvedValue("prev-mode-id") + vi.mocked(provider["providerSettingsManager"].setModeConfig) + .mockRejectedValueOnce(new Error("boom")) + .mockResolvedValue(undefined) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(provider["providerSettingsManager"].setModeConfig).toHaveBeenLastCalledWith( + expect.anything(), + "prev-mode-id", + ) + }) + it("only allows awsCustomArn to be reset, never set", async () => { mockStoredProfile({ apiProvider: providerIdentifiers.bedrock, From 458ba77161d8e7382ae50cf4258acb18e8da43e5 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Tue, 6 Oct 2026 19:11:22 +0900 Subject: [PATCH 08/11] fix(chat): scope model updates to the provider's model field and gate rollbacks Only the stored provider's own model id (plus reset-only fields) is patchable, so the unchecked LM Studio draft model cannot be changed. Recheck cancellation after reading the mode mapping, skip the activation rollback when a newer switch owns the activation, and clear a mode mapping that did not exist before the failed update. --- src/core/config/ProviderSettingsManager.ts | 17 ++++ src/core/webview/ClineProvider.ts | 24 +++-- .../ClineProvider.apiHandlerRebuild.spec.ts | 95 ++++++++++++++++++- 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 3fcc0e6e43..ea78e51082 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -523,6 +523,23 @@ export class ProviderSettingsManager { } } + /** + * Remove the API config mapping for a specific mode. + */ + public async clearModeConfig(mode: Mode) { + try { + return await this.lock(async () => { + const providerProfiles = await this.load() + if (providerProfiles.modeApiConfigs && mode in providerProfiles.modeApiConfigs) { + delete providerProfiles.modeApiConfigs[mode] + await this.store(providerProfiles) + } + }) + } catch (error) { + throw new Error(`Failed to clear mode config: ${error}`) + } + } + /** * Get the API config ID for a specific mode. */ diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 9e6972ec31..1b6ff3f87b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -55,6 +55,7 @@ import { DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, getModelId, + modelIdKeysByProvider, isRetiredProvider, providerIdentifiers, PROVIDER_SETTINGS_KEYS, @@ -1948,6 +1949,8 @@ export class ClineProvider modeConfigId: await this.providerSettingsManager.getModeConfigId(mode), } + if (signal.aborted) return abandon() + // allSettled so every started write has finished before the queue can advance. const writes = await Promise.allSettled([ this.updateGlobalState("listApiConfigMeta", listApiConfigMeta), @@ -1962,7 +1965,9 @@ export class ClineProvider const logRollbackError = (error: unknown) => this.log(`Profile rollback failed: ${error instanceof Error ? error.message : String(error)}`) await restore().catch(logRollbackError) - if (rollback && previousActivation) { + // A newer profile switch that already took over the activation must not be undone. + const ownsActivation = this.contextProxy.getValues().currentApiConfigName === name + if (rollback && previousActivation && ownsActivation) { const { listApiConfigMeta: prevMeta, currentApiConfigName: prevName, @@ -1972,7 +1977,9 @@ export class ClineProvider this.contextProxy.setProviderSettings(rollback.previous), this.updateGlobalState("listApiConfigMeta", prevMeta), this.updateGlobalState("currentApiConfigName", prevName), - modeConfigId ? this.providerSettingsManager.setModeConfig(mode, modeConfigId) : undefined, + modeConfigId + ? this.providerSettingsManager.setModeConfig(mode, modeConfigId) + : this.providerSettingsManager.clearModeConfig(mode), ]).then((results) => results.forEach((result) => result.status === "rejected" && logRollbackError(result.reason)), ) @@ -2030,10 +2037,15 @@ export class ClineProvider } // Only model selection and its side-effect resets may be patched (see handleModelChangeSideEffects). - const allowedKeys: ReadonlySet = new Set([ - ...PROVIDER_SETTINGS_KEYS.filter((key) => key.endsWith("ModelId")), - ...RESET_ONLY_KEYS, - ]) + // The only model field is the stored provider's own, so e.g. the LM Studio draft model, which the + // allow-list does not check, cannot be changed. + const providerModelKey: string | undefined = + storedProvider === providerIdentifiers.openai + ? "openAiModelId" + : modelIdKeysByProvider[storedProvider as keyof typeof modelIdKeysByProvider] + const allowedKeys: ReadonlySet = new Set( + providerModelKey ? [providerModelKey, ...RESET_ONLY_KEYS] : RESET_ONLY_KEYS, + ) const merged: Record = { ...stored, id } for (const [key, value] of Object.entries(patch)) { // The provider is never patchable, otherwise the expectedProvider guard could be bypassed. diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 1ded732a5f..2959643981 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -249,6 +249,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }, ]), setModeConfig: vi.fn(), + clearModeConfig: vi.fn(), getModeConfigId: vi.fn().mockResolvedValue(undefined), activateProfile: vi.fn().mockResolvedValue({ name: "test-config", @@ -693,10 +694,102 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect.objectContaining({ openRouterModelId: "openai/gpt-4" }), ) // The original activation error is what gets reported, not the rollback error. - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Error updating profile model")) + const errorLog = logSpy.mock.calls.map(([message]) => message).find((m) => m.includes("Error updating")) + expect(errorLog).toContain("boom") + expect(errorLog).not.toContain("restore failed") expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") }) + it("clears a mode mapping that did not exist before when an activation write fails", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + vi.mocked(provider["providerSettingsManager"].getModeConfigId).mockResolvedValue(undefined) + vi.mocked(provider["providerSettingsManager"].setModeConfig).mockResolvedValue(undefined) + vi.spyOn(provider.contextProxy, "setProviderSettings").mockRejectedValueOnce(new Error("boom")) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledTimes(1) + }) + + it("does not undo a newer profile switch when its activation write fails late", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + vi.mocked(provider["providerSettingsManager"].setModeConfig).mockImplementationOnce(async () => { + await provider.contextProxy.setValue("currentApiConfigName", "other-config") + throw new Error("boom") + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") + expect(provider["providerSettingsManager"].clearModeConfig).not.toHaveBeenCalled() + }) + + it("ignores model fields that do not belong to the stored provider", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.lmstudio, + lmStudioModelId: "model-a", + lmStudioDraftModelId: "draft-a", + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.lmstudio, { + lmStudioModelId: "model-b", + lmStudioDraftModelId: "draft-evil", + openRouterModelId: "x/y", + }) + + const saved = manager().saveConfig.mock.calls[0][1] + expect(saved.lmStudioModelId).toBe("model-b") + expect(saved.lmStudioDraftModelId).toBe("draft-a") + expect(saved).not.toHaveProperty("openRouterModelId") + }) + + it("rolls back and does not activate when the mutation times out while saving", async () => { + vi.useFakeTimers() + try { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + let releaseSave!: () => void + const saveGate = new Promise((resolve) => { + releaseSave = resolve + }) + manager().saveConfig.mockImplementationOnce(async (profileName, settings) => { + await saveGate + manager().getProfile.mockResolvedValue({ name: profileName, ...settings }) + return "test-id" + }) + manager().activateProfile.mockResolvedValue({ + name: "other-config", + id: "other-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "other/model", + }) + const setValueSpy = vi.spyOn(provider.contextProxy, "setValue") + + const updating = provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + await vi.advanceTimersByTimeAsync(ClineProvider.PENDING_OPERATION_TIMEOUT_MS) + const followUp = provider.activateProviderProfile({ name: "other-config" }) + + releaseSave() + await Promise.all([updating, followUp]) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenLastCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) + expect(setValueSpy).not.toHaveBeenCalledWith("currentApiConfigName", "test-config") + expect(provider.contextProxy.getValues().currentApiConfigName).toBe("other-config") + } finally { + vi.useRealTimers() + } + }) + it("accepts reasoning and token-limit fields only as resets", async () => { mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, From 4dd3dd4292f6aabfdb44e005e5c567e17a4dfe14 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Wed, 7 Oct 2026 19:35:13 +0900 Subject: [PATCH 09/11] test(chat): assert mode rollback target --- .../webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 2959643981..658bc67641 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -711,6 +711,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }) expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledTimes(1) + expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledWith("code") }) it("does not undo a newer profile switch when its activation write fails late", async () => { From 6fe3499163477c30460042817660a6a29d80c0fd Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Wed, 7 Oct 2026 19:50:26 +0900 Subject: [PATCH 10/11] test(chat): fix rollback mode expectation --- .../webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 658bc67641..cb3344d234 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -711,7 +711,7 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { }) expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledTimes(1) - expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledWith("code") + expect(provider["providerSettingsManager"].clearModeConfig).toHaveBeenCalledWith("architect") }) it("does not undo a newer profile switch when its activation write fails late", async () => { From 8cad29bf0673b7f8b0c0d93b5571585ec8450176 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Wed, 7 Oct 2026 20:28:23 +0900 Subject: [PATCH 11/11] fix(chat): guard model update transactions --- src/core/webview/ClineProvider.ts | 188 +++++++++++------- .../ClineProvider.apiHandlerRebuild.spec.ts | 33 +++ 2 files changed, 150 insertions(+), 71 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1b6ff3f87b..77e3e7c63a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -294,9 +294,10 @@ 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( + // Keep later mutations behind the actual operation. A timeout is reported to the + // caller promptly, but an already-started write must finish before another + // mutation can begin and potentially be overwritten by the late write. + this.providerProfileMutationQueue = run.then( () => undefined, () => undefined, ) @@ -1924,84 +1925,115 @@ export class ClineProvider await restore() return id } + const logRollbackError = (error: unknown) => + this.log(`Profile rollback failed: ${error instanceof Error ? error.message : String(error)}`) - if (signal.aborted) return abandon() + try { + if (signal.aborted) return abandon() - if (activate) { - const { mode } = await this.getState() - const listApiConfigMeta = await this.providerSettingsManager.listConfig() + if (activate) { + const { mode } = await this.getState() + const listApiConfigMeta = await this.providerSettingsManager.listConfig() + + if (signal.aborted) return abandon() + + // 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. + const previousActivation = rollback && { + listApiConfigMeta: this.contextProxy.getValues().listApiConfigMeta, + currentApiConfigName: this.contextProxy.getValues().currentApiConfigName, + modeConfigId: await this.providerSettingsManager.getModeConfigId(mode), + } - if (signal.aborted) return abandon() + if (signal.aborted) return abandon() + + // allSettled so every started write has finished before the queue can advance. + const writes = await Promise.allSettled([ + this.updateGlobalState("listApiConfigMeta", listApiConfigMeta), + this.updateGlobalState("currentApiConfigName", name), + this.providerSettingsManager.setModeConfig(mode, id), + this.contextProxy.setProviderSettings(providerSettings), + ]) + const failed = writes.find((write): write is PromiseRejectedResult => write.status === "rejected") + + if (failed) { + // Each rollback step runs even if the other fails, and neither may mask the original error. + await restore().catch(logRollbackError) + // A newer profile switch that already took over the activation must not be undone. + const ownsActivation = this.contextProxy.getValues().currentApiConfigName === name + if (rollback && previousActivation && ownsActivation) { + const { + listApiConfigMeta: prevMeta, + currentApiConfigName: prevName, + modeConfigId, + } = previousActivation + await Promise.allSettled([ + this.contextProxy.setProviderSettings(rollback.previous), + this.updateGlobalState("listApiConfigMeta", prevMeta), + this.updateGlobalState("currentApiConfigName", prevName), + modeConfigId + ? this.providerSettingsManager.setModeConfig(mode, modeConfigId) + : this.providerSettingsManager.clearModeConfig(mode), + ]).then((results) => + results.forEach( + (result) => result.status === "rejected" && logRollbackError(result.reason), + ), + ) + } + throw failed.reason + } - // 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. - const previousActivation = rollback && { - listApiConfigMeta: this.contextProxy.getValues().listApiConfigMeta, - currentApiConfigName: this.contextProxy.getValues().currentApiConfigName, - modeConfigId: await this.providerSettingsManager.getModeConfigId(mode), - } + // The writes above are committed, so there is nothing to roll back; just skip the follow-up work. + if (signal.aborted) return id - if (signal.aborted) return abandon() + // Change the provider for the current task. + // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). + this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true }) - // allSettled so every started write has finished before the queue can advance. - const writes = await Promise.allSettled([ - this.updateGlobalState("listApiConfigMeta", listApiConfigMeta), - this.updateGlobalState("currentApiConfigName", name), - this.providerSettingsManager.setModeConfig(mode, id), - this.contextProxy.setProviderSettings(providerSettings), - ]) - const failed = writes.find((write): write is PromiseRejectedResult => write.status === "rejected") - - if (failed) { - // Each rollback step runs even if the other fails, and neither may mask the original error. - const logRollbackError = (error: unknown) => - this.log(`Profile rollback failed: ${error instanceof Error ? error.message : String(error)}`) - await restore().catch(logRollbackError) - // A newer profile switch that already took over the activation must not be undone. - const ownsActivation = this.contextProxy.getValues().currentApiConfigName === name - if (rollback && previousActivation && ownsActivation) { - const { - listApiConfigMeta: prevMeta, - currentApiConfigName: prevName, - modeConfigId, - } = previousActivation - await Promise.allSettled([ - this.contextProxy.setProviderSettings(rollback.previous), - this.updateGlobalState("listApiConfigMeta", prevMeta), - this.updateGlobalState("currentApiConfigName", prevName), - modeConfigId - ? this.providerSettingsManager.setModeConfig(mode, modeConfigId) - : this.providerSettingsManager.clearModeConfig(mode), - ]).then((results) => - results.forEach((result) => result.status === "rejected" && logRollbackError(result.reason)), - ) - } - throw failed.reason + // 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()) } - // The writes above are committed, so there is nothing to roll back; just skip the follow-up work. - 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 }) + await this.postStateToWebview() + return id + } catch (error) { + // Saving succeeds before the activation preparation reads. Restore it when any + // later read or write fails so storage cannot diverge from the active context. + await restore().catch(logRollbackError) + throw error + } + } - // 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()) + private getOrganizationAllowListForProfileMutation() { + if (!CloudService.hasInstance()) { + return ORGANIZATION_ALLOW_ALL } - await this.postStateToWebview() - return id + try { + const cloudService = CloudService.instance + // An unauthenticated user has no organization policy to enforce. Once + // authenticated, absence of cached settings is not evidence of an + // unrestricted policy, so callers must reject the mutation. + if (!cloudService.isAuthenticated()) { + return ORGANIZATION_ALLOW_ALL + } + return cloudService.getOrganizationSettings()?.allowList + } catch (error) { + this.log( + `Unable to read organization allow-list for model update: ${error instanceof Error ? error.message : String(error)}`, + ) + return undefined + } } /** @@ -2014,7 +2046,8 @@ export class ClineProvider await this.enqueueProviderProfileMutation(async (signal) => { // Mirrors the profile name the webview is shown (see getStateToPostToWebview). const task = this.getCurrentTask() - const { currentApiConfigName, organizationAllowList } = await this.getState() + const { currentApiConfigName, organizationAllowList: stateOrganizationAllowList } = + await this.getState() const visibleProfileName = task ? task.taskApiConfigName : currentApiConfigName if (visibleProfileName !== name) { @@ -2063,7 +2096,20 @@ export class ClineProvider merged[key] = value === null ? undefined : value } + const authoritativeOrganizationAllowList = this.getOrganizationAllowListForProfileMutation() // The webview filters models, but this message can be sent by anything; enforce the allow-list here. + // Authenticated users without cached organization settings are denied rather than + // treating an unavailable policy as an unrestricted one. + if (!authoritativeOrganizationAllowList) { + this.log(`Ignoring model update for profile '${name}': organization allow-list is unavailable`) + vscode.window.showErrorMessage(t("common:errors.violated_organization_allowlist")) + return + } + // Preserve a restrictive policy already present in state. This avoids a + // transiently stale permissive cache widening the set of accepted models. + const organizationAllowList = stateOrganizationAllowList.allowAll + ? authoritativeOrganizationAllowList + : stateOrganizationAllowList if (!ProfileValidator.isProfileAllowed(merged as ProviderSettings, organizationAllowList)) { this.log(`Ignoring model update for profile '${name}': violates the organization allow-list`) vscode.window.showErrorMessage(t("common:errors.violated_organization_allowlist")) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index cb3344d234..6ae95a362d 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -537,6 +537,18 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(manager().saveConfig).toHaveBeenCalledTimes(1) }) + it("rejects a model update when the authenticated organization policy is unavailable", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "allowed/model" }) + provider["getOrganizationAllowListForProfileMutation"] = vi.fn().mockReturnValue(undefined) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "blocked/model", + }) + + expect(manager().saveConfig).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.violated_organization_allowlist") + }) + it("drops an update for a profile that is not the visible profile without reading or saving it", async () => { await provider.updateProfileModel("other-config", providerIdentifiers.openrouter, { openRouterModelId: "x/y", @@ -673,6 +685,27 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") }) + it("rolls back the saved model when activation preparation fails", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) + manager().saveConfig.mockImplementation(async (profileName, settings) => { + manager().getProfile.mockResolvedValue({ name: profileName, ...settings }) + return "test-id" + }) + vi.mocked(provider["providerSettingsManager"].listConfig).mockRejectedValueOnce(new Error("listing failed")) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenLastCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + it("still restores the context settings and logs when restoring the saved profile also fails", async () => { mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4" }) manager()