diff --git a/packages/cloud/src/WebAuthService.ts b/packages/cloud/src/WebAuthService.ts index 6e9c76b4632..e9961b02b88 100644 --- a/packages/cloud/src/WebAuthService.ts +++ b/packages/cloud/src/WebAuthService.ts @@ -704,7 +704,13 @@ export class WebAuthService extends EventEmitter implements A signal: AbortSignal.timeout(10000), }) - return clerkOrganizationMembershipsSchema.parse(await response.json()).response + if (response.ok) { + return clerkOrganizationMembershipsSchema.parse(await response.json()).response + } + + const errorMessage = `Failed to get organization memberships: ${response.status} ${response.statusText}` + this.log(`[auth] ${errorMessage}`) + throw new Error(errorMessage) } private async getOrganizationMetadata( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 5eacc9caea0..52217de88af 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -145,6 +145,10 @@ export class ClineProvider private pendingOperations: Map = new Map() private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds + private cloudOrganizationsCache: CloudOrganizationMembership[] | null = null + private cloudOrganizationsCacheTimestamp: number | null = null + private static readonly CLOUD_ORGANIZATIONS_CACHE_DURATION_MS = 5 * 1000 // 5 seconds + public isViewLaunched = false public settingsImportedAt?: number public readonly latestAnnouncementId = "nov-2025-v3.30.0-pr-fixer" // v3.30.0 PR Fixer announcement @@ -1919,7 +1923,19 @@ export class ClineProvider try { if (!CloudService.instance.isCloudAgent) { - cloudOrganizations = await CloudService.instance.getOrganizationMemberships() + const now = Date.now() + + if ( + this.cloudOrganizationsCache !== null && + this.cloudOrganizationsCacheTimestamp !== null && + now - this.cloudOrganizationsCacheTimestamp < ClineProvider.CLOUD_ORGANIZATIONS_CACHE_DURATION_MS + ) { + cloudOrganizations = this.cloudOrganizationsCache! + } else { + cloudOrganizations = await CloudService.instance.getOrganizationMemberships() + this.cloudOrganizationsCache = cloudOrganizations + this.cloudOrganizationsCacheTimestamp = now + } } } catch (error) { // Ignore this error. diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index a8ab39108d9..e9c306c4e13 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1,4 +1,4 @@ -// npx vitest core/webview/__tests__/ClineProvider.spec.ts +// pnpm --filter roo-cline test core/webview/__tests__/ClineProvider.spec.ts import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" @@ -786,7 +786,7 @@ describe("ClineProvider", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ type: "writeDelayMs", value: 2000 }) + await messageHandler({ type: "updateSettings", updatedSettings: { writeDelayMs: 2000 } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("writeDelayMs", 2000) expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000) @@ -800,24 +800,24 @@ describe("ClineProvider", () => { const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] // Simulate setting sound to enabled - await messageHandler({ type: "soundEnabled", bool: true }) + await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: true } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true) expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true) expect(mockPostMessage).toHaveBeenCalled() // Simulate setting sound to disabled - await messageHandler({ type: "soundEnabled", bool: false }) + await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: false } }) expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false) expect(mockPostMessage).toHaveBeenCalled() // Simulate setting tts to enabled - await messageHandler({ type: "ttsEnabled", bool: true }) + await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: true } }) expect(setTtsEnabled).toHaveBeenCalledWith(true) expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", true) expect(mockPostMessage).toHaveBeenCalled() // Simulate setting tts to disabled - await messageHandler({ type: "ttsEnabled", bool: false }) + await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: false } }) expect(setTtsEnabled).toHaveBeenCalledWith(false) expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", false) expect(mockPostMessage).toHaveBeenCalled() @@ -856,7 +856,7 @@ describe("ClineProvider", () => { test("handles autoCondenseContext message", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ type: "autoCondenseContext", bool: false }) + await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContext: false } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContext", false) expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContext", false) expect(mockPostMessage).toHaveBeenCalled() @@ -876,7 +876,7 @@ describe("ClineProvider", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ type: "autoCondenseContextPercent", value: 75 }) + await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContextPercent: 75 } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContextPercent", 75) expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContextPercent", 75) @@ -984,7 +984,7 @@ describe("ClineProvider", () => { const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] // Test browserToolEnabled - await messageHandler({ type: "browserToolEnabled", bool: true }) + await messageHandler({ type: "updateSettings", updatedSettings: { browserToolEnabled: true } }) expect(mockContext.globalState.update).toHaveBeenCalledWith("browserToolEnabled", true) expect(mockPostMessage).toHaveBeenCalled() @@ -1002,13 +1002,13 @@ describe("ClineProvider", () => { expect((await provider.getState()).showRooIgnoredFiles).toBe(false) // Test showRooIgnoredFiles with true - await messageHandler({ type: "showRooIgnoredFiles", bool: true }) + await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: true } }) expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true) expect(mockPostMessage).toHaveBeenCalled() expect((await provider.getState()).showRooIgnoredFiles).toBe(true) // Test showRooIgnoredFiles with false - await messageHandler({ type: "showRooIgnoredFiles", bool: false }) + await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: false } }) expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false) expect(mockPostMessage).toHaveBeenCalled() expect((await provider.getState()).showRooIgnoredFiles).toBe(false) @@ -1019,13 +1019,13 @@ describe("ClineProvider", () => { const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] // Test alwaysApproveResubmit - await messageHandler({ type: "alwaysApproveResubmit", bool: true }) + await messageHandler({ type: "updateSettings", updatedSettings: { alwaysApproveResubmit: true } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("alwaysApproveResubmit", true) expect(mockContext.globalState.update).toHaveBeenCalledWith("alwaysApproveResubmit", true) expect(mockPostMessage).toHaveBeenCalled() // Test requestDelaySeconds - await messageHandler({ type: "requestDelaySeconds", value: 10 }) + await messageHandler({ type: "updateSettings", updatedSettings: { requestDelaySeconds: 10 } }) expect(mockContext.globalState.update).toHaveBeenCalledWith("requestDelaySeconds", 10) expect(mockPostMessage).toHaveBeenCalled() }) @@ -1092,7 +1092,7 @@ describe("ClineProvider", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ type: "maxWorkspaceFiles", value: 300 }) + await messageHandler({ type: "updateSettings", updatedSettings: { maxWorkspaceFiles: 300 } }) expect(updateGlobalStateSpy).toHaveBeenCalledWith("maxWorkspaceFiles", 300) expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 3fd2a47f377..c436f160147 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -721,8 +721,8 @@ describe("webviewMessageHandler - mcpEnabled", () => { it("delegates enable=true to McpHub and posts updated state", async () => { await webviewMessageHandler(mockClineProvider, { - type: "mcpEnabled", - bool: true, + type: "updateSettings", + updatedSettings: { mcpEnabled: true }, }) expect((mockClineProvider as any).getMcpHub).toHaveBeenCalledTimes(1) @@ -733,8 +733,8 @@ describe("webviewMessageHandler - mcpEnabled", () => { it("delegates enable=false to McpHub and posts updated state", async () => { await webviewMessageHandler(mockClineProvider, { - type: "mcpEnabled", - bool: false, + type: "updateSettings", + updatedSettings: { mcpEnabled: false }, }) expect((mockClineProvider as any).getMcpHub).toHaveBeenCalledTimes(1) @@ -747,8 +747,8 @@ describe("webviewMessageHandler - mcpEnabled", () => { ;(mockClineProvider as any).getMcpHub = vi.fn().mockReturnValue(undefined) await webviewMessageHandler(mockClineProvider, { - type: "mcpEnabled", - bool: true, + type: "updateSettings", + updatedSettings: { mcpEnabled: true }, }) expect((mockClineProvider as any).getMcpHub).toHaveBeenCalledTimes(1) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 847ea1a1615..c85dea9d16f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -10,9 +10,11 @@ import { type GlobalState, type ClineMessage, type TelemetrySetting, + type UserSettingsConfig, TelemetryEventName, - UserSettingsConfig, - DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + RooCodeSettings, + Experiments, + ExperimentId, } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" @@ -507,16 +509,10 @@ export const webviewMessageHandler = async ( try { await provider.createTask(message.text, message.images) // Task created successfully - notify the UI to reset - await provider.postMessageToWebview({ - type: "invoke", - invoke: "newChat", - }) + await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" }) } catch (error) { // For all errors, reset the UI and show error - await provider.postMessageToWebview({ - type: "invoke", - invoke: "newChat", - }) + await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" }) // Show error to user vscode.window.showErrorMessage( `Failed to create task: ${error instanceof Error ? error.message : String(error)}`, @@ -526,69 +522,111 @@ export const webviewMessageHandler = async ( case "customInstructions": await provider.updateCustomInstructions(message.text) break - case "alwaysAllowReadOnly": - await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowReadOnlyOutsideWorkspace": - await updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowWrite": - await updateGlobalState("alwaysAllowWrite", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowWriteOutsideWorkspace": - await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowWriteProtected": - await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowExecute": - await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowBrowser": - await updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) - await provider.postStateToWebview() - break - case "alwaysAllowMcp": - await updateGlobalState("alwaysAllowMcp", message.bool) - await provider.postStateToWebview() - break - case "alwaysAllowModeSwitch": - await updateGlobalState("alwaysAllowModeSwitch", message.bool) - await provider.postStateToWebview() - break - case "allowedMaxRequests": - await updateGlobalState("allowedMaxRequests", message.value) - await provider.postStateToWebview() - break - case "allowedMaxCost": - await updateGlobalState("allowedMaxCost", message.value) - await provider.postStateToWebview() - break - case "alwaysAllowSubtasks": - await updateGlobalState("alwaysAllowSubtasks", message.bool) - await provider.postStateToWebview() - break - case "alwaysAllowUpdateTodoList": - await updateGlobalState("alwaysAllowUpdateTodoList", message.bool) - await provider.postStateToWebview() - break + case "askResponse": provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break - case "autoCondenseContext": - await updateGlobalState("autoCondenseContext", message.bool) - await provider.postStateToWebview() - break - case "autoCondenseContextPercent": - await updateGlobalState("autoCondenseContextPercent", message.value) - await provider.postStateToWebview() + + case "updateSettings": + if (message.updatedSettings) { + for (const [key, value] of Object.entries(message.updatedSettings)) { + let newValue = value + + if (key === "language") { + newValue = value ?? "en" + changeLanguage(newValue as Language) + } else if (key === "allowedCommands") { + const commands = value ?? [] + + newValue = Array.isArray(commands) + ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) + : [] + + await vscode.workspace + .getConfiguration(Package.name) + .update("allowedCommands", newValue, vscode.ConfigurationTarget.Global) + } else if (key === "deniedCommands") { + const commands = value ?? [] + + newValue = Array.isArray(commands) + ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) + : [] + + await vscode.workspace + .getConfiguration(Package.name) + .update("deniedCommands", newValue, vscode.ConfigurationTarget.Global) + } else if (key === "ttsEnabled") { + newValue = value ?? true + setTtsEnabled(newValue as boolean) + } else if (key === "ttsSpeed") { + newValue = value ?? 1.0 + setTtsSpeed(newValue as number) + } else if (key === "terminalShellIntegrationTimeout") { + if (value !== undefined) { + Terminal.setShellIntegrationTimeout(value as number) + } + } else if (key === "terminalShellIntegrationDisabled") { + if (value !== undefined) { + Terminal.setShellIntegrationDisabled(value as boolean) + } + } else if (key === "terminalCommandDelay") { + if (value !== undefined) { + Terminal.setCommandDelay(value as number) + } + } else if (key === "terminalPowershellCounter") { + if (value !== undefined) { + Terminal.setPowershellCounter(value as boolean) + } + } else if (key === "terminalZshClearEolMark") { + if (value !== undefined) { + Terminal.setTerminalZshClearEolMark(value as boolean) + } + } else if (key === "terminalZshOhMy") { + if (value !== undefined) { + Terminal.setTerminalZshOhMy(value as boolean) + } + } else if (key === "terminalZshP10k") { + if (value !== undefined) { + Terminal.setTerminalZshP10k(value as boolean) + } + } else if (key === "terminalZdotdir") { + if (value !== undefined) { + Terminal.setTerminalZdotdir(value as boolean) + } + } else if (key === "terminalCompressProgressBar") { + if (value !== undefined) { + Terminal.setCompressProgressBar(value as boolean) + } + } else if (key === "mcpEnabled") { + newValue = value ?? true + const mcpHub = provider.getMcpHub() + + if (mcpHub) { + await mcpHub.handleMcpEnabledChange(newValue as boolean) + } + } else if (key === "experiments") { + if (!value) { + continue + } + + newValue = { + ...(getGlobalState("experiments") ?? experimentDefault), + ...(value as Record), + } + } else if (key === "customSupportPrompts") { + if (!value) { + continue + } + } + + await provider.contextProxy.setValue(key as keyof RooCodeSettings, newValue) + } + + await provider.postStateToWebview() + } + break + case "terminalOperation": if (message.terminalOperation) { provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) @@ -1053,38 +1091,6 @@ export const webviewMessageHandler = async ( case "cancelTask": await provider.cancelTask() break - case "allowedCommands": { - // Validate and sanitize the commands array - const commands = message.commands ?? [] - const validCommands = Array.isArray(commands) - ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - await updateGlobalState("allowedCommands", validCommands) - - // Also update workspace settings. - await vscode.workspace - .getConfiguration(Package.name) - .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global) - - break - } - case "deniedCommands": { - // Validate and sanitize the commands array - const commands = message.commands ?? [] - const validCommands = Array.isArray(commands) - ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - await updateGlobalState("deniedCommands", validCommands) - - // Also update workspace settings. - await vscode.workspace - .getConfiguration(Package.name) - .update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global) - - break - } case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() @@ -1219,18 +1225,6 @@ export const webviewMessageHandler = async ( } break } - case "mcpEnabled": - const mcpEnabled = message.bool ?? true - await updateGlobalState("mcpEnabled", mcpEnabled) - - const mcpHubInstance = provider.getMcpHub() - - if (mcpHubInstance) { - await mcpHubInstance.handleMcpEnabledChange(mcpEnabled) - } - - await provider.postStateToWebview() - break case "enableMcpServerCreation": await updateGlobalState("enableMcpServerCreation", message.bool ?? true) await provider.postStateToWebview() @@ -1244,21 +1238,24 @@ export const webviewMessageHandler = async ( ) } break + case "taskSyncEnabled": const enabled = message.bool ?? false - const updatedSettings: Partial = { - taskSyncEnabled: enabled, - } - // If disabling task sync, also disable remote control + const updatedSettings: Partial = { taskSyncEnabled: enabled } + + // If disabling task sync, also disable remote control. if (!enabled) { updatedSettings.extensionBridgeEnabled = false } + try { await CloudService.instance.updateUserSettings(updatedSettings) } catch (error) { provider.log(`Failed to update cloud settings for task sync: ${error}`) } + break + case "refreshAllMcpServers": { const mcpHub = provider.getMcpHub() @@ -1268,16 +1265,7 @@ export const webviewMessageHandler = async ( break } - case "soundEnabled": - const soundEnabled = message.bool ?? true - await updateGlobalState("soundEnabled", soundEnabled) - await provider.postStateToWebview() - break - case "soundVolume": - const soundVolume = message.value ?? 0.5 - await updateGlobalState("soundVolume", soundVolume) - await provider.postStateToWebview() - break + case "ttsEnabled": const ttsEnabled = message.bool ?? true await updateGlobalState("ttsEnabled", ttsEnabled) @@ -1302,40 +1290,7 @@ export const webviewMessageHandler = async ( case "stopTts": stopTts() break - case "diffEnabled": - const diffEnabled = message.bool ?? true - await updateGlobalState("diffEnabled", diffEnabled) - await provider.postStateToWebview() - break - case "enableCheckpoints": - const enableCheckpoints = message.bool ?? true - await updateGlobalState("enableCheckpoints", enableCheckpoints) - await provider.postStateToWebview() - break - case "checkpointTimeout": - const checkpointTimeout = message.value ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS - await updateGlobalState("checkpointTimeout", checkpointTimeout) - await provider.postStateToWebview() - break - case "browserViewportSize": - const browserViewportSize = message.text ?? "900x600" - await updateGlobalState("browserViewportSize", browserViewportSize) - await provider.postStateToWebview() - break - case "remoteBrowserHost": - await updateGlobalState("remoteBrowserHost", message.text) - await provider.postStateToWebview() - break - case "remoteBrowserEnabled": - // Store the preference in global state - // remoteBrowserEnabled now means "enable remote browser connection" - await updateGlobalState("remoteBrowserEnabled", message.bool ?? false) - // If disabling remote browser connection, clear the remoteBrowserHost - if (!message.bool) { - await updateGlobalState("remoteBrowserHost", undefined) - } - await provider.postStateToWebview() - break + case "testBrowserConnection": // If no text is provided, try auto-discovery if (!message.text) { @@ -1372,10 +1327,7 @@ export const webviewMessageHandler = async ( }) } break - case "fuzzyMatchThreshold": - await updateGlobalState("fuzzyMatchThreshold", message.value) - await provider.postStateToWebview() - break + case "updateVSCodeSetting": { const { setting, value } = message @@ -1412,129 +1364,10 @@ export const webviewMessageHandler = async ( } break - case "alwaysApproveResubmit": - await updateGlobalState("alwaysApproveResubmit", message.bool ?? false) - await provider.postStateToWebview() - break - case "requestDelaySeconds": - await updateGlobalState("requestDelaySeconds", message.value ?? 5) - await provider.postStateToWebview() - break - case "writeDelayMs": - await updateGlobalState("writeDelayMs", message.value) - await provider.postStateToWebview() - break - case "diagnosticsEnabled": - await updateGlobalState("diagnosticsEnabled", message.bool ?? true) - await provider.postStateToWebview() - break - case "terminalOutputLineLimit": - // Validate that the line limit is a positive number - const lineLimit = message.value - if (typeof lineLimit === "number" && lineLimit > 0) { - await updateGlobalState("terminalOutputLineLimit", lineLimit) - await provider.postStateToWebview() - } else { - vscode.window.showErrorMessage( - t("common:errors.invalid_line_limit") || "Terminal output line limit must be a positive number", - ) - } - break - case "terminalOutputCharacterLimit": - // Validate that the character limit is a positive number - const charLimit = message.value - if (typeof charLimit === "number" && charLimit > 0) { - await updateGlobalState("terminalOutputCharacterLimit", charLimit) - await provider.postStateToWebview() - } else { - vscode.window.showErrorMessage( - t("common:errors.invalid_character_limit") || - "Terminal output character limit must be a positive number", - ) - } - break - case "terminalShellIntegrationTimeout": - await updateGlobalState("terminalShellIntegrationTimeout", message.value) - await provider.postStateToWebview() - if (message.value !== undefined) { - Terminal.setShellIntegrationTimeout(message.value) - } - break - case "terminalShellIntegrationDisabled": - await updateGlobalState("terminalShellIntegrationDisabled", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setShellIntegrationDisabled(message.bool) - } - break - case "terminalCommandDelay": - await updateGlobalState("terminalCommandDelay", message.value) - await provider.postStateToWebview() - if (message.value !== undefined) { - Terminal.setCommandDelay(message.value) - } - break - case "terminalPowershellCounter": - await updateGlobalState("terminalPowershellCounter", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setPowershellCounter(message.bool) - } - break - case "terminalZshClearEolMark": - await updateGlobalState("terminalZshClearEolMark", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setTerminalZshClearEolMark(message.bool) - } - break - case "terminalZshOhMy": - await updateGlobalState("terminalZshOhMy", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setTerminalZshOhMy(message.bool) - } - break - case "terminalZshP10k": - await updateGlobalState("terminalZshP10k", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setTerminalZshP10k(message.bool) - } - break - case "terminalZdotdir": - await updateGlobalState("terminalZdotdir", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setTerminalZdotdir(message.bool) - } - break - case "terminalCompressProgressBar": - await updateGlobalState("terminalCompressProgressBar", message.bool) - await provider.postStateToWebview() - if (message.bool !== undefined) { - Terminal.setCompressProgressBar(message.bool) - } - break + case "mode": await provider.handleModeSwitch(message.text as Mode) break - case "updateSupportPrompt": - try { - if (!message?.values) { - return - } - - // Replace all prompts with the new values from the cached state - await updateGlobalState("customSupportPrompts", message.values) - await provider.postStateToWebview() - } catch (error) { - provider.log( - `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.update_support_prompt")) - } - break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { const existingPrompts = getGlobalState("customModePrompts") ?? {} @@ -1594,96 +1427,12 @@ export const webviewMessageHandler = async ( } break } - case "screenshotQuality": - await updateGlobalState("screenshotQuality", message.value) - await provider.postStateToWebview() - break - case "maxOpenTabsContext": - const tabCount = Math.min(Math.max(0, message.value ?? 20), 500) - await updateGlobalState("maxOpenTabsContext", tabCount) - await provider.postStateToWebview() - break - case "maxWorkspaceFiles": - const fileCount = Math.min(Math.max(0, message.value ?? 200), 500) - await updateGlobalState("maxWorkspaceFiles", fileCount) - await provider.postStateToWebview() - break - case "alwaysAllowFollowupQuestions": - await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false) - await provider.postStateToWebview() - break - case "followupAutoApproveTimeoutMs": - await updateGlobalState("followupAutoApproveTimeoutMs", message.value) - await provider.postStateToWebview() - break - case "browserToolEnabled": - await updateGlobalState("browserToolEnabled", message.bool ?? true) - await provider.postStateToWebview() - break - case "language": - changeLanguage(message.text ?? "en") - await updateGlobalState("language", message.text as Language) - await provider.postStateToWebview() - break - case "openRouterImageApiKey": - await provider.contextProxy.setValue("openRouterImageApiKey", message.text) - await provider.postStateToWebview() - break - case "openRouterImageGenerationSelectedModel": - await provider.contextProxy.setValue("openRouterImageGenerationSelectedModel", message.text) - await provider.postStateToWebview() - break - case "showRooIgnoredFiles": - await updateGlobalState("showRooIgnoredFiles", message.bool ?? false) - await provider.postStateToWebview() - break + case "hasOpenedModeSelector": await updateGlobalState("hasOpenedModeSelector", message.bool ?? true) await provider.postStateToWebview() break - case "maxReadFileLine": - await updateGlobalState("maxReadFileLine", message.value) - await provider.postStateToWebview() - break - case "maxImageFileSize": - await updateGlobalState("maxImageFileSize", message.value) - await provider.postStateToWebview() - break - case "maxTotalImageSize": - await updateGlobalState("maxTotalImageSize", message.value) - await provider.postStateToWebview() - break - case "maxConcurrentFileReads": - const valueToSave = message.value // Capture the value intended for saving - await updateGlobalState("maxConcurrentFileReads", valueToSave) - await provider.postStateToWebview() - break - case "includeDiagnosticMessages": - // Only apply default if the value is truly undefined (not false) - const includeValue = message.bool !== undefined ? message.bool : true - await updateGlobalState("includeDiagnosticMessages", includeValue) - await provider.postStateToWebview() - break - case "includeCurrentTime": - await updateGlobalState("includeCurrentTime", message.bool ?? true) - await provider.postStateToWebview() - break - case "includeCurrentCost": - await updateGlobalState("includeCurrentCost", message.bool ?? true) - await provider.postStateToWebview() - break - case "maxDiagnosticMessages": - await updateGlobalState("maxDiagnosticMessages", message.value ?? 50) - await provider.postStateToWebview() - break - case "setHistoryPreviewCollapsed": // Add the new case handler - await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) - // No need to call postStateToWebview here as the UI already updated optimistically - break - case "setReasoningBlockCollapsed": - await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true) - // No need to call postStateToWebview here as the UI already updated optimistically - break + case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} @@ -1703,27 +1452,17 @@ export const webviewMessageHandler = async ( await updateGlobalState("enhancementApiConfigId", message.text) await provider.postStateToWebview() break - case "includeTaskHistoryInEnhance": - await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? true) - await provider.postStateToWebview() - break - case "condensingApiConfigId": - await updateGlobalState("condensingApiConfigId", message.text) - await provider.postStateToWebview() - break + case "updateCondensingPrompt": - // Store the condensing prompt in customSupportPrompts["CONDENSE"] instead of customCondensingPrompt + // Store the condensing prompt in customSupportPrompts["CONDENSE"] + // instead of customCondensingPrompt. const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } await updateGlobalState("customSupportPrompts", updatedSupportPrompts) - // Also update the old field for backward compatibility during migration + // Also update the old field for backward compatibility during migration. await updateGlobalState("customCondensingPrompt", message.text) await provider.postStateToWebview() break - case "profileThresholds": - await updateGlobalState("profileThresholds", message.values) - await provider.postStateToWebview() - break case "autoApprovalEnabled": await updateGlobalState("autoApprovalEnabled", message.bool ?? false) await provider.postStateToWebview() @@ -1755,7 +1494,6 @@ export const webviewMessageHandler = async ( }) if (result.success && result.enhancedText) { - // Capture telemetry for prompt enhancement MessageEnhancer.captureTelemetry(currentCline?.taskId, includeTaskHistoryInEnhance) await provider.postMessageToWebview({ type: "enhancedPrompt", text: result.enhancedText }) } else { @@ -2009,21 +1747,7 @@ export const webviewMessageHandler = async ( vscode.window.showErrorMessage(t("common:errors.list_api_config")) } break - case "updateExperimental": { - if (!message.values) { - break - } - - const updatedExperiments = { - ...(getGlobalState("experiments") ?? experimentDefault), - ...message.values, - } - await updateGlobalState("experiments", updatedExperiments) - - await provider.postStateToWebview() - break - } case "updateMcpTimeout": if (message.serverName && typeof message.timeout === "number") { try { @@ -2373,8 +2097,10 @@ export const webviewMessageHandler = async ( if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) { TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, telemetrySetting) } + // Update the telemetry state await updateGlobalState("telemetrySetting", telemetrySetting) + if (TelemetryService.hasInstance()) { TelemetryService.instance.updateTelemetryState(isOptedIn) } @@ -3161,6 +2887,7 @@ export const webviewMessageHandler = async ( break } + case "dismissUpsell": { if (message.upsellId) { try { @@ -3195,5 +2922,32 @@ export const webviewMessageHandler = async ( }) break } + default: { + // console.log(`Unhandled message type: ${message.type}`) + // + // Currently unhandled: + // + // "currentApiConfigName" | + // "codebaseIndexEnabled" | + // "enhancedPrompt" | + // "systemPrompt" | + // "exportModeResult" | + // "importModeResult" | + // "checkRulesDirectoryResult" | + // "browserConnectionResult" | + // "vsCodeSetting" | + // "indexingStatusUpdate" | + // "indexCleared" | + // "marketplaceInstallResult" | + // "shareTaskSuccess" | + // "playSound" | + // "draggedImages" | + // "setApiConfigPassword" | + // "setopenAiCustomModelInfo" | + // "marketplaceButtonClicked" | + // "cancelMarketplaceInstall" | + // "imageGenerationSettings" + break + } } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index c8e1083aeb4..80c5532930e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -219,9 +219,7 @@ export type ExtensionState = Pick< | "currentApiConfigName" | "listApiConfigMeta" | "pinnedApiConfigs" - // | "lastShownAnnouncementId" | "customInstructions" - // | "taskHistory" // Optional in GlobalSettings, required here. | "dismissedUpsells" | "autoApprovalEnabled" | "alwaysAllowReadOnly" @@ -229,10 +227,8 @@ export type ExtensionState = Pick< | "alwaysAllowWrite" | "alwaysAllowWriteOutsideWorkspace" | "alwaysAllowWriteProtected" - // | "writeDelayMs" // Optional in GlobalSettings, required here. | "alwaysAllowBrowser" | "alwaysApproveResubmit" - // | "requestDelaySeconds" // Optional in GlobalSettings, required here. | "alwaysAllowMcp" | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" @@ -250,16 +246,11 @@ export type ExtensionState = Pick< | "remoteBrowserEnabled" | "cachedChromeHostUrl" | "remoteBrowserHost" - // | "enableCheckpoints" // Optional in GlobalSettings, required here. | "ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume" - // | "maxOpenTabsContext" // Optional in GlobalSettings, required here. - // | "maxWorkspaceFiles" // Optional in GlobalSettings, required here. - // | "showRooIgnoredFiles" // Optional in GlobalSettings, required here. - // | "maxReadFileLine" // Optional in GlobalSettings, required here. - | "maxConcurrentFileReads" // Optional in GlobalSettings, required here. + | "maxConcurrentFileReads" | "terminalOutputLineLimit" | "terminalOutputCharacterLimit" | "terminalShellIntegrationTimeout" @@ -274,14 +265,8 @@ export type ExtensionState = Pick< | "diagnosticsEnabled" | "diffEnabled" | "fuzzyMatchThreshold" - // | "experiments" // Optional in GlobalSettings, required here. | "language" - // | "telemetrySetting" // Optional in GlobalSettings, required here. - // | "mcpEnabled" // Optional in GlobalSettings, required here. - // | "enableMcpServerCreation" // Optional in GlobalSettings, required here. - // | "mode" // Optional in GlobalSettings, required here. | "modeApiConfigs" - // | "customModes" // Optional in GlobalSettings, required here. | "customModePrompts" | "customSupportPrompts" | "enhancementApiConfigId" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index f10808cd428..02f0876ad3a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { + type RooCodeSettings, type ProviderSettings, type PromptComponent, type ModeConfig, @@ -38,17 +39,6 @@ export interface WebviewMessage { | "renameApiConfiguration" | "getListApiConfiguration" | "customInstructions" - | "allowedCommands" - | "deniedCommands" - | "alwaysAllowReadOnly" - | "alwaysAllowReadOnlyOutsideWorkspace" - | "alwaysAllowWrite" - | "alwaysAllowWriteOutsideWorkspace" - | "alwaysAllowWriteProtected" - | "alwaysAllowExecute" - | "alwaysAllowFollowupQuestions" - | "alwaysAllowUpdateTodoList" - | "followupAutoApproveTimeoutMs" | "webviewDidLaunch" | "newTask" | "askResponse" @@ -80,30 +70,12 @@ export interface WebviewMessage { | "updateVSCodeSetting" | "getVSCodeSetting" | "vsCodeSetting" - | "alwaysAllowBrowser" - | "alwaysAllowMcp" - | "alwaysAllowModeSwitch" - | "allowedMaxRequests" - | "allowedMaxCost" - | "alwaysAllowSubtasks" - | "alwaysAllowUpdateTodoList" - | "autoCondenseContext" - | "autoCondenseContextPercent" - | "condensingApiConfigId" | "updateCondensingPrompt" | "playSound" | "playTts" | "stopTts" - | "soundEnabled" | "ttsEnabled" | "ttsSpeed" - | "soundVolume" - | "diffEnabled" - | "enableCheckpoints" - | "checkpointTimeout" - | "browserViewportSize" - | "screenshotQuality" - | "remoteBrowserHost" | "openKeyboardShortcuts" | "openMcpSettings" | "openProjectMcpSettings" @@ -113,9 +85,6 @@ export interface WebviewMessage { | "toggleToolEnabledForPrompt" | "toggleMcpServer" | "updateMcpTimeout" - | "fuzzyMatchThreshold" - | "writeDelayMs" - | "diagnosticsEnabled" | "enhancePrompt" | "enhancedPrompt" | "draggedImages" @@ -123,34 +92,17 @@ export interface WebviewMessage { | "deleteMessageConfirm" | "submitEditedMessage" | "editMessageConfirm" - | "terminalOutputLineLimit" - | "terminalOutputCharacterLimit" - | "terminalShellIntegrationTimeout" - | "terminalShellIntegrationDisabled" - | "terminalCommandDelay" - | "terminalPowershellCounter" - | "terminalZshClearEolMark" - | "terminalZshOhMy" - | "terminalZshP10k" - | "terminalZdotdir" - | "terminalCompressProgressBar" - | "mcpEnabled" | "enableMcpServerCreation" | "remoteControlEnabled" | "taskSyncEnabled" | "searchCommits" - | "alwaysApproveResubmit" - | "requestDelaySeconds" | "setApiConfigPassword" | "mode" | "updatePrompt" - | "updateSupportPrompt" | "getSystemPrompt" | "copySystemPrompt" | "systemPrompt" | "enhancementApiConfigId" - | "includeTaskHistoryInEnhance" - | "updateExperimental" | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" @@ -159,29 +111,14 @@ export interface WebviewMessage { | "checkpointDiff" | "checkpointRestore" | "deleteMcpServer" - | "maxOpenTabsContext" - | "maxWorkspaceFiles" | "humanRelayResponse" | "humanRelayCancel" - | "browserToolEnabled" | "codebaseIndexEnabled" | "telemetrySetting" - | "showRooIgnoredFiles" | "testBrowserConnection" | "browserConnectionResult" - | "remoteBrowserEnabled" - | "language" - | "maxReadFileLine" - | "maxImageFileSize" - | "maxTotalImageSize" - | "maxConcurrentFileReads" - | "includeDiagnosticMessages" - | "maxDiagnosticMessages" - | "includeCurrentTime" - | "includeCurrentCost" | "searchFiles" | "toggleApiConfigPin" - | "setHistoryPreviewCollapsed" | "hasOpenedModeSelector" | "cloudButtonClicked" | "rooCloudSignIn" @@ -196,9 +133,6 @@ export interface WebviewMessage { | "indexingStatusUpdate" | "indexCleared" | "focusPanelRequest" - | "profileThresholds" - | "setHistoryPreviewCollapsed" - | "setReasoningBlockCollapsed" | "openExternal" | "filterMarketplaceItems" | "marketplaceButtonClicked" @@ -209,7 +143,6 @@ export interface WebviewMessage { | "marketplaceInstallResult" | "fetchMarketplaceData" | "switchTab" - | "profileThresholds" | "shareTaskSuccess" | "exportMode" | "exportModeResult" @@ -226,13 +159,12 @@ export interface WebviewMessage { | "insertTextIntoTextarea" | "showMdmAuthRequiredNotification" | "imageGenerationSettings" - | "openRouterImageApiKey" - | "openRouterImageGenerationSelectedModel" | "queueMessage" | "removeQueuedMessage" | "editQueuedMessage" | "dismissUpsell" | "getDismissedUpsells" + | "updateSettings" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -309,6 +241,7 @@ export interface WebviewMessage { codebaseIndexVercelAiGatewayApiKey?: string codebaseIndexOpenRouterApiKey?: string } + updatedSettings?: RooCodeSettings } export const checkoutDiffPayloadSchema = z.object({ diff --git a/webview-ui/src/components/chat/AutoApproveDropdown.tsx b/webview-ui/src/components/chat/AutoApproveDropdown.tsx index 29b0251899f..cdf6ad97aa6 100644 --- a/webview-ui/src/components/chat/AutoApproveDropdown.tsx +++ b/webview-ui/src/components/chat/AutoApproveDropdown.tsx @@ -2,14 +2,21 @@ import React from "react" import { ListChecks, LayoutList, Settings, CheckCheck, X } from "lucide-react" import { vscode } from "@/utils/vscode" + import { cn } from "@/lib/utils" + import { useExtensionState } from "@/context/ExtensionStateContext" + import { useAppTranslation } from "@/i18n/TranslationContext" + +import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles" +import { useAutoApprovalState } from "@/hooks/useAutoApprovalState" + import { useRooPortal } from "@/components/ui/hooks/useRooPortal" + import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, ToggleSwitch, Button } from "@/components/ui" + import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle" -import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles" -import { useAutoApprovalState } from "@/hooks/useAutoApprovalState" interface AutoApproveDropdownProps { disabled?: boolean @@ -39,7 +46,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: const baseToggles = useAutoApprovalToggles() - // Include alwaysApproveResubmit in addition to the base toggles + // Include alwaysApproveResubmit in addition to the base toggles. const toggles = React.useMemo( () => ({ ...baseToggles, @@ -50,9 +57,8 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: const onAutoApproveToggle = React.useCallback( (key: AutoApproveSetting, value: boolean) => { - vscode.postMessage({ type: key, bool: value }) + vscode.postMessage({ type: "updateSettings", updatedSettings: { [key]: value } }) - // Update the specific toggle state switch (key) { case "alwaysAllowReadOnly": setAlwaysAllowReadOnly(value) @@ -86,7 +92,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: break } - // If enabling any option, ensure autoApprovalEnabled is true + // If enabling any option, ensure autoApprovalEnabled is true. if (value && !autoApprovalEnabled) { setAutoApprovalEnabled(true) vscode.postMessage({ type: "autoApprovalEnabled", bool: true }) diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index b6da938edb4..0e9d2dd7184 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -89,8 +89,11 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec setAllowedCommands(newAllowed) setDeniedCommands(newDenied) - vscode.postMessage({ type: "allowedCommands", commands: newAllowed }) - vscode.postMessage({ type: "deniedCommands", commands: newDenied }) + + vscode.postMessage({ + type: "updateSettings", + updatedSettings: { allowedCommands: newAllowed, deniedCommands: newDenied }, + }) } const handleDenyPatternChange = (pattern: string) => { @@ -100,8 +103,11 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec setAllowedCommands(newAllowed) setDeniedCommands(newDenied) - vscode.postMessage({ type: "allowedCommands", commands: newAllowed }) - vscode.postMessage({ type: "deniedCommands", commands: newDenied }) + + vscode.postMessage({ + type: "updateSettings", + updatedSettings: { allowedCommands: newAllowed, deniedCommands: newDenied }, + }) } const onMessage = useCallback( diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx index 5afbdd93d4f..c8027edda35 100644 --- a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx @@ -1,3 +1,5 @@ +// pnpm --filter @roo-code/vscode-webview test src/components/chat/__tests__/CommandExecution.spec.tsx + import React from "react" import { render, screen, fireEvent } from "@testing-library/react" @@ -111,8 +113,13 @@ describe("CommandExecution", () => { expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm", "git push"]) expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith(["rm"]) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm", "git push"] }) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm"] }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { + allowedCommands: ["npm", "git push"], + deniedCommands: ["rm"], + }, + }) }) it("should handle deny command change", () => { @@ -127,8 +134,13 @@ describe("CommandExecution", () => { expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm"]) expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith(["rm", "docker run"]) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm"] }) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm", "docker run"] }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { + allowedCommands: ["npm"], + deniedCommands: ["rm", "docker run"], + }, + }) }) it("should toggle allowed command", () => { @@ -151,8 +163,13 @@ describe("CommandExecution", () => { // "npm test" is already in allowedCommands, so it should be removed expect(stateWithNpmTest.setAllowedCommands).toHaveBeenCalledWith([]) expect(stateWithNpmTest.setDeniedCommands).toHaveBeenCalledWith(["rm"]) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: [] }) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm"] }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { + allowedCommands: [], + deniedCommands: ["rm"], + }, + }) }) it("should toggle denied command", () => { @@ -175,8 +192,13 @@ describe("CommandExecution", () => { // "rm -rf" is already in deniedCommands, so it should be removed expect(stateWithRmRf.setAllowedCommands).toHaveBeenCalledWith(["npm"]) expect(stateWithRmRf.setDeniedCommands).toHaveBeenCalledWith([]) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm"] }) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: [] }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { + allowedCommands: ["npm"], + deniedCommands: [], + }, + }) }) it("should parse command with Output: separator", () => { @@ -311,8 +333,13 @@ Output here` // "rm file.txt" should be removed from denied and added to allowed expect(stateWithRmInDenied.setAllowedCommands).toHaveBeenCalledWith(["npm", "rm file.txt"]) expect(stateWithRmInDenied.setDeniedCommands).toHaveBeenCalledWith([]) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm", "rm file.txt"] }) - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: [] }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updateSettings", + updatedSettings: { + allowedCommands: ["npm", "rm file.txt"], + deniedCommands: [], + }, + }) }) describe("integration with CommandPatternSelector", () => { diff --git a/webview-ui/src/components/mcp/McpEnabledToggle.tsx b/webview-ui/src/components/mcp/McpEnabledToggle.tsx index 648cb77142d..e85738ac049 100644 --- a/webview-ui/src/components/mcp/McpEnabledToggle.tsx +++ b/webview-ui/src/components/mcp/McpEnabledToggle.tsx @@ -10,9 +10,13 @@ const McpEnabledToggle = () => { const handleChange = (e: Event | FormEvent) => { const target = ("target" in e ? e.target : null) as HTMLInputElement | null - if (!target) return + + if (!target) { + return + } + setMcpEnabled(target.checked) - vscode.postMessage({ type: "mcpEnabled", bool: target.checked }) + vscode.postMessage({ type: "updateSettings", updatedSettings: { mcpEnabled: target.checked } }) } return ( diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 332129ae035..8b267ecae2b 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -99,7 +99,7 @@ export const AutoApproveSettings = ({ const newCommands = [...currentCommands, commandInput] setCachedStateField("allowedCommands", newCommands) setCommandInput("") - vscode.postMessage({ type: "allowedCommands", commands: newCommands }) + vscode.postMessage({ type: "updateSettings", updatedSettings: { allowedCommands: newCommands } }) } } @@ -110,7 +110,7 @@ export const AutoApproveSettings = ({ const newCommands = [...currentCommands, deniedCommandInput] setCachedStateField("deniedCommands", newCommands) setDeniedCommandInput("") - vscode.postMessage({ type: "deniedCommands", commands: newCommands }) + vscode.postMessage({ type: "updateSettings", updatedSettings: { deniedCommands: newCommands } }) } } @@ -341,7 +341,11 @@ export const AutoApproveSettings = ({ onClick={() => { const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index) setCachedStateField("allowedCommands", newCommands) - vscode.postMessage({ type: "allowedCommands", commands: newCommands }) + + vscode.postMessage({ + type: "updateSettings", + updatedSettings: { allowedCommands: newCommands }, + }) }}>
{cmd}
@@ -392,7 +396,11 @@ export const AutoApproveSettings = ({ onClick={() => { const newCommands = (deniedCommands ?? []).filter((_, i) => i !== index) setCachedStateField("deniedCommands", newCommands) - vscode.postMessage({ type: "deniedCommands", commands: newCommands }) + + vscode.postMessage({ + type: "updateSettings", + updatedSettings: { deniedCommands: newCommands }, + }) }}>
{cmd}
diff --git a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx index e60b2db3232..633ff8be8d7 100644 --- a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx +++ b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx @@ -1,25 +1,17 @@ -import React, { useCallback } from "react" -import { Slider } from "@/components/ui" -import { useAppTranslation } from "@/i18n/TranslationContext" import { DEFAULT_CONSECUTIVE_MISTAKE_LIMIT } from "@roo-code/types" +import { useAppTranslation } from "@/i18n/TranslationContext" + +import { Slider } from "@/components/ui" + interface ConsecutiveMistakeLimitControlProps { value: number onChange: (value: number) => void } -export const ConsecutiveMistakeLimitControl: React.FC = ({ value, onChange }) => { +export const ConsecutiveMistakeLimitControl = ({ value, onChange }: ConsecutiveMistakeLimitControlProps) => { const { t } = useAppTranslation() - const handleValueChange = useCallback( - (newValue: number) => { - // Ensure value is not negative - const validValue = Math.max(0, newValue) - onChange(validValue) - }, - [onChange], - ) - return (
@@ -29,7 +21,7 @@ export const ConsecutiveMistakeLimitControl: React.FC handleValueChange(newValue[0])} + onValueChange={(newValue) => onChange(Math.max(0, newValue[0]))} /> {Math.max(0, value ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT)}
diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 217b205dd4a..c59ad0148ab 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -93,11 +93,9 @@ export const ContextManagementSettings = ({ ...profileThresholds, [selectedThresholdProfile]: value, } + setCachedStateField("profileThresholds", newThresholds) - vscode.postMessage({ - type: "profileThresholds", - values: newThresholds, - }) + vscode.postMessage({ type: "updateSettings", updatedSettings: { profileThresholds: newThresholds } }) } } return ( diff --git a/webview-ui/src/components/settings/MaxCostInput.tsx b/webview-ui/src/components/settings/MaxCostInput.tsx index 944b987d272..177ece5a1de 100644 --- a/webview-ui/src/components/settings/MaxCostInput.tsx +++ b/webview-ui/src/components/settings/MaxCostInput.tsx @@ -1,6 +1,5 @@ import { useTranslation } from "react-i18next" -import { vscode } from "@/utils/vscode" -import { useCallback } from "react" + import { FormattedTextField, unlimitedDecimalFormatter } from "../common/FormattedTextField" interface MaxCostInputProps { @@ -11,14 +10,6 @@ interface MaxCostInputProps { export function MaxCostInput({ allowedMaxCost, onValueChange }: MaxCostInputProps) { const { t } = useTranslation() - const handleValueChange = useCallback( - (value: number | undefined) => { - onValueChange(value) - vscode.postMessage({ type: "allowedMaxCost", value }) - }, - [onValueChange], - ) - return ( <> { - onValueChange(value) - vscode.postMessage({ type: "allowedMaxRequests", value }) - }, - [onValueChange], - ) - return ( <> @@ -215,8 +216,8 @@ const PromptsSettings = ({ } else { setCondensingApiConfigId(newConfigId) vscode.postMessage({ - type: "condensingApiConfigId", - text: newConfigId, + type: "updateSettings", + updatedSettings: { condensingApiConfigId: newConfigId }, }) } }}> @@ -257,12 +258,20 @@ const PromptsSettings = ({
{ - const value = e.target.checked - setIncludeTaskHistoryInEnhance(value) + onChange={(e: Event | FormEvent) => { + const target = ( + "target" in e ? e.target : null + ) as HTMLInputElement | null + + if (!target) { + return + } + + setIncludeTaskHistoryInEnhance(target.checked) + vscode.postMessage({ - type: "includeTaskHistoryInEnhance", - bool: value, + type: "updateSettings", + updatedSettings: { includeTaskHistoryInEnhance: target.checked }, }) }}> diff --git a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx index b01afd0ace5..e66232d3961 100644 --- a/webview-ui/src/components/settings/RateLimitSecondsControl.tsx +++ b/webview-ui/src/components/settings/RateLimitSecondsControl.tsx @@ -1,33 +1,20 @@ -import React, { useCallback } from "react" -import { Slider } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" +import { Slider } from "@/components/ui" + interface RateLimitSecondsControlProps { value: number onChange: (value: number) => void } -export const RateLimitSecondsControl: React.FC = ({ value, onChange }) => { +export const RateLimitSecondsControl = ({ value, onChange }: RateLimitSecondsControlProps) => { const { t } = useAppTranslation() - const handleValueChange = useCallback( - (newValue: number) => { - onChange(newValue) - }, - [onChange], - ) - return (
- handleValueChange(newValue[0])} - /> + onChange(newValue[0])} /> {value}s
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 0c9057f7c2a..3a9cb539a8e 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -27,7 +27,12 @@ import { Glasses, } from "lucide-react" -import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types" +import { + type ProviderSettings, + type ExperimentId, + type TelemetrySetting, + DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, +} from "@roo-code/types" import { vscode } from "@src/utils/vscode" import { cn } from "@src/lib/utils" @@ -285,20 +290,20 @@ const SettingsView = forwardRef(({ onDone, t const setOpenRouterImageApiKey = useCallback((apiKey: string) => { setCachedState((prevState) => { - // Only set change detected if value actually changed if (prevState.openRouterImageApiKey !== apiKey) { setChangeDetected(true) } + return { ...prevState, openRouterImageApiKey: apiKey } }) }, []) const setImageGenerationSelectedModel = useCallback((model: string) => { setCachedState((prevState) => { - // Only set change detected if value actually changed if (prevState.openRouterImageGenerationSelectedModel !== model) { setChangeDetected(true) } + return { ...prevState, openRouterImageGenerationSelectedModel: model } }) }, []) @@ -321,83 +326,89 @@ const SettingsView = forwardRef(({ onDone, t const handleSubmit = () => { if (isSettingValid) { - vscode.postMessage({ type: "language", text: language }) - vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ - type: "alwaysAllowReadOnlyOutsideWorkspace", - bool: alwaysAllowReadOnlyOutsideWorkspace, + type: "updateSettings", + updatedSettings: { + language, + alwaysAllowReadOnly: alwaysAllowReadOnly ?? undefined, + alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? undefined, + alwaysAllowWrite: alwaysAllowWrite ?? undefined, + alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? undefined, + alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? undefined, + alwaysAllowExecute: alwaysAllowExecute ?? undefined, + alwaysAllowBrowser: alwaysAllowBrowser ?? undefined, + alwaysAllowMcp, + alwaysAllowModeSwitch, + allowedCommands: allowedCommands ?? [], + deniedCommands: deniedCommands ?? [], + // Note that we use `null` instead of `undefined` since `JSON.stringify` + // will omit `undefined` when serializing the object and passing it to the + // extension host. We may need to do the same for other nullable fields. + allowedMaxRequests: allowedMaxRequests ?? null, + allowedMaxCost: allowedMaxCost ?? null, + autoCondenseContext, + autoCondenseContextPercent, + browserToolEnabled: browserToolEnabled ?? true, + soundEnabled: soundEnabled ?? true, + soundVolume: soundVolume ?? 0.5, + ttsEnabled, + ttsSpeed, + diffEnabled: diffEnabled ?? true, + enableCheckpoints: enableCheckpoints ?? false, + checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + browserViewportSize: browserViewportSize ?? "900x600", + remoteBrowserHost: remoteBrowserEnabled ? remoteBrowserHost : undefined, + remoteBrowserEnabled: remoteBrowserEnabled ?? false, + fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, + writeDelayMs, + screenshotQuality: screenshotQuality ?? 75, + terminalOutputLineLimit: terminalOutputLineLimit ?? 500, + terminalOutputCharacterLimit: terminalOutputCharacterLimit ?? 50_000, + terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 30_000, + terminalShellIntegrationDisabled, + terminalCommandDelay, + terminalPowershellCounter, + terminalZshClearEolMark, + terminalZshOhMy, + terminalZshP10k, + terminalZdotdir, + terminalCompressProgressBar, + mcpEnabled, + alwaysApproveResubmit: alwaysApproveResubmit ?? false, + requestDelaySeconds: requestDelaySeconds ?? 5, + maxOpenTabsContext: Math.min(Math.max(0, maxOpenTabsContext ?? 20), 500), + maxWorkspaceFiles: Math.min(Math.max(0, maxWorkspaceFiles ?? 200), 500), + showRooIgnoredFiles: showRooIgnoredFiles ?? true, + maxReadFileLine: maxReadFileLine ?? -1, + maxImageFileSize: maxImageFileSize ?? 5, + maxTotalImageSize: maxTotalImageSize ?? 20, + maxConcurrentFileReads: cachedState.maxConcurrentFileReads ?? 5, + includeDiagnosticMessages: + includeDiagnosticMessages !== undefined ? includeDiagnosticMessages : true, + maxDiagnosticMessages: maxDiagnosticMessages ?? 50, + alwaysAllowSubtasks, + alwaysAllowUpdateTodoList, + alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, + followupAutoApproveTimeoutMs, + condensingApiConfigId: condensingApiConfigId || "", + includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true, + reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, + includeCurrentTime: includeCurrentTime ?? true, + includeCurrentCost: includeCurrentCost ?? true, + profileThresholds, + openRouterImageApiKey, + openRouterImageGenerationSelectedModel, + experiments, + customSupportPrompts, + }, }) - vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) - vscode.postMessage({ type: "alwaysAllowWriteOutsideWorkspace", bool: alwaysAllowWriteOutsideWorkspace }) - vscode.postMessage({ type: "alwaysAllowWriteProtected", bool: alwaysAllowWriteProtected }) - vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) - vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser }) - vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp }) - vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) - vscode.postMessage({ type: "deniedCommands", commands: deniedCommands ?? [] }) - vscode.postMessage({ type: "allowedMaxRequests", value: allowedMaxRequests ?? undefined }) - vscode.postMessage({ type: "allowedMaxCost", value: allowedMaxCost ?? undefined }) - vscode.postMessage({ type: "autoCondenseContext", bool: autoCondenseContext }) - vscode.postMessage({ type: "autoCondenseContextPercent", value: autoCondenseContextPercent }) - vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled }) - vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) - vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled }) - vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed }) - vscode.postMessage({ type: "soundVolume", value: soundVolume }) - vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) - vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints }) - vscode.postMessage({ type: "checkpointTimeout", value: checkpointTimeout }) - vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize }) - vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost }) - vscode.postMessage({ type: "remoteBrowserEnabled", bool: remoteBrowserEnabled }) - vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) - vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs }) - vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 }) - vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 }) - vscode.postMessage({ type: "terminalOutputCharacterLimit", value: terminalOutputCharacterLimit ?? 50000 }) - vscode.postMessage({ type: "terminalShellIntegrationTimeout", value: terminalShellIntegrationTimeout }) - vscode.postMessage({ type: "terminalShellIntegrationDisabled", bool: terminalShellIntegrationDisabled }) - vscode.postMessage({ type: "terminalCommandDelay", value: terminalCommandDelay }) - vscode.postMessage({ type: "terminalPowershellCounter", bool: terminalPowershellCounter }) - vscode.postMessage({ type: "terminalZshClearEolMark", bool: terminalZshClearEolMark }) - vscode.postMessage({ type: "terminalZshOhMy", bool: terminalZshOhMy }) - vscode.postMessage({ type: "terminalZshP10k", bool: terminalZshP10k }) - vscode.postMessage({ type: "terminalZdotdir", bool: terminalZdotdir }) - vscode.postMessage({ type: "terminalCompressProgressBar", bool: terminalCompressProgressBar }) - vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled }) - vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit }) - vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds }) - vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext }) - vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 }) - vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) - vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) - vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 }) - vscode.postMessage({ type: "maxTotalImageSize", value: maxTotalImageSize ?? 20 }) - vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) - vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) - vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 }) - vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) - vscode.postMessage({ type: "updateExperimental", values: experiments }) - vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) - vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks }) - vscode.postMessage({ type: "alwaysAllowFollowupQuestions", bool: alwaysAllowFollowupQuestions }) - vscode.postMessage({ type: "alwaysAllowUpdateTodoList", bool: alwaysAllowUpdateTodoList }) - vscode.postMessage({ type: "followupAutoApproveTimeoutMs", value: followupAutoApproveTimeoutMs }) - vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" }) + + // These have more complex logic so they aren't (yet) handled + // by the `updateSettings` message. vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" }) - vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} }) - vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true }) - vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true }) - vscode.postMessage({ type: "includeCurrentTime", bool: includeCurrentTime ?? true }) - vscode.postMessage({ type: "includeCurrentCost", bool: includeCurrentCost ?? true }) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) - vscode.postMessage({ type: "profileThresholds", values: profileThresholds }) - vscode.postMessage({ type: "openRouterImageApiKey", text: openRouterImageApiKey }) - vscode.postMessage({ - type: "openRouterImageGenerationSelectedModel", - text: openRouterImageGenerationSelectedModel, - }) + setChangeDetected(false) } } diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 694ff174a70..80dfc7850f9 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -1,3 +1,5 @@ +// pnpm --filter @roo-code/vscode-webview test src/components/settings/__tests__/SettingsView.spec.tsx + import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" @@ -311,8 +313,10 @@ describe("SettingsView - Sound Settings", () => { expect(vscode.postMessage).toHaveBeenCalledWith( expect.objectContaining({ - type: "ttsEnabled", - bool: true, + type: "updateSettings", + updatedSettings: expect.objectContaining({ + ttsEnabled: true, + }), }), ) }) @@ -336,8 +340,10 @@ describe("SettingsView - Sound Settings", () => { expect(vscode.postMessage).toHaveBeenCalledWith( expect.objectContaining({ - type: "soundEnabled", - bool: true, + type: "updateSettings", + updatedSettings: expect.objectContaining({ + soundEnabled: true, + }), }), ) }) @@ -396,10 +402,14 @@ describe("SettingsView - Sound Settings", () => { fireEvent.click(saveButton) // Verify message sent to VSCode - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "ttsSpeed", - value: 0.75, - }) + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ + ttsSpeed: 0.75, + }), + }), + ) }) it("updates volume and sends message to VSCode when slider changes", () => { @@ -422,10 +432,14 @@ describe("SettingsView - Sound Settings", () => { fireEvent.click(saveButtons[0]) // Verify message sent to VSCode - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "soundVolume", - value: 0.75, - }) + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "updateSettings", + updatedSettings: expect.objectContaining({ + soundVolume: 0.75, + }), + }), + ) }) }) @@ -484,8 +498,10 @@ describe("SettingsView - Allowed Commands", () => { // Verify VSCode message was sent expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "allowedCommands", - commands: ["npm test"], + type: "updateSettings", + updatedSettings: { + allowedCommands: ["npm test"], + }, }) }) @@ -515,8 +531,10 @@ describe("SettingsView - Allowed Commands", () => { // Verify VSCode message was sent expect(vscode.postMessage).toHaveBeenLastCalledWith({ - type: "allowedCommands", - commands: [], + type: "updateSettings", + updatedSettings: { + allowedCommands: [], + }, }) }) @@ -631,8 +649,10 @@ describe("SettingsView - Duplicate Commands", () => { // Verify VSCode messages were sent expect(vscode.postMessage).toHaveBeenCalledWith( expect.objectContaining({ - type: "allowedCommands", - commands: ["npm test"], + type: "updateSettings", + updatedSettings: expect.objectContaining({ + allowedCommands: ["npm test"], + }), }), ) })