mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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
This commit is contained in:
parent
a79c3d04a6
commit
e19492fb57
4 changed files with 224 additions and 118 deletions
|
|
@ -1,14 +1,18 @@
|
|||
import { memo, useCallback, useMemo, useState } from "react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Stamp, ListChecks, LayoutList } from "lucide-react"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
|
||||
import { AutoApproveToggleDropdown } from "./AutoApproveToggleDropdown"
|
||||
import { StandardTooltip, Popover, PopoverContent, PopoverTrigger } from "@src/components/ui"
|
||||
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
|
||||
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
|
||||
import { cn } from "@src/lib/utils"
|
||||
import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
|
|
@ -16,6 +20,7 @@ interface AutoApproveMenuProps {
|
|||
|
||||
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const portalContainer = useRooPortal("roo-portal")
|
||||
|
||||
const {
|
||||
autoApprovalEnabled,
|
||||
|
|
@ -123,10 +128,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
],
|
||||
)
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const enabledActionsList = Object.entries(toggles)
|
||||
.filter(([_key, value]) => !!value)
|
||||
.map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey))
|
||||
|
|
@ -146,101 +147,118 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||
[],
|
||||
)
|
||||
|
||||
// Handler for Select All
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
|
||||
allSettings.forEach((key) => {
|
||||
if (!toggles[key]) {
|
||||
onAutoApproveToggle(key, true)
|
||||
}
|
||||
})
|
||||
}, [toggles, onAutoApproveToggle])
|
||||
|
||||
// Handler for Select None
|
||||
const handleSelectNone = useCallback(() => {
|
||||
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
|
||||
allSettings.forEach((key) => {
|
||||
if (toggles[key]) {
|
||||
onAutoApproveToggle(key, false)
|
||||
}
|
||||
})
|
||||
}, [toggles, onAutoApproveToggle])
|
||||
|
||||
const trigger = (
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-2 py-1 text-xs",
|
||||
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
|
||||
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
|
||||
"opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
|
||||
)}
|
||||
style={style}>
|
||||
<Stamp className="size-3.5 opacity-80 flex-shrink-0" />
|
||||
<span className="font-medium">{t("chat:autoApprove.title")}</span>
|
||||
<span className="text-vscode-descriptionForeground truncate max-w-[200px]">{displayText}</span>
|
||||
</PopoverTrigger>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "0 15px",
|
||||
userSelect: "none",
|
||||
borderTop: isExpanded
|
||||
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
|
||||
: "none",
|
||||
overflowY: "auto",
|
||||
...style,
|
||||
}}>
|
||||
{isExpanded && (
|
||||
<div className="flex flex-col gap-2 py-4">
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<Trans
|
||||
i18nKey="chat:autoApprove.description"
|
||||
components={{
|
||||
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
|
||||
}}
|
||||
/>
|
||||
<Popover open={isExpanded} onOpenChange={setIsExpanded}>
|
||||
<StandardTooltip content={t("chat:autoApprove.tooltip")}>{trigger}</StandardTooltip>
|
||||
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
container={portalContainer}
|
||||
className="p-0 overflow-hidden min-w-[400px] max-w-[500px]">
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Header with master toggle */}
|
||||
<div className="flex items-center justify-between p-3 border-b border-vscode-dropdown-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<StandardTooltip
|
||||
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
|
||||
<VSCodeCheckbox
|
||||
checked={effectiveAutoApprovalEnabled}
|
||||
disabled={!hasEnabledOptions}
|
||||
aria-label={
|
||||
hasEnabledOptions
|
||||
? t("chat:autoApprove.toggleAriaLabel")
|
||||
: t("chat:autoApprove.disabledAriaLabel")
|
||||
}
|
||||
onChange={() => {
|
||||
if (hasEnabledOptions) {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
<h4 className="m-0 font-medium text-sm">{t("chat:autoApprove.title")}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<StandardTooltip content={t("chat:autoApprove.selectAll")}>
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
|
||||
<ListChecks className="size-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("chat:autoApprove.selectNone")}>
|
||||
<button
|
||||
onClick={handleSelectNone}
|
||||
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
|
||||
<LayoutList className="size-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
|
||||
</div>
|
||||
)}
|
||||
{/* Description */}
|
||||
<div className="px-3 py-2 border-b border-vscode-dropdown-border">
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<Trans
|
||||
i18nKey="chat:autoApprove.description"
|
||||
components={{
|
||||
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
padding: "2px 0 0 0",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={toggleExpanded}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<StandardTooltip
|
||||
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
|
||||
<VSCodeCheckbox
|
||||
checked={effectiveAutoApprovalEnabled}
|
||||
disabled={!hasEnabledOptions}
|
||||
aria-label={
|
||||
hasEnabledOptions
|
||||
? t("chat:autoApprove.toggleAriaLabel")
|
||||
: t("chat:autoApprove.disabledAriaLabel")
|
||||
}
|
||||
onChange={() => {
|
||||
if (hasEnabledOptions) {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}
|
||||
// If no options enabled, do nothing
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
{/* Two-column layout for toggles */}
|
||||
<div className="p-3 max-h-[400px] overflow-y-auto">
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<AutoApproveToggleDropdown {...toggles} onToggle={onAutoApproveToggle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{t("chat:autoApprove.title")}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}>
|
||||
{displayText}
|
||||
</span>
|
||||
<span
|
||||
className={`codicon codicon-chevron-right flex-shrink-0 transition-transform duration-200 ease-in-out ${
|
||||
isExpanded ? "-rotate-90 ml-[2px]" : "rotate-0 -ml-[2px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
73
webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx
Normal file
73
webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { GlobalSettings } from "@roo-code/types"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
import { autoApproveSettingsConfig, AutoApproveSetting } from "../settings/AutoApproveToggle"
|
||||
|
||||
type AutoApproveToggles = Pick<
|
||||
GlobalSettings,
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowWrite"
|
||||
| "alwaysAllowBrowser"
|
||||
| "alwaysApproveResubmit"
|
||||
| "alwaysAllowMcp"
|
||||
| "alwaysAllowModeSwitch"
|
||||
| "alwaysAllowSubtasks"
|
||||
| "alwaysAllowExecute"
|
||||
| "alwaysAllowFollowupQuestions"
|
||||
| "alwaysAllowUpdateTodoList"
|
||||
>
|
||||
|
||||
type AutoApproveToggleDropdownProps = AutoApproveToggles & {
|
||||
onToggle: (key: AutoApproveSetting, value: boolean) => void
|
||||
}
|
||||
|
||||
export const AutoApproveToggleDropdown = ({ onToggle, ...props }: AutoApproveToggleDropdownProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
// Split settings into two columns for better layout
|
||||
const settings = Object.values(autoApproveSettingsConfig)
|
||||
const halfLength = Math.ceil(settings.length / 2)
|
||||
const leftColumn = settings.slice(0, halfLength)
|
||||
const rightColumn = settings.slice(halfLength)
|
||||
|
||||
const renderToggleItem = ({
|
||||
key,
|
||||
descriptionKey,
|
||||
labelKey,
|
||||
icon,
|
||||
testId,
|
||||
}: (typeof autoApproveSettingsConfig)[AutoApproveSetting]) => (
|
||||
<StandardTooltip key={key} content={t(descriptionKey || "")}>
|
||||
<button
|
||||
onClick={() => onToggle(key, !props[key])}
|
||||
aria-label={t(labelKey)}
|
||||
aria-pressed={!!props[key]}
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left",
|
||||
"transition-colors hover:bg-vscode-list-hoverBackground",
|
||||
props[key]
|
||||
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
|
||||
: "opacity-70",
|
||||
)}>
|
||||
<span className={cn("codicon", `codicon-${icon}`, "text-sm flex-shrink-0")} />
|
||||
<span className="flex-1 truncate">{t(labelKey)}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] px-1 rounded",
|
||||
props[key] ? "bg-vscode-badge-background text-vscode-badge-foreground" : "",
|
||||
)}>
|
||||
{props[key] ? "✓" : ""}
|
||||
</span>
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">{leftColumn.map(renderToggleItem)}</div>
|
||||
<div className="flex flex-col gap-1">{rightColumn.map(renderToggleItem)}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1882,11 +1882,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// This ensures it takes its natural height when there's space
|
||||
// but becomes scrollable when the viewport is too small
|
||||
*/}
|
||||
{!task && (
|
||||
<div className="mb-1 flex-initial min-h-0">
|
||||
<AutoApproveMenu />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
|
|
@ -1909,9 +1904,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex-initial min-h-0 ${!areButtonsVisible ? "mb-1" : ""}`}>
|
||||
<AutoApproveMenu />
|
||||
</div>
|
||||
{areButtonsVisible && (
|
||||
<div
|
||||
className={`flex h-9 items-center mb-1 px-[15px] ${
|
||||
|
|
@ -2025,6 +2017,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
modeShortcutText={modeShortcutText}
|
||||
/>
|
||||
|
||||
{/* Auto-Approve Menu positioned below text area */}
|
||||
<div className="px-3 pb-2">
|
||||
<AutoApproveMenu />
|
||||
</div>
|
||||
|
||||
{isProfileDisabled && (
|
||||
<div className="px-3">
|
||||
<ProfileViolationWarning />
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ describe("AutoApproveMenu", () => {
|
|||
expect(screen.getByText("Read-only operations")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should not allow toggling master checkbox when no options are selected", () => {
|
||||
it("should not allow toggling master checkbox when no options are selected", async () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: false,
|
||||
|
|
@ -124,6 +124,15 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("checkbox")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click on the master checkbox
|
||||
const masterCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(masterCheckbox)
|
||||
|
|
@ -132,7 +141,7 @@ describe("AutoApproveMenu", () => {
|
|||
expect(mockPostMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should toggle master checkbox when options are selected", () => {
|
||||
it("should toggle master checkbox when options are selected", async () => {
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
...defaultExtensionState,
|
||||
autoApprovalEnabled: true,
|
||||
|
|
@ -141,6 +150,15 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Wait for the dropdown to open
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("checkbox")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click on the master checkbox
|
||||
const masterCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(masterCheckbox)
|
||||
|
|
@ -164,9 +182,9 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Wait for the menu to expand and find the read-only button
|
||||
await waitFor(() => {
|
||||
|
|
@ -192,9 +210,9 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-write-toggle")).toBeInTheDocument()
|
||||
|
|
@ -240,9 +258,9 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
|
||||
|
|
@ -279,9 +297,9 @@ describe("AutoApproveMenu", () => {
|
|||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand the menu
|
||||
const menuContainer = screen.getByText("Auto-approve").parentElement
|
||||
fireEvent.click(menuContainer!)
|
||||
// Click to open the dropdown
|
||||
const trigger = screen.getByText("Auto-approve")
|
||||
fireEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue