Add AutoApprovalSettings state

This commit is contained in:
Saoud Rizwan 2024-12-16 21:54:44 -08:00
parent cd54c501b4
commit f406147a30
7 changed files with 200 additions and 83 deletions

View file

@ -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()

View file

@ -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<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@ -948,6 +978,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("customInstructions") as Promise<string | undefined>,
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
])
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
}
}

View file

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

View file

@ -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 {

View file

@ -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"

View file

@ -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<AutoApproveAction[]>([
{
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 (
<div
@ -85,23 +131,17 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: "pointer",
}}
onClick={toggleExpanded}>
onClick={() => setIsExpanded((prev) => !prev)}>
<VSCodeCheckbox
checked={enabledActions.length > 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()}
/>
<CollapsibleSection>
<span style={{ color: "var(--vscode-foreground)" }}>Auto-approve:</span>
<span style={{ color: "var(--vscode-foreground)", whiteSpace: "nowrap" }}>Auto-approve:</span>
<span
style={{
whiteSpace: "nowrap",
@ -113,7 +153,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
// fontSize: "14px",
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
@ -128,12 +167,18 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
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.
</div>
{actions.map((action) => (
{ACTION_METADATA.map((action) => (
<div key={action.id} style={{ margin: "6px 0" }}>
<VSCodeCheckbox checked={action.enabled} onChange={() => toggleAction(action.id)}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
@ -165,11 +210,11 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
value={maxRequests.toString()}
onChange={(e) => {
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) => {
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={enableNotifications}
onChange={() => setEnableNotifications((prev) => !prev)}>
checked={autoApprovalSettings.enableNotifications}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateNotifications(checked)
}}>
Enable Notifications
</VSCodeCheckbox>
<div

View file

@ -1,5 +1,6 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings"
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
import {
ApiConfiguration,
@ -7,10 +8,10 @@ import {
openRouterDefaultModelId,
openRouterDefaultModelInfo,
} from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
import { McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@ -33,6 +34,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)