mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdbb91cba2 | |||
| d5d092e6a3 | |||
| 95a89fdb0c | |||
| 8a1ea4b76c | |||
| 63add55335 | |||
| 0f8b618788 | |||
| 07e5569fe0 | |||
| 87e67cc18e | |||
| a109baa976 | |||
| 33d63e86c0 | |||
| b9cae3ff56 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updated auto approve with favorited settings
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user