diff --git a/package-lock.json b/package-lock.json index 1663a77f27..ed692dd4b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 176c1ea7aa..767e4366dd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/integrations/notifications/index.ts b/src/integrations/notifications/index.ts new file mode 100644 index 0000000000..c7aa5089c0 --- /dev/null +++ b/src/integrations/notifications/index.ts @@ -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 { + 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 { + 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 { + 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) + } +} diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx new file mode 100644 index 0000000000..645082dab9 --- /dev/null +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -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([ + { + 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 ( +
+
+ 0} + onChange={(e) => { + const checked = (e.target as HTMLInputElement).checked + setActions((prev) => + prev.map((action) => ({ + ...action, + enabled: checked, + })), + ) + e.stopPropagation() + }} + onClick={(e) => e.stopPropagation()} + /> + + Auto-approve: + + {enabledActions.length === 0 ? "None" : enabledActionsList} + + + +
+ {isExpanded && ( +
+
+ 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. +
+ {actions.map((action) => ( +
+ toggleAction(action.id)}> + {action.label} + +
+ {action.description} +
+
+ ))} +
+
+ Max Requests: + { + const value = parseInt((e.target as HTMLInputElement).value) + if (!isNaN(value) && value > 0) { + setMaxRequests(value) + } + }} + style={{ flex: 1 }} + /> +
+
+ Cline will make this many API requests before asking for approval to proceed with the task. +
+
+ setEnableNotifications((prev) => !prev)}> + Enable Notifications + +
+ Receive system notifications when Cline requires approval to proceed or when a task is + completed. +
+
+
+ )} +
+ ) +} + +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 diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 8c715725d7..6afd1afd8c 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -256,7 +256,7 @@ export const ChatRowContent = ({ Cline wants to edit this file:
{showAnnouncement && }
@@ -738,6 +741,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie {taskHistory.length > 0 && }
)} + + {/* + // 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 && ( + + )} + {task && ( <>
@@ -767,6 +796,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie initialTopMostItemIndex={groupedMessages.length - 1} />
+ {showScrollToBottom ? (
{primaryButtonText && !isStreaming && (