Compare commits

...

11 Commits

Author SHA1 Message Date
Trevor Hudson fdbb91cba2 remove opacity animation 2025-05-09 09:38:22 -07:00
Trevor Hudson d5d092e6a3 improve responsiveness 2025-05-09 08:09:38 -07:00
Trevor Hudson 95a89fdb0c 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>
2025-05-08 22:34:33 -07:00
Trevor Hudson 8a1ea4b76c 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>
2025-05-08 22:33:10 -07:00
Trevor Hudson 63add55335 use vscode colors 2025-05-08 22:32:05 -07:00
Trevor Hudson 0f8b618788 turn on parent when subAction is turned on 2025-05-08 22:01:16 -07:00
Trevor Hudson 07e5569fe0 back to checkboxes 2025-05-08 21:52:58 -07:00
Trevor Hudson 87e67cc18e add pills 2025-05-08 19:50:43 -07:00
Trevor Hudson a109baa976 changeset 2025-05-08 14:19:28 -07:00
Trevor Hudson 33d63e86c0 roll back chevron 2025-05-08 14:15:43 -07:00
Trevor Hudson b9cae3ff56 improved auto-approve 2025-05-08 14:02:43 -07:00
6 changed files with 618 additions and 513 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Updated auto approve with favorited settings
+2
View File
@@ -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: [],
}
@@ -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<HTMLDivElement>(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 (
<div
ref={menuRef}
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`
: "none",
overflowY: "auto",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: !hasEnabledActions ? "pointer" : "default",
}}
onMouseEnter={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(true)
}
}}
onMouseLeave={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(false)
}
}}
onClick={() => {
if (!hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<VSCodeCheckbox
style={{
pointerEvents: hasEnabledActions ? "auto" : "none",
}}
checked={hasEnabledActions && autoApprovalSettings.enabled}
disabled={!hasEnabledActions}
// onChange={(e) => {
// 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)
}}
/>
<CollapsibleSection
isHovered={isHoveringCollapsibleSection}
style={{ cursor: "pointer" }}
onClick={() => {
// to prevent this from counteracting parent
if (hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<span
style={{
color: getAsVar(VSC_FOREGROUND),
whiteSpace: "nowrap",
}}>
Auto-approve:
</span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{!hasEnabledActions ? "None" : enabledActionsList}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
/>
</CollapsibleSection>
</div>
{isExpanded && (
<div style={{ padding: "0" }}>
<div
style={{
marginBottom: "10px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
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.
</div>
{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 (
<SubOptionAnimateIn key={action.id} show={autoApprovalSettings.actions[parentAction] ?? false}>
<div
style={{
margin: "3px 0",
marginLeft: "28px",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
</SubOptionAnimateIn>
)
}
return (
<div
key={action.id}
style={{
margin: "6px 0",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
)
})}
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "15px 0",
opacity: 0.2,
}}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "10px",
marginBottom: "8px",
color: getAsVar(VSC_FOREGROUND),
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
// placeholder={DEFAULT_AUTO_APPROVAL_SETTINGS.maxRequests.toString()}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
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 }}
/>
</div>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
marginBottom: "10px",
}}>
Cline will automatically make this many API requests before asking for approval to proceed with the task.
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={autoApprovalSettings.enableNotifications}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateNotifications(checked)
}}>
Enable Notifications
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
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<{ 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
+2 -25
View File
@@ -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"
@@ -988,30 +988,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
</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 && <AutoApproveMenu />}
{task && (
<>
@@ -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<HTMLDivElement>(null)
const itemsContainerRef = useRef<HTMLDivElement>(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 (
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
condensed={true}
/>
)
}
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 (
<div
ref={menuRef}
style={{
padding: "0 10px",
margin: "0 5px",
userSelect: "none",
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
overflowY: "auto",
borderRadius: "10px 10px 0 0",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
{/* Collapsed view with favorited items */}
{!isExpanded && (
<div
onClick={() => setIsExpanded(true)}
style={{
cursor: "pointer",
paddingTop: "6px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "8px",
}}>
{favorites.length > 0 ? (
<div
style={{
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
overflowX: "auto",
msOverflowStyle: "none",
scrollbarWidth: "none",
WebkitOverflowScrolling: "touch",
gap: "4px",
whiteSpace: "nowrap", // Prevent text wrapping
}}>
{favorites.map((favId) => renderFavoritedItem(favId))}
</div>
) : (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "pointer",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<HeroTooltip
content="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."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND), left: "0" }}>Auto-approve</span>
</HeroTooltip>
</div>
</div>
)}
<span className="codicon codicon-chevron-right" />
</div>
)}
{/* Expanded view */}
<div
style={{
maxHeight: isExpanded ? "1000px" : favorites.length > 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
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px 0",
cursor: "pointer",
position: "relative", // Added for positioning context
}}
onClick={() => setIsExpanded(false)}>
<HeroTooltip
content="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."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND) }}>Auto-approve</span>
</HeroTooltip>
<span className="codicon codicon-chevron-down" />
</div>
<div
ref={itemsContainerRef}
style={{
display: containerWidth > 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 && (
<div
style={{
position: "absolute",
left: "50%",
top: "0",
bottom: "0",
width: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
opacity: 0.2,
transform: "translateX(-50%)", // Center the line
}}
/>
)}
{/* All items in a single list - CSS Grid will handle the column distribution */}
{ACTION_METADATA.map((action) => (
<div key={action.id} style={{ breakInside: "avoid" }}>
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
onToggleFavorite={toggleFavorite}
/>
</div>
))}
</div>
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "10px 0",
opacity: 0.2,
}}
/>
<HeroTooltip
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
placement="top">
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
paddingBottom: "10px",
}}>
<span className="codicon codicon-settings" style={{ color: "#CCCCCC", fontSize: "14px" }} />
<span style={{ color: "#CCCCCC", fontSize: "12px", fontWeight: 500 }}>Max Requests:</span>
<VSCodeTextField
style={{ flex: "1", width: "100%" }}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
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()
}
}}
/>
</div>
</HeroTooltip>
</>
)}
</div>
</div>
)
}
export default AutoApproveMenu
@@ -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 = (
<div>
<ActionButtonContainer>
<HeroTooltip content={action.description} delay={200}>
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
<div className="left-content">
<VSCodeCheckbox checked={checked} />
<span className={`codicon ${action.icon} icon`}></span>
<span className="label">{condensed ? action.shortName : action.label}</span>
</div>
{onToggleFavorite && !condensed && (
<span
className={`codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
onClick={(e) => {
e.stopPropagation()
onToggleFavorite?.(action.id)
}}
/>
)}
</CheckboxContainer>
</HeroTooltip>
</ActionButtonContainer>
{action.subAction && !condensed && (
<SubOptionAnimateIn show={isSubOptionOpen}>
<AutoApproveMenuItem
action={action.subAction}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={onToggle}
onToggleFavorite={onToggleFavorite}
/>
</SubOptionAnimateIn>
)}
</div>
)
return content
}
export default AutoApproveMenuItem