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
+
+
+