Skip to content

Commit 9ea7173

Browse files
brunobergherroomotehannesrudolphdaniel-lxsroomote[bot]
authored
ux: Smaller and more subtle auto-approve UI (#7894)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: Hannes Rudolph <[email protected]> Co-authored-by: daniel-lxs <[email protected]> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Bruno Bergher <[email protected]> Co-authored-by: Daniel <[email protected]> Co-authored-by: ItsOnlyBinary <[email protected]> Co-authored-by: Matt Rubens <[email protected]> Co-authored-by: John Richmond <[email protected]>
1 parent d09689b commit 9ea7173

File tree

25 files changed

+522
-712
lines changed

25 files changed

+522
-712
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export const ApiConfigSelector = ({
149149
disabled={disabled}
150150
data-testid="dropdown-trigger"
151151
className={cn(
152-
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
152+
"min-w-0 inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
153153
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
154154
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
155155
disabled
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import React from "react"
2+
import { ListChecks, LayoutList, Settings, CheckCheck } from "lucide-react"
3+
4+
import { vscode } from "@/utils/vscode"
5+
import { cn } from "@/lib/utils"
6+
import { useExtensionState } from "@/context/ExtensionStateContext"
7+
import { useAppTranslation } from "@/i18n/TranslationContext"
8+
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
9+
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
10+
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
11+
import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
12+
13+
interface AutoApproveDropdownProps {
14+
disabled?: boolean
15+
triggerClassName?: string
16+
}
17+
18+
export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: AutoApproveDropdownProps) => {
19+
const [open, setOpen] = React.useState(false)
20+
const portalContainer = useRooPortal("roo-portal")
21+
const { t } = useAppTranslation()
22+
23+
const {
24+
autoApprovalEnabled,
25+
setAutoApprovalEnabled,
26+
alwaysApproveResubmit,
27+
setAlwaysAllowReadOnly,
28+
setAlwaysAllowWrite,
29+
setAlwaysAllowExecute,
30+
setAlwaysAllowBrowser,
31+
setAlwaysAllowMcp,
32+
setAlwaysAllowModeSwitch,
33+
setAlwaysAllowSubtasks,
34+
setAlwaysApproveResubmit,
35+
setAlwaysAllowFollowupQuestions,
36+
setAlwaysAllowUpdateTodoList,
37+
} = useExtensionState()
38+
39+
const baseToggles = useAutoApprovalToggles()
40+
41+
// Include alwaysApproveResubmit in addition to the base toggles
42+
const toggles = React.useMemo(
43+
() => ({
44+
...baseToggles,
45+
alwaysApproveResubmit: alwaysApproveResubmit,
46+
}),
47+
[baseToggles, alwaysApproveResubmit],
48+
)
49+
50+
const onAutoApproveToggle = React.useCallback(
51+
(key: AutoApproveSetting, value: boolean) => {
52+
vscode.postMessage({ type: key, bool: value })
53+
54+
// Update the specific toggle state
55+
switch (key) {
56+
case "alwaysAllowReadOnly":
57+
setAlwaysAllowReadOnly(value)
58+
break
59+
case "alwaysAllowWrite":
60+
setAlwaysAllowWrite(value)
61+
break
62+
case "alwaysAllowExecute":
63+
setAlwaysAllowExecute(value)
64+
break
65+
case "alwaysAllowBrowser":
66+
setAlwaysAllowBrowser(value)
67+
break
68+
case "alwaysAllowMcp":
69+
setAlwaysAllowMcp(value)
70+
break
71+
case "alwaysAllowModeSwitch":
72+
setAlwaysAllowModeSwitch(value)
73+
break
74+
case "alwaysAllowSubtasks":
75+
setAlwaysAllowSubtasks(value)
76+
break
77+
case "alwaysApproveResubmit":
78+
setAlwaysApproveResubmit(value)
79+
break
80+
case "alwaysAllowFollowupQuestions":
81+
setAlwaysAllowFollowupQuestions(value)
82+
break
83+
case "alwaysAllowUpdateTodoList":
84+
setAlwaysAllowUpdateTodoList(value)
85+
break
86+
}
87+
88+
// If enabling any option, ensure autoApprovalEnabled is true
89+
if (value && !autoApprovalEnabled) {
90+
setAutoApprovalEnabled(true)
91+
vscode.postMessage({ type: "autoApprovalEnabled", bool: true })
92+
}
93+
},
94+
[
95+
autoApprovalEnabled,
96+
setAlwaysAllowReadOnly,
97+
setAlwaysAllowWrite,
98+
setAlwaysAllowExecute,
99+
setAlwaysAllowBrowser,
100+
setAlwaysAllowMcp,
101+
setAlwaysAllowModeSwitch,
102+
setAlwaysAllowSubtasks,
103+
setAlwaysApproveResubmit,
104+
setAlwaysAllowFollowupQuestions,
105+
setAlwaysAllowUpdateTodoList,
106+
setAutoApprovalEnabled,
107+
],
108+
)
109+
110+
const handleSelectAll = React.useCallback(() => {
111+
// Enable all options
112+
Object.keys(autoApproveSettingsConfig).forEach((key) => {
113+
onAutoApproveToggle(key as AutoApproveSetting, true)
114+
})
115+
// Enable master auto-approval
116+
if (!autoApprovalEnabled) {
117+
setAutoApprovalEnabled(true)
118+
vscode.postMessage({ type: "autoApprovalEnabled", bool: true })
119+
}
120+
}, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled])
121+
122+
const handleSelectNone = React.useCallback(() => {
123+
// Disable all options
124+
Object.keys(autoApproveSettingsConfig).forEach((key) => {
125+
onAutoApproveToggle(key as AutoApproveSetting, false)
126+
})
127+
// Disable master auto-approval
128+
if (autoApprovalEnabled) {
129+
setAutoApprovalEnabled(false)
130+
vscode.postMessage({ type: "autoApprovalEnabled", bool: false })
131+
}
132+
}, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled])
133+
134+
const handleOpenSettings = React.useCallback(
135+
() =>
136+
window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }),
137+
[],
138+
)
139+
140+
// Calculate enabled and total counts as separate properties
141+
const enabledCount = React.useMemo(() => {
142+
return Object.values(toggles).filter((value) => !!value).length
143+
}, [toggles])
144+
145+
const totalCount = React.useMemo(() => {
146+
return Object.keys(toggles).length
147+
}, [toggles])
148+
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+
155+
return (
156+
<Popover open={open} onOpenChange={setOpen} data-testid="auto-approve-dropdown-root">
157+
<StandardTooltip content={t("chat:autoApprove.tooltip")}>
158+
<PopoverTrigger
159+
disabled={disabled}
160+
data-testid="auto-approve-dropdown-trigger"
161+
className={cn(
162+
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
163+
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
164+
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
165+
disabled
166+
? "opacity-50 cursor-not-allowed"
167+
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
168+
triggerClassName,
169+
)}>
170+
<CheckCheck className="size-3" />
171+
<span className="truncate">
172+
{enabledCount === totalCount
173+
? t("chat:autoApprove.triggerLabelAll")
174+
: t("chat:autoApprove.triggerLabel", { count: enabledCount })}
175+
</span>
176+
</PopoverTrigger>
177+
</StandardTooltip>
178+
<PopoverContent
179+
align="start"
180+
sideOffset={4}
181+
container={portalContainer}
182+
className="p-0 overflow-hidden min-w-96 max-w-9/10"
183+
onOpenAutoFocus={(e) => e.preventDefault()}>
184+
<div className="flex flex-col w-full">
185+
{/* Header with description */}
186+
<div className="p-3 border-b border-vscode-dropdown-border">
187+
<div className="flex items-center justify-between gap-1 pr-1 pb-2">
188+
<h4 className="m-0 font-bold text-base text-vscode-foreground">
189+
{t("chat:autoApprove.title")}
190+
</h4>
191+
<Settings
192+
className="inline mb-0.5 mr-1 size-4 cursor-pointer"
193+
onClick={handleOpenSettings}
194+
/>
195+
</div>
196+
<p className="m-0 text-xs text-vscode-descriptionForeground">
197+
{t("chat:autoApprove.description")}
198+
</p>
199+
</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>
260+
</div>
261+
262+
{/* Bottom bar with Select All/None buttons */}
263+
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
264+
<div className="flex flex-row gap-1">
265+
<button
266+
aria-label={t("chat:autoApprove.selectAll")}
267+
onClick={handleSelectAll}
268+
className={cn(
269+
"relative inline-flex items-center justify-center gap-1",
270+
"bg-transparent border-none px-2 py-1",
271+
"rounded-md text-base font-bold",
272+
"text-vscode-foreground",
273+
"transition-all duration-150",
274+
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
275+
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
276+
"active:bg-[rgba(255,255,255,0.1)]",
277+
"cursor-pointer",
278+
)}>
279+
<ListChecks className="w-3.5 h-3.5" />
280+
<span>{t("chat:autoApprove.all")}</span>
281+
</button>
282+
<button
283+
aria-label={t("chat:autoApprove.selectNone")}
284+
onClick={handleSelectNone}
285+
className={cn(
286+
"relative inline-flex items-center justify-center gap-1",
287+
"bg-transparent border-none px-2 py-1",
288+
"rounded-md text-base font-bold",
289+
"text-vscode-foreground",
290+
"transition-all duration-150",
291+
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
292+
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
293+
"active:bg-[rgba(255,255,255,0.1)]",
294+
"cursor-pointer",
295+
)}>
296+
<LayoutList className="w-3.5 h-3.5" />
297+
<span>{t("chat:autoApprove.none")}</span>
298+
</button>
299+
</div>
300+
</div>
301+
</div>
302+
</PopoverContent>
303+
</Popover>
304+
)
305+
}

0 commit comments

Comments
 (0)