Skip to content

Commit 851617c

Browse files
committed
feat: restore command pattern extraction with editable UI
- Re-added command-parser.ts and its tests - Modified CommandPatternSelector to show both full command and extracted patterns - Each pattern (including full command) is now individually editable with click-to-edit functionality - Updated tests to match new component interface - Maintains the improved UI with hover effects and focus rings
1 parent 20f99a2 commit 851617c

File tree

5 files changed

+500
-171
lines changed

5 files changed

+500
-171
lines changed

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ 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+
}
1622

1723
interface CommandExecutionProps {
1824
executionId: string
@@ -66,22 +72,30 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
6672
// streaming output (this is the case for running commands).
6773
const output = streamingOutput || parsedOutput
6874

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)
75+
// Extract command patterns from the actual command that was executed
76+
const commandPatterns = useMemo<CommandPattern[]>(() => {
77+
const extractedPatterns = extractPatternsFromCommand(command)
78+
return extractedPatterns.map((pattern) => ({
79+
pattern,
80+
}))
81+
}, [command])
82+
83+
// Handle pattern changes
84+
const handleAllowPatternChange = (pattern: string) => {
85+
const isAllowed = allowedCommands.includes(pattern)
86+
const newAllowed = isAllowed ? allowedCommands.filter((p) => p !== pattern) : [...allowedCommands, pattern]
87+
const newDenied = deniedCommands.filter((p) => p !== pattern)
7488

7589
setAllowedCommands(newAllowed)
7690
setDeniedCommands(newDenied)
7791
vscode.postMessage({ type: "allowedCommands", commands: newAllowed })
7892
vscode.postMessage({ type: "deniedCommands", commands: newDenied })
7993
}
8094

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)
95+
const handleDenyPatternChange = (pattern: string) => {
96+
const isDenied = deniedCommands.includes(pattern)
97+
const newDenied = isDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern]
98+
const newAllowed = allowedCommands.filter((p) => p !== pattern)
8599

86100
setAllowedCommands(newAllowed)
87101
setDeniedCommands(newDenied)
@@ -181,10 +195,11 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
181195
{command && (
182196
<CommandPatternSelector
183197
command={command}
198+
patterns={commandPatterns}
184199
allowedCommands={allowedCommands}
185200
deniedCommands={deniedCommands}
186-
onAllowCommandChange={handleAllowCommandChange}
187-
onDenyCommandChange={handleDenyCommandChange}
201+
onAllowPatternChange={handleAllowPatternChange}
202+
onDenyPatternChange={handleDenyPatternChange}
188203
/>
189204
)}
190205
</div>

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

Lines changed: 142 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,179 @@
1-
import React, { useState } from "react"
1+
import React, { useState, useMemo } from "react"
22
import { Check, ChevronDown, Info, X } from "lucide-react"
33
import { cn } from "../../lib/utils"
44
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+
813
interface CommandPatternSelectorProps {
914
command: string
15+
patterns: CommandPattern[]
1016
allowedCommands: string[]
1117
deniedCommands: string[]
12-
onAllowCommandChange: (command: string) => void
13-
onDenyCommandChange: (command: string) => void
18+
onAllowPatternChange: (pattern: string) => void
19+
onDenyPatternChange: (pattern: string) => void
1420
}
1521

1622
export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
1723
command,
24+
patterns,
1825
allowedCommands,
1926
deniedCommands,
20-
onAllowCommandChange,
21-
onDenyCommandChange,
27+
onAllowPatternChange,
28+
onDenyPatternChange,
2229
}) => {
2330
const { t } = useTranslation()
2431
const [isExpanded, setIsExpanded] = useState(false)
25-
const [editedCommand, setEditedCommand] = useState(command)
26-
const [isEditing, setIsEditing] = useState(false)
32+
const [editingStates, setEditingStates] = useState<Record<string, { isEditing: boolean; value: string }>>({})
33+
34+
// Create a combined list with full command first, then patterns
35+
const allPatterns = useMemo(() => {
36+
const fullCommandPattern = { pattern: command, description: "Full command" }
37+
return [fullCommandPattern, ...patterns]
38+
}, [command, patterns])
2739

28-
const getCommandStatus = (cmd: string): "allowed" | "denied" | "none" => {
29-
if (allowedCommands.includes(cmd)) return "allowed"
30-
if (deniedCommands.includes(cmd)) return "denied"
40+
const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
41+
if (allowedCommands.includes(pattern)) return "allowed"
42+
if (deniedCommands.includes(pattern)) return "denied"
3143
return "none"
3244
}
3345

34-
const currentStatus = getCommandStatus(editedCommand)
46+
const getEditState = (pattern: string) => {
47+
return editingStates[pattern] || { isEditing: false, value: pattern }
48+
}
49+
50+
const setEditState = (pattern: string, isEditing: boolean, value?: string) => {
51+
setEditingStates((prev) => ({
52+
...prev,
53+
[pattern]: { isEditing, value: value ?? pattern },
54+
}))
55+
}
3556

3657
return (
3758
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
3859
<button
3960
onClick={() => setIsExpanded(!isExpanded)}
40-
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"
41-
aria-expanded={isExpanded}
42-
aria-label={t(
43-
isExpanded ? "chat:commandExecution.collapseManagement" : "chat:commandExecution.expandManagement",
44-
)}>
45-
<ChevronDown
46-
className={cn("size-3 transition-transform duration-200", {
47-
"rotate-0": isExpanded,
48-
"-rotate-90": !isExpanded,
49-
})}
50-
/>
51-
<span className="font-medium">{t("chat:commandExecution.manageCommands")}</span>
52-
<StandardTooltip
53-
content={
54-
<Trans
55-
i18nKey="chat:commandExecution.commandManagementDescription"
56-
components={{
57-
settingsLink: (
58-
<VSCodeLink
59-
href="#"
60-
onClick={(e) => {
61-
e.preventDefault()
62-
window.postMessage(
63-
{
64-
type: "action",
65-
action: "settingsButtonClicked",
66-
values: { section: "autoApprove" },
67-
},
68-
"*",
69-
)
61+
className="w-full px-3 py-2 flex items-center justify-between hover:bg-vscode-list-hoverBackground transition-colors">
62+
<div className="flex items-center gap-2">
63+
<ChevronDown
64+
className={cn("size-4 transition-transform", {
65+
"-rotate-90": !isExpanded,
66+
})}
67+
/>
68+
<span className="text-sm font-medium">{t("chat:commandExecution.commandPermissions")}</span>
69+
<StandardTooltip
70+
content={
71+
<div className="space-y-2 max-w-xs">
72+
<p>{t("chat:commandExecution.permissionsTooltip")}</p>
73+
<p>
74+
<Trans
75+
i18nKey="chat:commandExecution.learnMore"
76+
components={{
77+
link: (
78+
<VSCodeLink
79+
href="https://docs.roo-code.com/features/command-permissions"
80+
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground"
81+
/>
82+
),
7083
}}
71-
className="inline"
7284
/>
73-
),
74-
}}
75-
/>
76-
}>
77-
<Info className="size-3 ml-1" />
78-
</StandardTooltip>
85+
</p>
86+
</div>
87+
}>
88+
<Info className="size-3.5 text-vscode-descriptionForeground" />
89+
</StandardTooltip>
90+
</div>
91+
<div className="flex items-center gap-2 text-xs text-vscode-descriptionForeground">
92+
<span>
93+
{allowedCommands.length} {t("chat:commandExecution.allowed")}
94+
</span>
95+
<span></span>
96+
<span>
97+
{deniedCommands.length} {t("chat:commandExecution.denied")}
98+
</span>
99+
</div>
79100
</button>
80101

81102
{isExpanded && (
82-
<div className="px-3 pb-3 pt-2">
83-
<div className="ml-5 flex items-center gap-2">
84-
<div className="flex-1">
85-
{isEditing ? (
86-
<input
87-
type="text"
88-
value={editedCommand}
89-
onChange={(e) => setEditedCommand(e.target.value)}
90-
onBlur={() => setIsEditing(false)}
91-
onKeyDown={(e) => {
92-
if (e.key === "Enter") {
93-
setIsEditing(false)
94-
}
95-
if (e.key === "Escape") {
96-
setEditedCommand(command)
97-
setIsEditing(false)
98-
}
99-
}}
100-
className="font-mono text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1.5 w-full focus:outline-0"
101-
placeholder={command}
102-
autoFocus
103-
/>
104-
) : (
105-
<div
106-
onClick={() => setIsEditing(true)}
107-
className="font-mono text-xs text-vscode-foreground cursor-pointer hover:bg-vscode-list-hoverBackground px-2 py-1.5 rounded transition-colors border border-transparent"
108-
title="Click to edit command">
109-
{editedCommand}
103+
<div className="px-3 pb-3 space-y-2">
104+
{allPatterns.map((item) => {
105+
const editState = getEditState(item.pattern)
106+
const status = getPatternStatus(editState.value)
107+
108+
return (
109+
<div key={item.pattern} className="ml-5 flex items-center gap-2">
110+
<div className="flex-1">
111+
{editState.isEditing ? (
112+
<input
113+
type="text"
114+
value={editState.value}
115+
onChange={(e) => setEditState(item.pattern, true, e.target.value)}
116+
onBlur={() => setEditState(item.pattern, false)}
117+
onKeyDown={(e) => {
118+
if (e.key === "Enter") {
119+
setEditState(item.pattern, false)
120+
}
121+
if (e.key === "Escape") {
122+
setEditState(item.pattern, false, item.pattern)
123+
}
124+
}}
125+
className="font-mono text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1.5 w-full focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder"
126+
placeholder={item.pattern}
127+
autoFocus
128+
/>
129+
) : (
130+
<div
131+
onClick={() => setEditState(item.pattern, true)}
132+
className="font-mono text-xs text-vscode-foreground cursor-pointer hover:bg-vscode-list-hoverBackground px-2 py-1.5 rounded transition-colors border border-transparent"
133+
title="Click to edit pattern">
134+
<span>{editState.value}</span>
135+
{item.description && (
136+
<span className="text-vscode-descriptionForeground ml-2">
137+
- {item.description}
138+
</span>
139+
)}
140+
</div>
141+
)}
142+
</div>
143+
<div className="flex items-center gap-1">
144+
<button
145+
className={cn("p-1 rounded transition-all", {
146+
"bg-green-500/20 text-green-500 hover:bg-green-500/30":
147+
status === "allowed",
148+
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
149+
status !== "allowed",
150+
})}
151+
onClick={() => onAllowPatternChange(editState.value)}
152+
aria-label={t(
153+
status === "allowed"
154+
? "chat:commandExecution.removeFromAllowed"
155+
: "chat:commandExecution.addToAllowed",
156+
)}>
157+
<Check className="size-3.5" />
158+
</button>
159+
<button
160+
className={cn("p-1 rounded transition-all", {
161+
"bg-red-500/20 text-red-500 hover:bg-red-500/30": status === "denied",
162+
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
163+
status !== "denied",
164+
})}
165+
onClick={() => onDenyPatternChange(editState.value)}
166+
aria-label={t(
167+
status === "denied"
168+
? "chat:commandExecution.removeFromDenied"
169+
: "chat:commandExecution.addToDenied",
170+
)}>
171+
<X className="size-3.5" />
172+
</button>
110173
</div>
111-
)}
112-
</div>
113-
<div className="flex items-center gap-1">
114-
<button
115-
className={cn("p-1 rounded transition-all", {
116-
"bg-green-500/20 text-green-500 hover:bg-green-500/30": currentStatus === "allowed",
117-
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
118-
currentStatus !== "allowed",
119-
})}
120-
onClick={() => onAllowCommandChange(editedCommand)}
121-
aria-label={t(
122-
currentStatus === "allowed"
123-
? "chat:commandExecution.removeFromAllowed"
124-
: "chat:commandExecution.addToAllowed",
125-
)}>
126-
<Check className="size-3.5" />
127-
</button>
128-
<button
129-
className={cn("p-1 rounded transition-all", {
130-
"bg-red-500/20 text-red-500 hover:bg-red-500/30": currentStatus === "denied",
131-
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
132-
currentStatus !== "denied",
133-
})}
134-
onClick={() => onDenyCommandChange(editedCommand)}
135-
aria-label={t(
136-
currentStatus === "denied"
137-
? "chat:commandExecution.removeFromDenied"
138-
: "chat:commandExecution.addToDenied",
139-
)}>
140-
<X className="size-3.5" />
141-
</button>
142-
</div>
143-
</div>
174+
</div>
175+
)
176+
})}
144177
</div>
145178
)}
146179
</div>

0 commit comments

Comments
 (0)