Skip to content

Commit e19492f

Browse files
committed
feat: redesign Auto-Approve UI with dropdown and two-column layout
- Move AutoApproveMenu below text area with dropdown trigger similar to ModeSelector - Add Stamp icon for dropdown trigger - Implement text ellipsis for long text in dropdown trigger - Change to two-column layout for better organization - Add Select All/Select None functionality with Lucide icons (ListChecks, LayoutList) - Update tests to work with new dropdown UI
1 parent a79c3d0 commit e19492f

File tree

4 files changed

+224
-118
lines changed

4 files changed

+224
-118
lines changed

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

Lines changed: 114 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import { memo, useCallback, useMemo, useState } from "react"
22
import { Trans } from "react-i18next"
33
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
4+
import { Stamp, ListChecks, LayoutList } from "lucide-react"
45

56
import { vscode } from "@src/utils/vscode"
67
import { useExtensionState } from "@src/context/ExtensionStateContext"
78
import { useAppTranslation } from "@src/i18n/TranslationContext"
8-
import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
9-
import { StandardTooltip } from "@src/components/ui"
9+
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
10+
import { AutoApproveToggleDropdown } from "./AutoApproveToggleDropdown"
11+
import { StandardTooltip, Popover, PopoverContent, PopoverTrigger } from "@src/components/ui"
1012
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
1113
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
14+
import { cn } from "@src/lib/utils"
15+
import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
1216

1317
interface AutoApproveMenuProps {
1418
style?: React.CSSProperties
1519
}
1620

1721
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
1822
const [isExpanded, setIsExpanded] = useState(false)
23+
const portalContainer = useRooPortal("roo-portal")
1924

2025
const {
2126
autoApprovalEnabled,
@@ -123,10 +128,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
123128
],
124129
)
125130

126-
const toggleExpanded = useCallback(() => {
127-
setIsExpanded((prev) => !prev)
128-
}, [])
129-
130131
const enabledActionsList = Object.entries(toggles)
131132
.filter(([_key, value]) => !!value)
132133
.map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey))
@@ -146,101 +147,118 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
146147
[],
147148
)
148149

150+
// Handler for Select All
151+
const handleSelectAll = useCallback(() => {
152+
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
153+
allSettings.forEach((key) => {
154+
if (!toggles[key]) {
155+
onAutoApproveToggle(key, true)
156+
}
157+
})
158+
}, [toggles, onAutoApproveToggle])
159+
160+
// Handler for Select None
161+
const handleSelectNone = useCallback(() => {
162+
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
163+
allSettings.forEach((key) => {
164+
if (toggles[key]) {
165+
onAutoApproveToggle(key, false)
166+
}
167+
})
168+
}, [toggles, onAutoApproveToggle])
169+
170+
const trigger = (
171+
<PopoverTrigger
172+
className={cn(
173+
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-2 py-1 text-xs",
174+
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
175+
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
176+
"opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
177+
)}
178+
style={style}>
179+
<Stamp className="size-3.5 opacity-80 flex-shrink-0" />
180+
<span className="font-medium">{t("chat:autoApprove.title")}</span>
181+
<span className="text-vscode-descriptionForeground truncate max-w-[200px]">{displayText}</span>
182+
</PopoverTrigger>
183+
)
184+
149185
return (
150-
<div
151-
style={{
152-
padding: "0 15px",
153-
userSelect: "none",
154-
borderTop: isExpanded
155-
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
156-
: "none",
157-
overflowY: "auto",
158-
...style,
159-
}}>
160-
{isExpanded && (
161-
<div className="flex flex-col gap-2 py-4">
162-
<div
163-
style={{
164-
color: "var(--vscode-descriptionForeground)",
165-
fontSize: "12px",
166-
}}>
167-
<Trans
168-
i18nKey="chat:autoApprove.description"
169-
components={{
170-
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
171-
}}
172-
/>
186+
<Popover open={isExpanded} onOpenChange={setIsExpanded}>
187+
<StandardTooltip content={t("chat:autoApprove.tooltip")}>{trigger}</StandardTooltip>
188+
189+
<PopoverContent
190+
align="start"
191+
sideOffset={4}
192+
container={portalContainer}
193+
className="p-0 overflow-hidden min-w-[400px] max-w-[500px]">
194+
<div className="flex flex-col w-full">
195+
{/* Header with master toggle */}
196+
<div className="flex items-center justify-between p-3 border-b border-vscode-dropdown-border">
197+
<div className="flex items-center gap-2">
198+
<StandardTooltip
199+
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
200+
<VSCodeCheckbox
201+
checked={effectiveAutoApprovalEnabled}
202+
disabled={!hasEnabledOptions}
203+
aria-label={
204+
hasEnabledOptions
205+
? t("chat:autoApprove.toggleAriaLabel")
206+
: t("chat:autoApprove.disabledAriaLabel")
207+
}
208+
onChange={() => {
209+
if (hasEnabledOptions) {
210+
const newValue = !(autoApprovalEnabled ?? false)
211+
setAutoApprovalEnabled(newValue)
212+
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
213+
}
214+
}}
215+
/>
216+
</StandardTooltip>
217+
<h4 className="m-0 font-medium text-sm">{t("chat:autoApprove.title")}</h4>
218+
</div>
219+
<div className="flex items-center gap-1">
220+
<StandardTooltip content={t("chat:autoApprove.selectAll")}>
221+
<button
222+
onClick={handleSelectAll}
223+
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
224+
<ListChecks className="size-4" />
225+
</button>
226+
</StandardTooltip>
227+
<StandardTooltip content={t("chat:autoApprove.selectNone")}>
228+
<button
229+
onClick={handleSelectNone}
230+
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
231+
<LayoutList className="size-4" />
232+
</button>
233+
</StandardTooltip>
234+
</div>
173235
</div>
174236

175-
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
176-
</div>
177-
)}
237+
{/* Description */}
238+
<div className="px-3 py-2 border-b border-vscode-dropdown-border">
239+
<div
240+
style={{
241+
color: "var(--vscode-descriptionForeground)",
242+
fontSize: "12px",
243+
}}>
244+
<Trans
245+
i18nKey="chat:autoApprove.description"
246+
components={{
247+
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
248+
}}
249+
/>
250+
</div>
251+
</div>
178252

179-
<div
180-
style={{
181-
display: "flex",
182-
alignItems: "center",
183-
gap: "8px",
184-
padding: "2px 0 0 0",
185-
cursor: "pointer",
186-
}}
187-
onClick={toggleExpanded}>
188-
<div onClick={(e) => e.stopPropagation()}>
189-
<StandardTooltip
190-
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
191-
<VSCodeCheckbox
192-
checked={effectiveAutoApprovalEnabled}
193-
disabled={!hasEnabledOptions}
194-
aria-label={
195-
hasEnabledOptions
196-
? t("chat:autoApprove.toggleAriaLabel")
197-
: t("chat:autoApprove.disabledAriaLabel")
198-
}
199-
onChange={() => {
200-
if (hasEnabledOptions) {
201-
const newValue = !(autoApprovalEnabled ?? false)
202-
setAutoApprovalEnabled(newValue)
203-
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
204-
}
205-
// If no options enabled, do nothing
206-
}}
207-
/>
208-
</StandardTooltip>
209-
</div>
210-
<div
211-
style={{
212-
display: "flex",
213-
alignItems: "center",
214-
gap: "4px",
215-
flex: 1,
216-
minWidth: 0,
217-
}}>
218-
<span
219-
style={{
220-
color: "var(--vscode-foreground)",
221-
flexShrink: 0,
222-
}}>
223-
{t("chat:autoApprove.title")}
224-
</span>
225-
<span
226-
style={{
227-
color: "var(--vscode-descriptionForeground)",
228-
overflow: "hidden",
229-
textOverflow: "ellipsis",
230-
whiteSpace: "nowrap",
231-
flex: 1,
232-
minWidth: 0,
233-
}}>
234-
{displayText}
235-
</span>
236-
<span
237-
className={`codicon codicon-chevron-right flex-shrink-0 transition-transform duration-200 ease-in-out ${
238-
isExpanded ? "-rotate-90 ml-[2px]" : "rotate-0 -ml-[2px]"
239-
}`}
240-
/>
253+
{/* Two-column layout for toggles */}
254+
<div className="p-3 max-h-[400px] overflow-y-auto">
255+
<div className="grid grid-cols-2 gap-x-4">
256+
<AutoApproveToggleDropdown {...toggles} onToggle={onAutoApproveToggle} />
257+
</div>
258+
</div>
241259
</div>
242-
</div>
243-
</div>
260+
</PopoverContent>
261+
</Popover>
244262
)
245263
}
246264

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { GlobalSettings } from "@roo-code/types"
2+
import { useAppTranslation } from "@/i18n/TranslationContext"
3+
import { cn } from "@/lib/utils"
4+
import { StandardTooltip } from "@/components/ui"
5+
import { autoApproveSettingsConfig, AutoApproveSetting } from "../settings/AutoApproveToggle"
6+
7+
type AutoApproveToggles = Pick<
8+
GlobalSettings,
9+
| "alwaysAllowReadOnly"
10+
| "alwaysAllowWrite"
11+
| "alwaysAllowBrowser"
12+
| "alwaysApproveResubmit"
13+
| "alwaysAllowMcp"
14+
| "alwaysAllowModeSwitch"
15+
| "alwaysAllowSubtasks"
16+
| "alwaysAllowExecute"
17+
| "alwaysAllowFollowupQuestions"
18+
| "alwaysAllowUpdateTodoList"
19+
>
20+
21+
type AutoApproveToggleDropdownProps = AutoApproveToggles & {
22+
onToggle: (key: AutoApproveSetting, value: boolean) => void
23+
}
24+
25+
export const AutoApproveToggleDropdown = ({ onToggle, ...props }: AutoApproveToggleDropdownProps) => {
26+
const { t } = useAppTranslation()
27+
28+
// Split settings into two columns for better layout
29+
const settings = Object.values(autoApproveSettingsConfig)
30+
const halfLength = Math.ceil(settings.length / 2)
31+
const leftColumn = settings.slice(0, halfLength)
32+
const rightColumn = settings.slice(halfLength)
33+
34+
const renderToggleItem = ({
35+
key,
36+
descriptionKey,
37+
labelKey,
38+
icon,
39+
testId,
40+
}: (typeof autoApproveSettingsConfig)[AutoApproveSetting]) => (
41+
<StandardTooltip key={key} content={t(descriptionKey || "")}>
42+
<button
43+
onClick={() => onToggle(key, !props[key])}
44+
aria-label={t(labelKey)}
45+
aria-pressed={!!props[key]}
46+
data-testid={testId}
47+
className={cn(
48+
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
49+
"transition-colors hover:bg-vscode-list-hoverBackground",
50+
props[key]
51+
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
52+
: "opacity-70",
53+
)}>
54+
<span className={cn("codicon", `codicon-${icon}`, "text-sm flex-shrink-0")} />
55+
<span className="flex-1 truncate">{t(labelKey)}</span>
56+
<span
57+
className={cn(
58+
"text-[10px] px-1 rounded",
59+
props[key] ? "bg-vscode-badge-background text-vscode-badge-foreground" : "",
60+
)}>
61+
{props[key] ? "✓" : ""}
62+
</span>
63+
</button>
64+
</StandardTooltip>
65+
)
66+
67+
return (
68+
<>
69+
<div className="flex flex-col gap-1">{leftColumn.map(renderToggleItem)}</div>
70+
<div className="flex flex-col gap-1">{rightColumn.map(renderToggleItem)}</div>
71+
</>
72+
)
73+
}

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,11 +1882,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18821882
// This ensures it takes its natural height when there's space
18831883
// but becomes scrollable when the viewport is too small
18841884
*/}
1885-
{!task && (
1886-
<div className="mb-1 flex-initial min-h-0">
1887-
<AutoApproveMenu />
1888-
</div>
1889-
)}
18901885

18911886
{task && (
18921887
<>
@@ -1909,9 +1904,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
19091904
initialTopMostItemIndex={groupedMessages.length - 1}
19101905
/>
19111906
</div>
1912-
<div className={`flex-initial min-h-0 ${!areButtonsVisible ? "mb-1" : ""}`}>
1913-
<AutoApproveMenu />
1914-
</div>
19151907
{areButtonsVisible && (
19161908
<div
19171909
className={`flex h-9 items-center mb-1 px-[15px] ${
@@ -2025,6 +2017,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
20252017
modeShortcutText={modeShortcutText}
20262018
/>
20272019

2020+
{/* Auto-Approve Menu positioned below text area */}
2021+
<div className="px-3 pb-2">
2022+
<AutoApproveMenu />
2023+
</div>
2024+
20282025
{isProfileDisabled && (
20292026
<div className="px-3">
20302027
<ProfileViolationWarning />

0 commit comments

Comments
 (0)