diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 6d3f458b347c..653bfc39e54b 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -149,7 +149,7 @@ export const ApiConfigSelector = ({ disabled={disabled} data-testid="dropdown-trigger" className={cn( - "w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs", + "min-w-0 inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs", "bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground", "transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset", disabled diff --git a/webview-ui/src/components/chat/AutoApproveDropdown.tsx b/webview-ui/src/components/chat/AutoApproveDropdown.tsx new file mode 100644 index 000000000000..b3176d187c3d --- /dev/null +++ b/webview-ui/src/components/chat/AutoApproveDropdown.tsx @@ -0,0 +1,305 @@ +import React from "react" +import { ListChecks, LayoutList, Settings, CheckCheck } from "lucide-react" + +import { vscode } from "@/utils/vscode" +import { cn } from "@/lib/utils" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle" +import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles" + +interface AutoApproveDropdownProps { + disabled?: boolean + triggerClassName?: string +} + +export const AutoApproveDropdown = ({ disabled = false, triggerClassName = "" }: AutoApproveDropdownProps) => { + const [open, setOpen] = React.useState(false) + const portalContainer = useRooPortal("roo-portal") + const { t } = useAppTranslation() + + const { + autoApprovalEnabled, + setAutoApprovalEnabled, + alwaysApproveResubmit, + setAlwaysAllowReadOnly, + setAlwaysAllowWrite, + setAlwaysAllowExecute, + setAlwaysAllowBrowser, + setAlwaysAllowMcp, + setAlwaysAllowModeSwitch, + setAlwaysAllowSubtasks, + setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, + } = useExtensionState() + + const baseToggles = useAutoApprovalToggles() + + // Include alwaysApproveResubmit in addition to the base toggles + const toggles = React.useMemo( + () => ({ + ...baseToggles, + alwaysApproveResubmit: alwaysApproveResubmit, + }), + [baseToggles, alwaysApproveResubmit], + ) + + const onAutoApproveToggle = React.useCallback( + (key: AutoApproveSetting, value: boolean) => { + vscode.postMessage({ type: key, bool: value }) + + // Update the specific toggle state + switch (key) { + case "alwaysAllowReadOnly": + setAlwaysAllowReadOnly(value) + break + case "alwaysAllowWrite": + setAlwaysAllowWrite(value) + break + case "alwaysAllowExecute": + setAlwaysAllowExecute(value) + break + case "alwaysAllowBrowser": + setAlwaysAllowBrowser(value) + break + case "alwaysAllowMcp": + setAlwaysAllowMcp(value) + break + case "alwaysAllowModeSwitch": + setAlwaysAllowModeSwitch(value) + break + case "alwaysAllowSubtasks": + setAlwaysAllowSubtasks(value) + break + case "alwaysApproveResubmit": + setAlwaysApproveResubmit(value) + break + case "alwaysAllowFollowupQuestions": + setAlwaysAllowFollowupQuestions(value) + break + case "alwaysAllowUpdateTodoList": + setAlwaysAllowUpdateTodoList(value) + break + } + + // If enabling any option, ensure autoApprovalEnabled is true + if (value && !autoApprovalEnabled) { + setAutoApprovalEnabled(true) + vscode.postMessage({ type: "autoApprovalEnabled", bool: true }) + } + }, + [ + autoApprovalEnabled, + setAlwaysAllowReadOnly, + setAlwaysAllowWrite, + setAlwaysAllowExecute, + setAlwaysAllowBrowser, + setAlwaysAllowMcp, + setAlwaysAllowModeSwitch, + setAlwaysAllowSubtasks, + setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, + setAutoApprovalEnabled, + ], + ) + + const handleSelectAll = React.useCallback(() => { + // Enable all options + Object.keys(autoApproveSettingsConfig).forEach((key) => { + onAutoApproveToggle(key as AutoApproveSetting, true) + }) + // Enable master auto-approval + if (!autoApprovalEnabled) { + setAutoApprovalEnabled(true) + vscode.postMessage({ type: "autoApprovalEnabled", bool: true }) + } + }, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled]) + + const handleSelectNone = React.useCallback(() => { + // Disable all options + Object.keys(autoApproveSettingsConfig).forEach((key) => { + onAutoApproveToggle(key as AutoApproveSetting, false) + }) + // Disable master auto-approval + if (autoApprovalEnabled) { + setAutoApprovalEnabled(false) + vscode.postMessage({ type: "autoApprovalEnabled", bool: false }) + } + }, [onAutoApproveToggle, autoApprovalEnabled, setAutoApprovalEnabled]) + + const handleOpenSettings = React.useCallback( + () => + window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }), + [], + ) + + // Calculate enabled and total counts as separate properties + const enabledCount = React.useMemo(() => { + return Object.values(toggles).filter((value) => !!value).length + }, [toggles]) + + const totalCount = React.useMemo(() => { + return Object.keys(toggles).length + }, [toggles]) + + // Split settings into two columns + const settingsArray = Object.values(autoApproveSettingsConfig) + const halfLength = Math.ceil(settingsArray.length / 2) + const firstColumn = settingsArray.slice(0, halfLength) + const secondColumn = settingsArray.slice(halfLength) + + return ( + + + + + + {enabledCount === totalCount + ? t("chat:autoApprove.triggerLabelAll") + : t("chat:autoApprove.triggerLabel", { count: enabledCount })} + + + + e.preventDefault()}> +
+ {/* Header with description */} +
+
+

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

+ +
+

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

+
+ + {/* Two-column layout for approval options */} +
+
+ {/* First Column */} +
+ {firstColumn.map(({ key, labelKey, descriptionKey, icon }) => { + const isEnabled = toggles[key] + return ( + + + + ) + })} +
+ + {/* Second Column */} +
+ {secondColumn.map(({ key, labelKey, descriptionKey, icon }) => { + const isEnabled = toggles[key] + return ( + + + + ) + })} +
+
+
+ + {/* Bottom bar with Select All/None buttons */} +
+
+ + +
+
+
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx deleted file mode 100644 index 25b936eb7457..000000000000 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { memo, useCallback, useMemo, useState } from "react" -import { Trans } from "react-i18next" -import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/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 { useAutoApprovalState } from "@src/hooks/useAutoApprovalState" -import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles" -import DismissibleUpsell from "@src/components/common/DismissibleUpsell" -import { useCloudUpsell } from "@src/hooks/useCloudUpsell" -import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog" - -interface AutoApproveMenuProps { - style?: React.CSSProperties -} - -const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { - const [isExpanded, setIsExpanded] = useState(false) - - const { - autoApprovalEnabled, - setAutoApprovalEnabled, - alwaysApproveResubmit, - setAlwaysAllowReadOnly, - setAlwaysAllowWrite, - setAlwaysAllowExecute, - setAlwaysAllowBrowser, - setAlwaysAllowMcp, - setAlwaysAllowModeSwitch, - setAlwaysAllowSubtasks, - setAlwaysApproveResubmit, - setAlwaysAllowFollowupQuestions, - setAlwaysAllowUpdateTodoList, - } = useExtensionState() - - const { t } = useAppTranslation() - - const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({ - autoOpenOnAuth: false, - }) - - const baseToggles = useAutoApprovalToggles() - const enabledCount = useMemo(() => Object.values(baseToggles).filter(Boolean).length, [baseToggles]) - - // 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) - break - case "alwaysAllowWrite": - setAlwaysAllowWrite(value) - break - case "alwaysAllowExecute": - setAlwaysAllowExecute(value) - break - case "alwaysAllowBrowser": - setAlwaysAllowBrowser(value) - break - case "alwaysAllowMcp": - setAlwaysAllowMcp(value) - break - case "alwaysAllowModeSwitch": - setAlwaysAllowModeSwitch(value) - break - case "alwaysAllowSubtasks": - setAlwaysAllowSubtasks(value) - break - case "alwaysApproveResubmit": - setAlwaysApproveResubmit(value) - break - case "alwaysAllowFollowupQuestions": - setAlwaysAllowFollowupQuestions(value) - break - case "alwaysAllowUpdateTodoList": - 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, - setAlwaysAllowBrowser, - setAlwaysAllowMcp, - setAlwaysAllowModeSwitch, - setAlwaysAllowSubtasks, - 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)) - .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" } }), - [], - ) - - return ( -
- {isExpanded && ( -
-
- , - }} - /> -
- - - - {enabledCount > 7 && ( - <> - openUpsell()} - dismissOnClick={false} - variant="banner"> - , - }} - /> - - - )} -
- )} - -
-
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} - - -
-
- -
- ) -} - -export default memo(AutoApproveMenu) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e7a9f67b8b6c..b93a6c2a6ba0 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -26,6 +26,7 @@ import { StandardTooltip } from "@src/components/ui" import Thumbnails from "../common/Thumbnails" import { ModeSelector } from "./ModeSelector" import { ApiConfigSelector } from "./ApiConfigSelector" +import { AutoApproveDropdown } from "./AutoApproveDropdown" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { IndexingStatusBadge } from "./IndexingStatusBadge" @@ -1173,34 +1174,31 @@ export const ChatTextArea = forwardRef( /> )} -
-
-
- -
-
- -
+
+
+ + +
-
+
{isTtsPlaying && (