Skip to content

Commit f70e3fc

Browse files
authored
Merge pull request #610 from MuriloFP/feature/auto-approve-switch-modes
Feature/auto approve switch modes
2 parents 3862057 + f50214b commit f70e3fc

File tree

8 files changed

+215
-2
lines changed

8 files changed

+215
-2
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ type GlobalStateKey =
7979
| "alwaysAllowWrite"
8080
| "alwaysAllowExecute"
8181
| "alwaysAllowBrowser"
82+
| "alwaysAllowMcp"
83+
| "alwaysAllowModeSwitch"
8284
| "taskHistory"
8385
| "openAiBaseUrl"
8486
| "openAiModelId"
@@ -99,7 +101,6 @@ type GlobalStateKey =
99101
| "soundEnabled"
100102
| "soundVolume"
101103
| "diffEnabled"
102-
| "alwaysAllowMcp"
103104
| "browserViewportSize"
104105
| "screenshotQuality"
105106
| "fuzzyMatchThreshold"
@@ -620,6 +621,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
620621
await this.updateGlobalState("alwaysAllowMcp", message.bool)
621622
await this.postStateToWebview()
622623
break
624+
case "alwaysAllowModeSwitch":
625+
await this.updateGlobalState("alwaysAllowModeSwitch", message.bool)
626+
await this.postStateToWebview()
627+
break
623628
case "askResponse":
624629
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
625630
break
@@ -1848,6 +1853,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18481853
alwaysAllowExecute,
18491854
alwaysAllowBrowser,
18501855
alwaysAllowMcp,
1856+
alwaysAllowModeSwitch,
18511857
soundEnabled,
18521858
diffEnabled,
18531859
taskHistory,
@@ -1882,6 +1888,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18821888
alwaysAllowExecute: alwaysAllowExecute ?? false,
18831889
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
18841890
alwaysAllowMcp: alwaysAllowMcp ?? false,
1891+
alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
18851892
uriScheme: vscode.env.uriScheme,
18861893
clineMessages: this.cline?.clineMessages || [],
18871894
taskHistory: (taskHistory || [])
@@ -2009,6 +2016,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20092016
alwaysAllowExecute,
20102017
alwaysAllowBrowser,
20112018
alwaysAllowMcp,
2019+
alwaysAllowModeSwitch,
20122020
taskHistory,
20132021
allowedCommands,
20142022
soundEnabled,
@@ -2078,6 +2086,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20782086
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
20792087
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
20802088
this.getGlobalState("alwaysAllowMcp") as Promise<boolean | undefined>,
2089+
this.getGlobalState("alwaysAllowModeSwitch") as Promise<boolean | undefined>,
20812090
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
20822091
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
20832092
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
@@ -2166,6 +2175,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21662175
alwaysAllowExecute: alwaysAllowExecute ?? false,
21672176
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
21682177
alwaysAllowMcp: alwaysAllowMcp ?? false,
2178+
alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
21692179
taskHistory,
21702180
allowedCommands,
21712181
soundEnabled: soundEnabled ?? false,

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export interface ExtensionState {
9191
alwaysAllowBrowser?: boolean
9292
alwaysAllowMcp?: boolean
9393
alwaysApproveResubmit?: boolean
94+
alwaysAllowModeSwitch?: boolean
9495
requestDelaySeconds: number
9596
uriScheme?: string
9697
allowedCommands?: string[]

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface WebviewMessage {
4141
| "refreshOpenAiModels"
4242
| "alwaysAllowBrowser"
4343
| "alwaysAllowMcp"
44+
| "alwaysAllowModeSwitch"
4445
| "playSound"
4546
| "soundEnabled"
4647
| "soundVolume"

webview-ui/src/components/chat/AutoApproveMenu.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
2828
setAlwaysAllowBrowser,
2929
alwaysAllowMcp,
3030
setAlwaysAllowMcp,
31+
alwaysAllowModeSwitch,
32+
setAlwaysAllowModeSwitch,
3133
alwaysApproveResubmit,
3234
setAlwaysApproveResubmit,
3335
autoApprovalEnabled,
@@ -71,6 +73,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
7173
enabled: alwaysAllowMcp ?? false,
7274
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
7375
},
76+
{
77+
id: "switchModes",
78+
label: "Switch between modes",
79+
shortName: "Modes",
80+
enabled: alwaysAllowModeSwitch ?? false,
81+
description: "Allows automatic switching between different AI modes without requiring approval.",
82+
},
7483
{
7584
id: "retryRequests",
7685
label: "Retry failed requests",
@@ -120,6 +129,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
120129
vscode.postMessage({ type: "alwaysAllowMcp", bool: newValue })
121130
}, [alwaysAllowMcp, setAlwaysAllowMcp])
122131

132+
const handleModeSwitchChange = useCallback(() => {
133+
const newValue = !(alwaysAllowModeSwitch ?? false)
134+
setAlwaysAllowModeSwitch(newValue)
135+
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: newValue })
136+
}, [alwaysAllowModeSwitch, setAlwaysAllowModeSwitch])
137+
123138
const handleRetryChange = useCallback(() => {
124139
const newValue = !(alwaysApproveResubmit ?? false)
125140
setAlwaysApproveResubmit(newValue)
@@ -133,6 +148,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
133148
executeCommands: handleExecuteChange,
134149
useBrowser: handleBrowserChange,
135150
useMcp: handleMcpChange,
151+
switchModes: handleModeSwitchChange,
136152
retryRequests: handleRetryChange,
137153
}
138154

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
5555
mode,
5656
setMode,
5757
autoApprovalEnabled,
58+
alwaysAllowModeSwitch,
5859
} = useExtensionState()
5960

6061
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
@@ -565,7 +566,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
565566
(alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
566567
(alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
567568
(alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
568-
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
569+
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) ||
570+
(alwaysAllowModeSwitch &&
571+
message.ask === "tool" &&
572+
JSON.parse(message.text || "{}")?.tool === "switchMode")
569573
)
570574
},
571575
[
@@ -579,6 +583,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
579583
isAllowedCommand,
580584
alwaysAllowMcp,
581585
isMcpToolAlwaysAllowed,
586+
alwaysAllowModeSwitch,
582587
],
583588
)
584589

webview-ui/src/components/chat/__tests__/ChatView.auto-approve.test.tsx

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,168 @@ describe("ChatView - Auto Approval Tests", () => {
313313
})
314314
})
315315
})
316+
317+
it("auto-approves mode switch when enabled", async () => {
318+
render(
319+
<ExtensionStateContextProvider>
320+
<ChatView
321+
isHidden={false}
322+
showAnnouncement={false}
323+
hideAnnouncement={() => {}}
324+
showHistoryView={() => {}}
325+
/>
326+
</ExtensionStateContextProvider>,
327+
)
328+
329+
// First hydrate state with initial task
330+
mockPostMessage({
331+
alwaysAllowModeSwitch: true,
332+
autoApprovalEnabled: true,
333+
clineMessages: [
334+
{
335+
type: "say",
336+
say: "task",
337+
ts: Date.now() - 2000,
338+
text: "Initial task",
339+
},
340+
],
341+
})
342+
343+
// Then send the mode switch ask message
344+
mockPostMessage({
345+
alwaysAllowModeSwitch: true,
346+
autoApprovalEnabled: true,
347+
clineMessages: [
348+
{
349+
type: "say",
350+
say: "task",
351+
ts: Date.now() - 2000,
352+
text: "Initial task",
353+
},
354+
{
355+
type: "ask",
356+
ask: "tool",
357+
ts: Date.now(),
358+
text: JSON.stringify({ tool: "switchMode" }),
359+
partial: false,
360+
},
361+
],
362+
})
363+
364+
// Wait for the auto-approval message
365+
await waitFor(() => {
366+
expect(vscode.postMessage).toHaveBeenCalledWith({
367+
type: "askResponse",
368+
askResponse: "yesButtonClicked",
369+
})
370+
})
371+
})
372+
373+
it("does not auto-approve mode switch when disabled", async () => {
374+
render(
375+
<ExtensionStateContextProvider>
376+
<ChatView
377+
isHidden={false}
378+
showAnnouncement={false}
379+
hideAnnouncement={() => {}}
380+
showHistoryView={() => {}}
381+
/>
382+
</ExtensionStateContextProvider>,
383+
)
384+
385+
// First hydrate state with initial task
386+
mockPostMessage({
387+
alwaysAllowModeSwitch: false,
388+
autoApprovalEnabled: true,
389+
clineMessages: [
390+
{
391+
type: "say",
392+
say: "task",
393+
ts: Date.now() - 2000,
394+
text: "Initial task",
395+
},
396+
],
397+
})
398+
399+
// Then send the mode switch ask message
400+
mockPostMessage({
401+
alwaysAllowModeSwitch: false,
402+
autoApprovalEnabled: true,
403+
clineMessages: [
404+
{
405+
type: "say",
406+
say: "task",
407+
ts: Date.now() - 2000,
408+
text: "Initial task",
409+
},
410+
{
411+
type: "ask",
412+
ask: "tool",
413+
ts: Date.now(),
414+
text: JSON.stringify({ tool: "switchMode" }),
415+
partial: false,
416+
},
417+
],
418+
})
419+
420+
// Verify no auto-approval message was sent
421+
expect(vscode.postMessage).not.toHaveBeenCalledWith({
422+
type: "askResponse",
423+
askResponse: "yesButtonClicked",
424+
})
425+
})
426+
427+
it("does not auto-approve mode switch when auto-approval is disabled", async () => {
428+
render(
429+
<ExtensionStateContextProvider>
430+
<ChatView
431+
isHidden={false}
432+
showAnnouncement={false}
433+
hideAnnouncement={() => {}}
434+
showHistoryView={() => {}}
435+
/>
436+
</ExtensionStateContextProvider>,
437+
)
438+
439+
// First hydrate state with initial task
440+
mockPostMessage({
441+
alwaysAllowModeSwitch: true,
442+
autoApprovalEnabled: false,
443+
clineMessages: [
444+
{
445+
type: "say",
446+
say: "task",
447+
ts: Date.now() - 2000,
448+
text: "Initial task",
449+
},
450+
],
451+
})
452+
453+
// Then send the mode switch ask message
454+
mockPostMessage({
455+
alwaysAllowModeSwitch: true,
456+
autoApprovalEnabled: false,
457+
clineMessages: [
458+
{
459+
type: "say",
460+
say: "task",
461+
ts: Date.now() - 2000,
462+
text: "Initial task",
463+
},
464+
{
465+
type: "ask",
466+
ask: "tool",
467+
ts: Date.now(),
468+
text: JSON.stringify({ tool: "switchMode" }),
469+
partial: false,
470+
},
471+
],
472+
})
473+
474+
// Verify no auto-approval message was sent
475+
expect(vscode.postMessage).not.toHaveBeenCalledWith({
476+
type: "askResponse",
477+
askResponse: "yesButtonClicked",
478+
})
479+
})
316480
})

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
5353
listApiConfigMeta,
5454
experimentalDiffStrategy,
5555
setExperimentalDiffStrategy,
56+
alwaysAllowModeSwitch,
57+
setAlwaysAllowModeSwitch,
5658
} = useExtensionState()
5759
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
5860
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -93,6 +95,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
9395
apiConfiguration,
9496
})
9597
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
98+
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
9699
onDone()
97100
}
98101
}
@@ -328,6 +331,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
328331
</p>
329332
</div>
330333

334+
<div style={{ marginBottom: 15 }}>
335+
<VSCodeCheckbox
336+
checked={alwaysAllowModeSwitch}
337+
onChange={(e: any) => setAlwaysAllowModeSwitch(e.target.checked)}>
338+
<span style={{ fontWeight: "500" }}>Always approve mode switching</span>
339+
</VSCodeCheckbox>
340+
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
341+
Automatically switch between different AI modes without requiring approval
342+
</p>
343+
</div>
344+
331345
<div style={{ marginBottom: 15 }}>
332346
<VSCodeCheckbox
333347
checked={alwaysAllowExecute}

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface ExtensionStateContextType extends ExtensionState {
3333
setAlwaysAllowExecute: (value: boolean) => void
3434
setAlwaysAllowBrowser: (value: boolean) => void
3535
setAlwaysAllowMcp: (value: boolean) => void
36+
setAlwaysAllowModeSwitch: (value: boolean) => void
3637
setShowAnnouncement: (value: boolean) => void
3738
setAllowedCommands: (value: string[]) => void
3839
setSoundEnabled: (value: boolean) => void
@@ -253,6 +254,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
253254
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
254255
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
255256
setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
257+
setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
256258
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
257259
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
258260
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),

0 commit comments

Comments
 (0)