Просмотр исходного кода

Add auto-approve UI and notification integration

Saoud Rizwan 1 год назад
Родитель
Сommit
4b4905ec9e

+ 221 - 0
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<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

+ 32 - 2
webview-ui/src/components/chat/ChatView.tsx

@@ -25,6 +25,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
 import ChatRow from "./ChatRow"
 import ChatTextArea from "./ChatTextArea"
 import TaskHeader from "./TaskHeader"
+import AutoApproveMenu from "./AutoApproveMenu"
 import { AudioType } from "../../../../src/shared/WebviewMessage"
 import { validateCommand } from "../../utils/command-validation"
 
@@ -866,10 +867,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 }}>
@@ -885,6 +888,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}>
@@ -914,6 +943,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 							initialTopMostItemIndex={groupedMessages.length - 1}
 						/>
 					</div>
+					<AutoApproveMenu />
 					{showScrollToBottom ? (
 						<div
 							style={{
@@ -938,7 +968,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