فهرست منبع

feat: add lock toggle to pin API config across all modes in workspace (#11295)

* feat: add lock toggle to pin API config across all modes in workspace

Add a lock/unlock toggle inside the API config selector popover (next to
the settings gear) that, when enabled, applies the selected API
configuration to all modes in the current workspace.

- Add lockApiConfigAcrossModes to ExtensionState and WebviewMessage types
- Store setting in workspaceState (per-workspace, not global)
- When locked, activateProviderProfile sets config for all modes
- Lock icon in ApiConfigSelector popover bottom bar next to gear
- Full i18n: English + 17 locale translations (all mention workspace scope)
- 9 new tests: 2 ClineProvider, 2 handler, 5 UI (77 total pass)

* refactor: replace write-fan-out with read-time override for lock API config

The original lock implementation used setModeConfig() fan-out to write the
locked config to ALL modes globally. Since the lock flag lives in workspace-
scoped workspaceState but modeApiConfigs are in global secrets, this caused
cross-workspace data destruction.

Replaced with read-time guards:
- handleModeSwitch: early return when lock is on (skip per-mode config load)
- createTaskWithHistoryItem: skip mode-based config restoration under lock
- activateProviderProfile: removed fan-out block
- lockApiConfigAcrossModes handler: simplified to flag + state post only
- Fixed pre-existing workspaceState mock gap in ClineProvider.spec.ts and
  ClineProvider.sticky-profile.spec.ts
Hannes Rudolph 5 روز پیش
والد
کامیت
5d17f56db7
34فایلهای تغییر یافته به همراه732 افزوده شده و 4 حذف شده
  1. 2 0
      packages/types/src/vscode-extension-host.ts
  2. 3 3
      pnpm-lock.yaml
  3. 12 1
      src/core/webview/ClineProvider.ts
  4. 5 0
      src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts
  5. 372 0
      src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts
  6. 30 0
      src/core/webview/__tests__/ClineProvider.spec.ts
  7. 5 0
      src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  8. 5 0
      src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts
  9. 5 0
      src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
  10. 68 0
      src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts
  11. 8 0
      src/core/webview/webviewMessageHandler.ts
  12. 14 0
      webview-ui/src/components/chat/ApiConfigSelector.tsx
  13. 8 0
      webview-ui/src/components/chat/ChatTextArea.tsx
  14. 2 0
      webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
  15. 156 0
      webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx
  16. 1 0
      webview-ui/src/context/ExtensionStateContext.tsx
  17. 2 0
      webview-ui/src/i18n/locales/ca/chat.json
  18. 2 0
      webview-ui/src/i18n/locales/de/chat.json
  19. 2 0
      webview-ui/src/i18n/locales/en/chat.json
  20. 2 0
      webview-ui/src/i18n/locales/es/chat.json
  21. 2 0
      webview-ui/src/i18n/locales/fr/chat.json
  22. 2 0
      webview-ui/src/i18n/locales/hi/chat.json
  23. 2 0
      webview-ui/src/i18n/locales/id/chat.json
  24. 2 0
      webview-ui/src/i18n/locales/it/chat.json
  25. 2 0
      webview-ui/src/i18n/locales/ja/chat.json
  26. 2 0
      webview-ui/src/i18n/locales/ko/chat.json
  27. 2 0
      webview-ui/src/i18n/locales/nl/chat.json
  28. 2 0
      webview-ui/src/i18n/locales/pl/chat.json
  29. 2 0
      webview-ui/src/i18n/locales/pt-BR/chat.json
  30. 2 0
      webview-ui/src/i18n/locales/ru/chat.json
  31. 2 0
      webview-ui/src/i18n/locales/tr/chat.json
  32. 2 0
      webview-ui/src/i18n/locales/vi/chat.json
  33. 2 0
      webview-ui/src/i18n/locales/zh-CN/chat.json
  34. 2 0
      webview-ui/src/i18n/locales/zh-TW/chat.json

+ 2 - 0
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"

+ 3 - 3
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/[email protected])(@types/node@20.17.50)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+      vitest: 3.2.4(@types/[email protected])(@types/node@24.2.1)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
   '@vitest/[email protected]':
     dependencies:
@@ -22251,8 +22251,8 @@ snapshots:
 
   [email protected]([email protected]):
     dependencies:
-      '@ai-sdk/provider': 2.0.1
-      '@ai-sdk/provider-utils': 3.0.20([email protected])
+      '@ai-sdk/provider': 2.0.0
+      '@ai-sdk/provider-utils': 3.0.5([email protected])
     transitivePeerDependencies:
       - zod
 

+ 12 - 1
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,

+ 5 - 0
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" },

+ 372 - 0
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<string, unknown> = {
+			mode: "code",
+			currentApiConfigName: "default-profile",
+		}
+
+		const workspaceState: Record<string, unknown> = {}
+
+		const secrets: Record<string, string | undefined> = {}
+
+		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" })
+		})
+	})
+})

+ 30 - 0
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" },

+ 5 - 0
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" },

+ 5 - 0
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" },

+ 5 - 0
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" },

+ 68 - 0
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<typeof vi.fn>
+				update: ReturnType<typeof vi.fn>
+			}
+		}
+		getState: ReturnType<typeof vi.fn>
+		postStateToWebview: ReturnType<typeof vi.fn>
+		providerSettingsManager: {
+			setModeConfig: ReturnType<typeof vi.fn>
+		}
+		postMessageToWebview: ReturnType<typeof vi.fn>
+		getCurrentTask: ReturnType<typeof vi.fn>
+	}
+
+	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()
+	})
+})

+ 8 - 0
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") ?? {}

+ 14 - 0
webview-ui/src/components/chat/ApiConfigSelector.tsx

@@ -20,6 +20,8 @@ interface ApiConfigSelectorProps {
 	listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }>
 	pinnedApiConfigs?: Record<string, boolean>
 	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}
 							/>
+							<IconButton
+								iconClass={lockApiConfigAcrossModes ? "codicon-lock" : "codicon-unlock"}
+								title={
+									lockApiConfigAcrossModes
+										? t("chat:unlockApiConfigAcrossModes")
+										: t("chat:lockApiConfigAcrossModes")
+								}
+								className={lockApiConfigAcrossModes ? "text-vscode-focusBorder" : "opacity-60"}
+								onClick={onToggleLockApiConfig}
+							/>
 						</div>
 
 						{/* Info icon and title on the right with matching spacing */}

+ 8 - 0
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -103,6 +103,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			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<HTMLTextAreaElement, ChatTextAreaProps>(
 			vscode.postMessage({ type: "loadApiConfigurationById", text: value })
 		}, [])
 
+		const handleToggleLockApiConfig = useCallback(() => {
+			const newValue = !lockApiConfigAcrossModes
+			vscode.postMessage({ type: "lockApiConfigAcrossModes", bool: newValue })
+		}, [lockApiConfigAcrossModes])
+
 		return (
 			<div
 				className={cn(
@@ -1316,6 +1322,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							listApiConfigMeta={listApiConfigMeta || []}
 							pinnedApiConfigs={pinnedApiConfigs}
 							togglePinnedApiConfig={togglePinnedApiConfig}
+							lockApiConfigAcrossModes={!!lockApiConfigAcrossModes}
+							onToggleLockApiConfig={handleToggleLockApiConfig}
 						/>
 						<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
 					</div>

+ 2 - 0
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(() => {

+ 156 - 0
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<typeof vi.fn>
+
+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<typeof vi.fn>).mockReturnValue({
+				...defaultState,
+				lockApiConfigAcrossModes: false,
+			})
+
+			render(<ChatTextArea {...defaultProps} />)
+
+			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<typeof vi.fn>).mockReturnValue({
+				...defaultState,
+				lockApiConfigAcrossModes: true,
+			})
+
+			render(<ChatTextArea {...defaultProps} />)
+
+			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<typeof vi.fn>).mockReturnValue({
+				...defaultState,
+			})
+
+			render(<ChatTextArea {...defaultProps} />)
+
+			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<typeof vi.fn>).mockReturnValue({
+				...defaultState,
+				lockApiConfigAcrossModes: false,
+			})
+
+			render(<ChatTextArea {...defaultProps} />)
+
+			// 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<typeof vi.fn>).mockReturnValue({
+				...defaultState,
+				lockApiConfigAcrossModes: true,
+			})
+
+			render(<ChatTextArea {...defaultProps} />)
+
+			// Clear any initialization messages
+			mockPostMessage.mockClear()
+
+			const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes")
+			fireEvent.click(button)
+
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "lockApiConfigAcrossModes",
+				bool: false,
+			})
+		})
+	})
+})

+ 1 - 0
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)

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
webview-ui/src/i18n/locales/hi/chat.json

@@ -111,6 +111,8 @@
 	},
 	"selectMode": "इंटरैक्शन मोड चुनें",
 	"selectApiConfig": "एपीआई कॉन्फ़िगरेशन का चयन करें",
+	"lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें",
+	"unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)",
 	"enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ",
 	"addImages": "संदेश में चित्र जोड़ें",
 	"sendMessage": "संदेश भेजें",

+ 2 - 0
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": {

+ 2 - 0
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",

+ 2 - 0
webview-ui/src/i18n/locales/ja/chat.json

@@ -111,6 +111,8 @@
 	},
 	"selectMode": "対話モードを選択",
 	"selectApiConfig": "API構成を選択",
+	"lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック",
+	"unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)",
 	"enhancePrompt": "追加コンテキストでプロンプトを強化",
 	"addImages": "メッセージに画像を追加",
 	"sendMessage": "メッセージを送信",

+ 2 - 0
webview-ui/src/i18n/locales/ko/chat.json

@@ -111,6 +111,8 @@
 	},
 	"selectMode": "상호작용 모드 선택",
 	"selectApiConfig": "API 구성 선택",
+	"lockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성 잠금",
+	"unlockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성이 잠겨 있습니다 (클릭하여 해제)",
 	"enhancePrompt": "추가 컨텍스트로 프롬프트 향상",
 	"addImages": "메시지에 이미지 추가",
 	"sendMessage": "메시지 보내기",

+ 2 - 0
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": {

+ 2 - 0
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ść",

+ 2 - 0
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",

+ 2 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -111,6 +111,8 @@
 	},
 	"selectMode": "Выберите режим взаимодействия",
 	"selectApiConfig": "Выберите конфигурацию API",
+	"lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве",
+	"unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)",
 	"enhancePrompt": "Улучшить запрос с дополнительным контекстом",
 	"enhancePromptDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.",
 	"modeSelector": {

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -111,6 +111,8 @@
 	},
 	"selectMode": "选择交互模式",
 	"selectApiConfig": "选择 API 配置",
+	"lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置",
+	"unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)",
 	"enhancePrompt": "增强提示词",
 	"addImages": "添加图片到消息",
 	"sendMessage": "发送消息",

+ 2 - 0
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -122,6 +122,8 @@
 	},
 	"selectMode": "選擇互動模式",
 	"selectApiConfig": "選取 API 設定",
+	"lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定",
+	"unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)",
 	"enhancePrompt": "使用額外內容強化提示詞",
 	"modeSelector": {
 		"title": "模式",