diff --git a/webview-ui/src/components/chat/AutoApproveKeyboardShortcuts.tsx b/webview-ui/src/components/chat/AutoApproveKeyboardShortcuts.tsx new file mode 100644 index 0000000000..8b6f8c2fc7 --- /dev/null +++ b/webview-ui/src/components/chat/AutoApproveKeyboardShortcuts.tsx @@ -0,0 +1,124 @@ +import { useEffect, useCallback, useMemo, useRef } from "react" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" +import { AutoApproveSetting } from "../settings/AutoApproveToggle" +import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles" +import { KEYBOARD_SHORTCUTS, DEFAULT_KEYBOARD_CONFIG } from "@src/constants/autoApproveConstants" + +export const AutoApproveKeyboardShortcuts = () => { + const { + setAlwaysAllowReadOnly, + setAlwaysAllowWrite, + setAlwaysAllowExecute, + setAlwaysAllowBrowser, + setAlwaysAllowMcp, + setAlwaysAllowModeSwitch, + setAlwaysAllowSubtasks, + setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, + alwaysApproveResubmit, + } = useExtensionState() + + const baseToggles = useAutoApprovalToggles() + const toggles = useMemo( + () => ({ + ...baseToggles, + alwaysApproveResubmit, + }), + [baseToggles, alwaysApproveResubmit], + ) + + const handleToggle = useCallback( + (key: AutoApproveSetting) => { + const currentValue = toggles[key] + const newValue = !currentValue + + // Send message to extension + vscode.postMessage({ type: key, bool: newValue }) + + // Update local state + switch (key) { + case "alwaysAllowReadOnly": + setAlwaysAllowReadOnly(newValue) + break + case "alwaysAllowWrite": + setAlwaysAllowWrite(newValue) + break + case "alwaysAllowExecute": + setAlwaysAllowExecute(newValue) + break + case "alwaysAllowBrowser": + setAlwaysAllowBrowser(newValue) + break + case "alwaysAllowMcp": + setAlwaysAllowMcp(newValue) + break + case "alwaysAllowModeSwitch": + setAlwaysAllowModeSwitch(newValue) + break + case "alwaysAllowSubtasks": + setAlwaysAllowSubtasks(newValue) + break + case "alwaysApproveResubmit": + setAlwaysApproveResubmit(newValue) + break + case "alwaysAllowFollowupQuestions": + setAlwaysAllowFollowupQuestions(newValue) + break + case "alwaysAllowUpdateTodoList": + setAlwaysAllowUpdateTodoList(newValue) + break + } + }, + [ + toggles, + setAlwaysAllowReadOnly, + setAlwaysAllowWrite, + setAlwaysAllowExecute, + setAlwaysAllowBrowser, + setAlwaysAllowMcp, + setAlwaysAllowModeSwitch, + setAlwaysAllowSubtasks, + setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, + ], + ) + + // Store the handleToggle function in a ref to avoid re-registrations + const handleToggleRef = useRef(handleToggle) + useEffect(() => { + handleToggleRef.current = handleToggle + }, [handleToggle]) + + // Stable event handler that uses the ref + const handleKeyDown = useCallback((event: KeyboardEvent) => { + // Check if keyboard shortcuts are enabled + if (!DEFAULT_KEYBOARD_CONFIG.enabled) { + return + } + + // Support both Alt key and Ctrl+Shift key combinations based on configuration + const isValidModifier = DEFAULT_KEYBOARD_CONFIG.useCtrlShiftKey + ? event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey + : event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey + + if (isValidModifier) { + const shortcut = KEYBOARD_SHORTCUTS[event.key] + if (shortcut) { + event.preventDefault() + handleToggleRef.current(shortcut) + } + } + }, []) + + useEffect(() => { + window.addEventListener("keydown", handleKeyDown) + return () => { + window.removeEventListener("keydown", handleKeyDown) + } + }, [handleKeyDown]) + + return null // This component doesn't render anything +} diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 8961fc7f5d..dcdeca032e 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 { 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,10 +20,9 @@ interface AutoApproveMenuProps { const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { const [isExpanded, setIsExpanded] = useState(false) + const portalContainer = useRooPortal("roo-portal") const { - autoApprovalEnabled, - setAutoApprovalEnabled, alwaysApproveResubmit, setAlwaysAllowReadOnly, setAlwaysAllowWrite, @@ -46,7 +49,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { [baseToggles, alwaysApproveResubmit], ) - const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled) + const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, true) const onAutoApproveToggle = useCallback( (key: AutoApproveSetting, value: boolean) => { @@ -85,30 +88,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowUpdateTodoList(value) break } - - // Check if we need to update the master auto-approval state - // Create a new toggles state with the updated value - const updatedToggles = { - ...toggles, - [key]: value, - } - - const willHaveEnabledOptions = Object.values(updatedToggles).some((v) => !!v) - - // If enabling the first option, enable master auto-approval - if (value && !hasEnabledOptions && willHaveEnabledOptions) { - setAutoApprovalEnabled(true) - vscode.postMessage({ type: "autoApprovalEnabled", bool: true }) - } - // If disabling the last option, disable master auto-approval - else if (!value && hasEnabledOptions && !willHaveEnabledOptions) { - setAutoApprovalEnabled(false) - vscode.postMessage({ type: "autoApprovalEnabled", bool: false }) - } }, [ toggles, - hasEnabledOptions, setAlwaysAllowReadOnly, setAlwaysAllowWrite, setAlwaysAllowExecute, @@ -119,14 +101,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysApproveResubmit, setAlwaysAllowFollowupQuestions, setAlwaysAllowUpdateTodoList, - setAutoApprovalEnabled, ], ) - 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 +123,97 @@ 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.dropdownTitle")} + {displayText} + + ) + return ( -
- {isExpanded && ( -
-
- , - }} - /> + + {trigger} + + +
+ {/* Header */} +
+

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

+
+ , + }} + /> +
- -
- )} + {/* Two-column layout for toggles */} +
+
+ +
+
-
-
e.stopPropagation()}> - - { - if (hasEnabledOptions) { - const newValue = !(autoApprovalEnabled ?? false) - setAutoApprovalEnabled(newValue) - vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue }) - } - // If no options enabled, do nothing - }} - /> - -
-
- - {t("chat:autoApprove.title")} - - - {displayText} - - + {/* Footer with buttons on left and title on right */} +
+
+ + +
+
-
-
+ + ) } diff --git a/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx b/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx new file mode 100644 index 0000000000..cb0909ac70 --- /dev/null +++ b/webview-ui/src/components/chat/AutoApproveToggleDropdown.tsx @@ -0,0 +1,86 @@ +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" +import { KEYBOARD_SHORTCUTS_DISPLAY, DEFAULT_KEYBOARD_CONFIG } from "@/constants/autoApproveConstants" + +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]) => { + // Get the appropriate keyboard shortcut display based on configuration + const shortcutDisplay = DEFAULT_KEYBOARD_CONFIG.useCtrlShiftKey + ? KEYBOARD_SHORTCUTS_DISPLAY[key].replace("Alt+", "Ctrl+Shift+") + : KEYBOARD_SHORTCUTS_DISPLAY[key] + + const tooltipContent = DEFAULT_KEYBOARD_CONFIG.enabled + ? `${t(descriptionKey || "")} (${shortcutDisplay})` + : t(descriptionKey || "") + return ( + + + + ) + } + + return ( + <> +
{leftColumn.map(renderToggleItem)}
+
{rightColumn.map(renderToggleItem)}
+ + ) +} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 5135eca2f2..17f4cfa178 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -32,6 +32,7 @@ import { SlashCommandsPopover } from "./SlashCommandsPopover" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" import { EditModeControls } from "./EditModeControls" +import AutoApproveMenu from "./AutoApproveMenu" interface ChatTextAreaProps { inputValue: string @@ -917,31 +918,58 @@ const ChatTextArea = forwardRef( // Helper function to render non-edit mode controls const renderNonEditModeControls = () => ( -
-
-
{renderModeSelector()}
- -
- +
+
+
+
{renderModeSelector()}
+ +
+ +
+ +
+ +
-
-
- {isTtsPlaying && ( - +
+ {isTtsPlaying && ( + + + + )} + + + - )} - - - - - +
) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 44eeb33b66..46aada42b4 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -50,7 +50,7 @@ import BrowserSessionRow from "./BrowserSessionRow" import ChatRow from "./ChatRow" import ChatTextArea from "./ChatTextArea" import TaskHeader from "./TaskHeader" -import AutoApproveMenu from "./AutoApproveMenu" +import { AutoApproveKeyboardShortcuts } from "./AutoApproveKeyboardShortcuts" import SystemPromptWarning from "./SystemPromptWarning" import ProfileViolationWarning from "./ProfileViolationWarning" import { CheckpointWarning } from "./CheckpointWarning" @@ -1793,6 +1793,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + {(showAnnouncement || showAnnouncementModal) && ( { @@ -1882,11 +1883,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction - -
- )} {task && ( <> @@ -1909,9 +1905,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction
-
- -
{areButtonsVisible && (
({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock ExtensionStateContext +vi.mock("@src/context/ExtensionStateContext") + +// Mock the constants to control keyboard config +vi.mock("@src/constants/autoApproveConstants", () => ({ + KEYBOARD_SHORTCUTS: { + "1": "alwaysAllowReadOnly", + "2": "alwaysAllowWrite", + "3": "alwaysAllowBrowser", + "4": "alwaysAllowExecute", + "5": "alwaysAllowMcp", + "6": "alwaysAllowModeSwitch", + "7": "alwaysAllowSubtasks", + "8": "alwaysAllowFollowupQuestions", + "9": "alwaysAllowUpdateTodoList", + "0": "alwaysApproveResubmit", + }, + KEYBOARD_SHORTCUTS_DISPLAY: { + alwaysAllowReadOnly: "Alt+1", + alwaysAllowWrite: "Alt+2", + alwaysAllowBrowser: "Alt+3", + alwaysAllowExecute: "Alt+4", + alwaysAllowMcp: "Alt+5", + alwaysAllowModeSwitch: "Alt+6", + alwaysAllowSubtasks: "Alt+7", + alwaysAllowFollowupQuestions: "Alt+8", + alwaysAllowUpdateTodoList: "Alt+9", + alwaysApproveResubmit: "Alt+0", + }, + DEFAULT_KEYBOARD_CONFIG: { + enabled: true, + useAltKey: true, + useCtrlShiftKey: false, + }, +})) + +// Get the mocked postMessage function +const mockPostMessage = vscode.postMessage as ReturnType + +describe("AutoApproveKeyboardShortcuts", () => { + const defaultExtensionState = { + autoApprovalEnabled: true, + alwaysAllowReadOnly: false, + alwaysAllowReadOnlyOutsideWorkspace: false, + alwaysAllowWrite: false, + alwaysAllowWriteOutsideWorkspace: false, + alwaysAllowExecute: false, + alwaysAllowBrowser: false, + alwaysAllowMcp: false, + alwaysAllowModeSwitch: false, + alwaysAllowSubtasks: false, + alwaysApproveResubmit: false, + alwaysAllowFollowupQuestions: false, + alwaysAllowUpdateTodoList: false, + writeDelayMs: 3000, + allowedMaxRequests: undefined, + setAutoApprovalEnabled: vi.fn(), + setAlwaysAllowReadOnly: vi.fn(), + setAlwaysAllowWrite: vi.fn(), + setAlwaysAllowExecute: vi.fn(), + setAlwaysAllowBrowser: vi.fn(), + setAlwaysAllowMcp: vi.fn(), + setAlwaysAllowModeSwitch: vi.fn(), + setAlwaysAllowSubtasks: vi.fn(), + setAlwaysApproveResubmit: vi.fn(), + setAlwaysAllowFollowupQuestions: vi.fn(), + setAlwaysAllowUpdateTodoList: vi.fn(), + setAllowedMaxRequests: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + ;(useExtensionState as ReturnType).mockReturnValue(defaultExtensionState) + }) + + describe("Keyboard shortcut handling", () => { + it("should toggle read-only with Alt+1", async () => { + const mockSetAlwaysAllowReadOnly = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly, + }) + + render() + + // Simulate Alt+1 keypress + fireEvent.keyDown(window, { key: "1", altKey: true }) + + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "alwaysAllowReadOnly", + bool: true, + }) + expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(true) + }) + }) + + it("should toggle write with Alt+2", async () => { + const mockSetAlwaysAllowWrite = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + setAlwaysAllowWrite: mockSetAlwaysAllowWrite, + }) + + render() + + // Simulate Alt+2 keypress + fireEvent.keyDown(window, { key: "2", altKey: true }) + + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "alwaysAllowWrite", + bool: true, + }) + expect(mockSetAlwaysAllowWrite).toHaveBeenCalledWith(true) + }) + }) + + it("should toggle resubmit with Alt+0", async () => { + const mockSetAlwaysApproveResubmit = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + setAlwaysApproveResubmit: mockSetAlwaysApproveResubmit, + }) + + render() + + // Simulate Alt+0 keypress + fireEvent.keyDown(window, { key: "0", altKey: true }) + + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "alwaysApproveResubmit", + bool: true, + }) + expect(mockSetAlwaysApproveResubmit).toHaveBeenCalledWith(true) + }) + }) + + it("should not trigger with Ctrl+1", async () => { + render() + + // Simulate Ctrl+1 keypress (should not trigger) + fireEvent.keyDown(window, { key: "1", ctrlKey: true }) + + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + }) + + it("should not trigger with Shift+1", async () => { + render() + + // Simulate Shift+1 keypress (should not trigger) + fireEvent.keyDown(window, { key: "1", shiftKey: true }) + + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + }) + + it("should not trigger with Meta+1", async () => { + render() + + // Simulate Meta+1 keypress (should not trigger) + fireEvent.keyDown(window, { key: "1", metaKey: true }) + + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + }) + + it("should not trigger with Alt+Ctrl+1", async () => { + render() + + // Simulate Alt+Ctrl+1 keypress (should not trigger) + fireEvent.keyDown(window, { key: "1", altKey: true, ctrlKey: true }) + + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + }) + + it("should toggle off when already enabled", async () => { + const mockSetAlwaysAllowReadOnly = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + alwaysAllowReadOnly: true, // Already enabled + setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly, + }) + + render() + + // Simulate Alt+1 keypress + fireEvent.keyDown(window, { key: "1", altKey: true }) + + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "alwaysAllowReadOnly", + bool: false, // Should toggle off + }) + expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(false) + }) + }) + }) + + describe("Configuration support", () => { + it("should not trigger when keyboard shortcuts are disabled", async () => { + // Mock the config to disable shortcuts + DEFAULT_KEYBOARD_CONFIG.enabled = false + + render() + + // Simulate Alt+1 keypress + fireEvent.keyDown(window, { key: "1", altKey: true }) + + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + + // Reset config + DEFAULT_KEYBOARD_CONFIG.enabled = true + }) + + it("should use Ctrl+Shift when configured", async () => { + // Mock the config to use Ctrl+Shift + DEFAULT_KEYBOARD_CONFIG.useAltKey = false + DEFAULT_KEYBOARD_CONFIG.useCtrlShiftKey = true + + const mockSetAlwaysAllowReadOnly = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly, + }) + + render() + + // Alt+1 should not work + fireEvent.keyDown(window, { key: "1", altKey: true }) + await waitFor(() => { + expect(mockPostMessage).not.toHaveBeenCalled() + }) + + // Ctrl+Shift+1 should work + fireEvent.keyDown(window, { key: "1", ctrlKey: true, shiftKey: true }) + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "alwaysAllowReadOnly", + bool: true, + }) + }) + + // Reset config + DEFAULT_KEYBOARD_CONFIG.useAltKey = true + DEFAULT_KEYBOARD_CONFIG.useCtrlShiftKey = false + }) + }) + + describe("Event listener cleanup", () => { + it("should clean up event listeners on unmount", () => { + const addEventListenerSpy = vi.spyOn(window, "addEventListener") + const removeEventListenerSpy = vi.spyOn(window, "removeEventListener") + + const { unmount } = render() + + // Check that event listener was added + expect(addEventListenerSpy).toHaveBeenCalledWith("keydown", expect.any(Function)) + + // Unmount the component + unmount() + + // Check that event listener was removed + expect(removeEventListenerSpy).toHaveBeenCalledWith("keydown", expect.any(Function)) + + addEventListenerSpy.mockRestore() + removeEventListenerSpy.mockRestore() + }) + }) +}) 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() diff --git a/webview-ui/src/constants/autoApproveConstants.ts b/webview-ui/src/constants/autoApproveConstants.ts new file mode 100644 index 0000000000..847ab8dba8 --- /dev/null +++ b/webview-ui/src/constants/autoApproveConstants.ts @@ -0,0 +1,55 @@ +import { AutoApproveSetting } from "../components/settings/AutoApproveToggle" + +/** + * Keyboard shortcuts mapping for auto-approve options + * Maps keyboard keys (1-9, 0) to their corresponding auto-approve settings + */ +export const KEYBOARD_SHORTCUTS: Record = { + "1": "alwaysAllowReadOnly", + "2": "alwaysAllowWrite", + "3": "alwaysAllowBrowser", + "4": "alwaysAllowExecute", + "5": "alwaysAllowMcp", + "6": "alwaysAllowModeSwitch", + "7": "alwaysAllowSubtasks", + "8": "alwaysAllowFollowupQuestions", + "9": "alwaysAllowUpdateTodoList", + "0": "alwaysApproveResubmit", +} + +/** + * Keyboard shortcuts display mapping + * Maps auto-approve settings to their display shortcut strings + */ +export const KEYBOARD_SHORTCUTS_DISPLAY: Record = { + alwaysAllowReadOnly: "Alt+1", + alwaysAllowWrite: "Alt+2", + alwaysAllowBrowser: "Alt+3", + alwaysAllowExecute: "Alt+4", + alwaysAllowMcp: "Alt+5", + alwaysAllowModeSwitch: "Alt+6", + alwaysAllowSubtasks: "Alt+7", + alwaysAllowFollowupQuestions: "Alt+8", + alwaysAllowUpdateTodoList: "Alt+9", + alwaysApproveResubmit: "Alt+0", +} + +/** + * Configuration for keyboard shortcuts + * Can be extended in the future to support user preferences + */ +export interface KeyboardShortcutConfig { + enabled: boolean + useAltKey: boolean + useCtrlShiftKey: boolean +} + +/** + * Default keyboard shortcut configuration + * In the future, this can be loaded from user settings + */ +export const DEFAULT_KEYBOARD_CONFIG: KeyboardShortcutConfig = { + enabled: true, + useAltKey: true, + useCtrlShiftKey: false, +} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index ff93fa6e08..0a364001e3 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -265,9 +265,10 @@ "issues": "It seems like you're having Windows PowerShell issues, please see this" }, "autoApprove": { - "title": "Auto-approve:", + "title": "Auto-Approve", + "dropdownTitle": "Auto:", "none": "None", - "description": "Auto-approve allows Roo Code to perform actions without asking for permission. Only enable for actions you fully trust. More detailed configuration available in Settings.", + "description": "Run these actions without asking for permission.Only enable for actions you fully trust. More in Settings.", "selectOptionsFirst": "Select at least one option below to enable auto-approval", "toggleAriaLabel": "Toggle auto-approval", "disabledAriaLabel": "Auto-approval disabled - select options first"