mirror of
https://github.com/cline/cline.git
synced 2025-06-03 03:59:07 +00:00
Refactor auto approve menu to modal (#3537)
* refactor auto approval menu to modal * changeset * move constants to shared location; change chevron dynamically; remove useless notes * address comments * improve spacing --------- Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local> Co-authored-by: frostbournesb <frostbournesb@protonmail.com>
This commit is contained in:
parent
cc56486814
commit
a66724e312
5
.changeset/clean-games-train.md
Normal file
5
.changeset/clean-games-train.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Refactor auto approval menu to use the existing modal architecture defined by the cline rules and mcp servers modals
|
@ -22,7 +22,6 @@ import { TaskServiceClient, SlashServiceClient } 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/auto-approve-menu/AutoApproveMenu"
|
||||
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
|
||||
import ChatRow from "@/components/chat/ChatRow"
|
||||
import ChatTextArea from "@/components/chat/ChatTextArea"
|
||||
@ -34,7 +33,7 @@ import remarkStringify from "remark-stringify"
|
||||
import rehypeRemark from "rehype-remark"
|
||||
import rehypeParse from "rehype-parse"
|
||||
import HomeHeader from "../welcome/HomeHeader"
|
||||
|
||||
import AutoApproveBar from "./auto-approve-menu/AutoApproveBar"
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
showAnnouncement: boolean
|
||||
@ -1047,7 +1046,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!task && <AutoApproveMenu />}
|
||||
{!task && <AutoApproveBar />}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
@ -1081,7 +1080,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveMenu />
|
||||
<AutoApproveBar />
|
||||
{showScrollToBottom ? (
|
||||
<div
|
||||
style={{
|
||||
|
@ -0,0 +1,132 @@
|
||||
import { useCallback, useRef, useState, useMemo } from "react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import AutoApproveModal from "./AutoApproveModal"
|
||||
import { ACTION_METADATA, NOTIFICATIONS_SETTING } from "./constants"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
interface AutoApproveBarProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const renderFavoritedItem = (favId: string) => {
|
||||
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
|
||||
const action = actions.find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getQuickAccessItems = () => {
|
||||
const notificationsEnabled = autoApprovalSettings.enableNotifications
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof typeof autoApprovalSettings.actions],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
|
||||
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
|
||||
|
||||
if (notificationsEnabled) {
|
||||
minusFavorites.push(NOTIFICATIONS_SETTING)
|
||||
}
|
||||
|
||||
return [
|
||||
...favorites.map((favId) => renderFavoritedItem(favId)),
|
||||
minusFavorites.length > 0 ? (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] pl-[10px] opacity-60" key="separator">
|
||||
✓
|
||||
</span>
|
||||
) : null,
|
||||
...minusFavorites.map((action, index) => (
|
||||
<span className="text-[color:var(--vscode-foreground-muted)] opacity-60" key={action?.id}>
|
||||
{action?.shortName}
|
||||
{index < minusFavorites.length - 1 && ","}
|
||||
</span>
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const updateAction = useCallback(() => {
|
||||
// This is just a placeholder since we need to pass it to AutoApproveMenuItem
|
||||
// The actual implementation is in the modal component
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="px-[10px] mx-[5px] select-none rounded-[10px_10px_0_0]"
|
||||
style={{
|
||||
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
|
||||
overflowY: "auto",
|
||||
backgroundColor: isModalVisible ? CODE_BLOCK_BG_COLOR : "transparent",
|
||||
...style,
|
||||
}}>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="cursor-pointer py-[8px] pr-[2px] flex items-center justify-between gap-[8px]"
|
||||
onClick={() => {
|
||||
setIsModalVisible((prev) => !prev)
|
||||
}}>
|
||||
<div
|
||||
className="flex flex-nowrap items-center overflow-x-auto gap-[4px] whitespace-nowrap"
|
||||
style={{
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}>
|
||||
<span>Auto-approve:</span>
|
||||
{getQuickAccessItems()}
|
||||
</div>
|
||||
{isModalVisible ? (
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
) : (
|
||||
<span className="codicon codicon-chevron-up" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AutoApproveModal
|
||||
isVisible={isModalVisible}
|
||||
setIsVisible={setIsModalVisible}
|
||||
buttonRef={buttonRef}
|
||||
ACTION_METADATA={ACTION_METADATA}
|
||||
NOTIFICATIONS_SETTING={NOTIFICATIONS_SETTING}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveBar
|
@ -1,513 +0,0 @@
|
||||
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_FOREGROUND_MUTED } 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: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
|
||||
const NOTIFICATIONS_SETTING: ActionMetadata = {
|
||||
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) => {
|
||||
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
|
||||
const action = actions.find((a) => a?.id === favId)
|
||||
if (!action) return null
|
||||
|
||||
return (
|
||||
<AutoApproveMenuItem
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
condensed={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Render a favorited item with a checkbox
|
||||
const getQuickAccessItems = () => {
|
||||
const notificationsEnabled = autoApprovalSettings.enableNotifications
|
||||
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
|
||||
(key) => autoApprovalSettings.actions[key as keyof AutoApprovalSettings["actions"]],
|
||||
)
|
||||
const enabledActions = enabledActionsNames.map((action) => {
|
||||
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
|
||||
})
|
||||
|
||||
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
|
||||
|
||||
if (notificationsEnabled) {
|
||||
minusFavorites.push(NOTIFICATIONS_SETTING)
|
||||
}
|
||||
|
||||
return [
|
||||
...favorites.map((favId) => renderFavoritedItem(favId)),
|
||||
minusFavorites.length > 0 ? (
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND_MUTED), paddingLeft: "10px", opacity: 0.6 }} key="separator">
|
||||
✓
|
||||
</span>
|
||||
) : null,
|
||||
...minusFavorites.map((action, index) => (
|
||||
<span
|
||||
style={{
|
||||
color: getAsVar(VSC_FOREGROUND_MUTED),
|
||||
opacity: 0.6,
|
||||
}}
|
||||
key={action?.id}>
|
||||
{action?.shortName}
|
||||
{index < minusFavorites.length - 1 && ","}
|
||||
</span>
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
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 4px 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",
|
||||
paddingRight: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
overflowX: "auto",
|
||||
msOverflowStyle: "none",
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
gap: "4px",
|
||||
whiteSpace: "nowrap", // Prevent text wrapping
|
||||
}}>
|
||||
<span>Auto-approve:</span>
|
||||
{getQuickAccessItems()}
|
||||
</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",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}>
|
||||
{isExpanded && ( // Re-added conditional rendering for content
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 4px 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), fontWeight: 500 }}>Auto-approve:</span>
|
||||
</HeroTooltip>
|
||||
<span className="codicon codicon-chevron-down" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
style={{
|
||||
columnCount: containerWidth > breakpoint ? 2 : 1,
|
||||
columnGap: "4px",
|
||||
margin: "4px 0 16px 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) => (
|
||||
<AutoApproveMenuItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ color: getAsVar(VSC_FOREGROUND), marginBottom: 4, fontWeight: 500 }}>Quick Settings:</span>
|
||||
<AutoApproveMenuItem
|
||||
key={NOTIFICATIONS_SETTING.id}
|
||||
action={NOTIFICATIONS_SETTING}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div
|
||||
style={{
|
||||
margin: "2px 10px 20px 5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
}}>
|
||||
<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%", paddingRight: "35px" }}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<span
|
||||
className="codicon codicon-chevron-up"
|
||||
style={{
|
||||
paddingBottom: "4px",
|
||||
paddingRight: "3px",
|
||||
marginLeft: "auto",
|
||||
marginTop: "-20px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveMenu
|
@ -1,8 +1,7 @@
|
||||
import React, { type ChangeEvent, type ChangeEventHandler } from "react"
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import { ActionMetadata } from "./AutoApproveMenu"
|
||||
import { useState } from "react"
|
||||
import { ActionMetadata } from "./types"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface AutoApproveMenuItemProps {
|
||||
|
@ -0,0 +1,333 @@
|
||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import HeroTooltip from "@/components/common/HeroTooltip"
|
||||
import AutoApproveMenuItem from "./AutoApproveMenuItem"
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
const breakpoint = 500
|
||||
|
||||
interface AutoApproveModalProps {
|
||||
isVisible: boolean
|
||||
setIsVisible: (visible: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLDivElement>
|
||||
ACTION_METADATA: ActionMetadata[]
|
||||
NOTIFICATIONS_SETTING: ActionMetadata
|
||||
}
|
||||
|
||||
const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
|
||||
isVisible,
|
||||
setIsVisible,
|
||||
buttonRef,
|
||||
ACTION_METADATA,
|
||||
NOTIFICATIONS_SETTING,
|
||||
}) => {
|
||||
const { autoApprovalSettings } = useExtensionState()
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
|
||||
// Favorites are derived from autoApprovalSettings
|
||||
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
|
||||
|
||||
useClickAway(modalRef, (e) => {
|
||||
// Skip if click was on the button that toggles the modal
|
||||
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
|
||||
return
|
||||
}
|
||||
setIsVisible(false)
|
||||
})
|
||||
|
||||
// Calculate positions for modal and arrow
|
||||
useEffect(() => {
|
||||
if (isVisible && buttonRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect()
|
||||
const buttonCenter = buttonRect.left + buttonRect.width / 2
|
||||
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
|
||||
|
||||
setArrowPosition(rightPosition)
|
||||
setMenuPosition(buttonRect.top + 1)
|
||||
}
|
||||
}, [isVisible, viewportWidth, viewportHeight, buttonRef])
|
||||
|
||||
// Track container width for responsive layout
|
||||
useEffect(() => {
|
||||
if (!isVisible) 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()
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
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],
|
||||
)
|
||||
|
||||
// Check if action is enabled
|
||||
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
|
||||
}
|
||||
|
||||
// Check if action is favorited
|
||||
const isFavorited = (action: ActionMetadata): boolean => {
|
||||
return favorites.includes(action.id)
|
||||
}
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div
|
||||
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
overscrollBehavior: "contain",
|
||||
}}>
|
||||
<div
|
||||
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px)`,
|
||||
right: arrowPosition,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="m-0 text-base font-semibold">Auto-approve Settings</div>
|
||||
<VSCodeButton appearance="icon" onClick={() => setIsVisible(false)}>
|
||||
<span className="codicon codicon-close text-[10px]"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
|
||||
</div>
|
||||
</HeroTooltip>
|
||||
|
||||
<div
|
||||
ref={itemsContainerRef}
|
||||
className="relative mb-6"
|
||||
style={{
|
||||
columnCount: containerWidth > breakpoint ? 2 : 1,
|
||||
columnGap: "4px",
|
||||
}}>
|
||||
{/* Vertical separator line - only visible in two-column mode */}
|
||||
{containerWidth > breakpoint && (
|
||||
<div
|
||||
className="absolute left-1/2 top-0 bottom-0 w-[0.5px] opacity-20"
|
||||
style={{
|
||||
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
|
||||
transform: "translateX(-50%)", // Center the line
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* All items in a single list - CSS Grid will handle the column distribution */}
|
||||
{ACTION_METADATA.map((action) => (
|
||||
<AutoApproveMenuItem
|
||||
key={action.id}
|
||||
action={action}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<span className="text-[color:var(--vscode-foreground)] font-medium">Quick Settings:</span>
|
||||
</div>
|
||||
|
||||
<AutoApproveMenuItem
|
||||
key={NOTIFICATIONS_SETTING.id}
|
||||
action={NOTIFICATIONS_SETTING}
|
||||
isChecked={isChecked}
|
||||
isFavorited={isFavorited}
|
||||
onToggle={updateAction}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
|
||||
<HeroTooltip
|
||||
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
|
||||
placement="top">
|
||||
<div className="flex items-center pl-1.5 my-2">
|
||||
<span className="codicon codicon-settings text-[#CCCCCC] text-[14px]" />
|
||||
<span className="text-[#CCCCCC] text-xs font-medium ml-2">Max Requests:</span>
|
||||
<VSCodeTextField
|
||||
className="flex-1 w-full pr-[35px] ml-4"
|
||||
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 AutoApproveModal
|
@ -0,0 +1,79 @@
|
||||
import { ActionMetadata } from "./types"
|
||||
|
||||
export const ACTION_METADATA: ActionMetadata[] = [
|
||||
{
|
||||
id: "enableAll",
|
||||
label: "Enable all",
|
||||
shortName: "All",
|
||||
description: "Enable all actions.",
|
||||
icon: "codicon-checklist",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
|
||||
export const NOTIFICATIONS_SETTING: ActionMetadata = {
|
||||
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",
|
||||
}
|
12
webview-ui/src/components/chat/auto-approve-menu/types.ts
Normal file
12
webview-ui/src/components/chat/auto-approve-menu/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
|
||||
export interface ActionMetadata {
|
||||
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
|
||||
label: string
|
||||
shortName: string
|
||||
description: string
|
||||
icon: string
|
||||
subAction?: ActionMetadata
|
||||
sub?: boolean
|
||||
parentActionId?: string
|
||||
}
|
Loading…
Reference in New Issue
Block a user