Roo-Code/webview-ui/src/components/chat/AutoApproveDropdown.tsx

285 lines
9.6 KiB
TypeScript

import React from "react"
import { ListChecks, LayoutList, Settings, CheckCheck, X } from "lucide-react"
import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
import { useAutoApprovalState } from "@/hooks/useAutoApprovalState"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, ToggleSwitch, Button } from "@/components/ui"
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
interface AutoApproveDropdownProps {
disabled?: boolean
triggerClassName?: string
}
export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: AutoApproveDropdownProps) => {
const [open, setOpen] = React.useState(false)
const portalContainer = useRooPortal("roo-portal")
const { t } = useAppTranslation()
const {
autoApprovalEnabled,
setAutoApprovalEnabled,
setAlwaysAllowReadOnly,
setAlwaysAllowWrite,
setAlwaysAllowExecute,
setAlwaysAllowMcp,
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysAllowFollowupQuestions,
} = useExtensionState()
const toggles = useAutoApprovalToggles()
const onAutoApproveToggle = React.useCallback(
(key: AutoApproveSetting, value: boolean) => {
vscode.postMessage({ type: "updateSettings", updatedSettings: { [key]: value } })
switch (key) {
case "alwaysAllowReadOnly":
setAlwaysAllowReadOnly(value)
break
case "alwaysAllowWrite":
setAlwaysAllowWrite(value)
break
case "alwaysAllowExecute":
setAlwaysAllowExecute(value)
break
case "alwaysAllowMcp":
setAlwaysAllowMcp(value)
break
case "alwaysAllowModeSwitch":
setAlwaysAllowModeSwitch(value)
break
case "alwaysAllowSubtasks":
setAlwaysAllowSubtasks(value)
break
case "alwaysAllowFollowupQuestions":
setAlwaysAllowFollowupQuestions(value)
break
}
// If enabling any option, ensure autoApprovalEnabled is true.
if (value && !autoApprovalEnabled) {
setAutoApprovalEnabled(true)
vscode.postMessage({ type: "autoApprovalEnabled", bool: true })
}
},
[
autoApprovalEnabled,
setAlwaysAllowReadOnly,
setAlwaysAllowWrite,
setAlwaysAllowExecute,
setAlwaysAllowMcp,
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysAllowFollowupQuestions,
setAutoApprovalEnabled,
],
)
const handleSelectAll = React.useCallback(() => {
// Enable all options
Object.keys(autoApproveSettingsConfig).forEach((key) => {
onAutoApproveToggle(key as AutoApproveSetting, true)
})
// Enable master auto-approval
if (!autoApprovalEnabled) {
setAutoApprovalEnabled(true)
vscode.postMessage({ type: "autoApprovalEnabled", bool: true })
}
}, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled])
const handleSelectNone = React.useCallback(() => {
// Disable all options
Object.keys(autoApproveSettingsConfig).forEach((key) => {
onAutoApproveToggle(key as AutoApproveSetting, false)
})
}, [onAutoApproveToggle])
const handleOpenSettings = React.useCallback(
() =>
window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }),
[],
)
// Handle the main auto-approval toggle
const handleAutoApprovalToggle = React.useCallback(() => {
const newValue = !(autoApprovalEnabled ?? false)
setAutoApprovalEnabled(newValue)
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
}, [autoApprovalEnabled, setAutoApprovalEnabled])
// Calculate enabled and total counts as separate properties
const settingsArray = Object.values(autoApproveSettingsConfig)
const enabledCount = React.useMemo(() => {
return Object.values(toggles).filter((value) => !!value).length
}, [toggles])
const totalCount = React.useMemo(() => {
return Object.keys(toggles).length
}, [toggles])
const { effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
const tooltipText =
!effectiveAutoApprovalEnabled || enabledCount === 0
? t("chat:autoApprove.tooltipManage")
: t("chat:autoApprove.tooltipStatus", {
toggles: settingsArray
.filter((setting) => toggles[setting.key])
.map((setting) => t(setting.labelKey))
.join(", "),
})
return (
<Popover open={open} onOpenChange={setOpen} data-testid="auto-approve-dropdown-root">
<StandardTooltip content={tooltipText}>
<PopoverTrigger
disabled={disabled}
data-testid="auto-approve-dropdown-trigger"
className={cn(
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
"max-[300px]:shrink-0",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
)}>
{!effectiveAutoApprovalEnabled ? (
<X className="size-3 flex-shrink-0" />
) : (
<CheckCheck className="size-3 flex-shrink-0" />
)}
<span className="hidden min-[300px]:inline truncate min-w-0">
{!effectiveAutoApprovalEnabled
? t("chat:autoApprove.triggerLabelOff")
: enabledCount === totalCount
? t("chat:autoApprove.triggerLabelAll")
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
</span>
<span className="inline min-[300px]:hidden min-w-0">
{!effectiveAutoApprovalEnabled
? t("chat:autoApprove.triggerLabelOffShort")
: enabledCount === totalCount
? t("chat:autoApprove.triggerLabelAll")
: enabledCount}
</span>
</PopoverTrigger>
</StandardTooltip>
<PopoverContent
align="start"
sideOffset={4}
container={portalContainer}
className="p-0 overflow-hidden w-[min(440px,calc(100vw-2rem))]"
onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-col w-full">
{/* Header with description */}
<div className="p-3 border-b border-vscode-dropdown-border">
<div className="flex items-center justify-between gap-1 pr-1 pb-2">
<h4 className="m-0 font-bold text-base text-vscode-foreground">
{t("chat:autoApprove.title")}
</h4>
<Settings
className="inline mb-0.5 mr-1 size-4 cursor-pointer"
onClick={handleOpenSettings}
/>
</div>
<p className="m-0 text-xs text-vscode-descriptionForeground">
{t("chat:autoApprove.description")}
</p>
</div>
<div className="grid grid-cols-1 min-[340px]:grid-cols-2 gap-x-2 gap-y-2 p-3">
{settingsArray.map(({ key, labelKey, descriptionKey, icon }) => {
const isEnabled = toggles[key]
return (
<StandardTooltip key={key} content={t(descriptionKey)}>
<Button
variant={isEnabled ? "primary" : "secondary"}
onClick={() => onAutoApproveToggle(key, !isEnabled)}
className={cn(
"flex items-center gap-2 px-2 py-2 text-sm text-left justify-start h-auto",
"transition-all duration-150",
!effectiveAutoApprovalEnabled &&
"opacity-50 cursor-not-allowed hover:opacity-50",
!isEnabled && "bg-vscode-button-background/15",
)}
disabled={!effectiveAutoApprovalEnabled}
data-testid={`auto-approve-${key}`}>
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
<span className="flex-1 truncate">{t(labelKey)}</span>
</Button>
</StandardTooltip>
)
})}
</div>
{/* Bottom bar with Select All/None buttons */}
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
<div className="flex flex-row gap-1">
<Button
variant="ghost"
size="sm"
aria-label={t("chat:autoApprove.selectAll")}
onClick={handleSelectAll}
disabled={!effectiveAutoApprovalEnabled}
className={cn(
"gap-1 px-2 py-1 text-base font-bold h-auto",
!effectiveAutoApprovalEnabled && "opacity-50 hover:opacity-50 cursor-not-allowed",
)}>
<ListChecks className="w-3.5 h-3.5" />
<span>{t("chat:autoApprove.all")}</span>
</Button>
<Button
variant="ghost"
size="sm"
aria-label={t("chat:autoApprove.selectNone")}
onClick={handleSelectNone}
disabled={!effectiveAutoApprovalEnabled}
className={cn(
"gap-1 px-2 py-1 text-base font-bold h-auto",
!effectiveAutoApprovalEnabled && "opacity-50 hover:opacity-50 cursor-not-allowed",
)}>
<LayoutList className="w-3.5 h-3.5" />
<span>{t("chat:autoApprove.none")}</span>
</Button>
</div>
<label
className="flex items-center gap-2 pr-2 cursor-pointer"
onClick={(e) => {
// Prevent label click when clicking on the toggle switch itself
if ((e.target as HTMLElement).closest('[role="switch"]')) {
e.preventDefault()
return
}
handleAutoApprovalToggle()
}}>
<ToggleSwitch
checked={effectiveAutoApprovalEnabled}
aria-label="Toggle auto-approval"
onChange={handleAutoApprovalToggle}
/>
<span className={cn("text-sm font-bold select-none")}>Enabled</span>
</label>
</div>
</div>
</PopoverContent>
</Popover>
)
}