diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 51c7fa49d5..49a63c3ae4 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -336,6 +336,7 @@ export type ExtensionState = Pick< | "showWorktreesInHomeScreen" | "disabledTools" > & { + lockApiConfigAcrossModes?: boolean version: string clineMessages: ClineMessage[] currentTaskItem?: HistoryItem @@ -524,6 +525,7 @@ export interface WebviewMessage { | "searchFiles" | "toggleApiConfigPin" | "hasOpenedModeSelector" + | "lockApiConfigAcrossModes" | "clearCloudAuthSkipModel" | "cloudButtonClicked" | "rooCloudSignIn" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff809db9ad..7f48e153c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14958,7 +14958,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -22251,8 +22251,8 @@ snapshots: zhipu-ai-provider@0.2.2(zod@3.25.76): dependencies: - '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.20(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.5(zod@3.25.76) transitivePeerDependencies: - zod diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 5ff0f1c365..fc15a8dd5c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -899,7 +899,8 @@ export class ClineProvider // Load the saved API config for the restored mode if it exists. // Skip mode-based profile activation if historyItem.apiConfigName exists, // since the task's specific provider profile will override it anyway. - if (!historyItem.apiConfigName) { + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) + if (!historyItem.apiConfigName && !lockApiConfigAcrossModes) { const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode) const listApiConfig = await this.providerSettingsManager.listConfig() @@ -1316,6 +1317,13 @@ export class ClineProvider this.emit(RooCodeEventName.ModeChanged, newMode) + // If workspace lock is on, keep the current API config — don't load mode-specific config + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) + if (lockApiConfigAcrossModes) { + await this.postStateToWebview() + return + } + // Load the saved API config for the new mode if it exists. const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode) const listApiConfig = await this.providerSettingsManager.listConfig() @@ -2081,6 +2089,7 @@ export class ClineProvider openRouterImageGenerationSelectedModel, featureRoomoteControlEnabled, isBrowserSessionActive, + lockApiConfigAcrossModes, } = await this.getState() let cloudOrganizations: CloudOrganizationMembership[] = [] @@ -2229,6 +2238,7 @@ export class ClineProvider profileThresholds: profileThresholds ?? {}, cloudApiUrl: getRooCodeApiUrl(), hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, + lockApiConfigAcrossModes: lockApiConfigAcrossModes ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, includeDiagnosticMessages: includeDiagnosticMessages ?? true, @@ -2464,6 +2474,7 @@ export class ClineProvider stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider, }, profileThresholds: stateValues.profileThresholds ?? {}, + lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", false), includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true, maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true, diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 04f5d57792..9e57ae94b8 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -171,6 +171,11 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (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" }, diff --git a/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts new file mode 100644 index 0000000000..9b5e3b16ee --- /dev/null +++ b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts @@ -0,0 +1,372 @@ +// npx vitest run core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts + +import * as vscode from "vscode" +import { TelemetryService } from "@roo-code/telemetry" +import { ClineProvider } from "../ClineProvider" +import { ContextProxy } from "../../config/ContextProxy" + +vi.mock("vscode", () => ({ + ExtensionContext: vi.fn(), + OutputChannel: vi.fn(), + WebviewView: vi.fn(), + 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("../../task/Task", () => ({ + Task: vi.fn().mockImplementation((options) => ({ + taskId: options.taskId || "test-task-id", + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + overwriteClineMessages: vi.fn(), + overwriteApiConversationHistory: vi.fn(), + abortTask: vi.fn(), + handleWebviewAskResponse: vi.fn(), + getTaskNumber: vi.fn().mockReturnValue(0), + setTaskNumber: vi.fn(), + setParentTask: vi.fn(), + setRootTask: vi.fn(), + emit: vi.fn(), + parentTask: options.parentTask, + updateApiConfiguration: vi.fn(), + setTaskApiConfigName: vi.fn(), + _taskApiConfigName: options.historyItem?.apiConfigName, + taskApiConfigName: options.historyItem?.apiConfigName, + })), +})) + +vi.mock("../../prompts/sections/custom-instructions") + +vi.mock("../../../utils/safeWriteJson") + +vi.mock("../../../api", () => ({ + buildApiHandler: vi.fn().mockReturnValue({ + getModel: vi.fn().mockReturnValue({ + id: "claude-3-sonnet", + }), + }), +})) + +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(() => ({ + initializeFilePaths: vi.fn(), + dispose: vi.fn(), + })), +})) + +vi.mock("../../diff/strategies/multi-search-replace", () => ({ + MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({ + getName: () => "test-strategy", + applyDiff: vi.fn(), + })), +})) + +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: vi.fn().mockReturnValue(true), + get instance() { + return { + isAuthenticated: vi.fn().mockReturnValue(false), + } + }, + }, + BridgeOrchestrator: { + isEnabled: vi.fn().mockReturnValue(false), + }, + getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), +})) + +vi.mock("../../../shared/modes", () => { + const mockModes = [ + { + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }, + { + slug: "architect", + name: "Architect Mode", + roleDefinition: "You are an architect", + groups: ["read", "edit"], + }, + { + slug: "ask", + name: "Ask Mode", + roleDefinition: "You are an assistant", + groups: ["read"], + }, + { + slug: "debug", + name: "Debug Mode", + roleDefinition: "You are a debugger", + groups: ["read", "edit"], + }, + { + slug: "orchestrator", + name: "Orchestrator Mode", + roleDefinition: "You are an orchestrator", + groups: [], + }, + ] + + return { + modes: mockModes, + getAllModes: vi.fn((customModes?: Array<{ slug: string }>) => { + if (!customModes?.length) { + return [...mockModes] + } + const allModes = [...mockModes] + customModes.forEach((cm) => { + const idx = allModes.findIndex((m) => m.slug === cm.slug) + if (idx !== -1) { + allModes[idx] = cm as (typeof mockModes)[number] + } else { + allModes.push(cm as (typeof mockModes)[number]) + } + }) + return allModes + }), + getModeBySlug: vi.fn().mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }), + defaultModeSlug: "code", + } +}) + +vi.mock("../../prompts/system", () => ({ + SYSTEM_PROMPT: vi.fn().mockResolvedValue("mocked system prompt"), + codeMode: "code", +})) + +vi.mock("../../../api/providers/fetchers/modelCache", () => ({ + getModels: vi.fn().mockResolvedValue({}), + flushModels: vi.fn(), +})) + +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn().mockResolvedValue("Mock file content"), +})) + +vi.mock("p-wait-for", () => ({ + default: vi.fn().mockImplementation(async () => Promise.resolve()), +})) + +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("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn().mockReturnValue(true), + createInstance: vi.fn(), + get instance() { + return { + trackEvent: vi.fn(), + trackError: vi.fn(), + setProvider: vi.fn(), + captureModeSwitch: vi.fn(), + } + }, + }, +})) + +describe("ClineProvider - Lock API Config Across Modes", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + + beforeEach(() => { + vi.clearAllMocks() + + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + + const globalState: Record = { + mode: "code", + currentApiConfigName: "default-profile", + } + + const workspaceState: Record = {} + + const secrets: Record = {} + + mockContext = { + extensionPath: "/test/path", + extensionUri: {} as vscode.Uri, + globalState: { + get: vi.fn().mockImplementation((key: string) => globalState[key]), + update: vi.fn().mockImplementation((key: string, value: unknown) => { + globalState[key] = value + return Promise.resolve() + }), + keys: vi.fn().mockImplementation(() => Object.keys(globalState)), + }, + secrets: { + get: vi.fn().mockImplementation((key: string) => secrets[key]), + store: vi.fn().mockImplementation((key: string, value: string | undefined) => { + secrets[key] = value + return Promise.resolve() + }), + delete: vi.fn().mockImplementation((key: string) => { + delete secrets[key] + return Promise.resolve() + }), + }, + workspaceState: { + get: vi.fn().mockImplementation((key: string, defaultValue?: unknown) => { + return key in workspaceState ? workspaceState[key] : defaultValue + }), + update: vi.fn().mockImplementation((key: string, value: unknown) => { + workspaceState[key] = value + return Promise.resolve() + }), + keys: vi.fn().mockImplementation(() => Object.keys(workspaceState)), + }, + subscriptions: [], + extension: { + packageJSON: { version: "1.0.0" }, + }, + 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 + + const mockPostMessage = vi.fn() + + mockWebviewView = { + webview: { + postMessage: mockPostMessage, + html: "", + options: {}, + onDidReceiveMessage: vi.fn(), + asWebviewUri: vi.fn(), + cspSource: "vscode-webview://test-csp-source", + }, + visible: true, + onDidDispose: vi.fn().mockImplementation((callback) => { + callback() + 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)) + + // Mock getMcpHub method + provider.getMcpHub = vi.fn().mockReturnValue({ + listTools: vi.fn().mockResolvedValue([]), + callTool: vi.fn().mockResolvedValue({ content: [] }), + listResources: vi.fn().mockResolvedValue([]), + readResource: vi.fn().mockResolvedValue({ contents: [] }), + getAllServers: vi.fn().mockReturnValue([]), + }) + }) + + describe("handleModeSwitch honors lockApiConfigAcrossModes as a read-time override", () => { + beforeEach(async () => { + await provider.resolveWebviewView(mockWebviewView) + }) + + it("skips mode-specific config lookup/load when lockApiConfigAcrossModes is true", async () => { + await mockContext.workspaceState.update("lockApiConfigAcrossModes", true) + + const getModeConfigIdSpy = vi + .spyOn(provider.providerSettingsManager, "getModeConfigId") + .mockResolvedValue("architect-profile-id") + const listConfigSpy = vi + .spyOn(provider.providerSettingsManager, "listConfig") + .mockResolvedValue([ + { name: "architect-profile", id: "architect-profile-id", apiProvider: "anthropic" }, + ]) + const activateProviderProfileSpy = vi + .spyOn(provider, "activateProviderProfile") + .mockResolvedValue(undefined) + + await provider.handleModeSwitch("architect") + + expect(getModeConfigIdSpy).not.toHaveBeenCalled() + expect(listConfigSpy).not.toHaveBeenCalled() + expect(activateProviderProfileSpy).not.toHaveBeenCalled() + }) + + it("keeps normal mode-specific lookup/load behavior when lockApiConfigAcrossModes is false", async () => { + await mockContext.workspaceState.update("lockApiConfigAcrossModes", false) + + const getModeConfigIdSpy = vi + .spyOn(provider.providerSettingsManager, "getModeConfigId") + .mockResolvedValue("architect-profile-id") + vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([ + { name: "architect-profile", id: "architect-profile-id", apiProvider: "anthropic" }, + ]) + vi.spyOn(provider.providerSettingsManager, "getProfile").mockResolvedValue({ + name: "architect-profile", + apiProvider: "anthropic", + }) + + const activateProviderProfileSpy = vi + .spyOn(provider, "activateProviderProfile") + .mockResolvedValue(undefined) + + await provider.handleModeSwitch("architect") + + expect(getModeConfigIdSpy).toHaveBeenCalledWith("architect") + expect(activateProviderProfileSpy).toHaveBeenCalledWith({ name: "architect-profile" }) + }) + }) +}) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 26f6fbd8ab..4bad630ed5 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -405,6 +405,11 @@ describe("ClineProvider", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (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" }, @@ -2147,6 +2152,11 @@ describe("Project MCP Settings", () => { store: vi.fn(), delete: vi.fn(), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, @@ -2277,6 +2287,11 @@ describe.skip("ContextProxy integration", () => { update: vi.fn(), keys: vi.fn().mockReturnValue([]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() }, extensionUri: {} as vscode.Uri, globalStorageUri: { fsPath: "/test/path" }, @@ -2342,6 +2357,11 @@ describe("getTelemetryProperties", () => { update: vi.fn(), keys: vi.fn().mockReturnValue([]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() }, extensionUri: {} as vscode.Uri, globalStorageUri: { fsPath: "/test/path" }, @@ -2504,6 +2524,11 @@ describe("ClineProvider - Router Models", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (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" }, @@ -2857,6 +2882,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (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" }, diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index 27aab0b7da..af674d7a5e 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -227,6 +227,11 @@ describe("ClineProvider - Sticky Mode", () => { return Promise.resolve() }), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts index 80b14746a7..ee63b45b25 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts @@ -229,6 +229,11 @@ describe("ClineProvider - Sticky Provider Profile", () => { return Promise.resolve() }), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index f5e6afa7f0..e0f1d2dc29 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -287,6 +287,11 @@ describe("ClineProvider Task History Synchronization", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (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" }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts new file mode 100644 index 0000000000..fd9b4a7740 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts @@ -0,0 +1,68 @@ +// npx vitest run core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +describe("webviewMessageHandler - lockApiConfigAcrossModes", () => { + let mockProvider: { + context: { + workspaceState: { + get: ReturnType + update: ReturnType + } + } + getState: ReturnType + postStateToWebview: ReturnType + providerSettingsManager: { + setModeConfig: ReturnType + } + postMessageToWebview: ReturnType + getCurrentTask: ReturnType + } + + beforeEach(() => { + vi.clearAllMocks() + + mockProvider = { + context: { + workspaceState: { + get: vi.fn(), + update: vi.fn().mockResolvedValue(undefined), + }, + }, + getState: vi.fn().mockResolvedValue({ + currentApiConfigName: "test-config", + listApiConfigMeta: [{ name: "test-config", id: "config-123" }], + customModes: [], + }), + postStateToWebview: vi.fn(), + providerSettingsManager: { + setModeConfig: vi.fn(), + }, + postMessageToWebview: vi.fn(), + getCurrentTask: vi.fn(), + } + }) + + it("sets lockApiConfigAcrossModes to true and posts state without mode config fan-out", async () => { + await webviewMessageHandler(mockProvider as unknown as ClineProvider, { + type: "lockApiConfigAcrossModes", + bool: true, + }) + + expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("lockApiConfigAcrossModes", true) + expect(mockProvider.providerSettingsManager.setModeConfig).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("sets lockApiConfigAcrossModes to false without applying to all modes", async () => { + await webviewMessageHandler(mockProvider as unknown as ClineProvider, { + type: "lockApiConfigAcrossModes", + bool: false, + }) + + expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("lockApiConfigAcrossModes", false) + expect(mockProvider.providerSettingsManager.setModeConfig).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 75f1ce0ff4..ae0da75841 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1661,6 +1661,14 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break + case "lockApiConfigAcrossModes": { + const enabled = message.bool ?? false + await provider.context.workspaceState.update("lockApiConfigAcrossModes", enabled) + + await provider.postStateToWebview() + break + } + case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 4396019a2d..e370296ec3 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -20,6 +20,8 @@ interface ApiConfigSelectorProps { listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }> pinnedApiConfigs?: Record togglePinnedApiConfig: (id: string) => void + lockApiConfigAcrossModes: boolean + onToggleLockApiConfig: () => void } export const ApiConfigSelector = ({ @@ -32,6 +34,8 @@ export const ApiConfigSelector = ({ listApiConfigMeta, pinnedApiConfigs, togglePinnedApiConfig, + lockApiConfigAcrossModes, + onToggleLockApiConfig, }: ApiConfigSelectorProps) => { const { t } = useAppTranslation() const [open, setOpen] = useState(false) @@ -228,6 +232,16 @@ export const ApiConfigSelector = ({ onClick={handleEditClick} tooltip={false} /> + {/* Info icon and title on the right with matching spacing */} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 654f2e1011..4c0b2bbfd0 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -103,6 +103,7 @@ export const ChatTextArea = forwardRef( commands, cloudUserInfo, enterBehavior, + lockApiConfigAcrossModes, } = useExtensionState() // Find the ID and display text for the currently selected API configuration. @@ -945,6 +946,11 @@ export const ChatTextArea = forwardRef( vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) + const handleToggleLockApiConfig = useCallback(() => { + const newValue = !lockApiConfigAcrossModes + vscode.postMessage({ type: "lockApiConfigAcrossModes", bool: newValue }) + }, [lockApiConfigAcrossModes]) + return (
( listApiConfigMeta={listApiConfigMeta || []} pinnedApiConfigs={pinnedApiConfigs} togglePinnedApiConfig={togglePinnedApiConfig} + lockApiConfigAcrossModes={!!lockApiConfigAcrossModes} + onToggleLockApiConfig={handleToggleLockApiConfig} />
diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index ff1b95f949..a71216d96f 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -72,6 +72,8 @@ describe("ApiConfigSelector", () => { ], pinnedApiConfigs: { config1: true }, togglePinnedApiConfig: mockTogglePinnedApiConfig, + lockApiConfigAcrossModes: false, + onToggleLockApiConfig: vi.fn(), } beforeEach(() => { diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx new file mode 100644 index 0000000000..d3fb2b6890 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx @@ -0,0 +1,156 @@ +import { defaultModeSlug } from "@roo/modes" + +import { render, fireEvent, screen } from "@src/utils/test-utils" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +import { ChatTextArea } from "../ChatTextArea" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@src/components/common/CodeBlock") +vi.mock("@src/components/common/MarkdownBlock") +vi.mock("@src/utils/path-mentions", () => ({ + convertToMentionPath: vi.fn((path: string) => path), +})) + +// Mock ExtensionStateContext +vi.mock("@src/context/ExtensionStateContext") + +const mockPostMessage = vscode.postMessage as ReturnType + +describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => { + const defaultProps = { + inputValue: "", + setInputValue: vi.fn(), + onSend: vi.fn(), + sendingDisabled: false, + selectApiConfigDisabled: false, + onSelectImages: vi.fn(), + shouldDisableImages: false, + placeholderText: "Type a message...", + selectedImages: [] as string[], + setSelectedImages: vi.fn(), + onHeightChange: vi.fn(), + mode: defaultModeSlug, + setMode: vi.fn(), + modeShortcutText: "(⌘. for next mode)", + } + + const defaultState = { + filePaths: [], + openedTabs: [], + apiConfiguration: { apiProvider: "anthropic" }, + taskHistory: [], + cwd: "/test/workspace", + listApiConfigMeta: [{ id: "default", name: "Default", modelId: "claude-3" }], + currentApiConfigName: "Default", + pinnedApiConfigs: {}, + togglePinnedApiConfig: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + /** + * Helper: Opens the ApiConfigSelector popover by clicking the trigger, + * then returns the lock toggle button by its aria-label. + */ + const openPopoverAndGetLockToggle = (ariaLabel: string) => { + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + return screen.getByRole("button", { name: ariaLabel }) + } + + describe("rendering", () => { + it("renders with muted opacity when lockApiConfigAcrossModes is false", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: false, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Unlocked state has muted opacity + expect(button.className).toContain("opacity-60") + expect(button.className).not.toContain("text-vscode-focusBorder") + }) + + it("renders with highlight color when lockApiConfigAcrossModes is true", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: true, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Locked state has the focus border highlight color + expect(button.className).toContain("text-vscode-focusBorder") + expect(button.className).not.toContain("opacity-60") + }) + + it("renders in unlocked state when lockApiConfigAcrossModes is undefined (default)", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Default (undefined/falsy) renders in unlocked style + expect(button.className).toContain("opacity-60") + }) + }) + + describe("interaction", () => { + it("posts lockApiConfigAcrossModes=true message when locking", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: false, + }) + + render() + + // Clear any initialization messages + mockPostMessage.mockClear() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + fireEvent.click(button) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "lockApiConfigAcrossModes", + bool: true, + }) + }) + + it("posts lockApiConfigAcrossModes=false message when unlocking", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: true, + }) + + render() + + // Clear any initialization messages + mockPostMessage.mockClear() + + const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes") + fireEvent.click(button) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "lockApiConfigAcrossModes", + bool: false, + }) + }) + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 47110d0875..2378873f01 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -264,6 +264,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode openRouterImageGenerationSelectedModel: "", includeCurrentTime: true, includeCurrentCost: true, + lockApiConfigAcrossModes: false, }) const [didHydrateState, setDidHydrateState] = useState(false) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 5bba7dc445..070adc265d 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecciona el mode d'interacció", "selectApiConfig": "Seleccioneu la configuració de l'API", + "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", "addImages": "Afegeix imatges al missatge", "sendMessage": "Envia el missatge", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 58bc85b60c..bc550520d7 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Interaktionsmodus auswählen", "selectApiConfig": "API-Konfiguration auswählen", + "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", "addImages": "Bilder zur Nachricht hinzufügen", "sendMessage": "Nachricht senden", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b9652cfce5..bd97e041f2 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -122,6 +122,8 @@ }, "selectMode": "Select mode for interaction", "selectApiConfig": "Select API configuration", + "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", "modeSelector": { "title": "Modes", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 6c894642c8..0687bc7037 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Seleccionar modo de interacción", "selectApiConfig": "Seleccionar configuración de API", + "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", "addImages": "Agregar imágenes al mensaje", "sendMessage": "Enviar mensaje", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 598344de1d..f4887b7e6c 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Sélectionner le mode d'interaction", "selectApiConfig": "Sélectionner la configuration de l'API", + "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", "addImages": "Ajouter des images au message", "sendMessage": "Envoyer le message", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 74d5e4e3ee..580822b82f 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "इंटरैक्शन मोड चुनें", "selectApiConfig": "एपीआई कॉन्फ़िगरेशन का चयन करें", + "lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें", + "unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)", "enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ", "addImages": "संदेश में चित्र जोड़ें", "sendMessage": "संदेश भेजें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index f814b9d4a9..600708e4d9 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -125,6 +125,8 @@ }, "selectMode": "Pilih mode untuk interaksi", "selectApiConfig": "Pilih konfigurasi API", + "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", "enhancePromptDescription": "Tombol 'Tingkatkan Prompt' membantu memperbaiki prompt kamu dengan memberikan konteks tambahan, klarifikasi, atau penyusunan ulang. Coba ketik prompt di sini dan klik tombol lagi untuk melihat cara kerjanya.", "modeSelector": { diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index eca4264df2..ce8e45bf45 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Seleziona modalità di interazione", "selectApiConfig": "Seleziona la configurazione API", + "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", "addImages": "Aggiungi immagini al messaggio", "sendMessage": "Invia messaggio", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 9a0b1b2a35..f3c3d86f7a 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "対話モードを選択", "selectApiConfig": "API構成を選択", + "lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック", + "unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)", "enhancePrompt": "追加コンテキストでプロンプトを強化", "addImages": "メッセージに画像を追加", "sendMessage": "メッセージを送信", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 3b26c6e6e1..817768c275 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "상호작용 모드 선택", "selectApiConfig": "API 구성 선택", + "lockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성 잠금", + "unlockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성이 잠겨 있습니다 (클릭하여 해제)", "enhancePrompt": "추가 컨텍스트로 프롬프트 향상", "addImages": "메시지에 이미지 추가", "sendMessage": "메시지 보내기", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 241e9f2216..0a728aa38e 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecteer modus voor interactie", "selectApiConfig": "Selecteer API-configuratie", + "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", "enhancePromptDescription": "De knop 'Prompt verbeteren' helpt je prompt te verbeteren door extra context, verduidelijking of herformulering te bieden. Probeer hier een prompt te typen en klik opnieuw op de knop om te zien hoe het werkt.", "modeSelector": { diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index ae6b5a96ac..09a1c99492 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Wybierz tryb interakcji", "selectApiConfig": "Wybierz konfigurację API", + "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", "addImages": "Dodaj obrazy do wiadomości", "sendMessage": "Wyślij wiadomość", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 2ca72ebd97..cc4fbbd742 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecionar modo de interação", "selectApiConfig": "Selecionar configuração da API", + "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", "addImages": "Adicionar imagens à mensagem", "sendMessage": "Enviar mensagem", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 347bf1be81..c5e4a3f0e2 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Выберите режим взаимодействия", "selectApiConfig": "Выберите конфигурацию API", + "lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве", + "unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)", "enhancePrompt": "Улучшить запрос с дополнительным контекстом", "enhancePromptDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.", "modeSelector": { diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 2301541cdf..b913a5afb2 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Etkileşim modunu seçin", "selectApiConfig": "API yapılandırmasını seçin", + "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", "addImages": "Mesaja resim ekle", "sendMessage": "Mesaj gönder", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 33b13d9b26..9aecd5c385 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Chọn chế độ tương tác", "selectApiConfig": "Chọn cấu hình API", + "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", "addImages": "Thêm hình ảnh vào tin nhắn", "sendMessage": "Gửi tin nhắn", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 7a94bfb48d..853279506f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "选择交互模式", "selectApiConfig": "选择 API 配置", + "lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置", + "unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)", "enhancePrompt": "增强提示词", "addImages": "添加图片到消息", "sendMessage": "发送消息", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 926cb105fb..84c54900b6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -122,6 +122,8 @@ }, "selectMode": "選擇互動模式", "selectApiConfig": "選取 API 設定", + "lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定", + "unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)", "enhancePrompt": "使用額外內容強化提示詞", "modeSelector": { "title": "模式",