From 711b29e9ce9a5f97747e47a60216b087994d6d31 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 30 Apr 2026 18:54:25 +0000 Subject: [PATCH] fix: default lockApiConfigAcrossModes to true to prevent silent profile switching Changes the default value of lockApiConfigAcrossModes from false to true, so that by default, switching modes (whether user-initiated or AI-triggered) will NOT change the active API configuration/model. Users who want per-mode API configs can still unlock this via the lock icon in the API config selector popover. Also guards the cloud profile sync path to respect the lock setting, preventing silent profile changes from background cloud sync. Addresses #12222 and #12237. --- src/core/webview/ClineProvider.ts | 22 +++++++++++-------- .../ClineProvider.lockApiConfig.spec.ts | 21 ++++++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 2 ++ .../src/context/ExtensionStateContext.tsx | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1106d340050..dbaab430a1e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -431,11 +431,15 @@ export class ClineProvider await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) if (result.activeProfileChanged && result.activeProfileId) { - // Reload full settings for new active profile. - const profile = await this.providerSettingsManager.getProfile({ - id: result.activeProfileId, - }) - await this.activateProviderProfile({ name: profile.name }) + // Only switch the active profile if the user hasn't locked it across modes. + const lockApiConfig = this.context.workspaceState.get("lockApiConfigAcrossModes", true) + if (!lockApiConfig) { + // Reload full settings for new active profile. + const profile = await this.providerSettingsManager.getProfile({ + id: result.activeProfileId, + }) + await this.activateProviderProfile({ name: profile.name }) + } } await this.postStateToWebviewWithoutClineMessages() @@ -996,7 +1000,7 @@ 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. - const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", true) if (!historyItem.apiConfigName && !lockApiConfigAcrossModes && !skipProfileRestoreFromHistory) { const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode) @@ -1427,7 +1431,7 @@ 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) + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", true) if (lockApiConfigAcrossModes) { await this.postStateToWebview() return @@ -2341,7 +2345,7 @@ export class ClineProvider profileThresholds: profileThresholds ?? {}, cloudApiUrl: getRooCodeApiUrl(), hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, - lockApiConfigAcrossModes: lockApiConfigAcrossModes ?? false, + lockApiConfigAcrossModes: lockApiConfigAcrossModes ?? true, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, includeDiagnosticMessages: includeDiagnosticMessages ?? true, @@ -2562,7 +2566,7 @@ export class ClineProvider stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider, }, profileThresholds: stateValues.profileThresholds ?? {}, - lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", false), + lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", true), includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true, maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true, diff --git a/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts index 2cf9d4cae8b..5e9fc84a6ee 100644 --- a/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts @@ -320,6 +320,27 @@ describe("ClineProvider - Lock API Config Across Modes", () => { await provider.resolveWebviewView(mockWebviewView) }) + it("skips mode-specific config lookup/load by default (lockApiConfigAcrossModes defaults to true)", async () => { + // Do NOT set lockApiConfigAcrossModes - verify default behavior is locked + 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("skips mode-specific config lookup/load when lockApiConfigAcrossModes is true", async () => { await mockContext.workspaceState.update("lockApiConfigAcrossModes", true) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index da0fb2003fb..b492b786ae5 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1451,6 +1451,8 @@ describe("ClineProvider", () => { beforeEach(async () => { // Set up webview for each test await provider.resolveWebviewView(mockWebviewView) + // Unlock API config across modes so mode-specific configs are loaded + await mockContext.workspaceState.update("lockApiConfigAcrossModes", false) }) it("loads saved API config when switching modes", async () => { diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ce7a607d9a8..899803a9f2b 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -262,7 +262,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode openRouterImageGenerationSelectedModel: "", includeCurrentTime: true, includeCurrentCost: true, - lockApiConfigAcrossModes: false, + lockApiConfigAcrossModes: true, }) const [didHydrateState, setDidHydrateState] = useState(false)