From 59c5af3cc7dd7638d06171bd23d3af8ddd11d828 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Wed, 7 Oct 2026 23:36:10 +0900 Subject: [PATCH 1/6] feat(chat): add inline model selector to chat composer (#1502) --- .../electron-chat-dark-sidebar.png | Bin 30321 -> 29896 bytes packages/types/src/vscode-extension-host.ts | 1 + src/core/webview/ClineProvider.ts | 132 ++ .../ClineProvider.updateProfileModel.spec.ts | 423 ++++++ ...wMessageHandler.updateProfileModel.spec.ts | 41 + src/core/webview/webviewMessageHandler.ts | 10 + .../src/components/chat/ChatTextArea.tsx | 24 + .../src/components/chat/ModelSelector.tsx | 289 ++++ .../chat/__tests__/ChatTextArea.spec.tsx | 92 ++ .../chat/__tests__/ChatTextArea.visual.tsx | 2 +- .../chat/__tests__/ModelSelector.spec.tsx | 1305 +++++++++++++++++ .../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 + .../components/ui/hooks/useRouterModels.ts | 18 +- 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 + 57 files changed, 2407 insertions(+), 3 deletions(-) create mode 100644 src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts 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/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index f440c9f037..ecf0aa5ea5 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/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 07772816b4..908e834060 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -55,9 +55,17 @@ import { DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, getModelId, + modelIdKeysByProvider, isRetiredProvider, providerIdentifiers, } from "@roo-code/types" + +const RESET_ONLY_KEYS: readonly string[] = [ + "awsCustomArn", + "reasoningEffort", + "modelMaxTokens", + "modelMaxThinkingTokens", +] import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" import { TaskRegistry } from "../task/TaskRegistry" import { TaskScheduler } from "../task/TaskScheduler" @@ -1930,6 +1938,130 @@ export class ClineProvider } } + private getOrganizationAllowListForProfileMutation() { + if (!CloudService.hasInstance()) { + return ORGANIZATION_ALLOW_ALL + } + + try { + const cloudService = CloudService.instance + 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 + } + } + + /** + * 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. + */ + async updateProfileModel(name: string, expectedProvider: string, patch: Record): Promise { + try { + await this.enqueueProviderProfileMutation(async (signal) => { + // Mirrors the profile name the webview is shown (see getStateToPostToWebview). + const task = this.getCurrentTask() + const { currentApiConfigName, organizationAllowList: stateOrganizationAllowList } = + 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 + } + + // Only model selection and its side-effect resets may be patched (see handleModelChangeSideEffects). + 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. + if (key === "apiProvider" || !allowedKeys.has(key)) { + continue + } + if (value !== null && typeof value !== "string" && typeof value !== "number") { + continue + } + if (RESET_ONLY_KEYS.includes(key) && value !== null && !(key === "awsCustomArn" && value === "")) { + continue + } + merged[key] = value === null ? undefined : value + } + + const authoritativeOrganizationAllowList = this.getOrganizationAllowListForProfileMutation() + 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 + } + if ( + !ProfileValidator.isProfileAllowed(merged as ProviderSettings, authoritativeOrganizationAllowList) + ) { + this.log( + `Ignoring model update for profile '${name}': violates authoritative organization allow-list`, + ) + vscode.window.showErrorMessage(t("common:errors.violated_organization_allowlist")) + return + } + if ( + !stateOrganizationAllowList.allowAll && + !ProfileValidator.isProfileAllowed(merged as ProviderSettings, stateOrganizationAllowList) + ) { + this.log(`Ignoring model update for profile '${name}': violates state organization allow-list`) + vscode.window.showErrorMessage(t("common:errors.violated_organization_allowlist")) + return + } + + const savedId = await this.providerSettingsManager.saveConfig(name, merged as ProviderSettings) + + if (signal.aborted) return + + const { mode } = await this.getState() + + await Promise.all([ + this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()), + this.updateGlobalState("currentApiConfigName", name), + this.providerSettingsManager.setModeConfig(mode, savedId), + this.contextProxy.setProviderSettings(merged as ProviderSettings), + ]) + + if (signal.aborted) return + + this.updateTaskApiHandlerIfNeeded(merged as ProviderSettings, { forceRebuild: true }) + await this.persistStickyProviderProfileToCurrentTask(name) + await this.postStateToWebview() + }) + } 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.updateProfileModel.spec.ts b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts new file mode 100644 index 0000000000..bc65f28c26 --- /dev/null +++ b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts @@ -0,0 +1,423 @@ +// npx vitest run core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts + +import * as vscode from "vscode" +import { makeCompositeDisposable } from "../../../test-utils/vscode" +import { TelemetryService } from "@roo-code/telemetry" +import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" + +import { ContextProxy } from "../../config/ContextProxy" +import { ClineProvider } from "../ClineProvider" +import { WebviewFocusTracker } from "../WebviewFocusTracker" +import { Task } from "../../task/Task" + +vi.mock("vscode", () => ({ + ExtensionContext: vi.fn(), + OutputChannel: vi.fn(), + WebviewView: vi.fn(), + Disposable: { from: (...subscriptions: vscode.Disposable[]) => makeCompositeDisposable(...subscriptions) }, + Uri: { + joinPath: vi.fn(), + file: vi.fn(), + }, + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, + commands: { + executeCommand: vi.fn().mockResolvedValue(undefined), + }, + window: { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), + }, + workspace: { + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue([]), + update: vi.fn(), + }), + onDidChangeConfiguration: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })), + onDidSaveTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + }, + env: { + uriScheme: "vscode", + language: "en", + appName: "Visual Studio Code", + }, + ExtensionMode: { + Production: 1, + Development: 2, + Test: 3, + }, + version: "1.85.0", +})) + +vi.mock("../../../utils/tts", () => ({ + setTtsEnabled: vi.fn(), + setTtsSpeed: vi.fn(), +})) + +vi.mock("../../../api", () => ({ + buildApiHandler: vi.fn(), +})) + +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(function () { + return { + initializeFilePaths: vi.fn(), + dispose: vi.fn(), + } + }), +})) + +vi.mock("fs/promises", () => ({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(""), + unlink: vi.fn().mockResolvedValue(undefined), + rmdir: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../utils/storage", () => ({ + getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings/path"), + getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/task/path"), + getGlobalStoragePath: vi.fn().mockResolvedValue("/test/storage/path"), +})) + +vi.mock("../../task/Task", () => ({ + Task: vi.fn().mockImplementation(function ( + this: Record, + options: { historyItem?: { id: string }; apiConfiguration?: ProviderSettings } | undefined, + ) { + this.historyItem = options?.historyItem + this.abortTask = vi.fn() + this.handleWebviewAskResponse = vi.fn() + this.clineMessages = [] + this.apiConversationHistory = [] + this.overwriteClineMessages = vi.fn() + this.overwriteApiConversationHistory = vi.fn() + this.taskId = options?.historyItem?.id || "test-task-id" + this.emit = vi.fn() + this.setTaskApiConfigName = vi.fn() + this.updateApiConfiguration = vi.fn().mockImplementation((newConfig: ProviderSettings) => { + this.apiConfiguration = newConfig + }) + this.apiConfiguration = options?.apiConfiguration || { + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + } + }), +})) + +vi.mock("../../../i18n", async (importOriginal) => ({ + ...(await importOriginal()), + t: (key: string) => key, +})) + +vi.mock("@roo-code/cloud", () => ({ + getRooCodeApiUrl: vi.fn().mockReturnValue("https://api.roocode.com"), + CloudService: { + hasInstance: vi.fn().mockReturnValue(true), + get instance() { + return { + isCloudAgent: false, + isAuthenticated: vi.fn().mockReturnValue(false), + getAllowList: vi.fn().mockReturnValue({ allowAll: true }), + getOrganizationSettings: vi.fn().mockReturnValue(undefined), + getUserInfo: vi.fn().mockReturnValue(null), + on: vi.fn(), + off: vi.fn(), + } + }, + }, +})) + +describe("ClineProvider - updateProfileModel", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + + const manager = () => { + const settingsManager = provider["providerSettingsManager"] + return { + getProfile: vi.mocked(settingsManager.getProfile), + saveConfig: vi.mocked(settingsManager.saveConfig), + activateProfile: vi.mocked(settingsManager.activateProfile), + } + } + + const mockStoredProfile = (profile: ProviderSettings & { name?: string }) => + manager().getProfile.mockResolvedValue({ name: "test-config", id: "test-id", ...profile }) + + beforeEach(async () => { + vi.clearAllMocks() + + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + + const secrets: Record = {} + mockContext = { + globalState: { + get: vi.fn().mockImplementation((key: string) => { + if (key === "currentApiConfigName") return "test-config" + if (key === "listApiConfigMeta") { + return [ + { + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + }, + ] + } + return undefined + }), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + setKeysForSync: vi.fn(), + }, + secrets: { + get: vi.fn().mockImplementation((key: string) => secrets[key]), + store: vi.fn().mockImplementation((key: string, value: string) => { + return (secrets[key] = value) + }), + delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), + }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, + subscriptions: [], + extension: { packageJSON: { version: "1.0.0" } }, + extensionUri: { fsPath: "/test/extension/path" }, + globalStorageUri: { fsPath: "/test/storage/path" }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { + appendLine: vi.fn(), + clear: vi.fn(), + dispose: vi.fn(), + } as unknown as vscode.OutputChannel + + mockWebviewView = { + webview: { + postMessage: vi.fn(), + html: "", + options: {}, + onDidReceiveMessage: vi.fn(), + asWebviewUri: vi.fn(), + }, + visible: true, + onDidDispose: vi.fn().mockImplementation((cb) => { + cb() + return { dispose: vi.fn() } + }), + onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })), + } as unknown as vscode.WebviewView + + provider = new ClineProvider( + mockContext, + mockOutputChannel, + "sidebar", + new ContextProxy(mockContext), + new WebviewFocusTracker(), + ) + + // Test double for providerSettingsManager + Object.defineProperty(provider, "providerSettingsManager", { + value: { + saveConfig: vi.fn().mockResolvedValue("test-id"), + listConfig: vi.fn().mockResolvedValue([ + { + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + modelId: "openai/gpt-4", + }, + ]), + setModeConfig: vi.fn(), + getModeConfigId: vi.fn().mockResolvedValue(undefined), + activateProfile: vi.fn().mockResolvedValue({ + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }), + getProfile: vi.fn().mockResolvedValue({ + name: "test-config", + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }), + }, + configurable: true, + writable: true, + }) + + await provider.contextProxy.setValue("currentApiConfigName", "test-config") + }) + + 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("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("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", + }) + + 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 () => { + // Test double with minimal options + const mockTask = new Task({} as unknown as ConstructorParameters[0]) + 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() + }) +}) 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..86d5ce34e2 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts @@ -0,0 +1,41 @@ +// npx vitest run core/webview/__tests__/webviewMessageHandler.updateProfileModel.spec.ts + +import { providerIdentifiers } from "@roo-code/types" + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +describe("webviewMessageHandler - updateProfileModel", () => { + let mockProvider: { updateProfileModel: ReturnType } + + const send = (values: Record | undefined, text: string = "default") => + webviewMessageHandler(mockProvider as unknown as ClineProvider, { type: "updateProfileModel", text, values }) + + beforeEach(() => { + vi.clearAllMocks() + mockProvider = { updateProfileModel: vi.fn().mockResolvedValue(undefined) } + }) + + 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 }) + + 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.updateProfileModel).not.toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4ba94d454c..5816a3c398 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2283,6 +2283,16 @@ export const webviewMessageHandler = async ( await provider.upsertProviderProfile(message.text, message.apiConfiguration) } break + case "updateProfileModel": { + const expectedProvider: unknown = message.values?.expectedProvider + const patch: unknown = message.values?.patch + if (!message.text || typeof expectedProvider !== "string" || typeof patch !== "object" || patch === null) { + break + } + + await provider.updateProfileModel(message.text, expectedProvider, patch as Record) + 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 a78f3f0f8e..0d901e43b1 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -27,6 +27,7 @@ import { StandardTooltip } from "@src/components/ui" import Thumbnails from "../common/Thumbnails" import { ModeSelector } from "./ModeSelector" import { ApiConfigSelector } from "./ApiConfigSelector" +import { ModelSelector, type ModelSelection } 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( + (selection: ModelSelection) => { + vscode.postMessage({ + type: "updateProfileModel", + text: currentApiConfigName, + values: { expectedProvider: selection.expectedProvider, patch: selection.patch }, + }) + }, + [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/ModelSelector.tsx b/webview-ui/src/components/chat/ModelSelector.tsx new file mode 100644 index 0000000000..b96a10cdf9 --- /dev/null +++ b/webview-ui/src/components/chat/ModelSelector.tsx @@ -0,0 +1,289 @@ +import { useState, useMemo, useCallback } from "react" +import { Fzf } from "fzf" + +import { + type ModelInfo, + type ModelRecord, + type OrganizationAllowList, + type ProviderName, + 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, + 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: (selection: ModelSelection) => 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(() => { + 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). + 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(() => { + 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" }) + setOpen(false) + }, []) + + const handleSelect = useCallback( + (modelId: string) => { + if (!modelConfig) { + return + } + + const patch: ModelSelectionPatch = {} + const setField = (field: K, value: ProviderSettings[K]) => { + patch[field] = typeof value === "string" || typeof value === "number" ? value : null + } + handleModelChangeSideEffects(provider, modelId, setField) + patch[modelConfig.field] = modelId + + onChange({ expectedProvider: provider, patch }) + + setOpen(false) + setSearchValue("") + }, + [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 5cced6a92f..1322825d23 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", }) @@ -1239,6 +1252,85 @@ 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.toHaveBeenCalled() + }) + + 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).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") + }) + }) + 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..5abdff19d8 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx @@ -0,0 +1,1305 @@ +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({ + 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 () => { + 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).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", () => { + render( + , + ) + fireEvent.click(screen.getByTestId("model-selector-trigger")) + + fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) + + 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() + }) + + 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({ + expectedProvider: providerIdentifiers.openrouter, + patch: { + openRouterModelId: "openrouter/model-b", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) + }) + + 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({ + 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", () => { + 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({ + expectedProvider: providerIdentifiers.bedrock, + patch: { + apiModelId: "anthropic.claude-3-5-haiku-20241022-v1:0", + awsCustomArn: "", + reasoningEffort: null, + modelMaxTokens: null, + modelMaxThinkingTokens: null, + }, + }) + }) + + 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 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 G2L3Of2;d|D diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-high-contrast.png index 465ffc0263d816e953f37275f9ceb514223f33bf..dbe2d65f2dfbf0d17887c5fd7ef74b5b17cc4623 100644 GIT binary patch literal 10313 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�T)Xjeo%zI=0Jw}Nu$swu0L3TL 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 L@}d>O`houi$=aJZ diff --git a/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-light.png b/webview-ui/src/components/chat/__tests__/__screenshots__/chat-composer-resting-light.png index 1f238a4619657763f15f05e20656ec0f08a03eb9..1cb9115d042991a9037e9d68a43d80aa5209255c 100644 GIT binary patch literal 10017 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 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/components/ui/hooks/useRouterModels.ts b/webview-ui/src/components/ui/hooks/useRouterModels.ts index a7d6b36725..25ac8e142b 100644 --- a/webview-ui/src/components/ui/hooks/useRouterModels.ts +++ b/webview-ui/src/components/ui/hooks/useRouterModels.ts @@ -14,7 +14,7 @@ type UseRouterModelsOptions = { enabled?: boolean // gate fetching entirely } -export const fetchRouterModels = async (provider?: string) => +export const fetchRouterModels = async (provider?: string, signal?: AbortSignal) => new Promise((resolve, reject) => { const cleanup = () => { if (typeof window !== "undefined") { @@ -22,11 +22,24 @@ export const fetchRouterModels = async (provider?: string) => } } + if (signal?.aborted) { + reject(new Error("Router models request aborted")) + return + } + const timeout = setTimeout(() => { cleanup() reject(new Error("Router models request timed out")) }, 10000) + const abortHandler = () => { + clearTimeout(timeout) + cleanup() + reject(new Error("Router models request aborted")) + } + + signal?.addEventListener("abort", abortHandler, { once: true }) + const handler = (event: MessageEvent) => { const message: ExtensionMessage = event.data @@ -40,6 +53,7 @@ export const fetchRouterModels = async (provider?: string) => } clearTimeout(timeout) + signal?.removeEventListener("abort", abortHandler) cleanup() if (message.routerModels) { @@ -62,7 +76,7 @@ export const useRouterModels = (opts: UseRouterModelsOptions = {}) => { const provider = opts.provider || undefined return useQuery({ queryKey: [RouterModelsMessageType.routerModels, provider || allRouterModelsProvider], - queryFn: () => fetchRouterModels(provider), + queryFn: ({ signal }) => fetchRouterModels(provider, signal), enabled: opts.enabled !== false, }) } diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 76ec98e8f2..9dc312ac08 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 eb0903d0ba..75a8d36483 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 d3d219a54c..50bfeb1f0d 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 602a109faa..020fc62551 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 ffff4caada..08cf54aa58 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 3ac03b47c1..9e43868835 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 2833e146b9..b808f06f15 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 5f4daf25e3..1dc25f9875 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 868232b977..50d1c03b50 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 623a391b56..7a4f6899e8 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 9bb3d0e3dc..74ed3fc28e 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 21f903ced2..a2718babfe 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 1de53f8d2f..2fb3b3f347 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 77f561b679..bc55f0dc79 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 83c82c9aa2..5c75cd72bf 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 5ca5b9eb46..bb65fb75c2 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 bb7caf68c2..5606f2696c 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 4675e39848..0c947c709a 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 57ff5d9ba6b3ffe6542b13474c757d00b47bd839 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 8 Oct 2026 09:13:12 +0900 Subject: [PATCH 2/6] fix(chat): address PR review findings on model selector - Isolate task model updates to avoid overwriting global active profile or mode mapping - Add rollback on post-save write failure or aborted profile mutation - Unify and clean up timeout and abort listeners in fetchRouterModels - Add unit tests for useRouterModels lifecycle and profile rollback --- src/core/webview/ClineProvider.ts | 46 ++- .../ClineProvider.updateProfileModel.spec.ts | 100 +++++- .../chat/__tests__/ChatTextArea.spec.tsx | 28 ++ .../hooks/__tests__/useRouterModels.spec.ts | 289 ++++++++++++++++++ .../components/ui/hooks/useRouterModels.ts | 20 +- 5 files changed, 462 insertions(+), 21 deletions(-) create mode 100644 webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 908e834060..7ed0f777ce 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2037,20 +2037,42 @@ export class ClineProvider return } - const savedId = await this.providerSettingsManager.saveConfig(name, merged as ProviderSettings) - - if (signal.aborted) return - - const { mode } = await this.getState() + let savedConfig = false + let updatedContext = false + try { + await this.providerSettingsManager.saveConfig(name, merged as ProviderSettings) + savedConfig = true - await Promise.all([ - this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()), - this.updateGlobalState("currentApiConfigName", name), - this.providerSettingsManager.setModeConfig(mode, savedId), - this.contextProxy.setProviderSettings(merged as ProviderSettings), - ]) + if (signal.aborted) { + throw new Error("Provider profile mutation aborted") + } - if (signal.aborted) return + await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) + if (name === currentApiConfigName) { + await this.contextProxy.setProviderSettings(merged as ProviderSettings) + updatedContext = true + } + } catch (updateError) { + if (savedConfig) { + try { + await this.providerSettingsManager.saveConfig(name, { ...stored, id } as ProviderSettings) + await this.updateGlobalState( + "listApiConfigMeta", + await this.providerSettingsManager.listConfig(), + ) + if (updatedContext) { + await this.contextProxy.setProviderSettings({ ...stored, id } as ProviderSettings) + } + } catch (rollbackError) { + this.log( + `Failed to rollback profile '${name}' after update failure: ${ + rollbackError instanceof Error ? rollbackError.message : String(rollbackError) + }`, + ) + } + } + throw updateError + } this.updateTaskApiHandlerIfNeeded(merged as ProviderSettings, { forceRebuild: true }) await this.persistStickyProviderProfileToCurrentTask(name) diff --git a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts index bc65f28c26..dfeaa742c6 100644 --- a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts @@ -148,6 +148,8 @@ describe("ClineProvider - updateProfileModel", () => { getProfile: vi.mocked(settingsManager.getProfile), saveConfig: vi.mocked(settingsManager.saveConfig), activateProfile: vi.mocked(settingsManager.activateProfile), + setModeConfig: vi.mocked(settingsManager.setModeConfig), + listConfig: vi.mocked(settingsManager.listConfig), } } @@ -398,13 +400,15 @@ describe("ClineProvider - updateProfileModel", () => { 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 () => { + it("uses the task's sticky profile name as the visible profile when a task is active without modifying global profile or mode config", async () => { // Test double with minimal options const mockTask = new Task({} as unknown as ConstructorParameters[0]) Object.defineProperty(mockTask, "taskApiConfigName", { value: "sticky-config" }) await provider.addClineToStack(mockTask) mockStoredProfile({ name: "sticky-config", apiProvider: providerIdentifiers.openrouter }) + const setProviderSettingsSpy = vi.spyOn(provider.contextProxy, "setProviderSettings") + await provider.updateProfileModel("sticky-config", providerIdentifiers.openrouter, { openRouterModelId: "x/y", }) @@ -412,6 +416,12 @@ describe("ClineProvider - updateProfileModel", () => { "sticky-config", expect.objectContaining({ openRouterModelId: "x/y" }), ) + expect(manager().setModeConfig).not.toHaveBeenCalled() + expect(provider.contextProxy.getValues().currentApiConfigName).toBe("test-config") + expect(setProviderSettingsSpy).not.toHaveBeenCalled() + expect(mockTask.updateApiConfiguration).toHaveBeenCalledWith( + expect.objectContaining({ openRouterModelId: "x/y" }), + ) manager().saveConfig.mockClear() // The global profile name is not what the webview is shown while a task is active. @@ -420,4 +430,92 @@ describe("ClineProvider - updateProfileModel", () => { }) expect(manager().saveConfig).not.toHaveBeenCalled() }) + + it("updates contextProxy provider settings when updating the current global profile", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) + + const setProviderSettingsSpy = vi.spyOn(provider.contextProxy, "setProviderSettings") + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + expect(setProviderSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ openRouterModelId: "openai/gpt-4.5" }), + ) + expect(manager().setModeConfig).not.toHaveBeenCalled() + }) + + it("rolls back saved profile if post-save state update fails", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + openRouterApiKey: "my-key", + }) + + let listConfigCalls = 0 + manager().listConfig.mockImplementation(async () => { + listConfigCalls++ + if (listConfigCalls === 1) { + throw new Error("Global state sync failed") + } + return [] + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + // First call saves the patched model, second call rolls back to original stored config + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 1, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4.5" }), + ) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 2, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4", openRouterApiKey: "my-key" }), + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + + it("rolls back saved profile if mutation signal is aborted after saveConfig", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + openRouterApiKey: "my-key", + }) + + const controller = new AbortController() + provider["enqueueProviderProfileMutation"] = vi.fn().mockImplementation(async (fn) => { + return fn(controller.signal) + }) + + manager().saveConfig.mockImplementationOnce(async () => { + controller.abort() + return "test-id" + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 1, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4.5" }), + ) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 2, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4", openRouterApiKey: "my-key" }), + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) }) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index 1322825d23..5ce0df4bf5 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1329,6 +1329,34 @@ describe("ChatTextArea", () => { }) expect(message).not.toHaveProperty("apiConfiguration") }) + + it("updates the target profile when currentApiConfigName changes", () => { + const { rerender } = render() + + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + apiConfiguration: { apiProvider: providerIdentifiers.anthropic }, + currentTaskId: "task-local-context", + currentApiConfigName: "updated-profile", + taskHistory: [], + cwd: "/test/workspace", + }) + + rerender() + + mockPostMessage.mockClear() + fireEvent.click(screen.getByTestId("model-selector-trigger")) + fireEvent.click(screen.getAllByText(/claude-3-5-haiku/i)[0]) + + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateProfileModel", + text: "updated-profile", + }), + ) + }) }) describe("blank suggestion copy crash (issue #1226)", () => { diff --git a/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts new file mode 100644 index 0000000000..7286b6db91 --- /dev/null +++ b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts @@ -0,0 +1,289 @@ +import React from "react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { renderHook, waitFor } from "@testing-library/react" +import { + allRouterModelsProvider, + providerIdentifiers, + RouterModelsMessageType, + type RouterModels, +} from "@roo-code/types" + +import { vscode } from "@src/utils/vscode" +import { fetchRouterModels, useRouterModels } from "../useRouterModels" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +const mockRouterModels = { + [providerIdentifiers.openrouter]: { + "anthropic/claude-3.5-sonnet": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + }, + }, +} as unknown as RouterModels + +describe("fetchRouterModels", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useRealTimers() + }) + + it("posts requestRouterModels with provider when specified", async () => { + const promise = fetchRouterModels(providerIdentifiers.openrouter) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + values: { provider: providerIdentifiers.openrouter }, + }) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: mockRouterModels, + }, + }), + ) + + const result = await promise + expect(result).toEqual(mockRouterModels) + }) + + it("posts requestRouterModels without provider when omitted", async () => { + const promise = fetchRouterModels() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + }) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: undefined }, + routerModels: mockRouterModels, + }, + }), + ) + + const result = await promise + expect(result).toEqual(mockRouterModels) + }) + + it("ignores mismatched provider messages and resolves when matching message arrives", async () => { + const promise = fetchRouterModels(providerIdentifiers.openrouter) + + // Send message with different provider - should be ignored + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.ollama }, + routerModels: {}, + }, + }), + ) + + // Send matching message + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: mockRouterModels, + }, + }), + ) + + const result = await promise + expect(result).toEqual(mockRouterModels) + }) + + it("ignores unrelated messages", async () => { + const promise = fetchRouterModels(providerIdentifiers.openrouter) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: "unrelatedMessageType", + }, + }), + ) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: mockRouterModels, + }, + }), + ) + + const result = await promise + expect(result).toEqual(mockRouterModels) + }) + + it("rejects when routerModels field is missing in matching response", async () => { + const promise = fetchRouterModels(providerIdentifiers.openrouter) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + }, + }), + ) + + await expect(promise).rejects.toThrow("No router models in response") + }) + + it("rejects immediately if signal is already aborted", async () => { + const controller = new AbortController() + controller.abort() + + await expect(fetchRouterModels(providerIdentifiers.openrouter, controller.signal)).rejects.toThrow( + "Router models request aborted", + ) + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + + it("rejects and cleans up when signal aborts while waiting", async () => { + const controller = new AbortController() + const removeEventListenerSpy = vi.spyOn(window, "removeEventListener") + + const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + + controller.abort() + + await expect(promise).rejects.toThrow("Router models request aborted") + expect(removeEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + }) + + it("rejects and cleans up abort listener when timeout expires", async () => { + vi.useFakeTimers() + const controller = new AbortController() + const signalRemoveEventListenerSpy = vi.spyOn(controller.signal, "removeEventListener") + const windowRemoveEventListenerSpy = vi.spyOn(window, "removeEventListener") + + const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + + vi.advanceTimersByTime(10000) + + await expect(promise).rejects.toThrow("Router models request timed out") + expect(signalRemoveEventListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function)) + expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + }) + + it("removes abort listener and cleans up timer on successful response", async () => { + vi.useFakeTimers() + const controller = new AbortController() + const signalRemoveEventListenerSpy = vi.spyOn(controller.signal, "removeEventListener") + + const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: mockRouterModels, + }, + }), + ) + + const result = await promise + expect(result).toEqual(mockRouterModels) + expect(signalRemoveEventListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function)) + + // Advancing timers should not cause rejection or error + vi.advanceTimersByTime(15000) + // Aborting afterwards should not trigger rejection + controller.abort() + }) +}) + +describe("useRouterModels", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useRealTimers() + }) + + const makeQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } }) + + it("fetches router models with the specified provider", async () => { + const queryClient = makeQueryClient() + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children) + + const { result } = renderHook(() => useRouterModels({ provider: providerIdentifiers.openrouter }), { wrapper }) + + await waitFor(() => + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + values: { provider: providerIdentifiers.openrouter }, + }), + ) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: providerIdentifiers.openrouter }, + routerModels: mockRouterModels, + }, + }), + ) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(result.current.data).toEqual(mockRouterModels) + }) + + it("uses allRouterModelsProvider in query key when provider is not specified", async () => { + const queryClient = makeQueryClient() + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children) + + const { result } = renderHook(() => useRouterModels(), { wrapper }) + + await waitFor(() => + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + }), + ) + + const queryState = queryClient.getQueryCache().findAll() + expect(queryState[0].queryKey).toEqual([RouterModelsMessageType.routerModels, allRouterModelsProvider]) + + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: RouterModelsMessageType.routerModels, + values: { provider: undefined }, + routerModels: mockRouterModels, + }, + }), + ) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + }) + + it("does not fetch when enabled is false", async () => { + const queryClient = makeQueryClient() + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children) + + renderHook(() => useRouterModels({ provider: providerIdentifiers.openrouter, enabled: false }), { wrapper }) + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/components/ui/hooks/useRouterModels.ts b/webview-ui/src/components/ui/hooks/useRouterModels.ts index 25ac8e142b..66f6a4ff08 100644 --- a/webview-ui/src/components/ui/hooks/useRouterModels.ts +++ b/webview-ui/src/components/ui/hooks/useRouterModels.ts @@ -16,7 +16,14 @@ type UseRouterModelsOptions = { export const fetchRouterModels = async (provider?: string, signal?: AbortSignal) => new Promise((resolve, reject) => { + let timeout: ReturnType | undefined + const cleanup = () => { + if (timeout !== undefined) { + clearTimeout(timeout) + timeout = undefined + } + signal?.removeEventListener("abort", abortHandler) if (typeof window !== "undefined") { window.removeEventListener("message", handler) } @@ -27,17 +34,16 @@ export const fetchRouterModels = async (provider?: string, signal?: AbortSignal) return } - const timeout = setTimeout(() => { - cleanup() - reject(new Error("Router models request timed out")) - }, 10000) - const abortHandler = () => { - clearTimeout(timeout) cleanup() reject(new Error("Router models request aborted")) } + timeout = setTimeout(() => { + cleanup() + reject(new Error("Router models request timed out")) + }, 10000) + signal?.addEventListener("abort", abortHandler, { once: true }) const handler = (event: MessageEvent) => { @@ -52,8 +58,6 @@ export const fetchRouterModels = async (provider?: string, signal?: AbortSignal) return } - clearTimeout(timeout) - signal?.removeEventListener("abort", abortHandler) cleanup() if (message.routerModels) { From 567369cd00d6cc1c46a9fb713a3591ce4ddd13a1 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 8 Oct 2026 09:49:42 +0900 Subject: [PATCH 3/6] ci: re-trigger workflows From f5770c929c91415aca450f130f01718e3ce4fd71 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 8 Oct 2026 10:21:41 +0900 Subject: [PATCH 4/6] test(webview): assert exact listener reference removal in useRouterModels --- .../hooks/__tests__/useRouterModels.spec.ts | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts index 7286b6db91..d2a4d0d5bf 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useRouterModels.spec.ts @@ -159,37 +159,56 @@ describe("fetchRouterModels", () => { it("rejects and cleans up when signal aborts while waiting", async () => { const controller = new AbortController() - const removeEventListenerSpy = vi.spyOn(window, "removeEventListener") + const addWindowListenerSpy = vi.spyOn(window, "addEventListener") + const removeWindowListenerSpy = vi.spyOn(window, "removeEventListener") + const addSignalListenerSpy = vi.spyOn(controller.signal, "addEventListener") + const removeSignalListenerSpy = vi.spyOn(controller.signal, "removeEventListener") const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + const addedMessageHandler = addWindowListenerSpy.mock.calls.find(([type]) => type === "message")?.[1] + const addedAbortHandler = addSignalListenerSpy.mock.calls.find(([type]) => type === "abort")?.[1] controller.abort() await expect(promise).rejects.toThrow("Router models request aborted") - expect(removeEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + expect(typeof addedMessageHandler).toBe("function") + expect(removeWindowListenerSpy).toHaveBeenCalledWith("message", addedMessageHandler) + expect(typeof addedAbortHandler).toBe("function") + expect(removeSignalListenerSpy).toHaveBeenCalledWith("abort", addedAbortHandler) }) it("rejects and cleans up abort listener when timeout expires", async () => { vi.useFakeTimers() const controller = new AbortController() - const signalRemoveEventListenerSpy = vi.spyOn(controller.signal, "removeEventListener") - const windowRemoveEventListenerSpy = vi.spyOn(window, "removeEventListener") + const addSignalListenerSpy = vi.spyOn(controller.signal, "addEventListener") + const removeSignalListenerSpy = vi.spyOn(controller.signal, "removeEventListener") + const addWindowListenerSpy = vi.spyOn(window, "addEventListener") + const removeWindowListenerSpy = vi.spyOn(window, "removeEventListener") const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + const addedAbortHandler = addSignalListenerSpy.mock.calls.find(([type]) => type === "abort")?.[1] + const addedMessageHandler = addWindowListenerSpy.mock.calls.find(([type]) => type === "message")?.[1] vi.advanceTimersByTime(10000) await expect(promise).rejects.toThrow("Router models request timed out") - expect(signalRemoveEventListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function)) - expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + expect(typeof addedAbortHandler).toBe("function") + expect(removeSignalListenerSpy).toHaveBeenCalledWith("abort", addedAbortHandler) + expect(typeof addedMessageHandler).toBe("function") + expect(removeWindowListenerSpy).toHaveBeenCalledWith("message", addedMessageHandler) }) it("removes abort listener and cleans up timer on successful response", async () => { vi.useFakeTimers() const controller = new AbortController() - const signalRemoveEventListenerSpy = vi.spyOn(controller.signal, "removeEventListener") + const addSignalListenerSpy = vi.spyOn(controller.signal, "addEventListener") + const removeSignalListenerSpy = vi.spyOn(controller.signal, "removeEventListener") + const addWindowListenerSpy = vi.spyOn(window, "addEventListener") + const removeWindowListenerSpy = vi.spyOn(window, "removeEventListener") const promise = fetchRouterModels(providerIdentifiers.openrouter, controller.signal) + const addedAbortHandler = addSignalListenerSpy.mock.calls.find(([type]) => type === "abort")?.[1] + const addedMessageHandler = addWindowListenerSpy.mock.calls.find(([type]) => type === "message")?.[1] window.dispatchEvent( new MessageEvent("message", { @@ -203,7 +222,10 @@ describe("fetchRouterModels", () => { const result = await promise expect(result).toEqual(mockRouterModels) - expect(signalRemoveEventListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function)) + expect(typeof addedAbortHandler).toBe("function") + expect(removeSignalListenerSpy).toHaveBeenCalledWith("abort", addedAbortHandler) + expect(typeof addedMessageHandler).toBe("function") + expect(removeWindowListenerSpy).toHaveBeenCalledWith("message", addedMessageHandler) // Advancing timers should not cause rejection or error vi.advanceTimersByTime(15000) From cae69009339dfc09b7aab30211d3e3d8c0751902 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 8 Oct 2026 13:19:15 +0900 Subject: [PATCH 5/6] fix(provider): normalize apiProvider in updateProfileModel and harden mutation lifecycle --- src/core/webview/ClineProvider.ts | 10 +- .../ClineProvider.updateProfileModel.spec.ts | 132 ++++++++++++++++-- 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7ed0f777ce..20a67e81eb 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1998,7 +1998,7 @@ export class ClineProvider const allowedKeys: ReadonlySet = new Set( providerModelKey ? [providerModelKey, ...RESET_ONLY_KEYS] : RESET_ONLY_KEYS, ) - const merged: Record = { ...stored, id } + const merged: Record = { ...stored, id, apiProvider: storedProvider } for (const [key, value] of Object.entries(patch)) { // The provider is never patchable. if (key === "apiProvider" || !allowedKeys.has(key)) { @@ -2052,6 +2052,10 @@ export class ClineProvider await this.contextProxy.setProviderSettings(merged as ProviderSettings) updatedContext = true } + + if (signal.aborted) { + throw new Error("Provider profile mutation aborted") + } } catch (updateError) { if (savedConfig) { try { @@ -2074,6 +2078,10 @@ export class ClineProvider throw updateError } + if (signal.aborted) { + return + } + this.updateTaskApiHandlerIfNeeded(merged as ProviderSettings, { forceRebuild: true }) await this.persistStickyProviderProfileToCurrentTask(name) await this.postStateToWebview() diff --git a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts index dfeaa742c6..0a9b133133 100644 --- a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode" import { makeCompositeDisposable } from "../../../test-utils/vscode" import { TelemetryService } from "@roo-code/telemetry" +import { CloudService } from "@roo-code/cloud" import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { ContextProxy } from "../../config/ContextProxy" @@ -118,20 +119,24 @@ vi.mock("../../../i18n", async (importOriginal) => ({ t: (key: string) => key, })) +const { mockCloudInstance } = vi.hoisted(() => ({ + mockCloudInstance: { + isCloudAgent: false, + isAuthenticated: vi.fn().mockReturnValue(false), + getAllowList: vi.fn().mockReturnValue({ allowAll: true }), + getOrganizationSettings: vi.fn().mockReturnValue(undefined), + getUserInfo: vi.fn().mockReturnValue(null), + on: vi.fn(), + off: vi.fn(), + }, +})) + vi.mock("@roo-code/cloud", () => ({ getRooCodeApiUrl: vi.fn().mockReturnValue("https://api.roocode.com"), CloudService: { hasInstance: vi.fn().mockReturnValue(true), get instance() { - return { - isCloudAgent: false, - isAuthenticated: vi.fn().mockReturnValue(false), - getAllowList: vi.fn().mockReturnValue({ allowAll: true }), - getOrganizationSettings: vi.fn().mockReturnValue(undefined), - getUserInfo: vi.fn().mockReturnValue(null), - on: vi.fn(), - off: vi.fn(), - } + return mockCloudInstance }, }, })) @@ -158,6 +163,9 @@ describe("ClineProvider - updateProfileModel", () => { beforeEach(async () => { vi.clearAllMocks() + mockCloudInstance.isAuthenticated.mockReturnValue(false) + mockCloudInstance.getOrganizationSettings.mockReturnValue(undefined) + mockCloudInstance.getAllowList.mockReturnValue({ allowAll: true }) if (!TelemetryService.hasInstance()) { TelemetryService.createInstance([]) @@ -295,19 +303,62 @@ describe("ClineProvider - updateProfileModel", () => { expect(manager().saveConfig).not.toHaveBeenCalled() }) - it("treats a profile without apiProvider as OpenRouter", async () => { + it("treats a profile without apiProvider as OpenRouter and normalizes apiProvider when saving", async () => { mockStoredProfile({}) await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: "x/y", }) - expect(manager().saveConfig).toHaveBeenCalledWith( - "test-config", - expect.objectContaining({ openRouterModelId: "x/y" }), + expect(manager().saveConfig).toHaveBeenCalledWith("test-config", { + id: "test-id", + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "x/y", + }) + expect(provider.contextProxy.getValues().apiProvider).toBe(providerIdentifiers.openrouter) + }) + + it("normalizes apiProvider to OpenRouter when rebuilding task handler for provider-less profile", async () => { + const mockTask = new Task({} as unknown as ConstructorParameters[0]) + Object.defineProperty(mockTask, "taskApiConfigName", { value: "test-config" }) + await provider.addClineToStack(mockTask) + mockStoredProfile({}) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + }) + + expect(mockTask.updateApiConfiguration).toHaveBeenCalledWith( + expect.objectContaining({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "x/y", + }), ) }) + it("ignores non-null values for reset-only keys while updating model selection", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + reasoningEffort: "low", + modelMaxTokens: 4096, + modelMaxThinkingTokens: 2048, + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "x/y", + reasoningEffort: "high", + modelMaxTokens: 8192, + modelMaxThinkingTokens: 4096, + }) + + const saved = manager().saveConfig.mock.calls[0][1] + expect(saved.openRouterModelId).toBe("x/y") + expect(saved.reasoningEffort).toBe("low") + expect(saved.modelMaxTokens).toBe(4096) + expect(saved.modelMaxThinkingTokens).toBe(2048) + }) + 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( @@ -518,4 +569,59 @@ describe("ClineProvider - updateProfileModel", () => { ) expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") }) + + it("enforces organization allow-list when authenticated through CloudService", async () => { + mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "allowed/model" }) + mockCloudInstance.isAuthenticated.mockReturnValue(true) + mockCloudInstance.getOrganizationSettings.mockReturnValue({ + allowList: { + allowAll: false, + providers: { [providerIdentifiers.openrouter]: { allowAll: false, models: ["allowed/model"] } }, + }, + } as unknown as ReturnType) + + 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") + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "allowed/model", + }) + expect(manager().saveConfig).toHaveBeenCalledTimes(1) + expect(manager().saveConfig).toHaveBeenCalledWith( + "test-config", + expect.objectContaining({ openRouterModelId: "allowed/model" }), + ) + }) + + it("rolls back saved profile and prevents handler rebuild when mutation signal aborts during context update", async () => { + const mockTask = new Task({} as unknown as ConstructorParameters[0]) + Object.defineProperty(mockTask, "taskApiConfigName", { value: "test-config" }) + await provider.addClineToStack(mockTask) + + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + openRouterApiKey: "my-key", + }) + + const controller = new AbortController() + provider["enqueueProviderProfileMutation"] = vi.fn().mockImplementation(async (fn) => { + return fn(controller.signal) + }) + + vi.spyOn(provider.contextProxy, "setProviderSettings").mockImplementationOnce(async () => { + controller.abort() + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(mockTask.updateApiConfiguration).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) }) From f444d9af0cb8ac0b730ed530e5567c2006dc1697 Mon Sep 17 00:00:00 2001 From: daewoongoh Date: Thu, 8 Oct 2026 14:02:24 +0900 Subject: [PATCH 6/6] fix(provider): transactional context rollback and serialized mutation queue --- src/core/webview/ClineProvider.ts | 61 +++++++++++++----- .../ClineProvider.updateProfileModel.spec.ts | 63 ++++++++++++++++++- 2 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 20a67e81eb..f576f1d6c0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -259,6 +259,7 @@ export class ClineProvider private taskHistoryStoreInitialized = false public static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds private providerProfileMutationQueue = Promise.resolve() + private providerProfileMutationAbortController = new AbortController() private historyTaskCreationQueue = Promise.resolve() private runDelegationTransition(parentTaskId: string, fn: () => Promise): Promise { @@ -266,12 +267,23 @@ export class ClineProvider } private enqueueProviderProfileMutation(fn: (signal: AbortSignal) => Promise): Promise { + if (this._disposed) { + return Promise.reject(new Error("ClineProvider is disposed")) + } + const controller = new AbortController() + const onProviderDispose = () => controller.abort() + this.providerProfileMutationAbortController.signal.addEventListener("abort", onProviderDispose, { once: true }) + // Run fn after either outcome so a rejected mutation never poisons the queue. - const run = this.providerProfileMutationQueue.then( - () => fn(controller.signal), - () => fn(controller.signal), - ) + const run = this.providerProfileMutationQueue + .then( + () => fn(controller.signal), + () => fn(controller.signal), + ) + .finally(() => { + this.providerProfileMutationAbortController.signal.removeEventListener("abort", onProviderDispose) + }) const callerResult = this.withProviderProfileMutationTimeout(run, () => { controller.abort() this.log("Provider profile mutation timed out; aborting in-flight mutation") @@ -294,9 +306,8 @@ 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 serialization in place until the timed-out mutation and its rollback settle. + this.providerProfileMutationQueue = run.then( () => undefined, () => undefined, ) @@ -848,6 +859,7 @@ export class ClineProvider this._disposed = true this._postStateToWebviewThrottled.cancel() + this.providerProfileMutationAbortController.abort() this.log("Disposing ClineProvider...") // Reject any tasks still waiting for a scheduler permit so they don't @@ -1963,8 +1975,14 @@ export class ClineProvider * from is dropped instead of saved and reactivated. */ async updateProfileModel(name: string, expectedProvider: string, patch: Record): Promise { + if (this._disposed) { + return + } + try { await this.enqueueProviderProfileMutation(async (signal) => { + if (signal.aborted || this._disposed) return + // Mirrors the profile name the webview is shown (see getStateToPostToWebview). const task = this.getCurrentTask() const { currentApiConfigName, organizationAllowList: stateOrganizationAllowList } = @@ -1978,7 +1996,7 @@ export class ClineProvider const { name: _name, id, ...stored } = await this.providerSettingsManager.getProfile({ name }) - if (signal.aborted) return + if (signal.aborted || this._disposed) return // A profile without an explicit provider is treated as OpenRouter, matching the chat ModelSelector. const storedProvider = stored.apiProvider ?? providerIdentifiers.openrouter @@ -2038,35 +2056,33 @@ export class ClineProvider } let savedConfig = false - let updatedContext = false + let shouldRollbackContext = false + const originalContextSettings: ProviderSettings = { ...stored, id } as ProviderSettings try { await this.providerSettingsManager.saveConfig(name, merged as ProviderSettings) savedConfig = true - if (signal.aborted) { + if (signal.aborted || this._disposed) { throw new Error("Provider profile mutation aborted") } await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) if (name === currentApiConfigName) { + shouldRollbackContext = true await this.contextProxy.setProviderSettings(merged as ProviderSettings) - updatedContext = true } - if (signal.aborted) { + if (signal.aborted || this._disposed) { throw new Error("Provider profile mutation aborted") } } catch (updateError) { if (savedConfig) { try { - await this.providerSettingsManager.saveConfig(name, { ...stored, id } as ProviderSettings) + await this.providerSettingsManager.saveConfig(name, originalContextSettings) await this.updateGlobalState( "listApiConfigMeta", await this.providerSettingsManager.listConfig(), ) - if (updatedContext) { - await this.contextProxy.setProviderSettings({ ...stored, id } as ProviderSettings) - } } catch (rollbackError) { this.log( `Failed to rollback profile '${name}' after update failure: ${ @@ -2075,10 +2091,21 @@ export class ClineProvider ) } } + if (shouldRollbackContext) { + try { + await this.contextProxy.setProviderSettings(originalContextSettings) + } catch (rollbackError) { + this.log( + `Failed to rollback context settings for '${name}' after update failure: ${ + rollbackError instanceof Error ? rollbackError.message : String(rollbackError) + }`, + ) + } + } throw updateError } - if (signal.aborted) { + if (signal.aborted || this._disposed) { return } diff --git a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts index 0a9b133133..a772995063 100644 --- a/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.updateProfileModel.spec.ts @@ -411,9 +411,12 @@ describe("ClineProvider - updateProfileModel", () => { expect(manager().saveConfig).toHaveBeenCalledTimes(1) }) - it("rejects a model update when the authenticated organization policy is unavailable", async () => { + it("rejects a model update when the authenticated organization policy is unavailable or throws", async () => { mockStoredProfile({ apiProvider: providerIdentifiers.openrouter, openRouterModelId: "allowed/model" }) - provider["getOrganizationAllowListForProfileMutation"] = vi.fn().mockReturnValue(undefined) + mockCloudInstance.isAuthenticated.mockReturnValue(true) + mockCloudInstance.getOrganizationSettings.mockImplementation(() => { + throw new Error("Failed to fetch settings") + }) await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { openRouterModelId: "blocked/model", @@ -612,7 +615,9 @@ describe("ClineProvider - updateProfileModel", () => { return fn(controller.signal) }) - vi.spyOn(provider.contextProxy, "setProviderSettings").mockImplementationOnce(async () => { + const originalSetProviderSettings = provider.contextProxy.setProviderSettings.bind(provider.contextProxy) + vi.spyOn(provider.contextProxy, "setProviderSettings").mockImplementationOnce(async (settings) => { + await originalSetProviderSettings(settings) controller.abort() }) @@ -621,7 +626,59 @@ describe("ClineProvider - updateProfileModel", () => { }) expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 2, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4", openRouterApiKey: "my-key" }), + ) + expect(provider.contextProxy.getValues().openRouterModelId).toBe("openai/gpt-4") expect(mockTask.updateApiConfiguration).not.toHaveBeenCalled() expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") }) + + it("rolls back saved profile and restores context when contextProxy.setProviderSettings fails", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + openRouterApiKey: "my-key", + }) + + let setSettingsCalls = 0 + const originalSetProviderSettings = provider.contextProxy.setProviderSettings.bind(provider.contextProxy) + vi.spyOn(provider.contextProxy, "setProviderSettings").mockImplementation(async (settings) => { + setSettingsCalls++ + if (setSettingsCalls === 1) { + throw new Error("Context secret storage write failed") + } + return originalSetProviderSettings(settings) + }) + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + expect(manager().saveConfig).toHaveBeenCalledTimes(2) + expect(manager().saveConfig).toHaveBeenNthCalledWith( + 2, + "test-config", + expect.objectContaining({ openRouterModelId: "openai/gpt-4", openRouterApiKey: "my-key" }), + ) + expect(provider.contextProxy.setProviderSettings).toHaveBeenCalledTimes(2) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.save_api_config") + }) + + it("aborts active and queued profile mutations upon provider disposal", async () => { + mockStoredProfile({ + apiProvider: providerIdentifiers.openrouter, + openRouterModelId: "openai/gpt-4", + }) + + await provider.dispose() + + await provider.updateProfileModel("test-config", providerIdentifiers.openrouter, { + openRouterModelId: "openai/gpt-4.5", + }) + + expect(manager().saveConfig).not.toHaveBeenCalled() + }) })