From e3fb12a8b3f6658508c111e499bed3e8332d2e33 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 18 Jul 2025 17:34:56 +0000 Subject: [PATCH] feat: add optional persistent default mode setting - Add setAsDefault parameter to handleModeSwitch() method - Add setAsDefaultMode() method to update global state - Add setDefaultMode message type and handler - Update tests to use new setAsDefault parameter - Maintain backward compatibility with existing behavior - Address concern about removing persistent default mode functionality This allows users to optionally set a mode as the global default while maintaining multi-window mode isolation. --- apps/web-evals/next.config.js | 9 ++ src/core/webview/ClineProvider.ts | 18 ++- .../ClineProvider.multiWindow.spec.ts | 109 +++++++++++++++--- src/core/webview/webviewMessageHandler.ts | 8 +- src/shared/WebviewMessage.ts | 1 + 5 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 apps/web-evals/next.config.js diff --git a/apps/web-evals/next.config.js b/apps/web-evals/next.config.js new file mode 100644 index 0000000000..d9c03b2691 --- /dev/null +++ b/apps/web-evals/next.config.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var nextConfig = { + webpack: function (config) { + config.resolve.extensionAlias = { ".js": [".ts", ".tsx", ".js", ".jsx"] }; + return config; + }, +}; +exports.default = nextConfig; diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6b5725c03f..ead3c645d0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -802,8 +802,9 @@ export class ClineProvider /** * Handle switching to a new mode, including updating the associated API configuration * @param newMode The mode to switch to + * @param setAsDefault Whether to set this mode as the global default for new windows */ - public async handleModeSwitch(newMode: Mode) { + public async handleModeSwitch(newMode: Mode, setAsDefault: boolean = false) { const cline = this.getCurrentCline() if (cline) { @@ -814,6 +815,11 @@ export class ClineProvider // Store mode in instance-specific storage to prevent cross-window interference this.instanceMode = newMode + // If user wants to set this as the default mode, update global state + if (setAsDefault) { + await this.setAsDefaultMode(newMode) + } + // 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() @@ -844,6 +850,14 @@ export class ClineProvider await this.postStateToWebview() } + /** + * Set a mode as the global default for new windows + * @param mode The mode to set as default + */ + public async setAsDefaultMode(mode: Mode) { + await this.updateGlobalState("mode", mode) + } + // Provider Profile Management getProviderProfileEntries(): ProviderSettingsEntry[] { @@ -1416,6 +1430,7 @@ export class ClineProvider profileThresholds, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, + modeApiConfigs, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1529,6 +1544,7 @@ export class ClineProvider hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, + modeApiConfigs: modeApiConfigs ?? {}, } } diff --git a/src/core/webview/__tests__/ClineProvider.multiWindow.spec.ts b/src/core/webview/__tests__/ClineProvider.multiWindow.spec.ts index 4f10bc6bee..e710d363f7 100644 --- a/src/core/webview/__tests__/ClineProvider.multiWindow.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.multiWindow.spec.ts @@ -240,8 +240,8 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { expect(initialState1.mode).toBe("code") expect(initialState2.mode).toBe("code") - // Switch mode in provider1 to architect - await provider1.handleModeSwitch("architect") + // Switch mode in provider1 to architect (without setting as default) + await provider1.handleModeSwitch("architect", false) // Check states after mode switch const afterSwitchState1 = await provider1.getState() @@ -257,11 +257,11 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { await provider1.resolveWebviewView(mockWebviewView1) await provider2.resolveWebviewView(mockWebviewView2) - // Switch provider1 to architect mode - await provider1.handleModeSwitch("architect") + // Switch provider1 to architect mode (without setting as default) + await provider1.handleModeSwitch("architect", false) - // Switch provider2 to ask mode - await provider2.handleModeSwitch("ask") + // Switch provider2 to ask mode (without setting as default) + await provider2.handleModeSwitch("ask", false) // Check final states const finalState1 = await provider1.getState() @@ -273,8 +273,8 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { }) test("new provider instances initialize with global mode, not instance-specific modes", async () => { - // Switch provider1 to architect mode - await provider1.handleModeSwitch("architect") + // Switch provider1 to architect mode (without setting as default) + await provider1.handleModeSwitch("architect", false) // Create a new provider instance (simulating opening a new window) const contextProxy3 = new ContextProxy(mockContext1) @@ -301,9 +301,9 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { await provider1.resolveWebviewView(mockWebviewView1) await provider2.resolveWebviewView(mockWebviewView2) - // Switch modes in both providers - await provider1.handleModeSwitch("architect") - await provider2.handleModeSwitch("ask") + // Switch modes in both providers (without setting as default) + await provider1.handleModeSwitch("architect", false) + await provider2.handleModeSwitch("ask", false) // Get webview states const webviewState1 = await provider1.getStateToPostToWebview() @@ -348,8 +348,8 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { test("instance mode persists across state retrievals", async () => { await provider1.resolveWebviewView(mockWebviewView1) - // Switch to architect mode - await provider1.handleModeSwitch("architect") + // Switch to architect mode (without setting as default) + await provider1.handleModeSwitch("architect", false) // Get state multiple times const state1 = await provider1.getState() @@ -369,8 +369,8 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { const initialGlobalMode = mockContext1.globalState.get("mode") expect(initialGlobalMode).toBe("code") - // Switch instance mode - await provider1.handleModeSwitch("architect") + // Switch instance mode (without setting as default) + await provider1.handleModeSwitch("architect", false) // Global mode should remain unchanged const finalGlobalMode = mockContext1.globalState.get("mode") @@ -413,4 +413,83 @@ describe("ClineProvider - Multi-Window Mode Isolation", () => { // Should fallback to default mode expect(state.mode).toBe(defaultModeSlug) }) + + test("setting mode as default updates global state", async () => { + await provider1.resolveWebviewView(mockWebviewView1) + + // Get initial global mode + const initialGlobalMode = mockContext1.globalState.get("mode") + expect(initialGlobalMode).toBe("code") + + // Switch mode and set as default + await provider1.handleModeSwitch("architect", true) + + // Global mode should be updated + const finalGlobalMode = mockContext1.globalState.get("mode") + expect(finalGlobalMode).toBe("architect") + + // Instance should also have architect mode + const instanceState = await provider1.getState() + expect(instanceState.mode).toBe("architect") + }) + + test("new windows initialize with updated default mode", async () => { + await provider1.resolveWebviewView(mockWebviewView1) + + // Switch mode and set as default in provider1 + await provider1.handleModeSwitch("architect", true) + + // Create a new provider instance (simulating opening a new window) + const contextProxy3 = new ContextProxy(mockContext1) + await contextProxy3.initialize() + const provider3 = new ClineProvider(mockContext1, mockOutputChannel, "sidebar", contextProxy3) + provider3.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([]), + }) + + // New provider should initialize with the new default mode (architect) + const newProviderState = await provider3.getState() + expect(newProviderState.mode).toBe("architect") + }) + + test("setDefaultMode message handler updates global state", async () => { + await provider1.resolveWebviewView(mockWebviewView1) + + // Get initial global mode + const initialGlobalMode = mockContext1.globalState.get("mode") + expect(initialGlobalMode).toBe("code") + + // Get message handler + const messageHandler1 = (mockWebviewView1.webview.onDidReceiveMessage as any).mock.calls[0][0] + + // Send setDefaultMode message + await messageHandler1({ type: "setDefaultMode", mode: "architect" }) + + // Global mode should be updated + const finalGlobalMode = mockContext1.globalState.get("mode") + expect(finalGlobalMode).toBe("architect") + }) + + test("mode switch without setAsDefault preserves global state", async () => { + await provider1.resolveWebviewView(mockWebviewView1) + + // Get initial global mode + const initialGlobalMode = mockContext1.globalState.get("mode") + expect(initialGlobalMode).toBe("code") + + // Switch mode without setting as default + await provider1.handleModeSwitch("architect", false) + + // Global mode should remain unchanged + const finalGlobalMode = mockContext1.globalState.get("mode") + expect(finalGlobalMode).toBe("code") + + // But instance should have architect mode + const instanceState = await provider1.getState() + expect(instanceState.mode).toBe("architect") + }) }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6577fb2fb..7421014b7a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1133,7 +1133,13 @@ export const webviewMessageHandler = async ( } break case "mode": - await provider.handleModeSwitch(message.text as Mode) + await provider.handleModeSwitch(message.text as Mode, message.bool) + break + case "setDefaultMode": + if (message.text) { + await provider.setAsDefaultMode(message.text as Mode) + await provider.postStateToWebview() + } break case "updateSupportPrompt": try { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index fa9fb67310..d657807cd1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -128,6 +128,7 @@ export interface WebviewMessage { | "requestDelaySeconds" | "setApiConfigPassword" | "mode" + | "setDefaultMode" | "updatePrompt" | "updateSupportPrompt" | "getSystemPrompt"