Add auto-approve UI and notification integration

This commit is contained in:
Saoud Rizwan
2024-12-16 20:02:23 -08:00
committed by Matt Rubens
parent 201028b317
commit 4b4905ec9e
2 changed files with 253 additions and 2 deletions

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

@@ -25,6 +25,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
import ChatRow from "./ChatRow" import ChatRow from "./ChatRow"
import ChatTextArea from "./ChatTextArea" import ChatTextArea from "./ChatTextArea"
import TaskHeader from "./TaskHeader" import TaskHeader from "./TaskHeader"
import AutoApproveMenu from "./AutoApproveMenu"
import { AudioType } from "../../../../src/shared/WebviewMessage" import { AudioType } from "../../../../src/shared/WebviewMessage"
import { validateCommand } from "../../utils/command-validation" import { validateCommand } from "../../utils/command-validation"
@@ -866,10 +867,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
) : ( ) : (
<div <div
style={{ style={{
flexGrow: 1, flex: "1 1 0", // flex-grow: 1, flex-shrink: 1, flex-basis: 0
minHeight: 0,
overflowY: "auto", overflowY: "auto",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
paddingBottom: "10px",
}}> }}>
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />} {showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}> <div style={{ padding: "0 20px", flexShrink: 0 }}>
@@ -885,6 +888,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />} {taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div> </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 && ( {task && (
<> <>
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}> <div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
@@ -914,6 +943,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
initialTopMostItemIndex={groupedMessages.length - 1} initialTopMostItemIndex={groupedMessages.length - 1}
/> />
</div> </div>
<AutoApproveMenu />
{showScrollToBottom ? ( {showScrollToBottom ? (
<div <div
style={{ style={{
@@ -938,7 +968,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
: 0.5 : 0.5
: 0, : 0,
display: "flex", display: "flex",
padding: "10px 15px 0px 15px", padding: `${primaryButtonText || secondaryButtonText || isStreaming ? "10" : "0"}px 15px 0px 15px`,
}}> }}>
{primaryButtonText && !isStreaming && ( {primaryButtonText && !isStreaming && (
<VSCodeButton <VSCodeButton