Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 74 additions & 78 deletions webview-ui/src/components/chat/AutoApproveDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from "react"
import { ListChecks, LayoutList, Settings, CheckCheck } from "lucide-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 { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, ToggleSwitch } from "@/components/ui"
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
import { useAutoApprovalState } from "@/hooks/useAutoApprovalState"

interface AutoApproveDropdownProps {
disabled?: boolean
Expand Down Expand Up @@ -124,20 +125,24 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
Object.keys(autoApproveSettingsConfig).forEach((key) => {
onAutoApproveToggle(key as AutoApproveSetting, false)
})
// Disable master auto-approval
if (autoApprovalEnabled) {
setAutoApprovalEnabled(false)
vscode.postMessage({ type: "autoApprovalEnabled", bool: false })
}
}, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled])
}, [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])
Expand All @@ -146,11 +151,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
return Object.keys(toggles).length
}, [toggles])

// Split settings into two columns
const settingsArray = Object.values(autoApproveSettingsConfig)
const halfLength = Math.ceil(settingsArray.length / 2)
const firstColumn = settingsArray.slice(0, halfLength)
const secondColumn = settingsArray.slice(halfLength)
const { effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)

return (
<Popover open={open} onOpenChange={setOpen} data-testid="auto-approve-dropdown-root">
Expand All @@ -167,19 +168,26 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
)}>
<CheckCheck className="size-3 flex-shrink-0" />
{!effectiveAutoApprovalEnabled ? (
<X className="size-3 flex-shrink-0" />
) : (
<CheckCheck className="size-3 flex-shrink-0" />
)}

<span className="truncate min-w-0">
{enabledCount === totalCount
? t("chat:autoApprove.triggerLabelAll")
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
{!effectiveAutoApprovalEnabled
? t("chat:autoApprove.triggerLabelOff")
: enabledCount === totalCount
? t("chat:autoApprove.triggerLabelAll")
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
</span>
</PopoverTrigger>
</StandardTooltip>
<PopoverContent
align="start"
sideOffset={4}
container={portalContainer}
className="p-0 overflow-hidden min-w-96 max-w-9/10"
className="p-0 overflow-hidden min-w-90 max-w-9/10"
onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-col w-full">
{/* Header with description */}
Expand All @@ -197,66 +205,32 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
{t("chat:autoApprove.description")}
</p>
</div>

{/* Two-column layout for approval options */}
<div className="p-3">
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
{/* First Column */}
<div className="space-y-2">
{firstColumn.map(({ key, labelKey, descriptionKey, icon }) => {
const isEnabled = toggles[key]
return (
<StandardTooltip key={key} content={t(descriptionKey)}>
<button
onClick={() => onAutoApproveToggle(key, !isEnabled)}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
"transition-all duration-150",
"hover:bg-vscode-list-hoverBackground",
isEnabled
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-transparent text-vscode-foreground opacity-70 hover:opacity-100",
)}
data-testid={`auto-approve-${key}`}>
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
<span className="flex-1 truncate">{t(labelKey)}</span>
{isEnabled && (
<span className="codicon codicon-check text-xs flex-shrink-0" />
)}
</button>
</StandardTooltip>
)
})}
</div>

{/* Second Column */}
<div className="space-y-2">
{secondColumn.map(({ key, labelKey, descriptionKey, icon }) => {
const isEnabled = toggles[key]
return (
<StandardTooltip key={key} content={t(descriptionKey)}>
<button
onClick={() => onAutoApproveToggle(key, !isEnabled)}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
"transition-all duration-150",
"hover:bg-vscode-list-hoverBackground",
isEnabled
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-transparent text-vscode-foreground opacity-70 hover:opacity-100",
)}
data-testid={`auto-approve-${key}`}>
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
<span className="flex-1 truncate">{t(labelKey)}</span>
{isEnabled && (
<span className="codicon codicon-check text-xs flex-shrink-0" />
)}
</button>
</StandardTooltip>
)
})}
</div>
</div>
<div className="grid 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
onClick={() => onAutoApproveToggle(key, !isEnabled)}
className={cn(
"flex items-center gap-2 px-2 py-2 rounded text-sm text-left",
"transition-all duration-150",
"opacity-100 hover:opacity-70",
"cursor-pointer",
!effectiveAutoApprovalEnabled &&
"opacity-50 cursor-not-allowed hover:opacity-50",
isEnabled
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-vscode-button-background/15 text-vscode-foreground hover:bg-vscode-list-hoverBackground",
)}
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 */}
Expand All @@ -265,6 +239,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
<button
aria-label={t("chat:autoApprove.selectAll")}
onClick={handleSelectAll}
disabled={!effectiveAutoApprovalEnabled}
className={cn(
"relative inline-flex items-center justify-center gap-1",
"bg-transparent border-none px-2 py-1",
Expand All @@ -275,13 +250,15 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
"cursor-pointer",
!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
aria-label={t("chat:autoApprove.selectNone")}
onClick={handleSelectNone}
disabled={!effectiveAutoApprovalEnabled}
className={cn(
"relative inline-flex items-center justify-center gap-1",
"bg-transparent border-none px-2 py-1",
Expand All @@ -292,11 +269,30 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
"cursor-pointer",
!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>
Expand Down
40 changes: 12 additions & 28 deletions webview-ui/src/components/settings/AutoApproveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { X, CheckCheck } from "lucide-react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { Button, Input, Slider, StandardTooltip } from "@/components/ui"
import { Button, Input, Slider } from "@/components/ui"

import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
Expand Down Expand Up @@ -88,7 +88,7 @@ export const AutoApproveSettings = ({

const toggles = useAutoApprovalToggles()

const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
const { effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)

const handleAddCommand = () => {
const currentCommands = allowedCommands ?? []
Expand Down Expand Up @@ -124,32 +124,16 @@ export const AutoApproveSettings = ({
<Section>
<div className="space-y-4">
<div>
{!hasEnabledOptions ? (
<StandardTooltip content={t("settings:autoApprove.selectOptionsFirst")}>
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
disabled={!hasEnabledOptions}
aria-label={t("settings:autoApprove.disabledAriaLabel")}
onChange={() => {
// Do nothing when no options are enabled
return
}}>
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
</VSCodeCheckbox>
</StandardTooltip>
) : (
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
disabled={!hasEnabledOptions}
aria-label={t("settings:autoApprove.toggleAriaLabel")}
onChange={() => {
const newValue = !(autoApprovalEnabled ?? false)
setAutoApprovalEnabled(newValue)
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
}}>
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
</VSCodeCheckbox>
)}
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
aria-label={t("settings:autoApprove.toggleAriaLabel")}
onChange={() => {
const newValue = !(autoApprovalEnabled ?? false)
setAutoApprovalEnabled(newValue)
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
}}>
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.description")}
</div>
Expand Down
8 changes: 4 additions & 4 deletions webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("useAutoApprovalState", () => {
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
})

it("should return false when autoApprovalEnabled is true but no toggles are enabled", () => {
it("should return true when autoApprovalEnabled is true but no toggles are enabled", () => {
const toggles = {
alwaysAllowReadOnly: false,
alwaysAllowWrite: false,
Expand All @@ -140,7 +140,7 @@ describe("useAutoApprovalState", () => {

const { result } = renderHook(() => useAutoApprovalState(toggles, true))

expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
})

it("should return true when autoApprovalEnabled is true and at least one toggle is enabled", () => {
Expand Down Expand Up @@ -217,7 +217,7 @@ describe("useAutoApprovalState", () => {
rerender({ toggles: newToggles, autoApprovalEnabled: true })

expect(result.current.hasEnabledOptions).toBe(false)
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
})

it("should recompute effectiveAutoApprovalEnabled when autoApprovalEnabled changes", () => {
Expand Down Expand Up @@ -263,7 +263,7 @@ describe("useAutoApprovalState", () => {
const { result } = renderHook(() => useAutoApprovalState(toggles, true))

expect(result.current.hasEnabledOptions).toBe(false)
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
})

it("should handle mixed truthy/falsy values correctly", () => {
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/hooks/useAutoApprovalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export function useAutoApprovalState(toggles: AutoApprovalToggles, autoApprovalE
}, [toggles])

const effectiveAutoApprovalEnabled = useMemo(() => {
return hasEnabledOptions && (autoApprovalEnabled ?? false)
}, [hasEnabledOptions, autoApprovalEnabled])
return autoApprovalEnabled ?? false
}, [autoApprovalEnabled])

return {
hasEnabledOptions,
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ca/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/de/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@
"selectOptionsFirst": "Select at least one option below to enable auto-approval",
"toggleAriaLabel": "Toggle auto-approval",
"disabledAriaLabel": "Auto-approval disabled - select options first",
"triggerLabel_zero": "No auto-approve",
"triggerLabelOff": "Auto-approve off",
"triggerLabel_zero": "0 auto-approve",
"triggerLabel_one": "1 auto-approved",
"triggerLabel_other": "{{count}} auto-approved",
"triggerLabelAll": "YOLO"
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/es/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/fr/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading