From aa4d97f05dabe75da6a6571843a13e9d6497af4b Mon Sep 17 00:00:00 2001 From: Trevor Hudson Date: Fri, 9 May 2025 10:17:14 -0700 Subject: [PATCH] Trevhud/auto approve menu (#3405) * improved auto-approve * roll back chevron * changeset * add pills * back to checkboxes * turn on parent when subAction is turned on * use vscode colors * Update webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * improve responsiveness * remove opacity animation --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .changeset/twelve-seals-refuse.md | 5 + src/shared/AutoApprovalSettings.ts | 2 + .../src/components/chat/AutoApproveMenu.tsx | 488 ------------------ webview-ui/src/components/chat/ChatView.tsx | 27 +- .../auto-approve-menu/AutoApproveMenu.tsx | 478 +++++++++++++++++ .../auto-approve-menu/AutoApproveMenuItem.tsx | 131 +++++ 6 files changed, 618 insertions(+), 513 deletions(-) create mode 100644 .changeset/twelve-seals-refuse.md delete mode 100644 webview-ui/src/components/chat/AutoApproveMenu.tsx create mode 100644 webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx create mode 100644 webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenuItem.tsx diff --git a/.changeset/twelve-seals-refuse.md b/.changeset/twelve-seals-refuse.md new file mode 100644 index 0000000000..f73bcd7b09 --- /dev/null +++ b/.changeset/twelve-seals-refuse.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Updated auto approve with favorited settings diff --git a/src/shared/AutoApprovalSettings.ts b/src/shared/AutoApprovalSettings.ts index 42714c95a0..3dc4a5b5c5 100644 --- a/src/shared/AutoApprovalSettings.ts +++ b/src/shared/AutoApprovalSettings.ts @@ -17,6 +17,7 @@ export interface AutoApprovalSettings { // Global settings maxRequests: number // Maximum number of auto-approved requests enableNotifications: boolean // Show notifications for approval and task completion + favorites: string[] // IDs of actions favorited by the user for quick access } export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = { @@ -34,4 +35,5 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = { }, maxRequests: 20, enableNotifications: false, + favorites: [], } diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx deleted file mode 100644 index aebd9fd8f4..0000000000 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ /dev/null @@ -1,488 +0,0 @@ -import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { useCallback, useMemo, useRef, useState } from "react" -import styled from "styled-components" -import { useExtensionState } from "@/context/ExtensionStateContext" -import { AutoApprovalSettings } from "@shared/AutoApprovalSettings" -import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock" -import { vscode } from "@/utils/vscode" -import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles" -import { useClickAway } from "react-use" - -interface AutoApproveMenuProps { - style?: React.CSSProperties -} - -const SubOptionAnimateIn = styled.div<{ show: boolean }>` - max-height: ${(props) => (props.show ? "100px" : "0")}; - opacity: ${(props) => (props.show ? "1" : "0")}; - overflow: hidden; - transition: - max-height 0.2s ease-in-out, - opacity 0.2s ease-in-out; -` - -const ACTION_METADATA: { - id: keyof AutoApprovalSettings["actions"] - label: string - shortName: string - description: string -}[] = [ - { - id: "readFiles", - label: "Read project files", - shortName: "Read Local", - description: "Allows Cline to read files within your workspace.", - }, - { - id: "readFilesExternally", - label: "Read all files", - shortName: "Read (all)", - description: "Allows Cline to read any file on your computer.", - }, - { - id: "editFiles", - label: "Edit project files", - shortName: "Edit", - description: "Allows Cline to modify files within your workspace.", - }, - { - id: "editFilesExternally", - label: "Edit all files", - shortName: "Edit (all)", - description: "Allows Cline to modify any file on your computer.", - }, - { - id: "executeSafeCommands", - label: "Execute safe commands", - shortName: "Safe Commands", - description: - "Allows Cline to execute of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.", - }, - { - id: "executeAllCommands", - label: "Execute all commands", - shortName: "All Commands", - description: "Allows Cline to execute all terminal commands. Use at your own risk.", - }, - { - id: "useBrowser", - label: "Use the browser", - shortName: "Browser", - description: "Allows Cline to launch and interact with any website in a browser.", - }, - { - id: "useMcp", - label: "Use MCP servers", - shortName: "MCP", - description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.", - }, -] - -const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { - const { autoApprovalSettings } = useExtensionState() - const [isExpanded, setIsExpanded] = useState(false) - const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] = useState(false) - const menuRef = useRef(null) - // Careful not to use partials to mutate since spread operator only does shallow copy - - const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id]) - const enabledActionsList = useMemo(() => { - // When nested auto-approve options are used, display the more permissive one (file reads, edits, and commands) - const readFilesEnabled = enabledActions.some((action) => action.id === "readFiles") - const readFilesExternallyEnabled = enabledActions.some((action) => action.id === "readFilesExternally") - - const editFilesEnabled = enabledActions.some((action) => action.id === "editFiles") - const editFilesExternallyEnabled = enabledActions.some((action) => action.id === "editFilesExternally") ?? false - - const safeCommandsEnabled = enabledActions.some((action) => action.id === "executeSafeCommands") - const allCommandsEnabled = enabledActions.some((action) => action.id === "executeAllCommands") ?? false - // Filter out the potentially nested options so we don't display them twice - const otherActions = enabledActions - .filter( - (action) => - action.id !== "readFiles" && - action.id !== "readFilesExternally" && - action.id !== "editFiles" && - action.id !== "editFilesExternally" && - action.id !== "executeSafeCommands" && - action.id !== "executeAllCommands", - ) - .map((action) => action.shortName) - - const labels = [] - - // Handle read editing labels - if ((readFilesExternallyEnabled ?? false) && readFilesEnabled) { - labels.push("Read (All)") - } else if (readFilesEnabled) { - labels.push("Read") - } - - // Handle file editing labels - if ((editFilesExternallyEnabled ?? false) && editFilesEnabled) { - labels.push("Edit (All)") - } else if (editFilesEnabled) { - labels.push("Edit") - } - - // Handle command execution labels - if ((allCommandsEnabled ?? false) && safeCommandsEnabled) { - labels.push("All Commands") - } else if (safeCommandsEnabled) { - labels.push("Safe Commands") - } - - // Add remaining actions - return [...labels, ...otherActions].join(", ") - }, [enabledActions]) - - // This value is used to determine if the auto-approve menu should show 'Auto-approve: None' - // Note: we should use better logic to determine the state where no auto approve actions are in effect, regardless of the state of sub-auto-approve options - const hasEnabledActions = useMemo(() => { - let enabledActionsCount = enabledActions.length - - if (!autoApprovalSettings.actions.readFiles && autoApprovalSettings.actions.readFilesExternally) { - enabledActionsCount-- - } - - if (!autoApprovalSettings.actions.editFiles && autoApprovalSettings.actions.editFilesExternally) { - enabledActionsCount-- - } - - if (!autoApprovalSettings.actions.executeSafeCommands && autoApprovalSettings.actions.executeAllCommands) { - enabledActionsCount-- - } - - return enabledActionsCount > 0 - }, [enabledActions, autoApprovalSettings.actions]) - - // Get the full extension state to ensure we have the most up-to-date settings - const extensionState = useExtensionState() - - const updateEnabled = useCallback( - (enabled: boolean) => { - const currentSettings = extensionState.autoApprovalSettings - vscode.postMessage({ - type: "autoApprovalSettings", - autoApprovalSettings: { - ...currentSettings, - version: (currentSettings.version ?? 1) + 1, - enabled, - }, - }) - }, - [extensionState.autoApprovalSettings], - ) - - const updateAction = useCallback( - (actionId: keyof AutoApprovalSettings["actions"], value: boolean) => { - const currentSettings = extensionState.autoApprovalSettings - // Calculate what the new actions state will be - const newActions = { - ...currentSettings.actions, - [actionId]: value, - } - - // Check if this will result in any enabled actions - const willHaveEnabledActions = Object.values(newActions).some(Boolean) - - vscode.postMessage({ - type: "autoApprovalSettings", - autoApprovalSettings: { - ...currentSettings, - version: (currentSettings.version ?? 1) + 1, - actions: newActions, - // If no actions will be enabled, ensure the main toggle is off - enabled: willHaveEnabledActions ? currentSettings.enabled : false, - }, - }) - }, - [extensionState.autoApprovalSettings], - ) - - const updateMaxRequests = useCallback( - (maxRequests: number) => { - const currentSettings = extensionState.autoApprovalSettings - vscode.postMessage({ - type: "autoApprovalSettings", - autoApprovalSettings: { - ...currentSettings, - version: (currentSettings.version ?? 1) + 1, - maxRequests, - }, - }) - }, - [extensionState.autoApprovalSettings], - ) - - const updateNotifications = useCallback( - (enableNotifications: boolean) => { - const currentSettings = extensionState.autoApprovalSettings - vscode.postMessage({ - type: "autoApprovalSettings", - autoApprovalSettings: { - ...currentSettings, - version: (currentSettings.version ?? 1) + 1, - enableNotifications, - }, - }) - }, - [extensionState.autoApprovalSettings], - ) - - // Handle clicks outside the menu to close it - useClickAway(menuRef, () => { - if (isExpanded) { - setIsExpanded(false) - } - }) - - return ( -
-
{ - if (!hasEnabledActions) { - setIsHoveringCollapsibleSection(true) - } - }} - onMouseLeave={() => { - if (!hasEnabledActions) { - setIsHoveringCollapsibleSection(false) - } - }} - onClick={() => { - if (!hasEnabledActions) { - setIsExpanded((prev) => !prev) - } - }}> - { - // const checked = (e.target as HTMLInputElement).checked - // updateEnabled(checked) - // }} - onClick={(e) => { - /* - vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state. - */ - if (!hasEnabledActions) return - e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing - updateEnabled(!autoApprovalSettings.enabled) - }} - /> - { - // to prevent this from counteracting parent - if (hasEnabledActions) { - setIsExpanded((prev) => !prev) - } - }}> - - Auto-approve: - - - {!hasEnabledActions ? "None" : enabledActionsList} - - - -
- {isExpanded && ( -
-
- Auto-approve allows Cline to perform the following actions without asking for permission. Please use with - caution and only enable if you understand the risks. -
- {ACTION_METADATA.map((action) => { - // Handle readFilesExternally, editFilesExternally, and executeAllCommands as animated sub-options - if ( - action.id === "executeAllCommands" || - action.id === "editFilesExternally" || - action.id === "readFilesExternally" - ) { - const parentAction = - action.id === "executeAllCommands" - ? "executeSafeCommands" - : action.id === "readFilesExternally" - ? "readFiles" - : "editFiles" - return ( - -
- { - const checked = (e.target as HTMLInputElement).checked - updateAction(action.id, checked) - }}> - {action.label} - -
- {action.description} -
-
-
- ) - } - return ( -
- { - const checked = (e.target as HTMLInputElement).checked - updateAction(action.id, checked) - }}> - {action.label} - -
- {action.description} -
-
- ) - })} -
-
- Max Requests: - { - const input = e.target as HTMLInputElement - // Remove any non-numeric characters - input.value = input.value.replace(/[^0-9]/g, "") - const value = parseInt(input.value) - if (!isNaN(value) && value > 0) { - updateMaxRequests(value) - } - }} - onKeyDown={(e) => { - // Prevent non-numeric keys (except for backspace, delete, arrows) - if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) { - e.preventDefault() - } - }} - style={{ flex: 1 }} - /> -
-
- Cline will automatically make this many API requests before asking for approval to proceed with the task. -
-
- { - const checked = (e.target as HTMLInputElement).checked - updateNotifications(checked) - }}> - Enable Notifications - -
- Receive system notifications when Cline requires approval to proceed or when a task is completed. -
-
-
- )} -
- ) -} - -const CollapsibleSection = styled.div<{ isHovered?: boolean }>` - display: flex; - align-items: center; - gap: 4px; - color: ${(props) => (props.isHovered ? getAsVar(VSC_FOREGROUND) : getAsVar(VSC_DESCRIPTION_FOREGROUND))}; - flex: 1; - min-width: 0; - - &:hover { - color: ${getAsVar(VSC_FOREGROUND)}; - } -` - -export default AutoApproveMenu diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 8d296bb806..ca709e64a4 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -22,7 +22,7 @@ import { TaskServiceClient } from "@/services/grpc-client" import HistoryPreview from "@/components/history/HistoryPreview" import { normalizeApiConfiguration } from "@/components/settings/ApiOptions" import Announcement from "@/components/chat/Announcement" -import AutoApproveMenu from "@/components/chat/AutoApproveMenu" +import AutoApproveMenu from "@/components/chat/auto-approve-menu/AutoApproveMenu" import BrowserSessionRow from "@/components/chat/BrowserSessionRow" import ChatRow from "@/components/chat/ChatRow" import ChatTextArea from "@/components/chat/ChatTextArea" @@ -1009,30 +1009,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
)} - {/* - // 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 && } {task && ( <> diff --git a/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx b/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx new file mode 100644 index 0000000000..827197e561 --- /dev/null +++ b/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenu.tsx @@ -0,0 +1,478 @@ +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { AutoApprovalSettings } from "@shared/AutoApprovalSettings" +import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock" +import AutoApproveMenuItem from "./AutoApproveMenuItem" +import { vscode } from "@/utils/vscode" +import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles" +import { useClickAway } from "react-use" +import HeroTooltip from "@/components/common/HeroTooltip" + +const breakpoint = 500 + +interface AutoApproveMenuProps { + style?: React.CSSProperties +} + +export interface ActionMetadata { + id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll" + label: string + shortName: string + description: string + icon: string + subAction?: ActionMetadata + sub?: boolean + parentActionId?: string +} + +const ACTION_METADATA: ActionMetadata[] = [ + { + id: "readFiles", + label: "Read project files", + shortName: "Read", + description: "Allows Cline to read files within your workspace.", + icon: "codicon-search", + subAction: { + id: "readFilesExternally", + label: "Read all files", + shortName: "Read (all)", + description: "Allows Cline to read any file on your computer.", + icon: "codicon-folder-opened", + parentActionId: "readFiles", + }, + }, + { + id: "editFiles", + label: "Edit project files", + shortName: "Edit", + description: "Allows Cline to modify files within your workspace.", + icon: "codicon-edit", + subAction: { + id: "editFilesExternally", + label: "Edit all files", + shortName: "Edit (all)", + description: "Allows Cline to modify any file on your computer.", + icon: "codicon-files", + parentActionId: "editFiles", + }, + }, + { + id: "executeSafeCommands", + label: "Execute safe commands", + shortName: "Safe Commands", + description: + "Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.", + icon: "codicon-terminal", + subAction: { + id: "executeAllCommands", + label: "Execute all commands", + shortName: "All Commands", + description: "Allows Cline to execute all terminal commands. Use at your own risk.", + icon: "codicon-terminal-bash", + parentActionId: "executeSafeCommands", + }, + }, + { + id: "useBrowser", + label: "Use the browser", + shortName: "Browser", + description: "Allows Cline to launch and interact with any website in a browser.", + icon: "codicon-globe", + }, + { + id: "useMcp", + label: "Use MCP servers", + shortName: "MCP", + description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.", + icon: "codicon-server", + }, + { + id: "enableAll", + label: "Enable all", + shortName: "All", + description: "Enable all actions.", + icon: "codicon-checklist", + }, + { + id: "enableNotifications", + label: "Enable notifications", + shortName: "Notifications", + description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.", + icon: "codicon-bell", + }, +] + +const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { + const { autoApprovalSettings } = useExtensionState() + const [isExpanded, setIsExpanded] = useState(false) + const [containerWidth, setContainerWidth] = useState(0) + // Favorites are now derived from autoApprovalSettings + const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites]) + const menuRef = useRef(null) + const itemsContainerRef = useRef(null) + + // Track container width for responsive layout + useEffect(() => { + if (!isExpanded) return + + const updateWidth = () => { + if (itemsContainerRef.current) { + setContainerWidth(itemsContainerRef.current.offsetWidth) + } + } + + // Initial measurement + updateWidth() + + // Set up resize observer + const resizeObserver = new ResizeObserver(updateWidth) + if (itemsContainerRef.current) { + resizeObserver.observe(itemsContainerRef.current) + } + + // Clean up + return () => { + resizeObserver.disconnect() + } + }, [isExpanded]) + + const toggleFavorite = useCallback( + (actionId: string) => { + const currentFavorites = autoApprovalSettings.favorites || [] + let newFavorites: string[] + + if (currentFavorites.includes(actionId)) { + newFavorites = currentFavorites.filter((id) => id !== actionId) + } else { + newFavorites = [...currentFavorites, actionId] + } + + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + version: (autoApprovalSettings.version ?? 1) + 1, + favorites: newFavorites, + }, + }) + }, + [autoApprovalSettings], + ) + + const updateAction = useCallback( + (action: ActionMetadata, value: boolean) => { + const actionId = action.id + const subActionId = action.subAction?.id + + if (actionId === "enableAll" || subActionId === "enableAll") { + toggleAll(action, value) + return + } + + if (actionId === "enableNotifications" || subActionId === "enableNotifications") { + updateNotifications(action, value) + return + } + + let newActions = { + ...autoApprovalSettings.actions, + [actionId]: value, + } + + if (value === false && subActionId) { + newActions[subActionId] = false + } + + if (value === true && action.parentActionId) { + newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true + } + + // Check if this will result in any enabled actions + const willHaveEnabledActions = Object.values(newActions).some(Boolean) + + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + version: (autoApprovalSettings.version ?? 1) + 1, + actions: newActions, + enabled: willHaveEnabledActions, + }, + }) + }, + [autoApprovalSettings], + ) + + const updateMaxRequests = useCallback( + (maxRequests: number) => { + const currentSettings = autoApprovalSettings + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...currentSettings, + version: (currentSettings.version ?? 1) + 1, + maxRequests, + }, + }) + }, + [autoApprovalSettings], + ) + + const updateNotifications = useCallback( + (action: ActionMetadata, checked: boolean) => { + if (action.id === "enableNotifications") { + const currentSettings = autoApprovalSettings + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...currentSettings, + version: (currentSettings.version ?? 1) + 1, + enableNotifications: checked, + }, + }) + } + }, + [autoApprovalSettings], + ) + + const toggleAll = useCallback( + (action: ActionMetadata, checked: boolean) => { + let actions = { ...autoApprovalSettings.actions } + + for (const action of Object.keys(actions)) { + actions[action as keyof AutoApprovalSettings["actions"]] = checked + } + + vscode.postMessage({ + type: "autoApprovalSettings", + autoApprovalSettings: { + ...autoApprovalSettings, + version: (autoApprovalSettings.version ?? 1) + 1, + actions, + }, + }) + }, + [autoApprovalSettings], + ) + + // Handle clicks outside the menu to close it + useClickAway(menuRef, () => { + if (isExpanded) { + setIsExpanded(false) + } + }) + + // Render a favorited item with a checkbox + const renderFavoritedItem = (favId: string) => { + // Regular action item + const action = ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === favId) + if (!action) return null + + return ( + + ) + } + + const isChecked = (action: ActionMetadata): boolean => { + if (action.id === "enableNotifications") { + return autoApprovalSettings.enableNotifications + } + if (action.id === "enableAll") { + return Object.values(autoApprovalSettings.actions).every(Boolean) + } + return autoApprovalSettings.actions[action.id] ?? false + } + + const isFavorited = (action: ActionMetadata): boolean => { + return favorites.includes(action.id) + } + + return ( +
+ {/* Collapsed view with favorited items */} + {!isExpanded && ( +
setIsExpanded(true)} + style={{ + cursor: "pointer", + paddingTop: "6px", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "8px", + }}> + {favorites.length > 0 ? ( +
+ {favorites.map((favId) => renderFavoritedItem(favId))} +
+ ) : ( +
+
+ + Auto-approve + +
+
+ )} + +
+ )} + + {/* Expanded view */} +
0 ? "40px" : "22px", // Large enough to fit content + opacity: isExpanded ? 1 : 0, + overflow: "hidden", + transition: "max-height 0.3s ease-in-out, opacity 0.3s ease-in-out", // Removed padding to transition + }}> + {isExpanded && ( // Re-added conditional rendering for content + <> +
setIsExpanded(false)}> + + Auto-approve + + +
+ +
breakpoint ? "grid" : "flex", + gridTemplateColumns: containerWidth > breakpoint ? "1fr 1fr" : "1fr", + gridAutoRows: "min-content", + flexDirection: "column", + gap: "4px", + margin: "8px 0", + position: "relative", // For absolute positioning of the separator + }}> + {/* Vertical separator line - only visible in two-column mode */} + {containerWidth > breakpoint && ( +
+ )} + + {/* All items in a single list - CSS Grid will handle the column distribution */} + {ACTION_METADATA.map((action) => ( +
+ +
+ ))} +
+
+ +
+ + Max Requests: + { + const input = e.target as HTMLInputElement + // Remove any non-numeric characters + input.value = input.value.replace(/[^0-9]/g, "") + const value = parseInt(input.value) + if (!isNaN(value) && value > 0) { + updateMaxRequests(value) + } + }} + onKeyDown={(e) => { + // Prevent non-numeric keys (except for backspace, delete, arrows) + if ( + !/^\d$/.test(e.key) && + !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key) + ) { + e.preventDefault() + } + }} + /> +
+
+ + )} +
+
+ ) +} + +export default AutoApproveMenu diff --git a/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenuItem.tsx b/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenuItem.tsx new file mode 100644 index 0000000000..570c6d92a1 --- /dev/null +++ b/webview-ui/src/components/chat/auto-approve-menu/AutoApproveMenuItem.tsx @@ -0,0 +1,131 @@ +import React, { type ChangeEvent, type ChangeEventHandler } from "react" +import styled from "styled-components" +import HeroTooltip from "@/components/common/HeroTooltip" +import { ActionMetadata } from "./AutoApproveMenu" +import { useState } from "react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" + +interface AutoApproveMenuItemProps { + action: ActionMetadata + isChecked: (action: ActionMetadata) => boolean + isFavorited?: (action: ActionMetadata) => boolean + onToggle: (action: ActionMetadata, checked: boolean) => void + onToggleFavorite?: (actionId: string) => void + condensed?: boolean +} + +const CheckboxContainer = styled.div<{ + isFavorited?: boolean + onClick?: (e: MouseEvent) => void + onMouseDown?: (e: React.MouseEvent) => void +}>` + display: flex; + align-items: center; + justify-content: space-between; /* Push content to edges */ + padding: 0 4px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--vscode-textBlockQuote-background); + } + + .left-content { + display: flex; + align-items: center; + gap: 8px; + } + + .icon { + color: var(--vscode-foreground); + font-size: 14px; + } + + .label { + color: var(--vscode-foreground); + font-size: 12px; + font-weight: 500; + } + + .star { + color: ${(props) => (props.isFavorited ? "var(--vscode-terminal-ansiYellow)" : "var(--vscode-descriptionForeground)")}; + opacity: ${(props) => (props.isFavorited ? 1 : 0.6)}; + font-size: 12px; + } +` + +const SubOptionAnimateIn = styled.div<{ show: boolean }>` + position: relative; + transform: ${(props) => (props.show ? "scaleY(1)" : "scaleY(0)")}; + transform-origin: top; + padding-left: 24px; + opacity: ${(props) => (props.show ? "1" : "0")}; + height: ${(props) => (props.show ? "auto" : "0")}; /* Manage height for layout */ + overflow: visible; /* Allow tooltips to escape */ + transition: transform 0.2s ease-in-out; +` + +const ActionButtonContainer = styled.div` + margin: 4px; +` + +const AutoApproveMenuItem = ({ + action, + isChecked, + isFavorited, + onToggle, + onToggleFavorite, + condensed = false, +}: AutoApproveMenuItemProps) => { + const [isSubOptionOpen, setIsSubOptionOpen] = useState(isChecked(action)) + const checked = isChecked(action) + const favorited = isFavorited?.(action) + + const onChange = (e: Event) => { + e.stopPropagation() + const newChecked = !checked + setIsSubOptionOpen(newChecked) + onToggle(action, newChecked) + } + + const content = ( +
+ + + +
+ + + {condensed ? action.shortName : action.label} +
+ {onToggleFavorite && !condensed && ( + { + e.stopPropagation() + onToggleFavorite?.(action.id) + }} + /> + )} +
+
+
+ {action.subAction && !condensed && ( + + + + )} +
+ ) + + return content +} + +export default AutoApproveMenuItem