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..e9e9a42a1f 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -51,9 +51,42 @@ 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 + const { response, text, images } = await cline.ask("command", command) + + if (response === "yesButtonClicked" || response === "addAndRunButtonClicked") { + // Handle yesButtonClicked or addAndRunButtonClicked with text. + if (text) { + await cline.say("user_feedback", text, images) + } + + // Check if user selected "Add & Run" to add command to whitelist + if (response === "addAndRunButtonClicked") { + const clineProvider = await cline.providerRef.deref() + if (clineProvider) { + const state = await clineProvider.getState() + const currentCommands = state.allowedCommands ?? [] + + // Add command to whitelist if not already present + if (!currentCommands.includes(command)) { + const newCommands = [...currentCommands, command] + await clineProvider.setValue("allowedCommands", newCommands) + + // Notify webview of the updated commands + await clineProvider.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: `Command "${command}" added to whitelist.`, + }) + } + } + } + } else { + // Handle both messageResponse and noButtonClicked with text. + if (text) { + await cline.say("user_feedback", text, images) + } return } 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/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a4f18c870c..5ddce925ca 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} + + + )} + )}
)}