11import { useCallback , useState , memo , useMemo } from "react"
22import { useEvent } from "react-use"
33import { ChevronDown , Skull } from "lucide-react"
4+ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
45
56import { CommandExecutionStatus , commandExecutionStatusSchema } from "@roo-code/types"
67
@@ -9,6 +10,7 @@ import { safeJsonParse } from "@roo/safeJsonParse"
910import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
1011
1112import { vscode } from "@src/utils/vscode"
13+ import { extractCommandPattern , getPatternDescription } from "@src/utils/extract-command-pattern"
1214import { useExtensionState } from "@src/context/ExtensionStateContext"
1315import { cn } from "@src/lib/utils"
1416import { Button } from "@src/components/ui"
@@ -22,7 +24,7 @@ interface CommandExecutionProps {
2224}
2325
2426export const CommandExecution = ( { executionId, text, icon, title } : CommandExecutionProps ) => {
25- const { terminalShellIntegrationDisabled = false } = useExtensionState ( )
27+ const { terminalShellIntegrationDisabled = false , allowedCommands = [ ] } = useExtensionState ( )
2628
2729 const { command, output : parsedOutput } = useMemo ( ( ) => parseCommandAndOutput ( text ) , [ text ] )
2830
@@ -31,6 +33,159 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
3133 const [ isExpanded , setIsExpanded ] = useState ( terminalShellIntegrationDisabled )
3234 const [ streamingOutput , setStreamingOutput ] = useState ( "" )
3335 const [ status , setStatus ] = useState < CommandExecutionStatus | null > ( null )
36+ const [ isPatternSectionExpanded , setIsPatternSectionExpanded ] = useState ( false )
37+
38+ // Extract command patterns for whitelisting
39+ // For chained commands, extract individual patterns
40+ const commandPatterns = useMemo ( ( ) => {
41+ if ( ! command ?. trim ( ) ) return [ ]
42+
43+ // Check if this is a chained command
44+ const operators = [ "&&" , "||" , ";" , "|" ]
45+ const patterns : Array < { pattern : string ; description : string } > = [ ]
46+
47+ // Split by operators while respecting quotes
48+ let inSingleQuote = false
49+ let inDoubleQuote = false
50+ let escapeNext = false
51+ let currentCommand = ""
52+ let i = 0
53+
54+ while ( i < command . length ) {
55+ const char = command [ i ]
56+
57+ if ( escapeNext ) {
58+ currentCommand += char
59+ escapeNext = false
60+ i ++
61+ continue
62+ }
63+
64+ if ( char === "\\" ) {
65+ escapeNext = true
66+ currentCommand += char
67+ i ++
68+ continue
69+ }
70+
71+ if ( char === "'" && ! inDoubleQuote ) {
72+ inSingleQuote = ! inSingleQuote
73+ currentCommand += char
74+ i ++
75+ continue
76+ }
77+
78+ if ( char === '"' && ! inSingleQuote ) {
79+ inDoubleQuote = ! inDoubleQuote
80+ currentCommand += char
81+ i ++
82+ continue
83+ }
84+
85+ // Check for operators outside quotes
86+ if ( ! inSingleQuote && ! inDoubleQuote ) {
87+ let foundOperator = false
88+ for ( const op of operators ) {
89+ if ( command . substring ( i , i + op . length ) === op ) {
90+ // Found an operator, process the current command
91+ const trimmedCommand = currentCommand . trim ( )
92+ if ( trimmedCommand ) {
93+ // For npm commands, generate multiple pattern options
94+ if ( trimmedCommand . startsWith ( "npm " ) ) {
95+ // Add the specific pattern
96+ const specificPattern = extractCommandPattern ( trimmedCommand )
97+ if ( specificPattern ) {
98+ patterns . push ( {
99+ pattern : specificPattern ,
100+ description : getPatternDescription ( specificPattern ) ,
101+ } )
102+ }
103+
104+ // Add broader npm patterns
105+ if ( trimmedCommand . startsWith ( "npm run " ) ) {
106+ // Add "npm run" pattern
107+ patterns . push ( {
108+ pattern : "npm run" ,
109+ description : "Allow all npm run commands" ,
110+ } )
111+ }
112+
113+ // Add "npm" pattern
114+ patterns . push ( {
115+ pattern : "npm" ,
116+ description : "Allow all npm commands" ,
117+ } )
118+ } else {
119+ // For non-npm commands, just add the extracted pattern
120+ const pattern = extractCommandPattern ( trimmedCommand )
121+ if ( pattern ) {
122+ patterns . push ( {
123+ pattern,
124+ description : getPatternDescription ( pattern ) ,
125+ } )
126+ }
127+ }
128+ }
129+ currentCommand = ""
130+ i += op . length
131+ foundOperator = true
132+ break
133+ }
134+ }
135+ if ( foundOperator ) continue
136+ }
137+
138+ currentCommand += char
139+ i ++
140+ }
141+
142+ // Process the last command
143+ const trimmedCommand = currentCommand . trim ( )
144+ if ( trimmedCommand ) {
145+ // For npm commands, generate multiple pattern options
146+ if ( trimmedCommand . startsWith ( "npm " ) ) {
147+ // Add the specific pattern
148+ const specificPattern = extractCommandPattern ( trimmedCommand )
149+ if ( specificPattern ) {
150+ patterns . push ( {
151+ pattern : specificPattern ,
152+ description : getPatternDescription ( specificPattern ) ,
153+ } )
154+ }
155+
156+ // Add broader npm patterns
157+ if ( trimmedCommand . startsWith ( "npm run " ) ) {
158+ // Add "npm run" pattern
159+ patterns . push ( {
160+ pattern : "npm run" ,
161+ description : "Allow all npm run commands" ,
162+ } )
163+ }
164+
165+ // Add "npm" pattern
166+ patterns . push ( {
167+ pattern : "npm" ,
168+ description : "Allow all npm commands" ,
169+ } )
170+ } else {
171+ // For non-npm commands, just add the extracted pattern
172+ const pattern = extractCommandPattern ( trimmedCommand )
173+ if ( pattern ) {
174+ patterns . push ( {
175+ pattern,
176+ description : getPatternDescription ( pattern ) ,
177+ } )
178+ }
179+ }
180+ }
181+
182+ // Remove duplicates
183+ const uniquePatterns = patterns . filter (
184+ ( item , index , self ) => index === self . findIndex ( ( p ) => p . pattern === item . pattern ) ,
185+ )
186+
187+ return uniquePatterns
188+ } , [ command ] )
34189
35190 // The command's output can either come from the text associated with the
36191 // task message (this is the case for completed commands) or from the
@@ -73,6 +228,23 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
73228
74229 useEvent ( "message" , onMessage )
75230
231+ const handleAllowPatternChange = useCallback (
232+ ( pattern : string ) => {
233+ if ( ! pattern ) return
234+
235+ const isWhitelisted = allowedCommands . includes ( pattern )
236+ const updatedAllowedCommands = isWhitelisted
237+ ? allowedCommands . filter ( ( p ) => p !== pattern )
238+ : Array . from ( new Set ( [ ...allowedCommands , pattern ] ) )
239+
240+ vscode . postMessage ( {
241+ type : "allowedCommands" ,
242+ commands : updatedAllowedCommands ,
243+ } )
244+ } ,
245+ [ allowedCommands ] ,
246+ )
247+
76248 return (
77249 < >
78250 < div className = "flex flex-row items-center justify-between gap-2 mb-1" >
@@ -123,6 +295,39 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
123295
124296 < div className = "w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2" >
125297 < CodeBlock source = { command } language = "shell" />
298+
299+ { /* Command pattern display and checkboxes */ }
300+ { commandPatterns . length > 0 && (
301+ < div className = "mt-2 pt-2 border-t border-border/25" >
302+ < button
303+ onClick = { ( ) => setIsPatternSectionExpanded ( ! isPatternSectionExpanded ) }
304+ className = "flex items-center gap-1 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors w-full text-left" >
305+ < ChevronDown
306+ className = { cn ( "size-3 transition-transform duration-200" , {
307+ "rotate-0" : isPatternSectionExpanded ,
308+ "-rotate-90" : ! isPatternSectionExpanded ,
309+ } ) }
310+ />
311+ < span > Add to Allowed Auto-Execute Commands</ span >
312+ </ button >
313+ { isPatternSectionExpanded && (
314+ < div className = "mt-2 space-y-2" >
315+ { commandPatterns . map ( ( item , index ) => (
316+ < VSCodeCheckbox
317+ key = { `${ item . pattern } -${ index } ` }
318+ checked = { allowedCommands . includes ( item . pattern ) }
319+ onChange = { ( ) => handleAllowPatternChange ( item . pattern ) }
320+ className = "text-xs ml-4" >
321+ < span className = "font-medium text-vscode-foreground whitespace-nowrap" >
322+ { item . pattern }
323+ </ span >
324+ </ VSCodeCheckbox >
325+ ) ) }
326+ </ div >
327+ ) }
328+ </ div >
329+ ) }
330+
126331 < OutputContainer isExpanded = { isExpanded } output = { output } />
127332 </ div >
128333 </ >
0 commit comments