Skip to content

Commit 8fd1210

Browse files
committed
Address PR review comments
- Use extractCommandPattern function to extract base command patterns - Add proper error handling for providerRef.deref() - Use internationalization for user-facing messages - Extract addCommandToWhitelist helper function to reduce duplication - Add comments explaining why we can't use the shared askApproval function
1 parent 95b6c2a commit 8fd1210

File tree

3 files changed

+165
-20
lines changed

3 files changed

+165
-20
lines changed

src/core/tools/executeCommandTool.ts

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,116 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
1414
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
1515
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1616
import { Terminal } from "../../integrations/terminal/Terminal"
17+
import { t } from "../../i18n"
1718

1819
class 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+
20127
export 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

src/i18n/locales/en/tools.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
"errors": {
1313
"policy_restriction": "Failed to create new task due to policy restrictions."
1414
}
15+
},
16+
"executeCommand": {
17+
"patternAddedToWhitelist": "Command pattern '{{pattern}}' added to whitelist"
1518
}
1619
}

webview-ui/src/utils/command-validation.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@ import { parse } from "shell-quote"
22

33
type ShellToken = string | { op: string } | { command: string }
44

5+
/**
6+
* Extract the base command pattern from a full command string.
7+
* For example: "gh pr checkout 1234" -> "gh pr checkout"
8+
*
9+
* @param command The full command string
10+
* @returns The base command pattern suitable for whitelisting
11+
*/
12+
export function extractCommandPattern(command: string): string {
13+
if (!command?.trim()) return ""
14+
15+
// Parse the command to get tokens
16+
const tokens = parse(command.trim()) as ShellToken[]
17+
const patternParts: string[] = []
18+
19+
for (const token of tokens) {
20+
if (typeof token === "string") {
21+
// Check if this token looks like an argument (number, flag, etc.)
22+
// Common patterns to stop at:
23+
// - Pure numbers (like PR numbers, PIDs, etc.)
24+
// - Flags starting with - or --
25+
// - File paths or URLs
26+
// - Variable assignments (KEY=VALUE)
27+
if (
28+
/^\d+$/.test(token) ||
29+
token.startsWith("-") ||
30+
token.includes("/") ||
31+
token.includes("\\") ||
32+
token.includes("=") ||
33+
token.startsWith("http") ||
34+
token.includes(".")
35+
) {
36+
// Stop collecting pattern parts
37+
break
38+
}
39+
patternParts.push(token)
40+
} else if (typeof token === "object" && "op" in token) {
41+
// Stop at operators
42+
break
43+
}
44+
}
45+
46+
// Return the base command pattern
47+
return patternParts.join(" ")
48+
}
49+
550
/**
651
* Split a command string into individual sub-commands by
752
* chaining operators (&&, ||, ;, or |).

0 commit comments

Comments
 (0)