diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 8961fc7f5d..b76601ea9e 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -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 = ( + + + {t("chat:autoApprove.title")} + {displayText} + + ) + return ( -
- {isExpanded && ( -
-
- , - }} - /> + + {trigger} + + +
+ {/* Header with master toggle */} +
+
+ + { + if (hasEnabledOptions) { + const newValue = !(autoApprovalEnabled ?? false) + setAutoApprovalEnabled(newValue) + vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue }) + } + }} + /> + +

{t("chat:autoApprove.title")}

+
+
+ + + + + + +
- -
- )} + {/* Description */} +
+
+ , + }} + /> +
+
-
-
e.stopPropagation()}> - - { - if (hasEnabledOptions) { - const newValue = !(autoApprovalEnabled ?? false) - setAutoApprovalEnabled(newValue) - vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue }) - } - // If no options enabled, do nothing - }} - /> - + {/* Two-column layout for toggles */} +
+
+ +
+
-
- - {t("chat:autoApprove.title")} - - - {displayText} - - -
-
-
+ + ) } diff --git a/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx b/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx new file mode 100644 index 0000000000..279194593a --- /dev/null +++ b/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx @@ -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]) => ( + + + + ) + + return ( + <> +
{leftColumn.map(renderToggleItem)}
+
{rightColumn.map(renderToggleItem)}
+ + ) +} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 44eeb33b66..3cd10138e8 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1882,11 +1882,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction - -
- )} {task && ( <> @@ -1909,9 +1904,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction
-
- -
{areButtonsVisible && (
+ {/* Auto-Approve Menu positioned below text area */} +
+ +
+ {isProfileDisabled && (
diff --git a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx index 185e5eeec6..0c185fec4d 100644 --- a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx @@ -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).mockReturnValue({ ...defaultExtensionState, autoApprovalEnabled: false, @@ -124,6 +124,15 @@ describe("AutoApproveMenu", () => { render() + // 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).mockReturnValue({ ...defaultExtensionState, autoApprovalEnabled: true, @@ -141,6 +150,15 @@ describe("AutoApproveMenu", () => { render() + // 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() - // 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() - // 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() - // 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() - // 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()