Skip to content

Commit e597379

Browse files
brunobergherjqknono
authored andcommitted
feat: Bring back a way to temporarily and globally pause auto-approve without losing your toggle state (RooCodeInc#8024)
* Visual improvements and code cleanup (removes duplication) * Brings back an 'enabled' toggle for auto-approve
1 parent ad1be5a commit e597379

File tree

22 files changed

+128
-130
lines changed

22 files changed

+128
-130
lines changed

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

Lines changed: 74 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import React from "react"
2-
import { ListChecks, LayoutList, Settings, CheckCheck } from "lucide-react"
2+
import { ListChecks, LayoutList, Settings, CheckCheck, X } from "lucide-react"
33

44
import { vscode } from "@/utils/vscode"
55
import { cn } from "@/lib/utils"
66
import { useExtensionState } from "@/context/ExtensionStateContext"
77
import { useAppTranslation } from "@/i18n/TranslationContext"
88
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
9-
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
9+
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, ToggleSwitch } from "@/components/ui"
1010
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
1111
import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
12+
import { useAutoApprovalState } from "@/hooks/useAutoApprovalState"
1213

1314
interface AutoApproveDropdownProps {
1415
disabled?: boolean
@@ -124,20 +125,24 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
124125
Object.keys(autoApproveSettingsConfig).forEach((key) => {
125126
onAutoApproveToggle(key as AutoApproveSetting, false)
126127
})
127-
// Disable master auto-approval
128-
if (autoApprovalEnabled) {
129-
setAutoApprovalEnabled(false)
130-
vscode.postMessage({ type: "autoApprovalEnabled", bool: false })
131-
}
132-
}, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled])
128+
}, [onAutoApproveToggle])
133129

134130
const handleOpenSettings = React.useCallback(
135131
() =>
136132
window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }),
137133
[],
138134
)
139135

136+
// Handle the main auto-approval toggle
137+
const handleAutoApprovalToggle = React.useCallback(() => {
138+
const newValue = !(autoApprovalEnabled ?? false)
139+
setAutoApprovalEnabled(newValue)
140+
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
141+
}, [autoApprovalEnabled, setAutoApprovalEnabled])
142+
140143
// Calculate enabled and total counts as separate properties
144+
const settingsArray = Object.values(autoApproveSettingsConfig)
145+
141146
const enabledCount = React.useMemo(() => {
142147
return Object.values(toggles).filter((value) => !!value).length
143148
}, [toggles])
@@ -146,11 +151,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
146151
return Object.keys(toggles).length
147152
}, [toggles])
148153

149-
// Split settings into two columns
150-
const settingsArray = Object.values(autoApproveSettingsConfig)
151-
const halfLength = Math.ceil(settingsArray.length / 2)
152-
const firstColumn = settingsArray.slice(0, halfLength)
153-
const secondColumn = settingsArray.slice(halfLength)
154+
const { effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
154155

155156
return (
156157
<Popover open={open} onOpenChange={setOpen} data-testid="auto-approve-dropdown-root">
@@ -167,19 +168,26 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
167168
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
168169
triggerClassName,
169170
)}>
170-
<CheckCheck className="size-3 flex-shrink-0" />
171+
{!effectiveAutoApprovalEnabled ? (
172+
<X className="size-3 flex-shrink-0" />
173+
) : (
174+
<CheckCheck className="size-3 flex-shrink-0" />
175+
)}
176+
171177
<span className="truncate min-w-0">
172-
{enabledCount === totalCount
173-
? t("chat:autoApprove.triggerLabelAll")
174-
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
178+
{!effectiveAutoApprovalEnabled
179+
? t("chat:autoApprove.triggerLabelOff")
180+
: enabledCount === totalCount
181+
? t("chat:autoApprove.triggerLabelAll")
182+
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
175183
</span>
176184
</PopoverTrigger>
177185
</StandardTooltip>
178186
<PopoverContent
179187
align="start"
180188
sideOffset={4}
181189
container={portalContainer}
182-
className="p-0 overflow-hidden min-w-96 max-w-9/10"
190+
className="p-0 overflow-hidden min-w-90 max-w-9/10"
183191
onOpenAutoFocus={(e) => e.preventDefault()}>
184192
<div className="flex flex-col w-full">
185193
{/* Header with description */}
@@ -197,66 +205,32 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
197205
{t("chat:autoApprove.description")}
198206
</p>
199207
</div>
200-
201-
{/* Two-column layout for approval options */}
202-
<div className="p-3">
203-
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
204-
{/* First Column */}
205-
<div className="space-y-2">
206-
{firstColumn.map(({ key, labelKey, descriptionKey, icon }) => {
207-
const isEnabled = toggles[key]
208-
return (
209-
<StandardTooltip key={key} content={t(descriptionKey)}>
210-
<button
211-
onClick={() => onAutoApproveToggle(key, !isEnabled)}
212-
className={cn(
213-
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
214-
"transition-all duration-150",
215-
"hover:bg-vscode-list-hoverBackground",
216-
isEnabled
217-
? "bg-vscode-button-background text-vscode-button-foreground"
218-
: "bg-transparent text-vscode-foreground opacity-70 hover:opacity-100",
219-
)}
220-
data-testid={`auto-approve-${key}`}>
221-
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
222-
<span className="flex-1 truncate">{t(labelKey)}</span>
223-
{isEnabled && (
224-
<span className="codicon codicon-check text-xs flex-shrink-0" />
225-
)}
226-
</button>
227-
</StandardTooltip>
228-
)
229-
})}
230-
</div>
231-
232-
{/* Second Column */}
233-
<div className="space-y-2">
234-
{secondColumn.map(({ key, labelKey, descriptionKey, icon }) => {
235-
const isEnabled = toggles[key]
236-
return (
237-
<StandardTooltip key={key} content={t(descriptionKey)}>
238-
<button
239-
onClick={() => onAutoApproveToggle(key, !isEnabled)}
240-
className={cn(
241-
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
242-
"transition-all duration-150",
243-
"hover:bg-vscode-list-hoverBackground",
244-
isEnabled
245-
? "bg-vscode-button-background text-vscode-button-foreground"
246-
: "bg-transparent text-vscode-foreground opacity-70 hover:opacity-100",
247-
)}
248-
data-testid={`auto-approve-${key}`}>
249-
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
250-
<span className="flex-1 truncate">{t(labelKey)}</span>
251-
{isEnabled && (
252-
<span className="codicon codicon-check text-xs flex-shrink-0" />
253-
)}
254-
</button>
255-
</StandardTooltip>
256-
)
257-
})}
258-
</div>
259-
</div>
208+
<div className="grid grid-cols-2 gap-x-2 gap-y-2 p-3">
209+
{settingsArray.map(({ key, labelKey, descriptionKey, icon }) => {
210+
const isEnabled = toggles[key]
211+
return (
212+
<StandardTooltip key={key} content={t(descriptionKey)}>
213+
<button
214+
onClick={() => onAutoApproveToggle(key, !isEnabled)}
215+
className={cn(
216+
"flex items-center gap-2 px-2 py-2 rounded text-sm text-left",
217+
"transition-all duration-150",
218+
"opacity-100 hover:opacity-70",
219+
"cursor-pointer",
220+
!effectiveAutoApprovalEnabled &&
221+
"opacity-50 cursor-not-allowed hover:opacity-50",
222+
isEnabled
223+
? "bg-vscode-button-background text-vscode-button-foreground"
224+
: "bg-vscode-button-background/15 text-vscode-foreground hover:bg-vscode-list-hoverBackground",
225+
)}
226+
disabled={!effectiveAutoApprovalEnabled}
227+
data-testid={`auto-approve-${key}`}>
228+
<span className={`codicon codicon-${icon} text-sm flex-shrink-0`} />
229+
<span className="flex-1 truncate">{t(labelKey)}</span>
230+
</button>
231+
</StandardTooltip>
232+
)
233+
})}
260234
</div>
261235

262236
{/* Bottom bar with Select All/None buttons */}
@@ -265,6 +239,7 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
265239
<button
266240
aria-label={t("chat:autoApprove.selectAll")}
267241
onClick={handleSelectAll}
242+
disabled={!effectiveAutoApprovalEnabled}
268243
className={cn(
269244
"relative inline-flex items-center justify-center gap-1",
270245
"bg-transparent border-none px-2 py-1",
@@ -275,13 +250,15 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
275250
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
276251
"active:bg-[rgba(255,255,255,0.1)]",
277252
"cursor-pointer",
253+
!effectiveAutoApprovalEnabled && "opacity-50 hover:opacity-50 cursor-not-allowed",
278254
)}>
279255
<ListChecks className="w-3.5 h-3.5" />
280256
<span>{t("chat:autoApprove.all")}</span>
281257
</button>
282258
<button
283259
aria-label={t("chat:autoApprove.selectNone")}
284260
onClick={handleSelectNone}
261+
disabled={!effectiveAutoApprovalEnabled}
285262
className={cn(
286263
"relative inline-flex items-center justify-center gap-1",
287264
"bg-transparent border-none px-2 py-1",
@@ -292,11 +269,30 @@ export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }:
292269
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
293270
"active:bg-[rgba(255,255,255,0.1)]",
294271
"cursor-pointer",
272+
!effectiveAutoApprovalEnabled && "opacity-50 hover:opacity-50 cursor-not-allowed",
295273
)}>
296274
<LayoutList className="w-3.5 h-3.5" />
297275
<span>{t("chat:autoApprove.none")}</span>
298276
</button>
299277
</div>
278+
279+
<label
280+
className="flex items-center gap-2 pr-2 cursor-pointer"
281+
onClick={(e) => {
282+
// Prevent label click when clicking on the toggle switch itself
283+
if ((e.target as HTMLElement).closest('[role="switch"]')) {
284+
e.preventDefault()
285+
return
286+
}
287+
handleAutoApprovalToggle()
288+
}}>
289+
<ToggleSwitch
290+
checked={effectiveAutoApprovalEnabled}
291+
aria-label="Toggle auto-approval"
292+
onChange={handleAutoApprovalToggle}
293+
/>
294+
<span className={cn("text-sm font-bold select-none")}>Enabled</span>
295+
</label>
300296
</div>
301297
</div>
302298
</PopoverContent>

webview-ui/src/components/settings/AutoApproveSettings.tsx

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { X, CheckCheck } from "lucide-react"
44
import { useAppTranslation } from "@/i18n/TranslationContext"
55
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
66
import { vscode } from "@/utils/vscode"
7-
import { Button, Input, Slider, StandardTooltip } from "@/components/ui"
7+
import { Button, Input, Slider } from "@/components/ui"
88

99
import { SetCachedStateField } from "./types"
1010
import { SectionHeader } from "./SectionHeader"
@@ -88,7 +88,7 @@ export const AutoApproveSettings = ({
8888

8989
const toggles = useAutoApprovalToggles()
9090

91-
const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
91+
const { effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
9292

9393
const handleAddCommand = () => {
9494
const currentCommands = allowedCommands ?? []
@@ -124,32 +124,16 @@ export const AutoApproveSettings = ({
124124
<Section>
125125
<div className="space-y-4">
126126
<div>
127-
{!hasEnabledOptions ? (
128-
<StandardTooltip content={t("settings:autoApprove.selectOptionsFirst")}>
129-
<VSCodeCheckbox
130-
checked={effectiveAutoApprovalEnabled}
131-
disabled={!hasEnabledOptions}
132-
aria-label={t("settings:autoApprove.disabledAriaLabel")}
133-
onChange={() => {
134-
// Do nothing when no options are enabled
135-
return
136-
}}>
137-
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
138-
</VSCodeCheckbox>
139-
</StandardTooltip>
140-
) : (
141-
<VSCodeCheckbox
142-
checked={effectiveAutoApprovalEnabled}
143-
disabled={!hasEnabledOptions}
144-
aria-label={t("settings:autoApprove.toggleAriaLabel")}
145-
onChange={() => {
146-
const newValue = !(autoApprovalEnabled ?? false)
147-
setAutoApprovalEnabled(newValue)
148-
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
149-
}}>
150-
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
151-
</VSCodeCheckbox>
152-
)}
127+
<VSCodeCheckbox
128+
checked={effectiveAutoApprovalEnabled}
129+
aria-label={t("settings:autoApprove.toggleAriaLabel")}
130+
onChange={() => {
131+
const newValue = !(autoApprovalEnabled ?? false)
132+
setAutoApprovalEnabled(newValue)
133+
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
134+
}}>
135+
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
136+
</VSCodeCheckbox>
153137
<div className="text-vscode-descriptionForeground text-sm mt-1">
154138
{t("settings:autoApprove.description")}
155139
</div>

webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ describe("useAutoApprovalState", () => {
124124
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
125125
})
126126

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

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

143-
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
143+
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
144144
})
145145

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

219219
expect(result.current.hasEnabledOptions).toBe(false)
220-
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
220+
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
221221
})
222222

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

265265
expect(result.current.hasEnabledOptions).toBe(false)
266-
expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
266+
expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
267267
})
268268

269269
it("should handle mixed truthy/falsy values correctly", () => {

webview-ui/src/hooks/useAutoApprovalState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export function useAutoApprovalState(toggles: AutoApprovalToggles, autoApprovalE
1919
}, [toggles])
2020

2121
const effectiveAutoApprovalEnabled = useMemo(() => {
22-
return hasEnabledOptions && (autoApprovalEnabled ?? false)
23-
}, [hasEnabledOptions, autoApprovalEnabled])
22+
return autoApprovalEnabled ?? false
23+
}, [autoApprovalEnabled])
2424

2525
return {
2626
hasEnabledOptions,

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@
279279
"selectOptionsFirst": "Select at least one option below to enable auto-approval",
280280
"toggleAriaLabel": "Toggle auto-approval",
281281
"disabledAriaLabel": "Auto-approval disabled - select options first",
282-
"triggerLabel_zero": "No auto-approve",
282+
"triggerLabelOff": "Auto-approve off",
283+
"triggerLabel_zero": "0 auto-approve",
283284
"triggerLabel_one": "1 auto-approved",
284285
"triggerLabel_other": "{{count}} auto-approved",
285286
"triggerLabelAll": "YOLO"

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/chat.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)