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"