Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions webview-ui/src/components/chat/AutoApproveKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useEffect, useCallback, useMemo } from "react"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"
import { AutoApproveSetting } from "../settings/AutoApproveToggle"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"

// Keyboard shortcuts mapping for auto-approve options
const KEYBOARD_SHORTCUTS: Record<string, AutoApproveSetting> = {
"1": "alwaysAllowReadOnly",
"2": "alwaysAllowWrite",
"3": "alwaysAllowBrowser",
"4": "alwaysAllowExecute",
"5": "alwaysAllowMcp",
"6": "alwaysAllowModeSwitch",
"7": "alwaysAllowSubtasks",
"8": "alwaysAllowFollowupQuestions",
"9": "alwaysAllowUpdateTodoList",
"0": "alwaysApproveResubmit",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This KEYBOARD_SHORTCUTS constant is duplicated in AutoApproveToggleDropdown.tsx. We should extract this to a shared constants file to maintain a single source of truth. Consider creating a shared constants file like autoApproveConstants.ts:

Suggested change
"0": "alwaysApproveResubmit",
import { KEYBOARD_SHORTCUTS } from '@/constants/autoApproveConstants'

}

export const AutoApproveKeyboardShortcuts = () => {
const {
setAlwaysAllowReadOnly,
setAlwaysAllowWrite,
setAlwaysAllowExecute,
setAlwaysAllowBrowser,
setAlwaysAllowMcp,
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysApproveResubmit,
setAlwaysAllowFollowupQuestions,
setAlwaysAllowUpdateTodoList,
alwaysApproveResubmit,
} = useExtensionState()

const baseToggles = useAutoApprovalToggles()
const toggles = useMemo(
() => ({
...baseToggles,
alwaysApproveResubmit,
}),
[baseToggles, alwaysApproveResubmit],
)

const handleToggle = useCallback(
(key: AutoApproveSetting) => {
const currentValue = toggles[key]
const newValue = !currentValue

// Send message to extension
vscode.postMessage({ type: key, bool: newValue })

// Update local state
switch (key) {
case "alwaysAllowReadOnly":
setAlwaysAllowReadOnly(newValue)
break
case "alwaysAllowWrite":
setAlwaysAllowWrite(newValue)
break
case "alwaysAllowExecute":
setAlwaysAllowExecute(newValue)
break
case "alwaysAllowBrowser":
setAlwaysAllowBrowser(newValue)
break
case "alwaysAllowMcp":
setAlwaysAllowMcp(newValue)
break
case "alwaysAllowModeSwitch":
setAlwaysAllowModeSwitch(newValue)
break
case "alwaysAllowSubtasks":
setAlwaysAllowSubtasks(newValue)
break
case "alwaysApproveResubmit":
setAlwaysApproveResubmit(newValue)
break
case "alwaysAllowFollowupQuestions":
setAlwaysAllowFollowupQuestions(newValue)
break
case "alwaysAllowUpdateTodoList":
setAlwaysAllowUpdateTodoList(newValue)
break
}
},
[
toggles,
setAlwaysAllowReadOnly,
setAlwaysAllowWrite,
setAlwaysAllowExecute,
setAlwaysAllowBrowser,
setAlwaysAllowMcp,
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysApproveResubmit,
setAlwaysAllowFollowupQuestions,
setAlwaysAllowUpdateTodoList,
],
)

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Check if Alt/Option key is pressed along with a number key
if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Alt+Number shortcuts might conflict with browser or screen reader shortcuts. Should we consider adding a setting to disable keyboard shortcuts or use different key combinations (e.g., Ctrl+Shift+Number)?

const shortcut = KEYBOARD_SHORTCUTS[event.key]
if (shortcut) {
event.preventDefault()
handleToggle(shortcut)
}
}
}

window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [handleToggle])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The event listener cleanup depends on handleToggle which could cause unnecessary re-registrations. Consider using a ref to store the callback or wrapping it in useCallback with stable dependencies to prevent potential memory leaks.


return null // This component doesn't render anything
}
210 changes: 114 additions & 96 deletions webview-ui/src/components/chat/AutoApproveMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { memo, useCallback, useMemo, useState } from "react"
import { Trans } from "react-i18next"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Stamp, ListChecks, LayoutList } from "lucide-react"

import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
import { StandardTooltip } from "@src/components/ui"
import { AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
import { AutoApproveToggleDropdown } from "./AutoApproveToggleDropdown"
import { StandardTooltip, Popover, PopoverContent, PopoverTrigger } from "@src/components/ui"
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
import { cn } from "@src/lib/utils"
import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"

interface AutoApproveMenuProps {
style?: React.CSSProperties
}

const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const portalContainer = useRooPortal("roo-portal")

const {
autoApprovalEnabled,
Expand Down Expand Up @@ -123,10 +128,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
],
)

const toggleExpanded = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])

const enabledActionsList = Object.entries(toggles)
.filter(([_key, value]) => !!value)
.map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey))
Expand All @@ -146,101 +147,118 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
[],
)

// Handler for Select All
const handleSelectAll = useCallback(() => {
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
allSettings.forEach((key) => {
if (!toggles[key]) {
onAutoApproveToggle(key, true)
}
})
}, [toggles, onAutoApproveToggle])

// Handler for Select None
const handleSelectNone = useCallback(() => {
const allSettings: AutoApproveSetting[] = Object.keys(toggles) as AutoApproveSetting[]
allSettings.forEach((key) => {
if (toggles[key]) {
onAutoApproveToggle(key, false)
}
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition of Select All/Select None functionality! These handlers are well-implemented and will improve the user experience when managing multiple auto-approve options.

}, [toggles, onAutoApproveToggle])

const trigger = (
<PopoverTrigger
className={cn(
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-2 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",
"opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
)}
style={style}>
<Stamp className="size-3.5 opacity-80 flex-shrink-0" />
<span className="font-medium">{t("chat:autoApprove.title")}</span>
<span className="text-vscode-descriptionForeground truncate max-w-[200px]">{displayText}</span>
</PopoverTrigger>
)

return (
<div
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
: "none",
overflowY: "auto",
...style,
}}>
{isExpanded && (
<div className="flex flex-col gap-2 py-4">
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<Trans
i18nKey="chat:autoApprove.description"
components={{
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
}}
/>
<Popover open={isExpanded} onOpenChange={setIsExpanded}>
<StandardTooltip content={t("chat:autoApprove.tooltip")}>{trigger}</StandardTooltip>

<PopoverContent
align="start"
sideOffset={4}
container={portalContainer}
className="p-0 overflow-hidden min-w-[400px] max-w-[500px]">
<div className="flex flex-col w-full">
{/* Header with master toggle */}
<div className="flex items-center justify-between p-3 border-b border-vscode-dropdown-border">
<div className="flex items-center gap-2">
<StandardTooltip
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
disabled={!hasEnabledOptions}
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 })
}
}}
/>
</StandardTooltip>
<h4 className="m-0 font-medium text-sm">{t("chat:autoApprove.title")}</h4>
</div>
<div className="flex items-center gap-1">
<StandardTooltip content={t("chat:autoApprove.selectAll")}>
<button
onClick={handleSelectAll}
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
<ListChecks className="size-4" />
</button>
</StandardTooltip>
<StandardTooltip content={t("chat:autoApprove.selectNone")}>
<button
onClick={handleSelectNone}
className="p-1 rounded hover:bg-vscode-list-hoverBackground transition-colors">
<LayoutList className="size-4" />
</button>
</StandardTooltip>
</div>
</div>

<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
</div>
)}
{/* Description */}
<div className="px-3 py-2 border-b border-vscode-dropdown-border">
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<Trans
i18nKey="chat:autoApprove.description"
components={{
settingsLink: <VSCodeLink href="#" onClick={handleOpenSettings} />,
}}
/>
</div>
</div>

<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "2px 0 0 0",
cursor: "pointer",
}}
onClick={toggleExpanded}>
<div onClick={(e) => e.stopPropagation()}>
<StandardTooltip
content={!hasEnabledOptions ? t("chat:autoApprove.selectOptionsFirst") : undefined}>
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
disabled={!hasEnabledOptions}
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={{
display: "flex",
alignItems: "center",
gap: "4px",
flex: 1,
minWidth: 0,
}}>
<span
style={{
color: "var(--vscode-foreground)",
flexShrink: 0,
}}>
{t("chat:autoApprove.title")}
</span>
<span
style={{
color: "var(--vscode-descriptionForeground)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
minWidth: 0,
}}>
{displayText}
</span>
<span
className={`codicon codicon-chevron-right flex-shrink-0 transition-transform duration-200 ease-in-out ${
isExpanded ? "-rotate-90 ml-[2px]" : "rotate-0 -ml-[2px]"
}`}
/>
{/* Two-column layout for toggles */}
<div className="p-3 max-h-[400px] overflow-y-auto">
<div className="grid grid-cols-2 gap-x-4">
<AutoApproveToggleDropdown {...toggles} onToggle={onAutoApproveToggle} />
</div>
</div>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}

Expand Down
Loading
Loading