From 18e3cd1878934348b9cac5d29053517e30a55c55 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Mon, 14 Jul 2025 15:24:11 -0600 Subject: [PATCH] feat: add unified UI for managing allow/deny command lists - Updated CommandPatternSelector to show both allow and deny options - Added mutual exclusivity between allow and deny states - Added visual indicators (green check for allow, red X for deny) - Added translation strings for the new UI - Integrated deny command handling in CommandExecution component --- .../src/components/chat/CommandExecution.tsx | 32 ++++- .../chat/CommandPatternSelector.tsx | 110 +++++++++++++++--- webview-ui/src/i18n/locales/en/chat.json | 4 +- 3 files changed, 124 insertions(+), 22 deletions(-) diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index 6424483383..dca2a9d923 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -26,7 +26,7 @@ interface CommandExecutionProps { export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => { const { t } = useAppTranslation() - const { terminalShellIntegrationDisabled = false, allowedCommands = [] } = useExtensionState() + const { terminalShellIntegrationDisabled = false, allowedCommands = [], deniedCommands = [] } = useExtensionState() const { command, output: parsedOutput, suggestions } = useMemo(() => parseCommandAndOutput(text), [text]) @@ -266,6 +266,30 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec [allowedCommands], ) + const handleDenyPatternChange = useCallback( + (pattern: string) => { + if (!pattern) return + + const isDenied = deniedCommands.includes(pattern) + let updatedDeniedCommands: string[] + + if (isDenied) { + // Remove from deny list + updatedDeniedCommands = deniedCommands.filter((p) => p !== pattern) + } else { + // Add to deny list + updatedDeniedCommands = [...deniedCommands, pattern] + } + + // Send message to update denied commands + vscode.postMessage({ + type: "deniedCommands", + commands: updatedDeniedCommands, + }) + }, + [deniedCommands], + ) + return (
{/* Header section */} @@ -337,12 +361,14 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
- {/* Whitelist section */} + {/* Command management section */} {showSuggestions && ( )} diff --git a/webview-ui/src/components/chat/CommandPatternSelector.tsx b/webview-ui/src/components/chat/CommandPatternSelector.tsx index 706d43ea62..9c93ac6d21 100644 --- a/webview-ui/src/components/chat/CommandPatternSelector.tsx +++ b/webview-ui/src/components/chat/CommandPatternSelector.tsx @@ -1,6 +1,5 @@ import { useState } from "react" -import { ChevronDown } from "lucide-react" -import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { ChevronDown, Check, X } from "lucide-react" import { useAppTranslation } from "@src/i18n/TranslationContext" import { cn } from "@src/lib/utils" @@ -12,10 +11,18 @@ interface CommandPattern { interface CommandPatternSelectorProps { patterns: CommandPattern[] allowedCommands: string[] - onPatternChange: (pattern: string) => void + deniedCommands: string[] + onAllowPatternChange: (pattern: string) => void + onDenyPatternChange: (pattern: string) => void } -export const CommandPatternSelector = ({ patterns, allowedCommands, onPatternChange }: CommandPatternSelectorProps) => { +export const CommandPatternSelector = ({ + patterns, + allowedCommands, + deniedCommands, + onAllowPatternChange, + onDenyPatternChange, +}: CommandPatternSelectorProps) => { const { t } = useAppTranslation() const [isExpanded, setIsExpanded] = useState(false) @@ -23,12 +30,38 @@ export const CommandPatternSelector = ({ patterns, allowedCommands, onPatternCha return null } + const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => { + if (allowedCommands.includes(pattern)) return "allowed" + if (deniedCommands.includes(pattern)) return "denied" + return "none" + } + + const handleAllowClick = (pattern: string) => { + const status = getPatternStatus(pattern) + if (status === "denied") { + // Remove from denied list first + onDenyPatternChange(pattern) + } + // Toggle allow status + onAllowPatternChange(pattern) + } + + const handleDenyClick = (pattern: string) => { + const status = getPatternStatus(pattern) + if (status === "allowed") { + // Remove from allowed list first + onAllowPatternChange(pattern) + } + // Toggle deny status + onDenyPatternChange(pattern) + } + return (
{isExpanded && ( -
- {patterns.map((item, index) => ( -
- onPatternChange(item.pattern)} - className="text-xs" - aria-label={`Allow command pattern: ${item.pattern}`}> - {item.pattern} - -
- ))} +
+
+ {t("chat:commandExecution.commandManagementDescription")} +
+ {patterns.map((item, index) => { + const status = getPatternStatus(item.pattern) + return ( +
+
+ {item.pattern} + {item.description && ( + + - {item.description} + + )} +
+
+ + +
+
+ ) + })}
)}
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index f2b41a6d9c..77c1191bee 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -213,7 +213,9 @@ "exited": "Exited ({{exitCode}})", "addToAllowedCommands": "Add to Allowed Auto-Execute Commands", "allowAllNpmRun": "Allow all npm run commands", - "allowAllNpm": "Allow all npm commands" + "allowAllNpm": "Allow all npm commands", + "manageCommands": "Manage Command Permissions", + "commandManagementDescription": "Click ✓ to allow auto-execution, ✗ to deny execution" }, "commandOutput": "Command Output", "response": "Response",