feat: Bring back a way to temporarily and globally pause auto-approve without losing your toggle state (#8024)

* Visual improvements and code cleanup (removes duplication)

* Brings back an 'enabled' toggle for auto-approve
This commit is contained in:
Bruno Bergher 2025-09-16 13:59:17 +01:00 committed by GitHub
parent c987417889
commit 3d45d4aa13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 128 additions and 130 deletions

View file

@ -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
@ -124,12 +125,7 @@ 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(
() =>
@ -137,7 +133,16 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
[],
)
// 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])
@ -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">
@ -167,11 +168,18 @@ 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>
@ -179,7 +187,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
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 */}
@ -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 */}
@ -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",
@ -275,6 +250,7 @@ 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>
@ -282,6 +258,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
<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",
@ -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>

View file

@ -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"
@ -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 ?? []
@ -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>

View file

@ -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,
@ -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", () => {
@ -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", () => {
@ -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", () => {

View file

@ -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,

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Sense aprovació automàtica",
"triggerLabel_one": "1 aprovació automàtica",
"triggerLabel_other": "{{count}} aprovacions automàtiques",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Auto-approve desactivat"
},
"reasoning": {
"thinking": "Pensant",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Keine automatische Genehmigung",
"triggerLabel_one": "1 automatisch genehmigt",
"triggerLabel_other": "{{count}} automatisch genehmigt",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Automatische Genehmigung aus"
},
"reasoning": {
"thinking": "Denke nach",

View file

@ -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"

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Sin aprobación automática",
"triggerLabel_one": "1 aprobado automáticamente",
"triggerLabel_other": "{{count}} aprobados automáticamente",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Auto-aprobar desactivado"
},
"reasoning": {
"thinking": "Pensando",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Pas d'approbation automatique",
"triggerLabel_one": "1 approuvé automatiquement",
"triggerLabel_other": "{{count}} approuvés automatiquement",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Approbation automatique désactivée"
},
"reasoning": {
"thinking": "Réflexion",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "कोई स्वतः-अनुमोदन नहीं",
"triggerLabel_one": "1 स्वतः-अनुमोदित",
"triggerLabel_other": "{{count}} स्वतः-अनुमोदित",
"triggerLabelAll": "योलो"
"triggerLabelAll": "योलो",
"triggerLabelOff": "स्वतः-अनुमोदन बंद"
},
"reasoning": {
"thinking": "विचार कर रहा है",

View file

@ -285,7 +285,8 @@
"triggerLabel_zero": "Tidak ada persetujuan otomatis",
"triggerLabel_one": "1 disetujui otomatis",
"triggerLabel_other": "{{count}} disetujui otomatis",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Persetujuan otomatis nonaktif"
},
"announcement": {
"title": "🎉 Roo Code {{version}} Dirilis",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Nessuna approvazione automatica",
"triggerLabel_one": "1 approvato automaticamente",
"triggerLabel_other": "{{count}} approvati automaticamente",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Approvazione automatica disattivata"
},
"reasoning": {
"thinking": "Sto pensando",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "自動承認なし",
"triggerLabel_one": "1件自動承認済み",
"triggerLabel_other": "{{count}}件自動承認済み",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "自動承認オフ"
},
"reasoning": {
"thinking": "考え中",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "자동 승인 없음",
"triggerLabel_one": "1개 자동 승인됨",
"triggerLabel_other": "{{count}}개 자동 승인됨",
"triggerLabelAll": "욜로"
"triggerLabelAll": "욜로",
"triggerLabelOff": "자동 승인 꺼짐"
},
"reasoning": {
"thinking": "생각 중",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Geen automatische goedkeuring",
"triggerLabel_one": "1 automatisch goedgekeurd",
"triggerLabel_other": "{{count}} automatisch goedgekeurd",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Automatisch goedkeuren uit"
},
"announcement": {
"title": "🎉 Roo Code {{version}} uitgebracht",

View file

@ -260,7 +260,8 @@
"triggerLabel_few": "{{count}} automatycznie zaakceptowane",
"triggerLabel_many": "{{count}} automatycznie zaakceptowanych",
"triggerLabel_other": "{{count}} automatycznie zaakceptowanych",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Automatyczne zatwierdzanie wyłączone"
},
"reasoning": {
"thinking": "Myślenie",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Nenhuma aprovação automática",
"triggerLabel_one": "1 aprovado automaticamente",
"triggerLabel_other": "{{count}} aprovados automaticamente",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Aprovação automática desativada"
},
"reasoning": {
"thinking": "Pensando",

View file

@ -260,7 +260,8 @@
"triggerLabel_few": "{{count}} авто-утверждено",
"triggerLabel_many": "{{count}} авто-утверждено",
"triggerLabel_other": "{{count}} авто-утверждено",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Авто-утверждение выкл"
},
"announcement": {
"title": "🎉 Выпущен Roo Code {{version}}",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Otomatik onay yok",
"triggerLabel_one": "1 otomatik onaylandı",
"triggerLabel_other": "{{count}} otomatik onaylandı",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Otomatik onay kapalı"
},
"reasoning": {
"thinking": "Düşünüyor",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "Không có tự động phê duyệt",
"triggerLabel_one": "1 được tự động phê duyệt",
"triggerLabel_other": "{{count}} được tự động phê duyệt",
"triggerLabelAll": "YOLO"
"triggerLabelAll": "YOLO",
"triggerLabelOff": "Tự động phê duyệt tắt"
},
"reasoning": {
"thinking": "Đang suy nghĩ",

View file

@ -258,7 +258,8 @@
"triggerLabel_zero": "无自动批准",
"triggerLabel_one": "1 个自动批准",
"triggerLabel_other": "{{count}} 个自动批准",
"triggerLabelAll": "人生只有一次"
"triggerLabelAll": "人生只有一次",
"triggerLabelOff": "自动批准已关闭"
},
"reasoning": {
"thinking": "思考中",

View file

@ -282,7 +282,8 @@
"triggerLabel_zero": "無自動核准",
"triggerLabel_one": "1 個自動核准",
"triggerLabel_other": "{{count}} 個自動核准",
"triggerLabelAll": "人生只有一次"
"triggerLabelAll": "人生只有一次",
"triggerLabelOff": "自動批准已關閉"
},
"announcement": {
"title": "🎉 Roo Code {{version}} 已發布",