Skip to content

Commit 18e3cd1

Browse files
committed
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
1 parent a0e8c24 commit 18e3cd1

File tree

3 files changed

+124
-22
lines changed

3 files changed

+124
-22
lines changed

webview-ui/src/components/chat/CommandExecution.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface CommandExecutionProps {
2626

2727
export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => {
2828
const { t } = useAppTranslation()
29-
const { terminalShellIntegrationDisabled = false, allowedCommands = [] } = useExtensionState()
29+
const { terminalShellIntegrationDisabled = false, allowedCommands = [], deniedCommands = [] } = useExtensionState()
3030

3131
const { command, output: parsedOutput, suggestions } = useMemo(() => parseCommandAndOutput(text), [text])
3232

@@ -266,6 +266,30 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
266266
[allowedCommands],
267267
)
268268

269+
const handleDenyPatternChange = useCallback(
270+
(pattern: string) => {
271+
if (!pattern) return
272+
273+
const isDenied = deniedCommands.includes(pattern)
274+
let updatedDeniedCommands: string[]
275+
276+
if (isDenied) {
277+
// Remove from deny list
278+
updatedDeniedCommands = deniedCommands.filter((p) => p !== pattern)
279+
} else {
280+
// Add to deny list
281+
updatedDeniedCommands = [...deniedCommands, pattern]
282+
}
283+
284+
// Send message to update denied commands
285+
vscode.postMessage({
286+
type: "deniedCommands",
287+
commands: updatedDeniedCommands,
288+
})
289+
},
290+
[deniedCommands],
291+
)
292+
269293
return (
270294
<div className="w-full">
271295
{/* Header section */}
@@ -337,12 +361,14 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
337361
<CodeBlock source={command} language="shell" />
338362
</div>
339363

340-
{/* Whitelist section */}
364+
{/* Command management section */}
341365
{showSuggestions && (
342366
<CommandPatternSelector
343367
patterns={commandPatterns}
344368
allowedCommands={allowedCommands}
345-
onPatternChange={handleAllowPatternChange}
369+
deniedCommands={deniedCommands}
370+
onAllowPatternChange={handleAllowPatternChange}
371+
onDenyPatternChange={handleDenyPatternChange}
346372
/>
347373
)}
348374

webview-ui/src/components/chat/CommandPatternSelector.tsx

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useState } from "react"
2-
import { ChevronDown } from "lucide-react"
3-
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
2+
import { ChevronDown, Check, X } from "lucide-react"
43
import { useAppTranslation } from "@src/i18n/TranslationContext"
54
import { cn } from "@src/lib/utils"
65

@@ -12,45 +11,120 @@ interface CommandPattern {
1211
interface CommandPatternSelectorProps {
1312
patterns: CommandPattern[]
1413
allowedCommands: string[]
15-
onPatternChange: (pattern: string) => void
14+
deniedCommands: string[]
15+
onAllowPatternChange: (pattern: string) => void
16+
onDenyPatternChange: (pattern: string) => void
1617
}
1718

18-
export const CommandPatternSelector = ({ patterns, allowedCommands, onPatternChange }: CommandPatternSelectorProps) => {
19+
export const CommandPatternSelector = ({
20+
patterns,
21+
allowedCommands,
22+
deniedCommands,
23+
onAllowPatternChange,
24+
onDenyPatternChange,
25+
}: CommandPatternSelectorProps) => {
1926
const { t } = useAppTranslation()
2027
const [isExpanded, setIsExpanded] = useState(false)
2128

2229
if (patterns.length === 0) {
2330
return null
2431
}
2532

33+
const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
34+
if (allowedCommands.includes(pattern)) return "allowed"
35+
if (deniedCommands.includes(pattern)) return "denied"
36+
return "none"
37+
}
38+
39+
const handleAllowClick = (pattern: string) => {
40+
const status = getPatternStatus(pattern)
41+
if (status === "denied") {
42+
// Remove from denied list first
43+
onDenyPatternChange(pattern)
44+
}
45+
// Toggle allow status
46+
onAllowPatternChange(pattern)
47+
}
48+
49+
const handleDenyClick = (pattern: string) => {
50+
const status = getPatternStatus(pattern)
51+
if (status === "allowed") {
52+
// Remove from allowed list first
53+
onAllowPatternChange(pattern)
54+
}
55+
// Toggle deny status
56+
onDenyPatternChange(pattern)
57+
}
58+
2659
return (
2760
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
2861
<button
2962
onClick={() => setIsExpanded(!isExpanded)}
3063
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-vscode-list-hoverBackground transition-all"
31-
aria-label={isExpanded ? "Collapse allowed commands section" : "Expand allowed commands section"}
64+
aria-label={isExpanded ? "Collapse command management section" : "Expand command management section"}
3265
aria-expanded={isExpanded}>
3366
<ChevronDown
3467
className={cn("size-3 transition-transform duration-200", {
3568
"rotate-0": isExpanded,
3669
"-rotate-90": !isExpanded,
3770
})}
3871
/>
39-
<span className="font-medium">{t("chat:commandExecution.addToAllowedCommands")}</span>
72+
<span className="font-medium">{t("chat:commandExecution.manageCommands")}</span>
4073
</button>
4174
{isExpanded && (
42-
<div className="px-3 pb-3 space-y-1.5">
43-
{patterns.map((item, index) => (
44-
<div key={`${item.pattern}-${index}`} className="ml-5">
45-
<VSCodeCheckbox
46-
checked={allowedCommands.includes(item.pattern)}
47-
onChange={() => onPatternChange(item.pattern)}
48-
className="text-xs"
49-
aria-label={`Allow command pattern: ${item.pattern}`}>
50-
<span className="font-mono text-vscode-foreground">{item.pattern}</span>
51-
</VSCodeCheckbox>
52-
</div>
53-
))}
75+
<div className="px-3 pb-3 space-y-2">
76+
<div className="text-xs text-vscode-descriptionForeground mb-2 ml-5">
77+
{t("chat:commandExecution.commandManagementDescription")}
78+
</div>
79+
{patterns.map((item, index) => {
80+
const status = getPatternStatus(item.pattern)
81+
return (
82+
<div key={`${item.pattern}-${index}`} className="ml-5 flex items-center gap-2">
83+
<div className="flex-1">
84+
<span className="font-mono text-xs text-vscode-foreground">{item.pattern}</span>
85+
{item.description && (
86+
<span className="text-xs text-vscode-descriptionForeground ml-2">
87+
- {item.description}
88+
</span>
89+
)}
90+
</div>
91+
<div className="flex items-center gap-1">
92+
<button
93+
onClick={() => handleAllowClick(item.pattern)}
94+
className={cn(
95+
"p-1 rounded transition-all",
96+
status === "allowed"
97+
? "bg-green-500/20 text-green-500 hover:bg-green-500/30"
98+
: "text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10",
99+
)}
100+
aria-label={
101+
status === "allowed"
102+
? `Remove ${item.pattern} from allowed list`
103+
: `Add ${item.pattern} to allowed list`
104+
}
105+
title={status === "allowed" ? "Remove from allowed" : "Add to allowed"}>
106+
<Check className="size-3.5" />
107+
</button>
108+
<button
109+
onClick={() => handleDenyClick(item.pattern)}
110+
className={cn(
111+
"p-1 rounded transition-all",
112+
status === "denied"
113+
? "bg-red-500/20 text-red-500 hover:bg-red-500/30"
114+
: "text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10",
115+
)}
116+
aria-label={
117+
status === "denied"
118+
? `Remove ${item.pattern} from denied list`
119+
: `Add ${item.pattern} to denied list`
120+
}
121+
title={status === "denied" ? "Remove from denied" : "Add to denied"}>
122+
<X className="size-3.5" />
123+
</button>
124+
</div>
125+
</div>
126+
)
127+
})}
54128
</div>
55129
)}
56130
</div>

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@
213213
"exited": "Exited ({{exitCode}})",
214214
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
215215
"allowAllNpmRun": "Allow all npm run commands",
216-
"allowAllNpm": "Allow all npm commands"
216+
"allowAllNpm": "Allow all npm commands",
217+
"manageCommands": "Manage Command Permissions",
218+
"commandManagementDescription": "Click ✓ to allow auto-execution, ✗ to deny execution"
217219
},
218220
"commandOutput": "Command Output",
219221
"response": "Response",

0 commit comments

Comments
 (0)