Merge pull request #610 from MuriloFP/feature/auto-approve-switch-modes

Feature/auto approve switch modes
This commit is contained in:
Matt Rubens
2025-01-28 00:07:44 -05:00
committed by GitHub
8 changed files with 215 additions and 2 deletions

View File

@@ -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<boolean | undefined>,
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowMcp") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowModeSwitch") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
@@ -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,

View File

@@ -91,6 +91,7 @@ export interface ExtensionState {
alwaysAllowBrowser?: boolean
alwaysAllowMcp?: boolean
alwaysApproveResubmit?: boolean
alwaysAllowModeSwitch?: boolean
requestDelaySeconds: number
uriScheme?: string
allowedCommands?: string[]

View File

@@ -41,6 +41,7 @@ export interface WebviewMessage {
| "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "playSound"
| "soundEnabled"
| "soundVolume"

View File

@@ -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,
}

View File

@@ -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,
],
)

View File

@@ -313,4 +313,168 @@ describe("ChatView - Auto Approval Tests", () => {
})
})
})
it("auto-approves mode switch when enabled", async () => {
render(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// 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(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// 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(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// 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",
})
})
})

View File

@@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
listApiConfigMeta,
experimentalDiffStrategy,
setExperimentalDiffStrategy,
alwaysAllowModeSwitch,
setAlwaysAllowModeSwitch,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(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) => {
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={alwaysAllowModeSwitch}
onChange={(e: any) => setAlwaysAllowModeSwitch(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve mode switching</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically switch between different AI modes without requiring approval
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={alwaysAllowExecute}

View File

@@ -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 })),