diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5030055fea..b6569d4113 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,6 +723,9 @@ importers: serialize-error: specifier: ^12.0.0 version: 12.0.0 + shell-quote: + specifier: 1.8.3 + version: 1.8.3 simple-git: specifier: ^3.27.0 version: 3.27.0 @@ -814,6 +817,9 @@ importers: '@types/ps-tree': specifier: ^1.1.6 version: 1.1.6 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/stream-json': specifier: ^1.7.8 version: 1.7.8 @@ -1028,8 +1034,8 @@ importers: specifier: ^0.6.0 version: 0.6.2 shell-quote: - specifier: ^1.8.2 - version: 1.8.2 + specifier: ^1.8.3 + version: 1.8.3 shiki: specifier: ^3.2.1 version: 3.4.1 @@ -8478,10 +8484,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.2: - resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} - engines: {node: '>= 0.4'} - shell-quote@1.8.3: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} @@ -13295,7 +13297,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -17293,7 +17295,7 @@ snapshots: minimatch: 10.0.1 pidtree: 0.6.0 read-package-json-fast: 4.0.0 - shell-quote: 1.8.2 + shell-quote: 1.8.3 which: 5.0.0 npm-run-path@4.0.1: @@ -18522,10 +18524,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.2: {} - - shell-quote@1.8.3: - optional: true + shell-quote@1.8.3: {} shiki@3.4.1: dependencies: diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 21c973ab50..de41e91185 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -271,7 +271,14 @@ export async function presentAssistantMessage(cline: Task) { isProtected || false, ) - if (response !== "yesButtonClicked") { + if (response === "yesButtonClicked" || response === "addAndRunButtonClicked") { + // Handle yesButtonClicked or addAndRunButtonClicked with text. + if (text) { + await cline.say("user_feedback", text, images) + pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) + } + return true + } else { // Handle both messageResponse and noButtonClicked with text. if (text) { await cline.say("user_feedback", text, images) @@ -282,14 +289,6 @@ export async function presentAssistantMessage(cline: Task) { cline.didRejectTool = true return false } - - // Handle yesButtonClicked with text. - if (text) { - await cline.say("user_feedback", text, images) - pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) - } - - return true } const askFinishSubTaskApproval = async () => { diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 795beccc06..80a66834d2 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -14,9 +14,41 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization" import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { Terminal } from "../../integrations/terminal/Terminal" +import { t } from "../../i18n" +import { extractCommandPattern } from "../../shared/extract-command-pattern" class ShellIntegrationError extends Error {} +/** + * Adds a command pattern to the whitelist + */ +async function addCommandToWhitelist(cline: Task, command: string): Promise { + const clineProvider = cline.providerRef.deref() + if (!clineProvider) { + console.error("Provider reference is undefined, cannot add command to whitelist") + return + } + + const state = await clineProvider.getState() + const currentCommands = state.allowedCommands ?? [] + + // Extract the base command pattern for whitelisting + const commandPattern = extractCommandPattern(command) + + // Add command pattern to whitelist if not already present + if (commandPattern && !currentCommands.includes(commandPattern)) { + const newCommands = [...currentCommands, commandPattern] + await clineProvider.setValue("allowedCommands", newCommands) + + // Notify webview of the updated commands + await clineProvider.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: t("tools:executeCommand.patternAddedToWhitelist", { pattern: commandPattern }), + }) + } +} + export async function executeCommandTool( cline: Task, block: ToolUse, @@ -51,9 +83,32 @@ export async function executeCommandTool( cline.consecutiveMistakeCount = 0 command = unescapeHtmlEntities(command) // Unescape HTML entities. - const didApprove = await askApproval("command", command) - if (!didApprove) { + // We need to capture the actual response to check if "Add & Run" was clicked + // Note: We cannot use the provided askApproval function here because we need to + // differentiate between "yesButtonClicked" and "addAndRunButtonClicked" responses + const { response, text, images } = await cline.ask("command", command) + + if (response === "yesButtonClicked" || response === "addAndRunButtonClicked") { + // Handle yesButtonClicked or addAndRunButtonClicked with text (following askApproval pattern) + if (text) { + await cline.say("user_feedback", text, images) + pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) + } + + // Check if user selected "Add & Run" to add command to whitelist + if (response === "addAndRunButtonClicked") { + await addCommandToWhitelist(cline, command) + } + } else { + // Handle both messageResponse and noButtonClicked with text (following askApproval pattern) + if (text) { + await cline.say("user_feedback", text, images) + pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)) + } else { + pushToolResult(formatResponse.toolDenied()) + } + cline.didRejectTool = true return } diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 5b3a228bde..835523f147 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "No s'ha pogut crear una nova tasca a causa de restriccions de política." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index eb1afbc082..16e871dc66 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Neue Aufgabe konnte aufgrund von Richtlinienbeschränkungen nicht erstellt werden." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 0265a84398..9f3992d4de 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Failed to create new task due to policy restrictions." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern '{{pattern}}' added to whitelist" } } diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 303f5365ed..539010428e 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "No se pudo crear una nueva tarea debido a restricciones de política." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index a6c71aca33..e8bf5c3ce5 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Impossible de créer une nouvelle tâche en raison de restrictions de politique." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 0cb4aeb14e..f35a4798a5 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "नीति प्रतिबंधों के कारण नया कार्य बनाने में विफल।" } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 2e3c4f0c22..185a323b1a 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -15,5 +15,8 @@ "errors": { "policy_restriction": "Gagal membuat tugas baru karena pembatasan kebijakan." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index ffae474f1d..1733d157aa 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Impossibile creare una nuova attività a causa di restrizioni di policy." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index 04a5fcc085..0876a6c4b4 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "ポリシー制限により新しいタスクを作成できませんでした。" } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index e43a541794..fb888a231f 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "정책 제한으로 인해 새 작업을 생성하지 못했습니다." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 56a8cdbc46..4157e59340 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Kan geen nieuwe taak aanmaken vanwege beleidsbeperkingen." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 62568826aa..cd7e8a273e 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Nie udało się utworzyć nowego zadania z powodu ograniczeń polityki." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index f74e0f8196..9855d07875 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Falha ao criar nova tarefa devido a restrições de política." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 1e59d10499..39de1ef758 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Не удалось создать новую задачу из-за ограничений политики." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index e4c73cdc4b..f7d8682a9c 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Politika kısıtlamaları nedeniyle yeni görev oluşturulamadı." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 9811ee12c9..269b0beebb 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "Không thể tạo nhiệm vụ mới do hạn chế chính sách." } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 13641b8d43..e28d3b2113 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "由于策略限制,无法创建新任务。" } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index a726e3c919..4a20d9bac4 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -12,5 +12,8 @@ "errors": { "policy_restriction": "由於政策限制,無法建立新工作。" } + }, + "executeCommand": { + "patternAddedToWhitelist": "Command pattern \"{{pattern}}\" has been added to the allowed commands list." } } diff --git a/src/package.json b/src/package.json index 6412b4b858..79263d8b75 100644 --- a/src/package.json +++ b/src/package.json @@ -407,6 +407,7 @@ "sanitize-filename": "^1.6.3", "say": "^0.16.0", "serialize-error": "^12.0.0", + "shell-quote": "1.8.3", "simple-git": "^3.27.0", "sound-play": "^1.1.0", "stream-json": "^1.8.0", @@ -439,6 +440,7 @@ "@types/node-ipc": "^9.2.3", "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", + "@types/shell-quote": "^1.7.5", "@types/stream-json": "^1.7.8", "@types/string-similarity": "^4.0.2", "@types/tmp": "^0.2.6", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 73ebf59d4c..15f58720c1 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -112,7 +112,13 @@ export interface ExtensionMessage { | "didBecomeVisible" | "focusInput" | "switchTab" - invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage" + invoke?: + | "newChat" + | "sendMessage" + | "primaryButtonClick" + | "secondaryButtonClick" + | "tertiaryButtonClick" + | "setChatBoxMessage" state?: ExtensionState images?: string[] filePaths?: string[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7efc97e8c7..17a25b6a72 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -12,7 +12,12 @@ import { marketplaceItemSchema } from "@roo-code/types" import { Mode } from "./modes" -export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" | "objectResponse" +export type ClineAskResponse = + | "yesButtonClicked" + | "noButtonClicked" + | "addAndRunButtonClicked" + | "messageResponse" + | "objectResponse" export type PromptMode = Mode | "enhance" diff --git a/src/shared/extract-command-pattern.ts b/src/shared/extract-command-pattern.ts new file mode 100644 index 0000000000..6bf16d1d3a --- /dev/null +++ b/src/shared/extract-command-pattern.ts @@ -0,0 +1,50 @@ +import { parse } from "shell-quote" + +type ShellToken = string | { op: string } | { command: string } + +/** + * Extract the base command pattern from a full command string. + * For example: "gh pr checkout 1234" -> "gh pr checkout" + * + * Uses shell-quote v1.8.3 for proper shell parsing. + * + * @param command The full command string + * @returns The base command pattern suitable for whitelisting + */ +export function extractCommandPattern(command: string): string { + if (!command?.trim()) return "" + + // Parse the command to get tokens + const tokens = parse(command.trim()) as ShellToken[] + const patternParts: string[] = [] + + for (const token of tokens) { + if (typeof token === "string") { + // Check if this token looks like an argument (number, flag, etc.) + // Common patterns to stop at: + // - Pure numbers (like PR numbers, PIDs, etc.) + // - Flags starting with - or -- + // - File paths or URLs + // - Variable assignments (KEY=VALUE) + if ( + /^\d+$/.test(token) || + token.startsWith("-") || + token.includes("/") || + token.includes("\\") || + token.includes("=") || + token.startsWith("http") || + token.includes(".") + ) { + // Stop collecting pattern parts + break + } + patternParts.push(token) + } else if (typeof token === "object" && "op" in token) { + // Stop at operators + break + } + } + + // Return the base command pattern + return patternParts.join(" ") +} diff --git a/webview-ui/package.json b/webview-ui/package.json index 4c6edc7a2b..86dd21525d 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -65,7 +65,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remove-markdown": "^0.6.0", - "shell-quote": "^1.8.2", + "shell-quote": "^1.8.3", "shiki": "^3.2.1", "source-map": "^0.7.4", "styled-components": "^6.1.13", diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a4f18c870c..dc8c3c7a9f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -139,6 +139,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(false) const [primaryButtonText, setPrimaryButtonText] = useState(undefined) const [secondaryButtonText, setSecondaryButtonText] = useState(undefined) + const [tertiaryButtonText, setTertiaryButtonText] = useState(undefined) const [didClickCancel, setDidClickCancel] = useState(false) const virtuosoRef = useRef(null) const [expandedRows, setExpandedRows] = useState>({}) @@ -312,7 +313,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction 0)) { + vscode.postMessage({ + type: "askResponse", + askResponse: "addAndRunButtonClicked", + text: trimmedInput, + images: images, + }) + } else { + vscode.postMessage({ type: "askResponse", askResponse: "addAndRunButtonClicked" }) + } + // Clear input state after sending + setInputValue("") + setSelectedImages([]) + break case "tool": case "browser_action_launch": case "use_mcp_server": @@ -639,6 +657,36 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const trimmedInput = text?.trim() + + switch (clineAsk) { + case "command": + // For command case, tertiary button is "Reject" + // Only send text/images if they exist + if (trimmedInput || (images && images.length > 0)) { + vscode.postMessage({ + type: "askResponse", + askResponse: "noButtonClicked", + text: trimmedInput, + images: images, + }) + } else { + vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" }) + } + // Clear input state after sending + setInputValue("") + setSelectedImages([]) + break + } + setSendingDisabled(true) + setClineAsk(undefined) + setEnableButtons(false) + }, + [clineAsk], + ) + const handleTaskCloseButtonClick = useCallback(() => startNewTask(), [startNewTask]) const { info: model } = useSelectedModel(apiConfiguration) @@ -690,6 +738,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) : (
- {primaryButtonText && !isStreaming && ( - + {/* Three-button layout for command approval */} + {tertiaryButtonText && !isStreaming ? ( + <> + {/* Top row: Run and Add & Run buttons */} +
+ {primaryButtonText && ( + - handlePrimaryButtonClick(inputValue, selectedImages)}> - {primaryButtonText} - - - )} - {(secondaryButtonText || isStreaming) && ( - - handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} - - + }> + + handlePrimaryButtonClick(inputValue, selectedImages) + }> + {primaryButtonText} + + + )} + {secondaryButtonText && ( + + + handleSecondaryButtonClick(inputValue, selectedImages) + }> + {secondaryButtonText} + + + )} +
+ {/* Bottom row: Reject button */} +
+ + handleTertiaryButtonClick(inputValue, selectedImages)}> + {tertiaryButtonText} + + +
+ + ) : ( + /* Two-button layout for other cases */ + <> + {primaryButtonText && !isStreaming && ( + + handlePrimaryButtonClick(inputValue, selectedImages)}> + {primaryButtonText} + + + )} + {(secondaryButtonText || isStreaming) && !tertiaryButtonText && ( + + handleSecondaryButtonClick(inputValue, selectedImages)}> + {isStreaming ? t("chat:cancel.title") : secondaryButtonText} + + + )} + )}
)} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 4db48e40f3..82655c9786 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -46,6 +46,10 @@ "title": "Desar", "tooltip": "Desa els canvis del fitxer" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Rebutjar", "tooltip": "Rebutja aquesta acció" diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 3ee69f9ae4..1e243fb909 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -46,6 +46,10 @@ "title": "Speichern", "tooltip": "Dateiänderungen speichern" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Ablehnen", "tooltip": "Diese Aktion ablehnen" diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 9ee8e167bd..5257e42e8e 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -46,6 +46,10 @@ "tokensUsed": "Tokens used: {{used}} of {{total}}", "reservedForResponse": "Reserved for model response: {{amount}} tokens" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Reject", "tooltip": "Reject this action" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 7072d0ea16..ddafcab7ba 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -46,6 +46,10 @@ "tokensUsed": "Tokens utilizados: {{used}} de {{total}}", "reservedForResponse": "Reservado para respuesta del modelo: {{amount}} tokens" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Rechazar", "tooltip": "Rechazar esta acción" diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 25d5074f45..7a8888b38f 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -46,6 +46,10 @@ "title": "Enregistrer", "tooltip": "Sauvegarder les modifications du fichier" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Rejeter", "tooltip": "Rejeter cette action" diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index e67068d090..a023e7dadd 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -46,6 +46,10 @@ "title": "सहेजें", "tooltip": "फ़ाइल परिवर्तन सहेजें" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "अस्वीकार करें", "tooltip": "इस क्रिया को अस्वीकार करें" diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 259665a406..ddbfa5213f 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -52,6 +52,10 @@ "tokensUsed": "Token digunakan: {{used}} dari {{total}}", "reservedForResponse": "Dicadangkan untuk respons model: {{amount}} token" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Tolak", "tooltip": "Tolak aksi ini" diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index bbf8c8be6a..3a6eea790b 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -46,6 +46,10 @@ "title": "Salva", "tooltip": "Salva le modifiche al file" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Rifiuta", "tooltip": "Rifiuta questa azione" diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0278edd4b6..f18c5279a5 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -46,6 +46,10 @@ "title": "保存", "tooltip": "ファイル変更を保存" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "拒否", "tooltip": "このアクションを拒否" diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 5373831426..5513e74789 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -46,6 +46,10 @@ "title": "저장", "tooltip": "파일 변경사항 저장" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "거부", "tooltip": "이 작업 거부" diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 2dc60da09e..54a43756a1 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -46,6 +46,10 @@ "tokensUsed": "Gebruikte tokens: {{used}} van {{total}}", "reservedForResponse": "Gereserveerd voor modelantwoord: {{amount}} tokens" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Weigeren", "tooltip": "Deze actie weigeren" diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 0af70c595b..012f7f7aec 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -46,6 +46,10 @@ "title": "Zapisz", "tooltip": "Zapisz zmiany w pliku" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Odrzuć", "tooltip": "Odrzuć tę akcję" diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index a09f8174f6..57a516466b 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -46,6 +46,10 @@ "title": "Salvar", "tooltip": "Salvar as alterações do arquivo" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Rejeitar", "tooltip": "Rejeitar esta ação" diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 89d35f322a..ff09fe263c 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -46,6 +46,10 @@ "tokensUsed": "Использовано токенов: {{used}} из {{total}}", "reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Отклонить", "tooltip": "Отклонить это действие" diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index f12fa62bbb..65f50b062e 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -46,6 +46,10 @@ "title": "Kaydet", "tooltip": "Dosya değişikliklerini kaydet" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Reddet", "tooltip": "Bu eylemi reddet" diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index c2338e33aa..68c7f09302 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -46,6 +46,10 @@ "title": "Lưu", "tooltip": "Lưu các thay đổi tệp" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "Từ chối", "tooltip": "Từ chối hành động này" diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index f3fb857f06..9cfd95dcf2 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -46,6 +46,10 @@ "title": "保存", "tooltip": "保存文件更改" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "拒绝", "tooltip": "拒绝此操作" diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 9a20f129a3..46bc2e9eaa 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -46,6 +46,10 @@ "title": "儲存", "tooltip": "儲存檔案變更" }, + "addAndRun": { + "title": "Add & Run", + "tooltip": "Add command to whitelist and run it" + }, "reject": { "title": "拒絕", "tooltip": "拒絕此操作" diff --git a/webview-ui/src/utils/command-validation.ts b/webview-ui/src/utils/command-validation.ts index 7ad21b1675..d4943ec5af 100644 --- a/webview-ui/src/utils/command-validation.ts +++ b/webview-ui/src/utils/command-validation.ts @@ -2,6 +2,51 @@ import { parse } from "shell-quote" type ShellToken = string | { op: string } | { command: string } +/** + * Extract the base command pattern from a full command string. + * For example: "gh pr checkout 1234" -> "gh pr checkout" + * + * @param command The full command string + * @returns The base command pattern suitable for whitelisting + */ +export function extractCommandPattern(command: string): string { + if (!command?.trim()) return "" + + // Parse the command to get tokens + const tokens = parse(command.trim()) as ShellToken[] + const patternParts: string[] = [] + + for (const token of tokens) { + if (typeof token === "string") { + // Check if this token looks like an argument (number, flag, etc.) + // Common patterns to stop at: + // - Pure numbers (like PR numbers, PIDs, etc.) + // - Flags starting with - or -- + // - File paths or URLs + // - Variable assignments (KEY=VALUE) + if ( + /^\d+$/.test(token) || + token.startsWith("-") || + token.includes("/") || + token.includes("\\") || + token.includes("=") || + token.startsWith("http") || + token.includes(".") + ) { + // Stop collecting pattern parts + break + } + patternParts.push(token) + } else if (typeof token === "object" && "op" in token) { + // Stop at operators + break + } + } + + // Return the base command pattern + return patternParts.join(" ") +} + /** * Split a command string into individual sub-commands by * chaining operators (&&, ||, ;, or |).