Skip to content

Commit 8235029

Browse files
committed
feat: simplify command permissions UI to use full commands instead of patterns
- Replace pattern extraction with single text input for full command - Allow users to edit command before approving/denying - Show active state (check/x) based on current allowed/denied lists - Remove command-parser.ts and related pattern extraction logic - Update tests to match new simplified behavior This change addresses user feedback requesting a simpler interface where users can edit the full command before setting permissions.
1 parent ea36059 commit 8235029

File tree

6 files changed

+304
-459
lines changed

6 files changed

+304
-459
lines changed

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

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ import { cn } from "@src/lib/utils"
1313
import { Button } from "@src/components/ui"
1414
import CodeBlock from "../common/CodeBlock"
1515
import { CommandPatternSelector } from "./CommandPatternSelector"
16-
import { extractPatternsFromCommand } from "../../utils/command-parser"
17-
18-
interface CommandPattern {
19-
pattern: string
20-
description?: string
21-
}
2216

2317
interface CommandExecutionProps {
2418
executionId: string
@@ -43,7 +37,7 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
4337

4438
if (outputIndex !== -1) {
4539
// Text is split into command and output
46-
const cmd = (text ?? '').slice(0, outputIndex).trim()
40+
const cmd = (text ?? "").slice(0, outputIndex).trim()
4741
// Skip the newline and "Output:" text
4842
const afterSeparator = outputIndex + 1 + outputSeparator.length
4943
let startOfOutput = afterSeparator
@@ -72,31 +66,22 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
7266
// streaming output (this is the case for running commands).
7367
const output = streamingOutput || parsedOutput
7468

75-
// Extract command patterns from the actual command that was executed
76-
const commandPatterns = useMemo<CommandPattern[]>(() => {
77-
// Extract patterns from the actual command that was executed
78-
const extractedPatterns = extractPatternsFromCommand(command)
79-
return extractedPatterns.map((pattern) => ({
80-
pattern,
81-
}))
82-
}, [command])
83-
84-
// Handle pattern changes
85-
const handleAllowPatternChange = (pattern: string) => {
86-
const isAllowed = allowedCommands.includes(pattern)
87-
const newAllowed = isAllowed ? allowedCommands.filter((p) => p !== pattern) : [...allowedCommands, pattern]
88-
const newDenied = deniedCommands.filter((p) => p !== pattern)
69+
// Handle command changes
70+
const handleAllowCommandChange = (cmd: string) => {
71+
const isAllowed = allowedCommands.includes(cmd)
72+
const newAllowed = isAllowed ? allowedCommands.filter((c) => c !== cmd) : [...allowedCommands, cmd]
73+
const newDenied = deniedCommands.filter((c) => c !== cmd)
8974

9075
setAllowedCommands(newAllowed)
9176
setDeniedCommands(newDenied)
9277
vscode.postMessage({ type: "allowedCommands", commands: newAllowed })
9378
vscode.postMessage({ type: "deniedCommands", commands: newDenied })
9479
}
9580

96-
const handleDenyPatternChange = (pattern: string) => {
97-
const isDenied = deniedCommands.includes(pattern)
98-
const newDenied = isDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern]
99-
const newAllowed = allowedCommands.filter((p) => p !== pattern)
81+
const handleDenyCommandChange = (cmd: string) => {
82+
const isDenied = deniedCommands.includes(cmd)
83+
const newDenied = isDenied ? deniedCommands.filter((c) => c !== cmd) : [...deniedCommands, cmd]
84+
const newAllowed = allowedCommands.filter((c) => c !== cmd)
10085

10186
setAllowedCommands(newAllowed)
10287
setDeniedCommands(newDenied)
@@ -193,13 +178,13 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
193178
<CodeBlock source={command} language="shell" />
194179
<OutputContainer isExpanded={isExpanded} output={output} />
195180
</div>
196-
{commandPatterns.length > 0 && (
181+
{command && (
197182
<CommandPatternSelector
198-
patterns={commandPatterns}
183+
command={command}
199184
allowedCommands={allowedCommands}
200185
deniedCommands={deniedCommands}
201-
onAllowPatternChange={handleAllowPatternChange}
202-
onDenyPatternChange={handleDenyPatternChange}
186+
onAllowCommandChange={handleAllowCommandChange}
187+
onDenyCommandChange={handleDenyCommandChange}
203188
/>
204189
)}
205190
</div>

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

Lines changed: 54 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,33 @@ import { useTranslation, Trans } from "react-i18next"
55
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
66
import { StandardTooltip } from "../ui/standard-tooltip"
77

8-
interface CommandPattern {
9-
pattern: string
10-
description?: string
11-
}
12-
138
interface CommandPatternSelectorProps {
14-
patterns: CommandPattern[]
9+
command: string
1510
allowedCommands: string[]
1611
deniedCommands: string[]
17-
onAllowPatternChange: (pattern: string) => void
18-
onDenyPatternChange: (pattern: string) => void
12+
onAllowCommandChange: (command: string) => void
13+
onDenyCommandChange: (command: string) => void
1914
}
2015

2116
export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
22-
patterns,
17+
command,
2318
allowedCommands,
2419
deniedCommands,
25-
onAllowPatternChange,
26-
onDenyPatternChange,
20+
onAllowCommandChange,
21+
onDenyCommandChange,
2722
}) => {
2823
const { t } = useTranslation()
2924
const [isExpanded, setIsExpanded] = useState(false)
25+
const [editedCommand, setEditedCommand] = useState(command)
3026

31-
const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
32-
if (allowedCommands.includes(pattern)) return "allowed"
33-
if (deniedCommands.includes(pattern)) return "denied"
27+
const getCommandStatus = (cmd: string): "allowed" | "denied" | "none" => {
28+
if (allowedCommands.includes(cmd)) return "allowed"
29+
if (deniedCommands.includes(cmd)) return "denied"
3430
return "none"
3531
}
3632

33+
const currentStatus = getCommandStatus(editedCommand)
34+
3735
return (
3836
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
3937
<button
@@ -80,53 +78,48 @@ export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
8078
</button>
8179

8280
{isExpanded && (
83-
<div className="px-3 pb-3 space-y-2">
84-
{patterns.map((item) => {
85-
const status = getPatternStatus(item.pattern)
86-
return (
87-
<div key={item.pattern} className="ml-5 flex items-center gap-2">
88-
<div className="flex-1">
89-
<span className="font-mono text-xs text-vscode-foreground">{item.pattern}</span>
90-
{item.description && (
91-
<span className="text-xs text-vscode-descriptionForeground ml-2">
92-
- {item.description}
93-
</span>
94-
)}
95-
</div>
96-
<div className="flex items-center gap-1">
97-
<button
98-
className={cn("p-1 rounded transition-all", {
99-
"bg-green-500/20 text-green-500 hover:bg-green-500/30":
100-
status === "allowed",
101-
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
102-
status !== "allowed",
103-
})}
104-
onClick={() => onAllowPatternChange(item.pattern)}
105-
aria-label={t(
106-
status === "allowed"
107-
? "chat:commandExecution.removeFromAllowed"
108-
: "chat:commandExecution.addToAllowed",
109-
)}>
110-
<Check className="size-3.5" />
111-
</button>
112-
<button
113-
className={cn("p-1 rounded transition-all", {
114-
"bg-red-500/20 text-red-500 hover:bg-red-500/30": status === "denied",
115-
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
116-
status !== "denied",
117-
})}
118-
onClick={() => onDenyPatternChange(item.pattern)}
119-
aria-label={t(
120-
status === "denied"
121-
? "chat:commandExecution.removeFromDenied"
122-
: "chat:commandExecution.addToDenied",
123-
)}>
124-
<X className="size-3.5" />
125-
</button>
126-
</div>
127-
</div>
128-
)
129-
})}
81+
<div className="px-3 pb-3">
82+
<div className="ml-5 flex items-center gap-2">
83+
<div className="flex-1">
84+
<input
85+
type="text"
86+
value={editedCommand}
87+
onChange={(e) => setEditedCommand(e.target.value)}
88+
className="font-mono text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1 w-full focus:outline-none focus:border-vscode-focusBorder"
89+
placeholder={command}
90+
/>
91+
</div>
92+
<div className="flex items-center gap-1">
93+
<button
94+
className={cn("p-1 rounded transition-all", {
95+
"bg-green-500/20 text-green-500 hover:bg-green-500/30": currentStatus === "allowed",
96+
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
97+
currentStatus !== "allowed",
98+
})}
99+
onClick={() => onAllowCommandChange(editedCommand)}
100+
aria-label={t(
101+
currentStatus === "allowed"
102+
? "chat:commandExecution.removeFromAllowed"
103+
: "chat:commandExecution.addToAllowed",
104+
)}>
105+
<Check className="size-3.5" />
106+
</button>
107+
<button
108+
className={cn("p-1 rounded transition-all", {
109+
"bg-red-500/20 text-red-500 hover:bg-red-500/30": currentStatus === "denied",
110+
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
111+
currentStatus !== "denied",
112+
})}
113+
onClick={() => onDenyCommandChange(editedCommand)}
114+
aria-label={t(
115+
currentStatus === "denied"
116+
? "chat:commandExecution.removeFromDenied"
117+
: "chat:commandExecution.addToDenied",
118+
)}>
119+
<X className="size-3.5" />
120+
</button>
121+
</div>
122+
</div>
130123
</div>
131124
)}
132125
</div>

0 commit comments

Comments
 (0)