Răsfoiți Sursa

fix: Resolve confusing auto-approve checkbox states (#5602)

Co-authored-by: Daniel Riccio <[email protected]>
Hannes Rudolph 6 luni în urmă
părinte
comite
6cf376f832
44 a modificat fișierele cu 1414 adăugiri și 81 ștergeri
  1. 76 45
      webview-ui/src/components/chat/AutoApproveMenu.tsx
  2. 14 0
      webview-ui/src/components/chat/ChatView.tsx
  3. 307 0
      webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx
  4. 480 0
      webview-ui/src/components/chat/__tests__/ChatView.auto-approve-new.spec.tsx
  5. 33 1
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  6. 282 0
      webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts
  7. 29 0
      webview-ui/src/hooks/useAutoApprovalState.ts
  8. 50 0
      webview-ui/src/hooks/useAutoApprovalToggles.ts
  9. 4 1
      webview-ui/src/i18n/locales/ca/chat.json
  10. 4 1
      webview-ui/src/i18n/locales/ca/settings.json
  11. 4 1
      webview-ui/src/i18n/locales/de/chat.json
  12. 4 1
      webview-ui/src/i18n/locales/de/settings.json
  13. 4 1
      webview-ui/src/i18n/locales/en/chat.json
  14. 4 1
      webview-ui/src/i18n/locales/en/settings.json
  15. 4 1
      webview-ui/src/i18n/locales/es/chat.json
  16. 4 1
      webview-ui/src/i18n/locales/es/settings.json
  17. 4 1
      webview-ui/src/i18n/locales/fr/chat.json
  18. 3 0
      webview-ui/src/i18n/locales/fr/settings.json
  19. 4 1
      webview-ui/src/i18n/locales/hi/chat.json
  20. 4 1
      webview-ui/src/i18n/locales/hi/settings.json
  21. 4 1
      webview-ui/src/i18n/locales/id/chat.json
  22. 4 1
      webview-ui/src/i18n/locales/id/settings.json
  23. 4 1
      webview-ui/src/i18n/locales/it/chat.json
  24. 4 1
      webview-ui/src/i18n/locales/it/settings.json
  25. 4 1
      webview-ui/src/i18n/locales/ja/chat.json
  26. 4 1
      webview-ui/src/i18n/locales/ja/settings.json
  27. 4 1
      webview-ui/src/i18n/locales/ko/chat.json
  28. 4 1
      webview-ui/src/i18n/locales/ko/settings.json
  29. 4 1
      webview-ui/src/i18n/locales/nl/chat.json
  30. 4 1
      webview-ui/src/i18n/locales/nl/settings.json
  31. 4 1
      webview-ui/src/i18n/locales/pl/chat.json
  32. 4 1
      webview-ui/src/i18n/locales/pl/settings.json
  33. 4 1
      webview-ui/src/i18n/locales/pt-BR/chat.json
  34. 4 1
      webview-ui/src/i18n/locales/pt-BR/settings.json
  35. 4 1
      webview-ui/src/i18n/locales/ru/chat.json
  36. 4 1
      webview-ui/src/i18n/locales/ru/settings.json
  37. 4 1
      webview-ui/src/i18n/locales/tr/chat.json
  38. 4 1
      webview-ui/src/i18n/locales/tr/settings.json
  39. 4 1
      webview-ui/src/i18n/locales/vi/chat.json
  40. 4 1
      webview-ui/src/i18n/locales/vi/settings.json
  41. 4 1
      webview-ui/src/i18n/locales/zh-CN/chat.json
  42. 4 1
      webview-ui/src/i18n/locales/zh-CN/settings.json
  43. 4 1
      webview-ui/src/i18n/locales/zh-TW/chat.json
  44. 4 1
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 76 - 45
webview-ui/src/components/chat/AutoApproveMenu.tsx

@@ -6,6 +6,9 @@ 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 { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
+import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
 
 interface AutoApproveMenuProps {
 	style?: React.CSSProperties
@@ -17,16 +20,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 	const {
 		autoApprovalEnabled,
 		setAutoApprovalEnabled,
-		alwaysAllowReadOnly,
-		alwaysAllowWrite,
-		alwaysAllowExecute,
-		alwaysAllowBrowser,
-		alwaysAllowMcp,
-		alwaysAllowModeSwitch,
-		alwaysAllowSubtasks,
 		alwaysApproveResubmit,
-		alwaysAllowFollowupQuestions,
-		alwaysAllowUpdateTodoList,
 		allowedMaxRequests,
 		setAlwaysAllowReadOnly,
 		setAlwaysAllowWrite,
@@ -43,10 +37,24 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 
 	const { t } = useAppTranslation()
 
+	const baseToggles = useAutoApprovalToggles()
+
+	// AutoApproveMenu needs alwaysApproveResubmit in addition to the base toggles
+	const toggles = useMemo(
+		() => ({
+			...baseToggles,
+			alwaysApproveResubmit: alwaysApproveResubmit,
+		}),
+		[baseToggles, alwaysApproveResubmit],
+	)
+
+	const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
+
 	const onAutoApproveToggle = useCallback(
 		(key: AutoApproveSetting, value: boolean) => {
 			vscode.postMessage({ type: key, bool: value })
 
+			// Update the specific toggle state
 			switch (key) {
 				case "alwaysAllowReadOnly":
 					setAlwaysAllowReadOnly(value)
@@ -79,8 +87,30 @@ 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,
@@ -91,43 +121,32 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 			setAlwaysApproveResubmit,
 			setAlwaysAllowFollowupQuestions,
 			setAlwaysAllowUpdateTodoList,
+			setAutoApprovalEnabled,
 		],
 	)
 
-	const toggleExpanded = useCallback(() => setIsExpanded((prev) => !prev), [])
+	const toggleExpanded = useCallback(() => {
+		setIsExpanded((prev) => !prev)
+	}, [])
 
-	const toggles = useMemo(
-		() => ({
-			alwaysAllowReadOnly: alwaysAllowReadOnly,
-			alwaysAllowWrite: alwaysAllowWrite,
-			alwaysAllowExecute: alwaysAllowExecute,
-			alwaysAllowBrowser: alwaysAllowBrowser,
-			alwaysAllowMcp: alwaysAllowMcp,
-			alwaysAllowModeSwitch: alwaysAllowModeSwitch,
-			alwaysAllowSubtasks: alwaysAllowSubtasks,
-			alwaysApproveResubmit: alwaysApproveResubmit,
-			alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions,
-			alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList,
-		}),
-		[
-			alwaysAllowReadOnly,
-			alwaysAllowWrite,
-			alwaysAllowExecute,
-			alwaysAllowBrowser,
-			alwaysAllowMcp,
-			alwaysAllowModeSwitch,
-			alwaysAllowSubtasks,
-			alwaysApproveResubmit,
-			alwaysAllowFollowupQuestions,
-			alwaysAllowUpdateTodoList,
-		],
-	)
+	// Disable main checkbox while menu is open or no options selected
+	const isCheckboxDisabled = useMemo(() => {
+		return !hasEnabledOptions || isExpanded
+	}, [hasEnabledOptions, isExpanded])
 
 	const enabledActionsList = Object.entries(toggles)
 		.filter(([_key, value]) => !!value)
 		.map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey))
 		.join(", ")
 
+	// Update displayed text logic
+	const displayText = useMemo(() => {
+		if (!effectiveAutoApprovalEnabled || !hasEnabledOptions) {
+			return t("chat:autoApprove.none")
+		}
+		return enabledActionsList || t("chat:autoApprove.none")
+	}, [effectiveAutoApprovalEnabled, hasEnabledOptions, enabledActionsList, t])
+
 	const handleOpenSettings = useCallback(
 		() =>
 			window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }),
@@ -155,14 +174,26 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 				}}
 				onClick={toggleExpanded}>
 				<div onClick={(e) => e.stopPropagation()}>
-					<VSCodeCheckbox
-						checked={autoApprovalEnabled ?? false}
-						onChange={() => {
-							const newValue = !(autoApprovalEnabled ?? false)
-							setAutoApprovalEnabled(newValue)
-							vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
-						}}
-					/>
+					<StandardTooltip
+						content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
+						<VSCodeCheckbox
+							checked={effectiveAutoApprovalEnabled}
+							disabled={isCheckboxDisabled}
+							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>
 				</div>
 				<div
 					style={{
@@ -188,7 +219,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 							flex: 1,
 							minWidth: 0,
 						}}>
-						{enabledActionsList || t("chat:autoApprove.none")}
+						{displayText}
 					</span>
 					<span
 						className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}

+ 14 - 0
webview-ui/src/components/chat/ChatView.tsx

@@ -38,6 +38,8 @@ import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
 import RooHero from "@src/components/welcome/RooHero"
 import RooTips from "@src/components/welcome/RooTips"
 import { StandardTooltip } from "@src/components/ui"
+import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
+import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
 
 import TelemetryBanner from "../common/TelemetryBanner"
 import VersionIndicator from "../common/VersionIndicator"
@@ -959,12 +961,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		[deniedCommands],
 	)
 
+	// Create toggles object for useAutoApprovalState hook
+	const autoApprovalToggles = useAutoApprovalToggles()
+
+	const { hasEnabledOptions } = useAutoApprovalState(autoApprovalToggles, autoApprovalEnabled)
+
 	const isAutoApproved = useCallback(
 		(message: ClineMessage | undefined) => {
+			// First check if auto-approval is enabled AND we have at least one permission
 			if (!autoApprovalEnabled || !message || message.type !== "ask") {
 				return false
 			}
 
+			// Use the hook's result instead of duplicating the logic
+			if (!hasEnabledOptions) {
+				return false
+			}
+
 			if (message.ask === "followup") {
 				return alwaysAllowFollowupQuestions
 			}
@@ -1038,6 +1051,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		},
 		[
 			autoApprovalEnabled,
+			hasEnabledOptions,
 			alwaysAllowBrowser,
 			alwaysAllowReadOnly,
 			alwaysAllowReadOnlyOutsideWorkspace,

+ 307 - 0
webview-ui/src/components/chat/__tests__/AutoApproveMenu.spec.tsx

@@ -0,0 +1,307 @@
+import { render, fireEvent, screen, waitFor } from "@/utils/test-utils"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+import { vscode } from "@src/utils/vscode"
+import AutoApproveMenu from "../AutoApproveMenu"
+
+// Mock vscode API
+vi.mock("@src/utils/vscode", () => ({
+	vscode: {
+		postMessage: vi.fn(),
+	},
+}))
+
+// Mock ExtensionStateContext
+vi.mock("@src/context/ExtensionStateContext")
+
+// Mock translation hook
+vi.mock("@src/i18n/TranslationContext", () => ({
+	useAppTranslation: () => ({
+		t: (key: string) => {
+			const translations: Record<string, string> = {
+				"chat:autoApprove.title": "Auto-approve",
+				"chat:autoApprove.none": "None selected",
+				"chat:autoApprove.selectOptionsFirst": "Select at least one option below to enable auto-approval",
+				"chat:autoApprove.description": "Configure auto-approval settings",
+				"settings:autoApprove.readOnly.label": "Read-only operations",
+				"settings:autoApprove.write.label": "Write operations",
+				"settings:autoApprove.execute.label": "Execute operations",
+				"settings:autoApprove.browser.label": "Browser operations",
+				"settings:autoApprove.modeSwitch.label": "Mode switches",
+				"settings:autoApprove.mcp.label": "MCP operations",
+				"settings:autoApprove.subtasks.label": "Subtasks",
+				"settings:autoApprove.resubmit.label": "Resubmit",
+				"settings:autoApprove.followupQuestions.label": "Follow-up questions",
+				"settings:autoApprove.updateTodoList.label": "Update todo list",
+				"settings:autoApprove.apiRequestLimit.title": "API request limit",
+				"settings:autoApprove.apiRequestLimit.unlimited": "Unlimited",
+				"settings:autoApprove.apiRequestLimit.description": "Limit the number of API requests",
+				"settings:autoApprove.readOnly.outsideWorkspace": "Also allow outside workspace",
+				"settings:autoApprove.write.outsideWorkspace": "Also allow outside workspace",
+				"settings:autoApprove.write.delay": "Delay",
+			}
+			return translations[key] || key
+		},
+	}),
+}))
+
+// Get the mocked postMessage function
+const mockPostMessage = vscode.postMessage as ReturnType<typeof vi.fn>
+
+describe("AutoApproveMenu", () => {
+	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<typeof vi.fn>).mockReturnValue(defaultExtensionState)
+	})
+
+	describe("Master checkbox behavior", () => {
+		it("should show 'None selected' when no sub-options are selected", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: false,
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowModeSwitch: false,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Check that the text shows "None selected"
+			expect(screen.getByText("None selected")).toBeInTheDocument()
+		})
+
+		it("should show enabled options when sub-options are selected", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Check that the text shows the enabled option
+			expect(screen.getByText("Read-only operations")).toBeInTheDocument()
+		})
+
+		it("should not allow toggling master checkbox when no options are selected", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: false,
+				alwaysAllowReadOnly: false,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Click on the master checkbox
+			const masterCheckbox = screen.getByRole("checkbox")
+			fireEvent.click(masterCheckbox)
+
+			// Should not send any message since no options are selected
+			expect(mockPostMessage).not.toHaveBeenCalled()
+		})
+
+		it("should toggle master checkbox when options are selected", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Click on the master checkbox
+			const masterCheckbox = screen.getByRole("checkbox")
+			fireEvent.click(masterCheckbox)
+
+			// Should toggle the master checkbox
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "autoApprovalEnabled",
+				bool: false,
+			})
+		})
+	})
+
+	describe("Sub-option toggles", () => {
+		it("should toggle read-only operations", async () => {
+			const mockSetAlwaysAllowReadOnly = vi.fn()
+
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Expand the menu
+			const menuContainer = screen.getByText("Auto-approve").parentElement
+			fireEvent.click(menuContainer!)
+
+			// Wait for the menu to expand and find the read-only button
+			await waitFor(() => {
+				expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
+			})
+
+			const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
+			fireEvent.click(readOnlyButton)
+
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "alwaysAllowReadOnly",
+				bool: true,
+			})
+		})
+
+		it("should toggle write operations", async () => {
+			const mockSetAlwaysAllowWrite = vi.fn()
+
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Expand the menu
+			const menuContainer = screen.getByText("Auto-approve").parentElement
+			fireEvent.click(menuContainer!)
+
+			await waitFor(() => {
+				expect(screen.getByTestId("always-allow-write-toggle")).toBeInTheDocument()
+			})
+
+			const writeButton = screen.getByTestId("always-allow-write-toggle")
+			fireEvent.click(writeButton)
+
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "alwaysAllowWrite",
+				bool: true,
+			})
+		})
+	})
+
+	describe("Complex scenarios", () => {
+		it("should display multiple enabled options in summary text", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Should show all enabled options in the summary
+			expect(screen.getByText("Read-only operations, Write operations, Execute operations")).toBeInTheDocument()
+		})
+
+		it("should handle enabling first option when none selected", async () => {
+			const mockSetAutoApprovalEnabled = vi.fn()
+			const mockSetAlwaysAllowReadOnly = vi.fn()
+
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: false,
+				alwaysAllowReadOnly: false,
+				setAutoApprovalEnabled: mockSetAutoApprovalEnabled,
+				setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Expand the menu
+			const menuContainer = screen.getByText("Auto-approve").parentElement
+			fireEvent.click(menuContainer!)
+
+			await waitFor(() => {
+				expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
+			})
+
+			// Enable read-only
+			const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
+			fireEvent.click(readOnlyButton)
+
+			// Should enable the sub-option
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "alwaysAllowReadOnly",
+				bool: true,
+			})
+
+			// Should also enable master auto-approval
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "autoApprovalEnabled",
+				bool: true,
+			})
+		})
+
+		it("should handle disabling last option", async () => {
+			const mockSetAutoApprovalEnabled = vi.fn()
+			const mockSetAlwaysAllowReadOnly = vi.fn()
+
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				...defaultExtensionState,
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				setAutoApprovalEnabled: mockSetAutoApprovalEnabled,
+				setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
+			})
+
+			render(<AutoApproveMenu />)
+
+			// Expand the menu
+			const menuContainer = screen.getByText("Auto-approve").parentElement
+			fireEvent.click(menuContainer!)
+
+			await waitFor(() => {
+				expect(screen.getByTestId("always-allow-readonly-toggle")).toBeInTheDocument()
+			})
+
+			// Disable read-only (the last enabled option)
+			const readOnlyButton = screen.getByTestId("always-allow-readonly-toggle")
+			fireEvent.click(readOnlyButton)
+
+			// Should disable the sub-option
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "alwaysAllowReadOnly",
+				bool: false,
+			})
+
+			// Should also disable master auto-approval
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "autoApprovalEnabled",
+				bool: false,
+			})
+		})
+	})
+})

+ 480 - 0
webview-ui/src/components/chat/__tests__/ChatView.auto-approve-new.spec.tsx

@@ -0,0 +1,480 @@
+// npx vitest run src/components/chat/__tests__/ChatView.auto-approve-new.spec.tsx
+
+import { render, waitFor } from "@/utils/test-utils"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
+import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
+import { vscode } from "@src/utils/vscode"
+
+import ChatView, { ChatViewProps } from "../ChatView"
+
+// Mock vscode API
+vi.mock("@src/utils/vscode", () => ({
+	vscode: {
+		postMessage: vi.fn(),
+	},
+}))
+
+// Mock all problematic dependencies
+vi.mock("rehype-highlight", () => ({
+	default: () => () => {},
+}))
+
+vi.mock("hast-util-to-text", () => ({
+	default: () => "",
+}))
+
+// Mock components that use ESM dependencies
+vi.mock("../BrowserSessionRow", () => ({
+	default: function MockBrowserSessionRow({ messages }: { messages: any[] }) {
+		return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
+	},
+}))
+
+vi.mock("../ChatRow", () => ({
+	default: function MockChatRow({ message }: { message: any }) {
+		return <div data-testid="chat-row">{JSON.stringify(message)}</div>
+	},
+}))
+
+vi.mock("../TaskHeader", () => ({
+	default: function MockTaskHeader({ task }: { task: any }) {
+		return <div data-testid="task-header">{JSON.stringify(task)}</div>
+	},
+}))
+
+vi.mock("../AutoApproveMenu", () => ({
+	default: () => null,
+}))
+
+vi.mock("@src/components/common/CodeBlock", () => ({
+	default: () => null,
+	CODE_BLOCK_BG_COLOR: "rgb(30, 30, 30)",
+}))
+
+vi.mock("@src/components/common/CodeAccordion", () => ({
+	default: () => null,
+}))
+
+vi.mock("@src/components/chat/ContextMenu", () => ({
+	default: () => null,
+}))
+
+// Mock window.postMessage to trigger state hydration
+const mockPostMessage = (state: any) => {
+	window.postMessage(
+		{
+			type: "state",
+			state: {
+				version: "1.0.0",
+				clineMessages: [],
+				taskHistory: [],
+				shouldShowAnnouncement: false,
+				allowedCommands: [],
+				alwaysAllowExecute: false,
+				autoApprovalEnabled: true,
+				...state,
+			},
+		},
+		"*",
+	)
+}
+
+const queryClient = new QueryClient()
+
+const defaultProps: ChatViewProps = {
+	isHidden: false,
+	showAnnouncement: false,
+	hideAnnouncement: () => {},
+}
+
+const renderChatView = (props: Partial<ChatViewProps> = {}) => {
+	return render(
+		<ExtensionStateContextProvider>
+			<QueryClientProvider client={queryClient}>
+				<ChatView {...defaultProps} {...props} />
+			</QueryClientProvider>
+		</ExtensionStateContextProvider>,
+	)
+}
+
+describe("ChatView - New Auto Approval Logic Tests", () => {
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("Master auto-approval with no sub-options enabled", () => {
+		it("should NOT auto-approve when autoApprovalEnabled is true but no sub-options are enabled", async () => {
+			renderChatView()
+
+			// First hydrate state with initial task
+			mockPostMessage({
+				autoApprovalEnabled: true, // Master is enabled
+				alwaysAllowReadOnly: false, // But no sub-options are enabled
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowModeSwitch: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then send a read tool ask message
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowModeSwitch: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait and verify no auto-approval message was sent
+			await new Promise((resolve) => setTimeout(resolve, 100))
+			expect(vscode.postMessage).not.toHaveBeenCalledWith({
+				type: "askResponse",
+				askResponse: "yesButtonClicked",
+			})
+		})
+
+		it("should NOT auto-approve write operations when only master is enabled", async () => {
+			renderChatView()
+
+			// First hydrate state with initial task
+			mockPostMessage({
+				autoApprovalEnabled: true, // Master is enabled
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false, // Write is not enabled
+				writeDelayMs: 0,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then send a write tool ask message
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+				writeDelayMs: 0,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait and verify no auto-approval message was sent
+			await new Promise((resolve) => setTimeout(resolve, 100))
+			expect(vscode.postMessage).not.toHaveBeenCalledWith({
+				type: "askResponse",
+				askResponse: "yesButtonClicked",
+			})
+		})
+
+		it("should NOT auto-approve browser actions when only master is enabled", async () => {
+			renderChatView()
+
+			// First hydrate state with initial task
+			mockPostMessage({
+				autoApprovalEnabled: true, // Master is enabled
+				alwaysAllowBrowser: false, // Browser is not enabled
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then send a browser action ask message
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowBrowser: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "browser_action_launch",
+						ts: Date.now(),
+						text: JSON.stringify({ action: "launch", url: "http://example.com" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait and verify no auto-approval message was sent
+			await new Promise((resolve) => setTimeout(resolve, 100))
+			expect(vscode.postMessage).not.toHaveBeenCalledWith({
+				type: "askResponse",
+				askResponse: "yesButtonClicked",
+			})
+		})
+	})
+
+	describe("Correct auto-approval with sub-options enabled", () => {
+		it("should auto-approve when master and at least one sub-option are enabled", async () => {
+			renderChatView()
+
+			// First hydrate state with initial task
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true, // At least one sub-option is enabled
+				alwaysAllowWrite: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then send a read tool ask message
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait for the auto-approval message
+			await waitFor(() => {
+				expect(vscode.postMessage).toHaveBeenCalledWith({
+					type: "askResponse",
+					askResponse: "yesButtonClicked",
+				})
+			})
+		})
+
+		it("should auto-approve when multiple sub-options are enabled", async () => {
+			renderChatView()
+
+			// First hydrate state with initial task
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true, // Multiple sub-options enabled
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+				writeDelayMs: 0,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then send a write tool ask message
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+				writeDelayMs: 0,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait for the auto-approval message
+			await waitFor(() => {
+				expect(vscode.postMessage).toHaveBeenCalledWith({
+					type: "askResponse",
+					askResponse: "yesButtonClicked",
+				})
+			})
+		})
+	})
+
+	describe("Edge cases", () => {
+		it("should handle state transitions correctly", async () => {
+			renderChatView()
+
+			// Start with auto-approval properly configured
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: true,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Then transition to a state where no sub-options are enabled
+			mockPostMessage({
+				autoApprovalEnabled: true, // Master still true
+				alwaysAllowReadOnly: false, // All sub-options now false
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowModeSwitch: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
+						partial: false,
+					},
+				],
+			})
+
+			// Wait and verify no auto-approval message was sent
+			await new Promise((resolve) => setTimeout(resolve, 100))
+			expect(vscode.postMessage).not.toHaveBeenCalledWith({
+				type: "askResponse",
+				askResponse: "yesButtonClicked",
+			})
+		})
+
+		it("should respect the hasEnabledOptions check in isAutoApproved", async () => {
+			renderChatView()
+
+			// Configure state where master is true but effective approval should be false
+			mockPostMessage({
+				autoApprovalEnabled: true,
+				alwaysAllowReadOnly: false,
+				alwaysAllowReadOnlyOutsideWorkspace: false,
+				alwaysAllowWrite: false,
+				alwaysAllowWriteOutsideWorkspace: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowModeSwitch: false,
+				clineMessages: [
+					{
+						type: "say",
+						say: "task",
+						ts: Date.now() - 2000,
+						text: "Initial task",
+					},
+				],
+			})
+
+			// Try various tool types - none should auto-approve
+			const toolRequests = [
+				{ tool: "readFile", path: "test.txt" },
+				{ tool: "editedExistingFile", path: "test.txt" },
+				{ tool: "executeCommand", command: "ls" },
+				{ tool: "switchMode", mode: "architect" },
+			]
+
+			for (const toolRequest of toolRequests) {
+				vi.clearAllMocks()
+
+				mockPostMessage({
+					autoApprovalEnabled: true,
+					alwaysAllowReadOnly: false,
+					alwaysAllowWrite: false,
+					alwaysAllowExecute: false,
+					alwaysAllowBrowser: false,
+					alwaysAllowModeSwitch: false,
+					clineMessages: [
+						{
+							type: "say",
+							say: "task",
+							ts: Date.now() - 2000,
+							text: "Initial task",
+						},
+						{
+							type: "ask",
+							ask: "tool",
+							ts: Date.now(),
+							text: JSON.stringify(toolRequest),
+							partial: false,
+						},
+					],
+				})
+
+				// Wait and verify no auto-approval for any tool type
+				await new Promise((resolve) => setTimeout(resolve, 100))
+				expect(vscode.postMessage).not.toHaveBeenCalledWith({
+					type: "askResponse",
+					askResponse: "yesButtonClicked",
+				})
+			}
+		})
+	})
+})

+ 33 - 1
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -4,12 +4,15 @@ import { X } from "lucide-react"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
 import { vscode } from "@/utils/vscode"
-import { Button, Input, Slider } from "@/components/ui"
+import { Button, Input, Slider, StandardTooltip } from "@/components/ui"
 
 import { SetCachedStateField } from "./types"
 import { SectionHeader } from "./SectionHeader"
 import { Section } from "./Section"
 import { AutoApproveToggle } from "./AutoApproveToggle"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+import { useAutoApprovalState } from "@/hooks/useAutoApprovalState"
+import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles"
 
 type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	alwaysAllowReadOnly?: boolean
@@ -77,6 +80,11 @@ export const AutoApproveSettings = ({
 	const { t } = useAppTranslation()
 	const [commandInput, setCommandInput] = useState("")
 	const [deniedCommandInput, setDeniedCommandInput] = useState("")
+	const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState()
+
+	const toggles = useAutoApprovalToggles()
+
+	const { hasEnabledOptions, effectiveAutoApprovalEnabled } = useAutoApprovalState(toggles, autoApprovalEnabled)
 
 	const handleAddCommand = () => {
 		const currentCommands = allowedCommands ?? []
@@ -104,6 +112,30 @@ export const AutoApproveSettings = ({
 		<div {...props}>
 			<SectionHeader description={t("settings:autoApprove.description")}>
 				<div className="flex items-center gap-2">
+					{!hasEnabledOptions ? (
+						<StandardTooltip content={t("settings:autoApprove.selectOptionsFirst")}>
+							<VSCodeCheckbox
+								checked={effectiveAutoApprovalEnabled}
+								disabled={!hasEnabledOptions}
+								aria-label={t("settings:autoApprove.disabledAriaLabel")}
+								onChange={() => {
+									// Do nothing when no options are enabled
+									return
+								}}
+							/>
+						</StandardTooltip>
+					) : (
+						<VSCodeCheckbox
+							checked={effectiveAutoApprovalEnabled}
+							disabled={!hasEnabledOptions}
+							aria-label={t("settings:autoApprove.toggleAriaLabel")}
+							onChange={() => {
+								const newValue = !(autoApprovalEnabled ?? false)
+								setAutoApprovalEnabled(newValue)
+								vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
+							}}
+						/>
+					)}
 					<span className="codicon codicon-check w-4" />
 					<div>{t("settings:sections.autoApprove")}</div>
 				</div>

+ 282 - 0
webview-ui/src/hooks/__tests__/useAutoApprovalState.spec.ts

@@ -0,0 +1,282 @@
+import { renderHook } from "@testing-library/react"
+import { useAutoApprovalState } from "../useAutoApprovalState"
+
+describe("useAutoApprovalState", () => {
+	describe("hasEnabledOptions", () => {
+		it("should return false when all toggles are false", () => {
+			const toggles = {
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowMcp: false,
+				alwaysAllowModeSwitch: false,
+				alwaysAllowSubtasks: false,
+				alwaysApproveResubmit: false,
+				alwaysAllowFollowupQuestions: false,
+				alwaysAllowUpdateTodoList: false,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(false)
+		})
+
+		it("should return false when all toggles are undefined", () => {
+			const toggles = {
+				alwaysAllowReadOnly: undefined,
+				alwaysAllowWrite: undefined,
+				alwaysAllowExecute: undefined,
+				alwaysAllowBrowser: undefined,
+				alwaysAllowMcp: undefined,
+				alwaysAllowModeSwitch: undefined,
+				alwaysAllowSubtasks: undefined,
+				alwaysApproveResubmit: undefined,
+				alwaysAllowFollowupQuestions: undefined,
+				alwaysAllowUpdateTodoList: undefined,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(false)
+		})
+
+		it("should return true when at least one toggle is true", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowMcp: false,
+				alwaysAllowModeSwitch: false,
+				alwaysAllowSubtasks: false,
+				alwaysApproveResubmit: false,
+				alwaysAllowFollowupQuestions: false,
+				alwaysAllowUpdateTodoList: false,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(true)
+		})
+
+		it("should return true when multiple toggles are true", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+				alwaysAllowBrowser: false,
+				alwaysAllowMcp: false,
+				alwaysAllowModeSwitch: false,
+				alwaysAllowSubtasks: false,
+				alwaysApproveResubmit: false,
+				alwaysAllowFollowupQuestions: false,
+				alwaysAllowUpdateTodoList: false,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(true)
+		})
+
+		it("should return true when all toggles are true", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+				alwaysAllowBrowser: true,
+				alwaysAllowMcp: true,
+				alwaysAllowModeSwitch: true,
+				alwaysAllowSubtasks: true,
+				alwaysApproveResubmit: true,
+				alwaysAllowFollowupQuestions: true,
+				alwaysAllowUpdateTodoList: true,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(true)
+		})
+	})
+
+	describe("effectiveAutoApprovalEnabled", () => {
+		it("should return false when autoApprovalEnabled is false regardless of toggles", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, false))
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+
+		it("should return false when autoApprovalEnabled is undefined regardless of toggles", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: true,
+				alwaysAllowExecute: true,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, undefined))
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+
+		it("should return false when autoApprovalEnabled is true but no toggles are enabled", () => {
+			const toggles = {
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+				alwaysAllowBrowser: false,
+				alwaysAllowMcp: false,
+				alwaysAllowModeSwitch: false,
+				alwaysAllowSubtasks: false,
+				alwaysApproveResubmit: false,
+				alwaysAllowFollowupQuestions: false,
+				alwaysAllowUpdateTodoList: false,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+
+		it("should return true when autoApprovalEnabled is true and at least one toggle is enabled", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+				alwaysAllowExecute: false,
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
+		})
+	})
+
+	describe("memoization", () => {
+		it("should not recompute hasEnabledOptions when toggles object reference changes but values are the same", () => {
+			const initialToggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+			}
+
+			const { result, rerender } = renderHook(
+				({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
+				{
+					initialProps: {
+						toggles: initialToggles,
+						autoApprovalEnabled: true,
+					},
+				},
+			)
+
+			const firstHasEnabledOptions = result.current.hasEnabledOptions
+			const firstEffectiveAutoApprovalEnabled = result.current.effectiveAutoApprovalEnabled
+
+			// Create new object with same values
+			const newToggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+			}
+
+			rerender({ toggles: newToggles, autoApprovalEnabled: true })
+
+			// The computed values should be the same due to memoization
+			expect(result.current.hasEnabledOptions).toBe(firstHasEnabledOptions)
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(firstEffectiveAutoApprovalEnabled)
+		})
+
+		it("should recompute when toggle values change", () => {
+			const initialToggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+			}
+
+			const { result, rerender } = renderHook(
+				({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
+				{
+					initialProps: {
+						toggles: initialToggles,
+						autoApprovalEnabled: true,
+					},
+				},
+			)
+
+			expect(result.current.hasEnabledOptions).toBe(true)
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
+
+			// Change toggle values
+			const newToggles = {
+				alwaysAllowReadOnly: false,
+				alwaysAllowWrite: false,
+			}
+
+			rerender({ toggles: newToggles, autoApprovalEnabled: true })
+
+			expect(result.current.hasEnabledOptions).toBe(false)
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+
+		it("should recompute effectiveAutoApprovalEnabled when autoApprovalEnabled changes", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				alwaysAllowWrite: false,
+			}
+
+			const { result, rerender } = renderHook(
+				({ toggles, autoApprovalEnabled }) => useAutoApprovalState(toggles, autoApprovalEnabled),
+				{
+					initialProps: {
+						toggles,
+						autoApprovalEnabled: true,
+					},
+				},
+			)
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
+
+			rerender({ toggles, autoApprovalEnabled: false })
+
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+	})
+
+	describe("edge cases", () => {
+		it("should handle partial toggle objects", () => {
+			const toggles = {
+				alwaysAllowReadOnly: true,
+				// Other properties are optional
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(true)
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(true)
+		})
+
+		it("should handle empty toggle object", () => {
+			const toggles = {}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(false)
+			expect(result.current.effectiveAutoApprovalEnabled).toBe(false)
+		})
+
+		it("should handle mixed truthy/falsy values correctly", () => {
+			const toggles = {
+				alwaysAllowReadOnly: 1 as any, // truthy non-boolean
+				alwaysAllowWrite: "" as any, // falsy non-boolean
+				alwaysAllowExecute: null as any, // falsy non-boolean
+				alwaysAllowBrowser: "yes" as any, // truthy non-boolean
+			}
+
+			const { result } = renderHook(() => useAutoApprovalState(toggles, true))
+
+			expect(result.current.hasEnabledOptions).toBe(true) // Because some values are truthy
+		})
+	})
+})

+ 29 - 0
webview-ui/src/hooks/useAutoApprovalState.ts

@@ -0,0 +1,29 @@
+import { useMemo } from "react"
+
+interface AutoApprovalToggles {
+	alwaysAllowReadOnly?: boolean
+	alwaysAllowWrite?: boolean
+	alwaysAllowExecute?: boolean
+	alwaysAllowBrowser?: boolean
+	alwaysAllowMcp?: boolean
+	alwaysAllowModeSwitch?: boolean
+	alwaysAllowSubtasks?: boolean
+	alwaysApproveResubmit?: boolean
+	alwaysAllowFollowupQuestions?: boolean
+	alwaysAllowUpdateTodoList?: boolean
+}
+
+export function useAutoApprovalState(toggles: AutoApprovalToggles, autoApprovalEnabled?: boolean) {
+	const hasEnabledOptions = useMemo(() => {
+		return Object.values(toggles).some((value) => !!value)
+	}, [toggles])
+
+	const effectiveAutoApprovalEnabled = useMemo(() => {
+		return hasEnabledOptions && (autoApprovalEnabled ?? false)
+	}, [hasEnabledOptions, autoApprovalEnabled])
+
+	return {
+		hasEnabledOptions,
+		effectiveAutoApprovalEnabled,
+	}
+}

+ 50 - 0
webview-ui/src/hooks/useAutoApprovalToggles.ts

@@ -0,0 +1,50 @@
+import { useMemo } from "react"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+
+/**
+ * Custom hook that creates and returns the auto-approval toggles object
+ * This encapsulates the logic for creating the toggles object from extension state
+ */
+export function useAutoApprovalToggles() {
+	const {
+		alwaysAllowReadOnly,
+		alwaysAllowWrite,
+		alwaysAllowExecute,
+		alwaysAllowBrowser,
+		alwaysAllowMcp,
+		alwaysAllowModeSwitch,
+		alwaysAllowSubtasks,
+		alwaysApproveResubmit,
+		alwaysAllowFollowupQuestions,
+		alwaysAllowUpdateTodoList,
+	} = useExtensionState()
+
+	const toggles = useMemo(
+		() => ({
+			alwaysAllowReadOnly,
+			alwaysAllowWrite,
+			alwaysAllowExecute,
+			alwaysAllowBrowser,
+			alwaysAllowMcp,
+			alwaysAllowModeSwitch,
+			alwaysAllowSubtasks,
+			alwaysApproveResubmit,
+			alwaysAllowFollowupQuestions,
+			alwaysAllowUpdateTodoList,
+		}),
+		[
+			alwaysAllowReadOnly,
+			alwaysAllowWrite,
+			alwaysAllowExecute,
+			alwaysAllowBrowser,
+			alwaysAllowMcp,
+			alwaysAllowModeSwitch,
+			alwaysAllowSubtasks,
+			alwaysApproveResubmit,
+			alwaysAllowFollowupQuestions,
+			alwaysAllowUpdateTodoList,
+		],
+	)
+
+	return toggles
+}

+ 4 - 1
webview-ui/src/i18n/locales/ca/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Aprovació automàtica:",
 		"none": "Cap",
-		"description": "L'aprovació automàtica permet a Roo Code realitzar accions sense demanar permís. Activa-la només per a accions en les que confies plenament. Configuració més detallada disponible a la <settingsLink>Configuració</settingsLink>."
+		"description": "L'aprovació automàtica permet a Roo Code realitzar accions sense demanar permís. Activa-la només per a accions en les que confies plenament. Configuració més detallada disponible a la <settingsLink>Configuració</settingsLink>.",
+		"selectOptionsFirst": "Selecciona almenys una opció a continuació per activar l'aprovació automàtica",
+		"toggleAriaLabel": "Commuta l'aprovació automàtica",
+		"disabledAriaLabel": "Aprovació automàtica desactivada: seleccioneu primer les opcions"
 	},
 	"reasoning": {
 		"thinking": "Pensant",

+ 4 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Permet que Roo realitzi operacions automàticament sense requerir aprovació. Activeu aquesta configuració només si confieu plenament en la IA i enteneu els riscos de seguretat associats.",
+		"toggleAriaLabel": "Commuta l'aprovació automàtica",
+		"disabledAriaLabel": "Aprovació automàtica desactivada: seleccioneu primer les opcions",
 		"readOnly": {
 			"label": "Llegir",
 			"description": "Quan està activat, Roo veurà automàticament el contingut del directori i llegirà fitxers sense que calgui fer clic al botó Aprovar.",
@@ -190,7 +192,8 @@
 			"title": "Màximes Sol·licituds",
 			"description": "Fes aquesta quantitat de sol·licituds API automàticament abans de demanar aprovació per continuar amb la tasca.",
 			"unlimited": "Il·limitat"
-		}
+		},
+		"selectOptionsFirst": "Seleccioneu almenys una opció a continuació per activar l'aprovació automàtica"
 	},
 	"providers": {
 		"providerDocumentation": "Documentació de {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/de/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Automatische Genehmigung:",
 		"none": "Keine",
-		"description": "Automatische Genehmigung erlaubt Roo Code, Aktionen ohne Nachfrage auszuführen. Aktiviere dies nur für Aktionen, denen du vollständig vertraust. Detailliertere Konfiguration verfügbar in den <settingsLink>Einstellungen</settingsLink>."
+		"description": "Automatische Genehmigung erlaubt Roo Code, Aktionen ohne Nachfrage auszuführen. Aktiviere dies nur für Aktionen, denen du vollständig vertraust. Detailliertere Konfiguration verfügbar in den <settingsLink>Einstellungen</settingsLink>.",
+		"selectOptionsFirst": "Wähle mindestens eine der folgenden Optionen aus, um die automatische Genehmigung zu aktivieren",
+		"toggleAriaLabel": "Automatische Genehmigung umschalten",
+		"disabledAriaLabel": "Automatische Genehmigung deaktiviert - zuerst Optionen auswählen"
 	},
 	"reasoning": {
 		"thinking": "Denke nach",

+ 4 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Erlaubt Roo, Operationen automatisch ohne Genehmigung durchzuführen. Aktiviere diese Einstellungen nur, wenn du der KI vollständig vertraust und die damit verbundenen Sicherheitsrisiken verstehst.",
+		"toggleAriaLabel": "Automatische Genehmigung umschalten",
+		"disabledAriaLabel": "Automatische Genehmigung deaktiviert - zuerst Optionen auswählen",
 		"readOnly": {
 			"label": "Lesen",
 			"description": "Wenn aktiviert, wird Roo automatisch Verzeichnisinhalte anzeigen und Dateien lesen, ohne dass du auf die Genehmigen-Schaltfläche klicken musst.",
@@ -190,7 +192,8 @@
 			"title": "Maximale Anfragen",
 			"description": "Automatisch so viele API-Anfragen stellen, bevor du um die Erlaubnis gebeten wirst, mit der Aufgabe fortzufahren.",
 			"unlimited": "Unbegrenzt"
-		}
+		},
+		"selectOptionsFirst": "Wähle mindestens eine Option unten aus, um die automatische Genehmigung zu aktivieren"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}}-Dokumentation",

+ 4 - 1
webview-ui/src/i18n/locales/en/chat.json

@@ -244,7 +244,10 @@
 	"autoApprove": {
 		"title": "Auto-approve:",
 		"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 <settingsLink>Settings</settingsLink>."
+		"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 <settingsLink>Settings</settingsLink>.",
+		"selectOptionsFirst": "Select at least one option below to enable auto-approval",
+		"toggleAriaLabel": "Toggle auto-approval",
+		"disabledAriaLabel": "Auto-approval disabled - select options first"
 	},
 	"announcement": {
 		"title": "🎉 Roo Code {{version}} Released",

+ 4 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -190,7 +190,10 @@
 			"title": "Max Requests",
 			"description": "Automatically make this many API requests before asking for approval to continue with the task.",
 			"unlimited": "Unlimited"
-		}
+		},
+		"toggleAriaLabel": "Toggle auto-approval",
+		"disabledAriaLabel": "Auto-approval disabled - select options first",
+		"selectOptionsFirst": "Select at least one option below to enable auto-approval"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} documentation",

+ 4 - 1
webview-ui/src/i18n/locales/es/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Auto-aprobar:",
 		"none": "Ninguno",
-		"description": "Auto-aprobar permite a Roo Code realizar acciones sin pedir permiso. Habilita solo para acciones en las que confíes plenamente. Configuración más detallada disponible en <settingsLink>Configuración</settingsLink>."
+		"description": "Auto-aprobar permite a Roo Code realizar acciones sin pedir permiso. Habilita solo para acciones en las que confíes plenamente. Configuración más detallada disponible en <settingsLink>Configuración</settingsLink>.",
+		"selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática",
+		"toggleAriaLabel": "Alternar aprobación automática",
+		"disabledAriaLabel": "Aprobación automática desactivada: seleccione primero las opciones"
 	},
 	"reasoning": {
 		"thinking": "Pensando",

+ 4 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Permitir que Roo realice operaciones automáticamente sin requerir aprobación. Habilite esta configuración solo si confía plenamente en la IA y comprende los riesgos de seguridad asociados.",
+		"toggleAriaLabel": "Alternar aprobación automática",
+		"disabledAriaLabel": "Aprobación automática desactivada: seleccione primero las opciones",
 		"readOnly": {
 			"label": "Lectura",
 			"description": "Cuando está habilitado, Roo verá automáticamente el contenido del directorio y leerá archivos sin que necesite hacer clic en el botón Aprobar.",
@@ -190,7 +192,8 @@
 			"title": "Solicitudes máximas",
 			"description": "Realizar automáticamente esta cantidad de solicitudes a la API antes de pedir aprobación para continuar con la tarea.",
 			"unlimited": "Ilimitado"
-		}
+		},
+		"selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática"
 	},
 	"providers": {
 		"providerDocumentation": "Documentación de {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/fr/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Auto-approbation :",
 		"none": "Aucune",
-		"description": "L'auto-approbation permet à Roo Code d'effectuer des actions sans demander d'autorisation. Activez-la uniquement pour les actions auxquelles vous faites entièrement confiance. Configuration plus détaillée disponible dans les <settingsLink>Paramètres</settingsLink>."
+		"description": "L'auto-approbation permet à Roo Code d'effectuer des actions sans demander d'autorisation. Activez-la uniquement pour les actions auxquelles vous faites entièrement confiance. Configuration plus détaillée disponible dans les <settingsLink>Paramètres</settingsLink>.",
+		"selectOptionsFirst": "Sélectionnez au moins une option ci-dessous pour activer l'auto-approbation",
+		"toggleAriaLabel": "Activer/désactiver l'approbation automatique",
+		"disabledAriaLabel": "Approbation automatique désactivée - sélectionnez d'abord les options"
 	},
 	"reasoning": {
 		"thinking": "Réflexion",

+ 3 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -123,6 +123,9 @@
 	},
 	"autoApprove": {
 		"description": "Permettre à Roo d'effectuer automatiquement des opérations sans requérir d'approbation. Activez ces paramètres uniquement si vous faites entièrement confiance à l'IA et que vous comprenez les risques de sécurité associés.",
+		"toggleAriaLabel": "Activer/désactiver l'approbation automatique",
+		"disabledAriaLabel": "Approbation automatique désactivée - sélectionnez d'abord les options",
+		"selectOptionsFirst": "Sélectionnez au moins une option ci-dessous pour activer l'approbation automatique",
 		"readOnly": {
 			"label": "Lecture",
 			"description": "Lorsque cette option est activée, Roo affichera automatiquement le contenu des répertoires et lira les fichiers sans que vous ayez à cliquer sur le bouton Approuver.",

+ 4 - 1
webview-ui/src/i18n/locales/hi/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "स्वत:-स्वीकृति:",
 		"none": "कोई नहीं",
-		"description": "स्वत:-स्वीकृति Roo Code को अनुमति मांगे बिना क्रियाएँ करने की अनुमति देती है। केवल उन क्रियाओं के लिए सक्षम करें जिन पर आप पूरी तरह से विश्वास करते हैं। अधिक विस्तृत कॉन्फ़िगरेशन <settingsLink>सेटिंग्स</settingsLink> में उपलब्ध है।"
+		"description": "स्वत:-स्वीकृति Roo Code को अनुमति मांगे बिना क्रियाएँ करने की अनुमति देती है। केवल उन क्रियाओं के लिए सक्षम करें जिन पर आप पूरी तरह से विश्वास करते हैं। अधिक विस्तृत कॉन्फ़िगरेशन <settingsLink>सेटिंग्स</settingsLink> में उपलब्ध है।",
+		"selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे दिए گئے विकल्पों में से कम से कम एक का चयन करें",
+		"toggleAriaLabel": "स्वतः-अनुमोदन टॉगल करें",
+		"disabledAriaLabel": "स्वतः-अनुमोदन अक्षम - पहले विकल्प चुनें"
 	},
 	"reasoning": {
 		"thinking": "विचार कर रहा है",

+ 4 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Roo को अनुमोदन की आवश्यकता के बिना स्वचालित रूप से ऑपरेशन करने की अनुमति दें। इन सेटिंग्स को केवल तभी सक्षम करें जब आप AI पर पूरी तरह से भरोसा करते हों और संबंधित सुरक्षा जोखिमों को समझते हों।",
+		"toggleAriaLabel": "स्वतः-अनुमोदन टॉगल करें",
+		"disabledAriaLabel": "स्वतः-अनुमोदन अक्षम - पहले विकल्प चुनें",
 		"readOnly": {
 			"label": "पढ़ें",
 			"description": "जब सक्षम होता है, तो Roo आपके अनुमोदित बटन पर क्लिक किए बिना स्वचालित रूप से निर्देशिका सामग्री देखेगा और फाइलें पढ़ेगा।",
@@ -190,7 +192,8 @@
 			"title": "अधिकतम अनुरोध",
 			"description": "कार्य जारी रखने के लिए अनुमति मांगने से पहले स्वचालित रूप से इतने API अनुरोध करें।",
 			"unlimited": "असीमित"
-		}
+		},
+		"selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे से कम से कम एक विकल्प चुनें"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} दस्तावेज़ीकरण",

+ 4 - 1
webview-ui/src/i18n/locales/id/chat.json

@@ -250,7 +250,10 @@
 	"autoApprove": {
 		"title": "Auto-approve:",
 		"none": "Tidak Ada",
-		"description": "Auto-approve memungkinkan Roo Code melakukan aksi tanpa meminta izin. Hanya aktifkan untuk aksi yang benar-benar kamu percayai. Konfigurasi lebih detail tersedia di <settingsLink>Pengaturan</settingsLink>."
+		"description": "Auto-approve memungkinkan Roo Code melakukan aksi tanpa meminta izin. Hanya aktifkan untuk aksi yang benar-benar kamu percayai. Konfigurasi lebih detail tersedia di <settingsLink>Pengaturan</settingsLink>.",
+		"selectOptionsFirst": "Pilih setidaknya satu opsi di bawah untuk mengaktifkan persetujuan otomatis",
+		"toggleAriaLabel": "Beralih persetujuan otomatis",
+		"disabledAriaLabel": "Persetujuan otomatis dinonaktifkan - pilih opsi terlebih dahulu"
 	},
 	"announcement": {
 		"title": "🎉 Roo Code {{version}} Dirilis",

+ 4 - 1
webview-ui/src/i18n/locales/id/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Izinkan Roo untuk secara otomatis melakukan operasi tanpa memerlukan persetujuan. Aktifkan pengaturan ini hanya jika kamu sepenuhnya mempercayai AI dan memahami risiko keamanan yang terkait.",
+		"toggleAriaLabel": "Beralih persetujuan otomatis",
+		"disabledAriaLabel": "Persetujuan otomatis dinonaktifkan - pilih opsi terlebih dahulu",
 		"readOnly": {
 			"label": "Baca",
 			"description": "Ketika diaktifkan, Roo akan secara otomatis melihat konten direktori dan membaca file tanpa memerlukan kamu mengklik tombol Setujui.",
@@ -194,7 +196,8 @@
 			"title": "Permintaan Maks",
 			"description": "Secara otomatis membuat sejumlah permintaan API ini sebelum meminta persetujuan untuk melanjutkan tugas.",
 			"unlimited": "Tidak terbatas"
-		}
+		},
+		"selectOptionsFirst": "Pilih setidaknya satu opsi di bawah ini untuk mengaktifkan persetujuan otomatis"
 	},
 	"providers": {
 		"providerDocumentation": "Dokumentasi {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/it/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Auto-approvazione:",
 		"none": "Nessuna",
-		"description": "L'auto-approvazione permette a Roo Code di eseguire azioni senza chiedere permesso. Abilita solo per azioni di cui ti fidi completamente. Configurazione più dettagliata disponibile nelle <settingsLink>Impostazioni</settingsLink>."
+		"description": "L'auto-approvazione permette a Roo Code di eseguire azioni senza chiedere permesso. Abilita solo per azioni di cui ti fidi completamente. Configurazione più dettagliata disponibile nelle <settingsLink>Impostazioni</settingsLink>.",
+		"selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'auto-approvazione",
+		"toggleAriaLabel": "Attiva/disattiva approvazione automatica",
+		"disabledAriaLabel": "Approvazione automatica disabilitata - seleziona prima le opzioni"
 	},
 	"reasoning": {
 		"thinking": "Sto pensando",

+ 4 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Permetti a Roo di eseguire automaticamente operazioni senza richiedere approvazione. Abilita queste impostazioni solo se ti fidi completamente dell'IA e comprendi i rischi di sicurezza associati.",
+		"toggleAriaLabel": "Attiva/disattiva approvazione automatica",
+		"disabledAriaLabel": "Approvazione automatica disabilitata - seleziona prima le opzioni",
 		"readOnly": {
 			"label": "Leggi",
 			"description": "Quando abilitato, Roo visualizzerà automaticamente i contenuti della directory e leggerà i file senza richiedere di cliccare sul pulsante Approva.",
@@ -190,7 +192,8 @@
 			"title": "Richieste massime",
 			"description": "Esegui automaticamente questo numero di richieste API prima di chiedere l'approvazione per continuare con l'attività.",
 			"unlimited": "Illimitato"
-		}
+		},
+		"selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'approvazione automatica"
 	},
 	"providers": {
 		"providerDocumentation": "Documentazione {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/ja/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "自動承認:",
 		"none": "なし",
-		"description": "自動承認はRoo Codeに許可を求めずに操作を実行する権限を与えます。完全に信頼できる操作のみ有効にしてください。より詳細な設定は<settingsLink>設定</settingsLink>で利用できます。"
+		"description": "自動承認はRoo Codeに許可を求めずに操作を実行する権限を与えます。完全に信頼できる操作のみ有効にしてください。より詳細な設定は<settingsLink>設定</settingsLink>で利用できます。",
+		"selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください",
+		"toggleAriaLabel": "自動承認の切り替え",
+		"disabledAriaLabel": "自動承認が無効です - 最初にオプションを選択してください"
 	},
 	"reasoning": {
 		"thinking": "考え中",

+ 4 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Rooが承認なしで自動的に操作を実行できるようにします。AIを完全に信頼し、関連するセキュリティリスクを理解している場合にのみ、これらの設定を有効にしてください。",
+		"toggleAriaLabel": "自動承認の切り替え",
+		"disabledAriaLabel": "自動承認が無効です - 最初にオプションを選択してください",
 		"readOnly": {
 			"label": "読み取り",
 			"description": "有効にすると、Rooは承認ボタンをクリックすることなく、自動的にディレクトリの内容を表示してファイルを読み取ります。",
@@ -190,7 +192,8 @@
 			"title": "最大リクエスト数",
 			"description": "タスクを続行するための承認を求める前に、自動的にこの数のAPIリクエストを行います。",
 			"unlimited": "無制限"
-		}
+		},
+		"selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}}のドキュメント",

+ 4 - 1
webview-ui/src/i18n/locales/ko/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "자동 승인:",
 		"none": "없음",
-		"description": "자동 승인을 사용하면 Roo Code가 권한을 요청하지 않고 작업을 수행할 수 있습니다. 완전히 신뢰할 수 있는 작업에만 활성화하세요. 더 자세한 구성은 <settingsLink>설정</settingsLink>에서 사용할 수 있습니다."
+		"description": "자동 승인을 사용하면 Roo Code가 권한을 요청하지 않고 작업을 수행할 수 있습니다. 완전히 신뢰할 수 있는 작업에만 활성화하세요. 더 자세한 구성은 <settingsLink>설정</settingsLink>에서 사용할 수 있습니다.",
+		"selectOptionsFirst": "자동 승인을 활성화하려면 아래 옵션 중 하나 이상을 선택하세요",
+		"toggleAriaLabel": "자동 승인 전환",
+		"disabledAriaLabel": "자동 승인 비활성화됨 - 먼저 옵션을 선택하세요"
 	},
 	"reasoning": {
 		"thinking": "생각 중",

+ 4 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Roo가 승인 없이 자동으로 작업을 수행할 수 있도록 허용합니다. AI를 완전히 신뢰하고 관련 보안 위험을 이해하는 경우에만 이러한 설정을 활성화하세요.",
+		"toggleAriaLabel": "자동 승인 전환",
+		"disabledAriaLabel": "자동 승인 비활성화됨 - 먼저 옵션을 선택하세요",
 		"readOnly": {
 			"label": "읽기",
 			"description": "활성화되면 Roo는 승인 버튼을 클릭하지 않고도 자동으로 디렉토리 내용을 보고 파일을 읽습니다.",
@@ -190,7 +192,8 @@
 			"title": "최대 요청 수",
 			"description": "작업을 계속하기 위한 승인을 요청하기 전에 자동으로 이 수의 API 요청을 수행합니다.",
 			"unlimited": "무제한"
-		}
+		},
+		"selectOptionsFirst": "자동 승인을 활성화하려면 아래에서 하나 이상의 옵션을 선택하세요"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} 문서",

+ 4 - 1
webview-ui/src/i18n/locales/nl/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Automatisch goedkeuren:",
 		"none": "Geen",
-		"description": "Met automatisch goedkeuren kan Roo Code acties uitvoeren zonder om toestemming te vragen. Schakel dit alleen in voor acties die je volledig vertrouwt. Meer gedetailleerde configuratie beschikbaar in de <settingsLink>Instellingen</settingsLink>."
+		"description": "Met automatisch goedkeuren kan Roo Code acties uitvoeren zonder om toestemming te vragen. Schakel dit alleen in voor acties die je volledig vertrouwt. Meer gedetailleerde configuratie beschikbaar in de <settingsLink>Instellingen</settingsLink>.",
+		"selectOptionsFirst": "Selecteer hieronder minstens één optie om automatische goedkeuring in te schakelen",
+		"toggleAriaLabel": "Automatisch goedkeuren in-/uitschakelen",
+		"disabledAriaLabel": "Automatisch goedkeuren uitgeschakeld - selecteer eerst opties"
 	},
 	"announcement": {
 		"title": "🎉 Roo Code {{version}} uitgebracht",

+ 4 - 1
webview-ui/src/i18n/locales/nl/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Sta Roo toe om automatisch handelingen uit te voeren zonder goedkeuring. Schakel deze instellingen alleen in als je de AI volledig vertrouwt en de bijbehorende beveiligingsrisico's begrijpt.",
+		"toggleAriaLabel": "Automatisch goedkeuren in-/uitschakelen",
+		"disabledAriaLabel": "Automatisch goedkeuren uitgeschakeld - selecteer eerst opties",
 		"readOnly": {
 			"label": "Lezen",
 			"description": "Indien ingeschakeld, bekijkt Roo automatisch de inhoud van mappen en leest bestanden zonder dat je op de Goedkeuren-knop hoeft te klikken.",
@@ -190,7 +192,8 @@
 			"title": "Maximale verzoeken",
 			"description": "Voer automatisch dit aantal API-verzoeken uit voordat om goedkeuring wordt gevraagd om door te gaan met de taak.",
 			"unlimited": "Onbeperkt"
-		}
+		},
+		"selectOptionsFirst": "Selecteer ten minste één optie hieronder om automatische goedkeuring in te schakelen"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} documentatie",

+ 4 - 1
webview-ui/src/i18n/locales/pl/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Automatyczne zatwierdzanie:",
 		"none": "Brak",
-		"description": "Automatyczne zatwierdzanie pozwala Roo Code wykonywać działania bez pytania o pozwolenie. Włącz tylko dla działań, którym w pełni ufasz. Bardziej szczegółowa konfiguracja dostępna w <settingsLink>Ustawieniach</settingsLink>."
+		"description": "Automatyczne zatwierdzanie pozwala Roo Code wykonywać działania bez pytania o pozwolenie. Włącz tylko dla działań, którym w pełni ufasz. Bardziej szczegółowa konfiguracja dostępna w <settingsLink>Ustawieniach</settingsLink>.",
+		"selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie",
+		"toggleAriaLabel": "Przełącz automatyczne zatwierdzanie",
+		"disabledAriaLabel": "Automatyczne zatwierdzanie wyłączone - najpierw wybierz opcje"
 	},
 	"reasoning": {
 		"thinking": "Myślenie",

+ 4 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Pozwól Roo na automatyczne wykonywanie operacji bez wymagania zatwierdzenia. Włącz te ustawienia tylko jeśli w pełni ufasz AI i rozumiesz związane z tym zagrożenia bezpieczeństwa.",
+		"toggleAriaLabel": "Przełącz automatyczne zatwierdzanie",
+		"disabledAriaLabel": "Automatyczne zatwierdzanie wyłączone - najpierw wybierz opcje",
 		"readOnly": {
 			"label": "Odczyt",
 			"description": "Gdy włączone, Roo automatycznie będzie wyświetlać zawartość katalogów i czytać pliki bez konieczności klikania przycisku Zatwierdź.",
@@ -190,7 +192,8 @@
 			"title": "Maksymalna liczba żądań",
 			"description": "Automatycznie wykonaj tyle żądań API przed poproszeniem o zgodę na kontynuowanie zadania.",
 			"unlimited": "Bez limitu"
-		}
+		},
+		"selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie"
 	},
 	"providers": {
 		"providerDocumentation": "Dokumentacja {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Aprovação automática:",
 		"none": "Nenhuma",
-		"description": "A aprovação automática permite que o Roo Code execute ações sem pedir permissão. Ative apenas para ações nas quais você confia totalmente. Configuração mais detalhada disponível nas <settingsLink>Configurações</settingsLink>."
+		"description": "A aprovação automática permite que o Roo Code execute ações sem pedir permissão. Ative apenas para ações nas quais você confia totalmente. Configuração mais detalhada disponível nas <settingsLink>Configurações</settingsLink>.",
+		"selectOptionsFirst": "Selecione pelo menos uma opção abaixo para ativar a aprovação automática",
+		"toggleAriaLabel": "Alternar aprovação automática",
+		"disabledAriaLabel": "Aprovação automática desativada - selecione as opções primeiro"
 	},
 	"reasoning": {
 		"thinking": "Pensando",

+ 4 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Permitir que o Roo realize operações automaticamente sem exigir aprovação. Ative essas configurações apenas se confiar totalmente na IA e compreender os riscos de segurança associados.",
+		"toggleAriaLabel": "Alternar aprovação automática",
+		"disabledAriaLabel": "Aprovação automática desativada - selecione as opções primeiro",
 		"readOnly": {
 			"label": "Leitura",
 			"description": "Quando ativado, o Roo visualizará automaticamente o conteúdo do diretório e lerá arquivos sem que você precise clicar no botão Aprovar.",
@@ -190,7 +192,8 @@
 			"title": "Máximo de Solicitações",
 			"description": "Fazer automaticamente este número de requisições à API antes de pedir aprovação para continuar com a tarefa.",
 			"unlimited": "Ilimitado"
-		}
+		},
+		"selectOptionsFirst": "Selecione pelo menos uma opção abaixo para habilitar a aprovação automática"
 	},
 	"providers": {
 		"providerDocumentation": "Documentação do {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/ru/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Автоодобрение:",
 		"none": "Нет",
-		"description": "Автоодобрение позволяет Roo Code выполнять действия без запроса разрешения. Включайте только для полностью доверенных действий. Более подробная настройка доступна в <settingsLink>Настройках</settingsLink>."
+		"description": "Автоодобрение позволяет Roo Code выполнять действия без запроса разрешения. Включайте только для полностью доверенных действий. Более подробная настройка доступна в <settingsLink>Настройках</settingsLink>.",
+		"selectOptionsFirst": "Выберите хотя бы один параметр ниже, чтобы включить автоодобрение",
+		"toggleAriaLabel": "Переключить автоодобрение",
+		"disabledAriaLabel": "Автоодобрение отключено - сначала выберите опции"
 	},
 	"announcement": {
 		"title": "🎉 Выпущен Roo Code {{version}}",

+ 4 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Разрешить Roo автоматически выполнять операции без необходимости одобрения. Включайте эти параметры только если полностью доверяете ИИ и понимаете связанные с этим риски безопасности.",
+		"toggleAriaLabel": "Переключить автоодобрение",
+		"disabledAriaLabel": "Автоодобрение отключено - сначала выберите опции",
 		"readOnly": {
 			"label": "Чтение",
 			"description": "Если включено, Roo будет автоматически просматривать содержимое каталогов и читать файлы без необходимости нажимать кнопку \"Одобрить\".",
@@ -190,7 +192,8 @@
 			"title": "Максимум запросов",
 			"description": "Автоматически выполнять это количество API-запросов перед запросом разрешения на продолжение задачи.",
 			"unlimited": "Без ограничений"
-		}
+		},
+		"selectOptionsFirst": "Выберите хотя бы один вариант ниже, чтобы включить автоодобрение"
 	},
 	"providers": {
 		"providerDocumentation": "Документация {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/tr/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Otomatik-onay:",
 		"none": "Hiçbiri",
-		"description": "Otomatik onay, Roo Code'un izin istemeden işlemler gerçekleştirmesine olanak tanır. Yalnızca tamamen güvendiğiniz eylemler için etkinleştirin. Daha detaylı yapılandırma <settingsLink>Ayarlar</settingsLink>'da mevcuttur."
+		"description": "Otomatik onay, Roo Code'un izin istemeden işlemler gerçekleştirmesine olanak tanır. Yalnızca tamamen güvendiğiniz eylemler için etkinleştirin. Daha detaylı yapılandırma <settingsLink>Ayarlar</settingsLink>'da mevcuttur.",
+		"selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek belirleyin",
+		"toggleAriaLabel": "Otomatik onayı değiştir",
+		"disabledAriaLabel": "Otomatik onay devre dışı - önce seçenekleri belirleyin"
 	},
 	"reasoning": {
 		"thinking": "Düşünüyor",

+ 4 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Roo'nun onay gerektirmeden otomatik olarak işlemler gerçekleştirmesine izin verin. Bu ayarları yalnızca yapay zekaya tamamen güveniyorsanız ve ilgili güvenlik risklerini anlıyorsanız etkinleştirin.",
+		"toggleAriaLabel": "Otomatik onayı değiştir",
+		"disabledAriaLabel": "Otomatik onay devre dışı - önce seçenekleri belirleyin",
 		"readOnly": {
 			"label": "Okuma",
 			"description": "Etkinleştirildiğinde, Roo otomatik olarak dizin içeriğini görüntüleyecek ve Onayla düğmesine tıklamanıza gerek kalmadan dosyaları okuyacaktır.",
@@ -190,7 +192,8 @@
 			"title": "Maksimum İstek",
 			"description": "Göreve devam etmek için onay istemeden önce bu sayıda API isteği otomatik olarak yap.",
 			"unlimited": "Sınırsız"
-		}
+		},
+		"selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek seçin"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} Dokümantasyonu",

+ 4 - 1
webview-ui/src/i18n/locales/vi/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "Tự động phê duyệt:",
 		"none": "Không",
-		"description": "Tự động phê duyệt cho phép Roo Code thực hiện hành động mà không cần xin phép. Chỉ bật cho các hành động bạn hoàn toàn tin tưởng. Cấu hình chi tiết hơn có sẵn trong <settingsLink>Cài đặt</settingsLink>."
+		"description": "Tự động phê duyệt cho phép Roo Code thực hiện hành động mà không cần xin phép. Chỉ bật cho các hành động bạn hoàn toàn tin tưởng. Cấu hình chi tiết hơn có sẵn trong <settingsLink>Cài đặt</settingsLink>.",
+		"selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt",
+		"toggleAriaLabel": "Chuyển đổi tự động phê duyệt",
+		"disabledAriaLabel": "Tự động phê duyệt bị vô hiệu hóa - hãy chọn các tùy chọn trước"
 	},
 	"reasoning": {
 		"thinking": "Đang suy nghĩ",

+ 4 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "Cho phép Roo tự động thực hiện các hoạt động mà không cần phê duyệt. Chỉ bật những cài đặt này nếu bạn hoàn toàn tin tưởng AI và hiểu rõ các rủi ro bảo mật liên quan.",
+		"toggleAriaLabel": "Chuyển đổi tự động phê duyệt",
+		"disabledAriaLabel": "Tự động phê duyệt bị vô hiệu hóa - hãy chọn các tùy chọn trước",
 		"readOnly": {
 			"label": "Đọc",
 			"description": "Khi được bật, Roo sẽ tự động xem nội dung thư mục và đọc tệp mà không yêu cầu bạn nhấp vào nút Phê duyệt.",
@@ -190,7 +192,8 @@
 			"title": "Số lượng yêu cầu tối đa",
 			"description": "Tự động thực hiện số lượng API request này trước khi yêu cầu phê duyệt để tiếp tục với nhiệm vụ.",
 			"unlimited": "Không giới hạn"
-		}
+		},
+		"selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt"
 	},
 	"providers": {
 		"providerDocumentation": "Tài liệu {{provider}}",

+ 4 - 1
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "自动批准:",
 		"none": "无",
-		"description": "允许直接执行操作无需确认,请谨慎启用。前往<settingsLink>设置</settingsLink>调整"
+		"description": "允许直接执行操作无需确认,请谨慎启用。前往<settingsLink>设置</settingsLink>调整",
+		"selectOptionsFirst": "选择至少一个下面的选项以启用自动批准",
+		"toggleAriaLabel": "切换自动批准",
+		"disabledAriaLabel": "自动批准已禁用 - 请先选择选项"
 	},
 	"reasoning": {
 		"thinking": "思考中",

+ 4 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "允许 Roo 自动执行操作而无需批准。只有在您完全信任 AI 并了解相关安全风险的情况下才启用这些设置。",
+		"toggleAriaLabel": "切换自动批准",
+		"disabledAriaLabel": "自动批准已禁用 - 请先选择选项",
 		"readOnly": {
 			"label": "读取",
 			"description": "启用后,Roo 将自动浏览目录和读取文件内容,无需人工确认。",
@@ -190,7 +192,8 @@
 			"title": "最大请求数",
 			"description": "在请求批准以继续执行任务之前,自动发出此数量的 API 请求。",
 			"unlimited": "无限制"
-		}
+		},
+		"selectOptionsFirst": "请至少选择以下一个选项以启用自动批准"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} 文档",

+ 4 - 1
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -223,7 +223,10 @@
 	"autoApprove": {
 		"title": "自動核准:",
 		"none": "無",
-		"description": "自動核准讓 Roo Code 可以在無需徵求您同意的情況下執行動作。請僅對您完全信任的動作啟用此功能。您可以在<settingsLink>設定</settingsLink>中進行更詳細的調整。"
+		"description": "自動核准讓 Roo Code 可以在無需徵求您同意的情況下執行動作。請僅對您完全信任的動作啟用此功能。您可以在<settingsLink>設定</settingsLink>中進行更詳細的調整。",
+		"selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准",
+		"toggleAriaLabel": "切換自動核准",
+		"disabledAriaLabel": "自動核准已停用 - 請先選取選項"
 	},
 	"reasoning": {
 		"thinking": "思考中",

+ 4 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -123,6 +123,8 @@
 	},
 	"autoApprove": {
 		"description": "允許 Roo 無需核准即執行操作。僅在您完全信任 AI 並了解相關安全風險時啟用這些設定。",
+		"toggleAriaLabel": "切換自動核准",
+		"disabledAriaLabel": "自動核准已停用 - 請先選取選項",
 		"readOnly": {
 			"label": "讀取",
 			"description": "啟用後,Roo 將自動檢視目錄內容並讀取檔案,無需點選核准按鈕。",
@@ -190,7 +192,8 @@
 			"title": "最大請求數",
 			"description": "在請求批准以繼續執行工作之前,自動發出此數量的 API 請求。",
 			"unlimited": "無限制"
-		}
+		},
+		"selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准"
 	},
 	"providers": {
 		"providerDocumentation": "{{provider}} 文件",