From f406147a30d3d345303602ad52a1b11272e920dc Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:54:44 -0800 Subject: [PATCH] Add AutoApprovalSettings state --- src/core/Cline.ts | 6 +- src/core/webview/ClineProvider.ts | 42 +++- src/shared/AutoApprovalSettings.ts | 28 +++ src/shared/ExtensionMessage.ts | 2 + src/shared/WebviewMessage.ts | 5 +- .../src/components/chat/AutoApproveMenu.tsx | 194 +++++++++++------- .../src/context/ExtensionStateContext.tsx | 6 +- 7 files changed, 200 insertions(+), 83 deletions(-) create mode 100644 src/shared/AutoApprovalSettings.ts diff --git a/src/core/Cline.ts b/src/core/Cline.ts index f54a7149cf..85ba079972 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -46,9 +46,9 @@ import { formatResponse } from "./prompts/responses" import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system" import { truncateHalfConversation } from "./sliding-window" import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" -import { showOmissionWarning } from "../integrations/editor/detect-omission" import { BrowserSession } from "../services/browser/BrowserSession" import { constructNewFileContent } from "./assistant-message/diff" +import { AutoApprovalSettings } from "../shared/AutoApprovalSettings" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution @@ -67,6 +67,7 @@ export class Cline { private didEditFile: boolean = false customInstructions?: string alwaysAllowReadOnly: boolean + autoApprovalSettings: AutoApprovalSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] private askResponse?: ClineAskResponse @@ -94,6 +95,7 @@ export class Cline { constructor( provider: ClineProvider, apiConfiguration: ApiConfiguration, + autoApprovalSettings: AutoApprovalSettings, customInstructions?: string, alwaysAllowReadOnly?: boolean, task?: string, @@ -108,7 +110,7 @@ export class Cline { this.diffViewProvider = new DiffViewProvider(cwd) this.customInstructions = customInstructions this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false - + this.autoApprovalSettings = autoApprovalSettings if (historyItem) { this.taskId = historyItem.id this.resumeTaskFromHistory() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index c14892ef07..be8dad8cc2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -22,6 +22,7 @@ import { Cline } from "../Cline" import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" +import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -59,6 +60,7 @@ type GlobalStateKey = | "azureApiVersion" | "openRouterModelId" | "openRouterModelInfo" + | "autoApprovalSettings" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -198,16 +200,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { async initClineWithTask(task?: string, images?: string[]) { await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one - const { apiConfiguration, customInstructions, alwaysAllowReadOnly } = await this.getState() - this.cline = new Cline(this, apiConfiguration, customInstructions, alwaysAllowReadOnly, task, images) + const { apiConfiguration, customInstructions, alwaysAllowReadOnly, autoApprovalSettings } = + await this.getState() + this.cline = new Cline( + this, + apiConfiguration, + autoApprovalSettings, + customInstructions, + alwaysAllowReadOnly, + task, + images, + ) } async initClineWithHistoryItem(historyItem: HistoryItem) { await this.clearTask() - const { apiConfiguration, customInstructions, alwaysAllowReadOnly } = await this.getState() + const { apiConfiguration, customInstructions, alwaysAllowReadOnly, autoApprovalSettings } = + await this.getState() this.cline = new Cline( this, apiConfiguration, + autoApprovalSettings, customInstructions, alwaysAllowReadOnly, undefined, @@ -420,6 +433,15 @@ export class ClineProvider implements vscode.WebviewViewProvider { } await this.postStateToWebview() break + case "autoApprovalSettings": + if (message.autoApprovalSettings) { + await this.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings) + if (this.cline) { + this.cline.autoApprovalSettings = message.autoApprovalSettings + } + await this.postStateToWebview() + } + break case "askResponse": this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break @@ -824,8 +846,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { } async getStateToPostToWebview() { - const { apiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, taskHistory } = - await this.getState() + const { + apiConfiguration, + lastShownAnnouncementId, + customInstructions, + alwaysAllowReadOnly, + taskHistory, + autoApprovalSettings, + } = await this.getState() return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -835,6 +863,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { clineMessages: this.cline?.clineMessages || [], taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, + autoApprovalSettings, } } @@ -919,6 +948,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { customInstructions, alwaysAllowReadOnly, taskHistory, + autoApprovalSettings, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -948,6 +978,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("customInstructions") as Promise, this.getGlobalState("alwaysAllowReadOnly") as Promise, this.getGlobalState("taskHistory") as Promise, + this.getGlobalState("autoApprovalSettings") as Promise, ]) let apiProvider: ApiProvider @@ -995,6 +1026,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { customInstructions, alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, taskHistory, + autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string } } diff --git a/src/shared/AutoApprovalSettings.ts b/src/shared/AutoApprovalSettings.ts new file mode 100644 index 0000000000..28376d4e06 --- /dev/null +++ b/src/shared/AutoApprovalSettings.ts @@ -0,0 +1,28 @@ +export interface AutoApprovalSettings { + // Whether auto-approval is enabled + enabled: boolean + // Individual action permissions + actions: { + readFiles: boolean // Read files and directories + editFiles: boolean // Edit files + executeCommands: boolean // Execute safe commands + useBrowser: boolean // Use browser + useMcp: boolean // Use MCP servers + } + // Global settings + maxRequests: number // Maximum number of auto-approved requests + enableNotifications: boolean // Show notifications for approval and task completion +} + +export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = { + enabled: false, + actions: { + readFiles: false, + editFiles: false, + executeCommands: false, + useBrowser: false, + useMcp: false, + }, + maxRequests: 20, + enableNotifications: false, +} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f930f3f441..efa43e1dc3 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -1,6 +1,7 @@ // type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello' import { ApiConfiguration, ModelInfo } from "./api" +import { AutoApprovalSettings } from "./AutoApprovalSettings" import { HistoryItem } from "./HistoryItem" import { McpServer } from "./mcp" @@ -45,6 +46,7 @@ export interface ExtensionState { clineMessages: ClineMessage[] taskHistory: HistoryItem[] shouldShowAnnouncement: boolean + autoApprovalSettings: AutoApprovalSettings } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 859fd7120c..0d0f727aa9 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -1,4 +1,5 @@ -import { ApiConfiguration, ApiProvider } from "./api" +import { ApiConfiguration } from "./api" +import { AutoApprovalSettings } from "./AutoApprovalSettings" export interface WebviewMessage { type: @@ -25,11 +26,13 @@ export interface WebviewMessage { | "refreshOpenRouterModels" | "openMcpSettings" | "restartMcpServer" + | "autoApprovalSettings" text?: string askResponse?: ClineAskResponse apiConfiguration?: ApiConfiguration images?: string[] bool?: boolean + autoApprovalSettings?: AutoApprovalSettings } export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 645082dab9..e60156b7a3 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -1,70 +1,116 @@ import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { useCallback, useState } from "react" import styled from "styled-components" - -interface AutoApproveAction { - id: string - label: string - enabled: boolean - description: string -} +import { useExtensionState } from "../../context/ExtensionStateContext" +import { AutoApprovalSettings } from "../../../../src/shared/AutoApprovalSettings" +import { vscode } from "../../utils/vscode" interface AutoApproveMenuProps { style?: React.CSSProperties } -const DEFAULT_MAX_REQUESTS = 50 +const ACTION_METADATA: { + id: keyof AutoApprovalSettings["actions"] + label: string + shortName: string + description: string +}[] = [ + { + id: "readFiles", + label: "Read files and directories", + shortName: "Read", + description: "Allows access to read any file on your computer.", + }, + { + id: "editFiles", + label: "Edit files", + shortName: "Edit", + description: "Allows modification of any files on your computer.", + }, + { + id: "executeCommands", + label: "Execute safe commands", + shortName: "Commands", + description: + "Allows execution of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.", + }, + { + id: "useBrowser", + label: "Use the browser", + shortName: "Browser", + description: "Allows ability to launch and interact with any website in a headless browser.", + }, + { + id: "useMcp", + label: "Use MCP servers", + shortName: "MCP", + description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.", + }, +] const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { + const { autoApprovalSettings } = useExtensionState() const [isExpanded, setIsExpanded] = useState(false) - const [actions, setActions] = useState([ - { - id: "readFiles", - label: "Read files and directories", - enabled: false, - description: "Allows access to read any file on your computer.", - }, - { - id: "editFiles", - label: "Edit files", - enabled: false, - description: "Allows modification of any files on your computer.", - }, - { - id: "executeCommands", - label: "Execute safe commands", - enabled: false, - description: - "Allows automatic execution of safe terminal commands. The model will determine if a command is potentially destructive and ask for explicit approval.", - }, - { - id: "useBrowser", - label: "Use the browser", - enabled: false, - description: "Allows ability to launch and interact with any website in a headless browser.", - }, - { - id: "useMcp", - label: "Use MCP servers", - enabled: false, - description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.", - }, - ]) - const [maxRequests, setMaxRequests] = useState(DEFAULT_MAX_REQUESTS) - const [enableNotifications, setEnableNotifications] = useState(false) - const toggleExpanded = useCallback(() => { - setIsExpanded((prev) => !prev) - }, []) + // Careful not to use partials to mutate since spread operator only does shallow copy - const toggleAction = useCallback((actionId: string) => { - setActions((prev) => - prev.map((action) => (action.id === actionId ? { ...action, enabled: !action.enabled } : action)), - ) - }, []) + const updateEnabled = useCallback( + (enabled: boolean) => { + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + enabled, + }, + }) + }, + [autoApprovalSettings], + ) - const enabledActions = actions.filter((action) => action.enabled) - const enabledActionsList = enabledActions.map((action) => action.label).join(", ") + const updateAction = useCallback( + (actionId: keyof AutoApprovalSettings["actions"], value: boolean) => { + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + actions: { + ...autoApprovalSettings.actions, + [actionId]: value, + }, + }, + }) + }, + [autoApprovalSettings], + ) + + const updateMaxRequests = useCallback( + (maxRequests: number) => { + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + maxRequests, + }, + }) + }, + [autoApprovalSettings], + ) + + const updateNotifications = useCallback( + (enableNotifications: boolean) => { + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + enableNotifications, + }, + }) + }, + [autoApprovalSettings], + ) + + const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id]) + const enabledActionsList = enabledActions.map((action) => action.shortName).join(", ") return (
{ padding: isExpanded ? "8px 0" : "8px 0 0 0", cursor: "pointer", }} - onClick={toggleExpanded}> + onClick={() => setIsExpanded((prev) => !prev)}> 0} + checked={autoApprovalSettings.enabled} onChange={(e) => { const checked = (e.target as HTMLInputElement).checked - setActions((prev) => - prev.map((action) => ({ - ...action, - enabled: checked, - })), - ) - e.stopPropagation() + updateEnabled(checked) }} onClick={(e) => e.stopPropagation()} /> - Auto-approve: + Auto-approve: { { color: "var(--vscode-descriptionForeground)", fontSize: "12px", }}> - Auto-approve allows Cline to perform actions without asking for permission. Only enable for - actions you fully trust, and consider setting a low request limit as a safeguard. + Auto-approve allows Cline to perform the following actions without asking for permission. This + is potentially dangerous and could lead to unwanted system modifications. Please use with + caution and only enable if you understand the risks.
- {actions.map((action) => ( + {ACTION_METADATA.map((action) => (
- toggleAction(action.id)}> + { + const checked = (e.target as HTMLInputElement).checked + updateAction(action.id, checked) + }}> {action.label}
{ }}> Max Requests: { + value={autoApprovalSettings.maxRequests.toString()} + onInput={(e) => { const value = parseInt((e.target as HTMLInputElement).value) if (!isNaN(value) && value > 0) { - setMaxRequests(value) + updateMaxRequests(value) } }} style={{ flex: 1 }} @@ -185,8 +230,11 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
setEnableNotifications((prev) => !prev)}> + checked={autoApprovalSettings.enableNotifications} + onChange={(e) => { + const checked = (e.target as HTMLInputElement).checked + updateNotifications(checked) + }}> Enable Notifications