From b3be00c05011eb48867fea3b64cf17ed290a576b Mon Sep 17 00:00:00 2001 From: MFPires Date: Tue, 28 Jan 2025 01:20:19 -0300 Subject: [PATCH 1/2] feat: Add auto-approval for mode switching Implements automatic approval for mode switching operations when enabled, following existing auto-approval patterns in the codebase. Implementation: - Added `alwaysAllowModeSwitch` to state management - Updated `isAutoApproved` function in ChatView to handle mode switch requests - Added mode switch option to AutoApproveMenu with appropriate handler - Integrated with existing auto-approval flow Tests: - Added three test cases in ChatView.auto-approve.test.tsx: 1. Verifies mode switch auto-approval when enabled 2. Verifies no auto-approval when mode switch setting is disabled 3. Verifies no auto-approval when global auto-approval is disabled The implementation follows existing patterns for other auto-approve features (read, write, browser, etc.) to maintain consistency in the codebase. --- src/core/webview/ClineProvider.ts | 12 +- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../src/components/chat/AutoApproveMenu.tsx | 16 ++ webview-ui/src/components/chat/ChatView.tsx | 7 +- .../__tests__/ChatView.auto-approve.test.tsx | 164 ++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 2 + 7 files changed, 201 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d532c27..0f96df2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -79,6 +79,8 @@ type GlobalStateKey = | "alwaysAllowWrite" | "alwaysAllowExecute" | "alwaysAllowBrowser" + | "alwaysAllowMcp" + | "alwaysAllowModeSwitch" | "taskHistory" | "openAiBaseUrl" | "openAiModelId" @@ -99,7 +101,6 @@ type GlobalStateKey = | "soundEnabled" | "soundVolume" | "diffEnabled" - | "alwaysAllowMcp" | "browserViewportSize" | "screenshotQuality" | "fuzzyMatchThreshold" @@ -620,6 +621,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("alwaysAllowMcp", message.bool) await this.postStateToWebview() break + case "alwaysAllowModeSwitch": + await this.updateGlobalState("alwaysAllowModeSwitch", message.bool) + await this.postStateToWebview() + break case "askResponse": this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break @@ -1848,6 +1853,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute, alwaysAllowBrowser, alwaysAllowMcp, + alwaysAllowModeSwitch, soundEnabled, diffEnabled, taskHistory, @@ -1882,6 +1888,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowBrowser: alwaysAllowBrowser ?? false, alwaysAllowMcp: alwaysAllowMcp ?? false, + alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false, uriScheme: vscode.env.uriScheme, clineMessages: this.cline?.clineMessages || [], taskHistory: (taskHistory || []) @@ -2009,6 +2016,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute, alwaysAllowBrowser, alwaysAllowMcp, + alwaysAllowModeSwitch, taskHistory, allowedCommands, soundEnabled, @@ -2078,6 +2086,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("alwaysAllowExecute") as Promise, this.getGlobalState("alwaysAllowBrowser") as Promise, this.getGlobalState("alwaysAllowMcp") as Promise, + this.getGlobalState("alwaysAllowModeSwitch") as Promise, this.getGlobalState("taskHistory") as Promise, this.getGlobalState("allowedCommands") as Promise, this.getGlobalState("soundEnabled") as Promise, @@ -2166,6 +2175,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowBrowser: alwaysAllowBrowser ?? false, alwaysAllowMcp: alwaysAllowMcp ?? false, + alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false, taskHistory, allowedCommands, soundEnabled: soundEnabled ?? false, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index e8f61b3..56075b8 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -91,6 +91,7 @@ export interface ExtensionState { alwaysAllowBrowser?: boolean alwaysAllowMcp?: boolean alwaysApproveResubmit?: boolean + alwaysAllowModeSwitch?: boolean requestDelaySeconds: number uriScheme?: string allowedCommands?: string[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 113b233..a027a93 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -41,6 +41,7 @@ export interface WebviewMessage { | "refreshOpenAiModels" | "alwaysAllowBrowser" | "alwaysAllowMcp" + | "alwaysAllowModeSwitch" | "playSound" | "soundEnabled" | "soundVolume" diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index c317109..b6b5c9a 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -28,6 +28,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowBrowser, alwaysAllowMcp, setAlwaysAllowMcp, + alwaysAllowModeSwitch, + setAlwaysAllowModeSwitch, alwaysApproveResubmit, setAlwaysApproveResubmit, autoApprovalEnabled, @@ -71,6 +73,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { enabled: alwaysAllowMcp ?? false, description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.", }, + { + id: "switchModes", + label: "Switch between modes", + shortName: "Modes", + enabled: alwaysAllowModeSwitch ?? false, + description: "Allows automatic switching between different AI modes without requiring approval.", + }, { id: "retryRequests", label: "Retry failed requests", @@ -120,6 +129,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { vscode.postMessage({ type: "alwaysAllowMcp", bool: newValue }) }, [alwaysAllowMcp, setAlwaysAllowMcp]) + const handleModeSwitchChange = useCallback(() => { + const newValue = !(alwaysAllowModeSwitch ?? false) + setAlwaysAllowModeSwitch(newValue) + vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: newValue }) + }, [alwaysAllowModeSwitch, setAlwaysAllowModeSwitch]) + const handleRetryChange = useCallback(() => { const newValue = !(alwaysApproveResubmit ?? false) setAlwaysApproveResubmit(newValue) @@ -133,6 +148,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { executeCommands: handleExecuteChange, useBrowser: handleBrowserChange, useMcp: handleMcpChange, + switchModes: handleModeSwitchChange, retryRequests: handleRetryChange, } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index da15b4e..59510cf 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -55,6 +55,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie mode, setMode, autoApprovalEnabled, + alwaysAllowModeSwitch, } = useExtensionState() //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined @@ -565,7 +566,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) || (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) || (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) || - (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) + (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) || + (alwaysAllowModeSwitch && + message.ask === "tool" && + JSON.parse(message.text || "{}")?.tool === "switchMode") ) }, [ @@ -579,6 +583,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie isAllowedCommand, alwaysAllowMcp, isMcpToolAlwaysAllowed, + alwaysAllowModeSwitch, ], ) diff --git a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.test.tsx b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.test.tsx index 6e720d9..f16e045 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.test.tsx @@ -313,4 +313,168 @@ describe("ChatView - Auto Approval Tests", () => { }) }) }) + + it("auto-approves mode switch when enabled", async () => { + render( + + {}} + showHistoryView={() => {}} + /> + , + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowModeSwitch: true, + autoApprovalEnabled: true, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + ], + }) + + // Then send the mode switch ask message + mockPostMessage({ + alwaysAllowModeSwitch: true, + autoApprovalEnabled: true, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + { + type: "ask", + ask: "tool", + ts: Date.now(), + text: JSON.stringify({ tool: "switchMode" }), + partial: false, + }, + ], + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "askResponse", + askResponse: "yesButtonClicked", + }) + }) + }) + + it("does not auto-approve mode switch when disabled", async () => { + render( + + {}} + showHistoryView={() => {}} + /> + , + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowModeSwitch: false, + autoApprovalEnabled: true, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + ], + }) + + // Then send the mode switch ask message + mockPostMessage({ + alwaysAllowModeSwitch: false, + autoApprovalEnabled: true, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + { + type: "ask", + ask: "tool", + ts: Date.now(), + text: JSON.stringify({ tool: "switchMode" }), + partial: false, + }, + ], + }) + + // Verify no auto-approval message was sent + expect(vscode.postMessage).not.toHaveBeenCalledWith({ + type: "askResponse", + askResponse: "yesButtonClicked", + }) + }) + + it("does not auto-approve mode switch when auto-approval is disabled", async () => { + render( + + {}} + showHistoryView={() => {}} + /> + , + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowModeSwitch: true, + autoApprovalEnabled: false, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + ], + }) + + // Then send the mode switch ask message + mockPostMessage({ + alwaysAllowModeSwitch: true, + autoApprovalEnabled: false, + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now() - 2000, + text: "Initial task", + }, + { + type: "ask", + ask: "tool", + ts: Date.now(), + text: JSON.stringify({ tool: "switchMode" }), + partial: false, + }, + ], + }) + + // Verify no auto-approval message was sent + expect(vscode.postMessage).not.toHaveBeenCalledWith({ + type: "askResponse", + askResponse: "yesButtonClicked", + }) + }) }) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 2d9fda0..7d0159d 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -33,6 +33,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowExecute: (value: boolean) => void setAlwaysAllowBrowser: (value: boolean) => void setAlwaysAllowMcp: (value: boolean) => void + setAlwaysAllowModeSwitch: (value: boolean) => void setShowAnnouncement: (value: boolean) => void setAllowedCommands: (value: string[]) => void setSoundEnabled: (value: boolean) => void @@ -253,6 +254,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })), setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })), setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })), + setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), From f50214b01736ddea82619ee82076be06b9373e20 Mon Sep 17 00:00:00 2001 From: MFPires Date: Tue, 28 Jan 2025 01:48:47 -0300 Subject: [PATCH 2/2] feat(settings): Add auto-approve mode switching option to Settings UI Add the ability to configure automatic mode switching approval in the Settings UI. Implementation: - Added alwaysAllowModeSwitch checkbox in the Auto-Approve Settings section - Added state management integration with useExtensionState - Added vscode.postMessage handler for state updates - Placed the setting logically between MCP tools and execute operations settings The new setting allows users to: - Enable/disable automatic approval of mode switching operations - Configure mode switching approval independently of other auto-approve settings - Maintain consistent UX with other auto-approve settings This completes the mode switching auto-approval feature, working in conjunction with: - Previously added state management in ExtensionStateContext - Core logic changes in ClineProvider - WebviewMessage type updates - Existing test coverage in ChatView.auto-approve.test.tsx --- .../src/components/settings/SettingsView.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 81a929c..12b97f7 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { listApiConfigMeta, experimentalDiffStrategy, setExperimentalDiffStrategy, + alwaysAllowModeSwitch, + setAlwaysAllowModeSwitch, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -93,6 +95,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { apiConfiguration, }) vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy }) + vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) onDone() } } @@ -328,6 +331,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {

+
+ setAlwaysAllowModeSwitch(e.target.checked)}> + Always approve mode switching + +

+ Automatically switch between different AI modes without requiring approval +

+
+