|
1 | | -import React, { useState } from "react" |
| 1 | +import React, { useState, useMemo } from "react" |
2 | 2 | import { Check, ChevronDown, Info, X } from "lucide-react" |
3 | 3 | import { cn } from "../../lib/utils" |
4 | 4 | import { useTranslation, Trans } from "react-i18next" |
5 | 5 | import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" |
6 | 6 | import { StandardTooltip } from "../ui/standard-tooltip" |
7 | 7 |
|
| 8 | +interface CommandPattern { |
| 9 | + pattern: string |
| 10 | + description?: string |
| 11 | +} |
| 12 | + |
8 | 13 | interface CommandPatternSelectorProps { |
9 | 14 | command: string |
| 15 | + patterns: CommandPattern[] |
10 | 16 | allowedCommands: string[] |
11 | 17 | deniedCommands: string[] |
12 | | - onAllowCommandChange: (command: string) => void |
13 | | - onDenyCommandChange: (command: string) => void |
| 18 | + onAllowPatternChange: (pattern: string) => void |
| 19 | + onDenyPatternChange: (pattern: string) => void |
14 | 20 | } |
15 | 21 |
|
16 | 22 | export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({ |
17 | 23 | command, |
| 24 | + patterns, |
18 | 25 | allowedCommands, |
19 | 26 | deniedCommands, |
20 | | - onAllowCommandChange, |
21 | | - onDenyCommandChange, |
| 27 | + onAllowPatternChange, |
| 28 | + onDenyPatternChange, |
22 | 29 | }) => { |
23 | 30 | const { t } = useTranslation() |
24 | 31 | 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]) |
27 | 39 |
|
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" |
31 | 43 | return "none" |
32 | 44 | } |
33 | 45 |
|
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 | + } |
35 | 56 |
|
36 | 57 | return ( |
37 | 58 | <div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30"> |
38 | 59 | <button |
39 | 60 | 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 | + ), |
70 | 83 | }} |
71 | | - className="inline" |
72 | 84 | /> |
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> |
79 | 100 | </button> |
80 | 101 |
|
81 | 102 | {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> |
110 | 173 | </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 | + })} |
144 | 177 | </div> |
145 | 178 | )} |
146 | 179 | </div> |
|
0 commit comments