Add auto-approve UI and notification integration

This commit is contained in:
Saoud Rizwan 2024-12-16 20:02:23 -08:00
parent fdcf514d61
commit cd54c501b4
6 changed files with 545 additions and 14 deletions

235
package-lock.json generated
View File

@ -25,6 +25,7 @@
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
"globby": "^14.0.2",
"isbinaryfile": "^5.0.2",
@ -2859,6 +2860,24 @@
"node": ">=18"
}
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@smithy/abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz",
@ -6607,6 +6626,112 @@
"node": ">=6"
}
},
"node_modules/execa": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
"integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.3",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.0",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.0.0"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/execa/node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/execa/node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/execa/node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/execa/node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/execa/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/execa/node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -6713,6 +6838,33 @@
"pend": "~1.2.0"
}
},
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/figures/node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -7016,17 +7168,6 @@
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
@ -7477,6 +7618,15 @@
"node": ">= 14"
}
},
"node_modules/human-signals": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
"integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@ -7885,6 +8035,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@ -9389,6 +9551,18 @@
"node": ">=4"
}
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@ -9588,6 +9762,21 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-ms": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
"license": "MIT",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -10532,6 +10721,18 @@
"node": ">=4"
}
},
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -11526,6 +11727,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoctocolors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",

View File

@ -179,6 +179,7 @@
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
"globby": "^14.0.2",
"isbinaryfile": "^5.0.2",

View File

@ -0,0 +1,66 @@
import { execa } from "execa"
import { platform } from "os"
interface NotificationOptions {
title?: string
subtitle?: string
message: string
}
async function showMacOSNotification(options: NotificationOptions): Promise<void> {
const { title, subtitle = "", message } = options
const script = `display notification "${message}" with title "${title}" subtitle "${subtitle}" sound name "Tink"`
try {
await execa("osascript", ["-e", script])
} catch (error) {
throw new Error(`Failed to show macOS notification: ${error}`)
}
}
async function showWindowsNotification(options: NotificationOptions): Promise<void> {
const { title, message } = options
const duration = 6 // seconds
const script = `
Add-Type -AssemblyName System.Windows.Forms
$balloon = New-Object System.Windows.Forms.NotifyIcon
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
$balloon.BalloonTipTitle = '${title}'
$balloon.BalloonTipText = '${message}'
$balloon.Visible = $true
$balloon.ShowBalloonTip(${duration * 1000})
Start-Sleep -Seconds ${duration}
$balloon.Visible = $false
$balloon.Dispose()
`
try {
await execa("powershell", ["-Command", script])
} catch (error) {
throw new Error(`Failed to show Windows notification: ${error}`)
}
}
export async function showSystemNotification(options: NotificationOptions): Promise<void> {
try {
const { title = "Cline", message } = options
if (!message) {
throw new Error("Message is required")
}
switch (platform()) {
case "darwin":
await showMacOSNotification({ ...options, title })
break
case "win32":
await showWindowsNotification({ ...options, title })
break
default:
throw new Error("Unsupported platform")
}
} catch (error) {
console.error("Could not show system notification", error)
}
}

View File

@ -0,0 +1,221 @@
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
}
interface AutoApproveMenuProps {
style?: React.CSSProperties
}
const DEFAULT_MAX_REQUESTS = 50
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
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)
}, [])
const toggleAction = useCallback((actionId: string) => {
setActions((prev) =>
prev.map((action) => (action.id === actionId ? { ...action, enabled: !action.enabled } : action)),
)
}, [])
const enabledActions = actions.filter((action) => action.enabled)
const enabledActionsList = enabledActions.map((action) => action.label).join(", ")
return (
<div
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
: "none",
overflowY: "auto",
...style,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: "pointer",
}}
onClick={toggleExpanded}>
<VSCodeCheckbox
checked={enabledActions.length > 0}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
setActions((prev) =>
prev.map((action) => ({
...action,
enabled: checked,
})),
)
e.stopPropagation()
}}
onClick={(e) => e.stopPropagation()}
/>
<CollapsibleSection>
<span style={{ color: "var(--vscode-foreground)" }}>Auto-approve:</span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{enabledActions.length === 0 ? "None" : enabledActionsList}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
// fontSize: "14px",
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
/>
</CollapsibleSection>
</div>
{isExpanded && (
<div style={{ padding: "0" }}>
<div
style={{
marginBottom: "10px",
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.
</div>
{actions.map((action) => (
<div key={action.id} style={{ margin: "6px 0" }}>
<VSCodeCheckbox checked={action.enabled} onChange={() => toggleAction(action.id)}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
{action.description}
</div>
</div>
))}
<div
style={{
height: "0.5px",
background: "var(--vscode-titleBar-inactiveForeground)",
margin: "15px 0",
opacity: 0.2,
}}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "10px",
marginBottom: "8px",
color: "var(--vscode-foreground)",
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
value={maxRequests.toString()}
onChange={(e) => {
const value = parseInt((e.target as HTMLInputElement).value)
if (!isNaN(value) && value > 0) {
setMaxRequests(value)
}
}}
style={{ flex: 1 }}
/>
</div>
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
marginBottom: "10px",
}}>
Cline will make this many API requests before asking for approval to proceed with the task.
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={enableNotifications}
onChange={() => setEnableNotifications((prev) => !prev)}>
Enable Notifications
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
Receive system notifications when Cline requires approval to proceed or when a task is
completed.
</div>
</div>
</div>
)}
</div>
)
}
const CollapsibleSection = styled.div`
display: flex;
align-items: center;
gap: 4px;
color: var(--vscode-descriptionForeground);
flex: 1;
min-width: 0;
&:hover {
color: var(--vscode-foreground);
}
`
export default AutoApproveMenu

View File

@ -256,7 +256,7 @@ export const ChatRowContent = ({
<span style={{ fontWeight: "bold" }}>Cline wants to edit this file:</span>
</div>
<CodeAccordian
isLoading={message.partial}
// isLoading={message.partial}
code={tool.diff!}
path={tool.path!}
isExpanded={isExpanded}

View File

@ -24,6 +24,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
import ChatRow from "./ChatRow"
import ChatTextArea from "./ChatTextArea"
import TaskHeader from "./TaskHeader"
import AutoApproveMenu from "./AutoApproveMenu"
interface ChatViewProps {
isHidden: boolean
@ -714,10 +715,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
) : (
<div
style={{
flexGrow: 1,
flex: "1 1 0", // flex-grow: 1, flex-shrink: 1, flex-basis: 0
minHeight: 0,
overflowY: "auto",
display: "flex",
flexDirection: "column",
paddingBottom: "10px",
}}>
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
@ -738,6 +741,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
{/*
// Flex layout explanation:
// 1. Content div above uses flex: "1 1 0" to:
// - Grow to fill available space (flex-grow: 1)
// - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
// - Start from zero size (flex-basis: 0) to ensure proper distribution
// minHeight: 0 allows it to shrink below its content height
//
// 2. AutoApproveMenu uses flex: "0 1 auto" to:
// - Not grow beyond its content (flex-grow: 0)
// - Shrink when viewport is small (flex-shrink: 1)
// - Use its content size as basis (flex-basis: auto)
// This ensures it takes its natural height when there's space
// but becomes scrollable when the viewport is too small
*/}
{!task && (
<AutoApproveMenu
style={{
marginBottom: -2,
flex: "0 1 auto", // flex-grow: 0, flex-shrink: 1, flex-basis: auto
minHeight: 0,
}}
/>
)}
{task && (
<>
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
@ -767,6 +796,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
initialTopMostItemIndex={groupedMessages.length - 1}
/>
</div>
<AutoApproveMenu />
{showScrollToBottom ? (
<div
style={{
@ -791,7 +821,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
: 0.5
: 0,
display: "flex",
padding: "10px 15px 0px 15px",
padding: `${primaryButtonText || secondaryButtonText || isStreaming ? "10" : "0"}px 15px 0px 15px`,
}}>
{primaryButtonText && !isStreaming && (
<VSCodeButton