@@ -14,9 +14,116 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
1414import { ExitCodeDetails , RooTerminalCallbacks , RooTerminalProcess } from "../../integrations/terminal/types"
1515import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1616import { Terminal } from "../../integrations/terminal/Terminal"
17+ import { t } from "../../i18n"
1718
1819class ShellIntegrationError extends Error { }
1920
21+ /**
22+ * Extract the base command pattern from a full command string.
23+ * For example: "gh pr checkout 1234" -> "gh pr checkout"
24+ *
25+ * @param command The full command string
26+ * @returns The base command pattern suitable for whitelisting
27+ */
28+ function extractCommandPattern ( command : string ) : string {
29+ if ( ! command ?. trim ( ) ) return ""
30+
31+ // Split by whitespace, handling quoted strings
32+ const parts : string [ ] = [ ]
33+ let current = ""
34+ let inQuotes = false
35+ let quoteChar = ""
36+
37+ for ( let i = 0 ; i < command . length ; i ++ ) {
38+ const char = command [ i ]
39+ const prevChar = i > 0 ? command [ i - 1 ] : ""
40+
41+ if ( ( char === '"' || char === "'" ) && prevChar !== "\\" ) {
42+ if ( ! inQuotes ) {
43+ inQuotes = true
44+ quoteChar = char
45+ } else if ( char === quoteChar ) {
46+ inQuotes = false
47+ quoteChar = ""
48+ }
49+ current += char
50+ } else if ( char === " " && ! inQuotes ) {
51+ if ( current ) {
52+ parts . push ( current )
53+ current = ""
54+ }
55+ } else {
56+ current += char
57+ }
58+ }
59+
60+ if ( current ) {
61+ parts . push ( current )
62+ }
63+
64+ // Extract pattern parts, stopping at arguments
65+ const patternParts : string [ ] = [ ]
66+
67+ for ( const part of parts ) {
68+ // Remove quotes for analysis
69+ const unquoted = part . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" )
70+
71+ // Stop at common argument patterns:
72+ // - Pure numbers (like PR numbers, PIDs, etc.)
73+ // - Flags starting with - or --
74+ // - File paths or URLs
75+ // - Variable assignments (KEY=VALUE)
76+ // - Operators (&&, ||, |, ;, >, <, etc.)
77+ if (
78+ / ^ \d + $ / . test ( unquoted ) ||
79+ unquoted . startsWith ( "-" ) ||
80+ unquoted . includes ( "/" ) ||
81+ unquoted . includes ( "\\" ) ||
82+ unquoted . includes ( "=" ) ||
83+ unquoted . startsWith ( "http" ) ||
84+ unquoted . includes ( "." ) ||
85+ [ "&&" , "||" , "|" , ";" , ">" , "<" , ">>" , "<<" , "&" ] . includes ( unquoted )
86+ ) {
87+ // Stop collecting pattern parts
88+ break
89+ }
90+ patternParts . push ( part )
91+ }
92+
93+ // Return the base command pattern
94+ return patternParts . join ( " " )
95+ }
96+
97+ /**
98+ * Adds a command pattern to the whitelist
99+ */
100+ async function addCommandToWhitelist ( cline : Task , command : string ) : Promise < void > {
101+ const clineProvider = cline . providerRef . deref ( )
102+ if ( ! clineProvider ) {
103+ console . error ( "Provider reference is undefined, cannot add command to whitelist" )
104+ return
105+ }
106+
107+ const state = await clineProvider . getState ( )
108+ const currentCommands = state . allowedCommands ?? [ ]
109+
110+ // Extract the base command pattern for whitelisting
111+ const commandPattern = extractCommandPattern ( command )
112+
113+ // Add command pattern to whitelist if not already present
114+ if ( commandPattern && ! currentCommands . includes ( commandPattern ) ) {
115+ const newCommands = [ ...currentCommands , commandPattern ]
116+ await clineProvider . setValue ( "allowedCommands" , newCommands )
117+
118+ // Notify webview of the updated commands
119+ await clineProvider . postMessageToWebview ( {
120+ type : "invoke" ,
121+ invoke : "setChatBoxMessage" ,
122+ text : t ( "tools:executeCommand.patternAddedToWhitelist" , { pattern : commandPattern } ) ,
123+ } )
124+ }
125+ }
126+
20127export async function executeCommandTool (
21128 cline : Task ,
22129 block : ToolUse ,
@@ -53,40 +160,30 @@ export async function executeCommandTool(
53160 command = unescapeHtmlEntities ( command ) // Unescape HTML entities.
54161
55162 // We need to capture the actual response to check if "Add & Run" was clicked
163+ // Note: We cannot use the provided askApproval function here because we need to
164+ // differentiate between "yesButtonClicked" and "addAndRunButtonClicked" responses
56165 const { response, text, images } = await cline . ask ( "command" , command )
57166
58167 if ( response === "yesButtonClicked" || response === "addAndRunButtonClicked" ) {
59- // Handle yesButtonClicked or addAndRunButtonClicked with text.
168+ // Handle yesButtonClicked or addAndRunButtonClicked with text (following askApproval pattern)
60169 if ( text ) {
61170 await cline . say ( "user_feedback" , text , images )
171+ pushToolResult ( formatResponse . toolResult ( formatResponse . toolApprovedWithFeedback ( text ) , images ) )
62172 }
63173
64174 // Check if user selected "Add & Run" to add command to whitelist
65175 if ( response === "addAndRunButtonClicked" ) {
66- const clineProvider = await cline . providerRef . deref ( )
67- if ( clineProvider ) {
68- const state = await clineProvider . getState ( )
69- const currentCommands = state . allowedCommands ?? [ ]
70-
71- // Add command to whitelist if not already present
72- if ( ! currentCommands . includes ( command ) ) {
73- const newCommands = [ ...currentCommands , command ]
74- await clineProvider . setValue ( "allowedCommands" , newCommands )
75-
76- // Notify webview of the updated commands
77- await clineProvider . postMessageToWebview ( {
78- type : "invoke" ,
79- invoke : "setChatBoxMessage" ,
80- text : `Command "${ command } " added to whitelist.` ,
81- } )
82- }
83- }
176+ await addCommandToWhitelist ( cline , command )
84177 }
85178 } else {
86- // Handle both messageResponse and noButtonClicked with text.
179+ // Handle both messageResponse and noButtonClicked with text (following askApproval pattern)
87180 if ( text ) {
88181 await cline . say ( "user_feedback" , text , images )
182+ pushToolResult ( formatResponse . toolResult ( formatResponse . toolDeniedWithFeedback ( text ) , images ) )
183+ } else {
184+ pushToolResult ( formatResponse . toolDenied ( ) )
89185 }
186+ cline . didRejectTool = true
90187 return
91188 }
92189
0 commit comments