From e3e4bc225103f97f3000c9abe2c341cfdb25bcfa Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 2 Dec 2025 06:07:53 +0000 Subject: [PATCH] feat: add manual approve/reject buttons for tool requests when auto-approval is disabled - Created new ToolApprovalButtons component with Approve/Reject buttons - Integrated buttons into ChatRow for various tool types (file edits, diffs, commands, etc.) - Buttons only appear when auto-approval is disabled - Buttons send appropriate askResponse messages to backend - Hide buttons after user interaction to prevent duplicate responses Fixes #9739 --- webview-ui/src/components/chat/ChatRow.tsx | 56 +++++++++++++++- .../components/chat/ToolApprovalButtons.tsx | 65 +++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/chat/ToolApprovalButtons.tsx diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 22886be2da..124fc66b85 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -29,6 +29,7 @@ import ErrorRow from "./ErrorRow" import McpResourceRow from "../mcp/McpResourceRow" import { Mention } from "./Mention" +import { ToolApprovalButtons } from "./ToolApprovalButtons" import { CheckpointSaved } from "./checkpoints/CheckpointSaved" import { FollowUpSuggest } from "./FollowUpSuggest" import { BatchFilePermission } from "./BatchFilePermission" @@ -164,7 +165,15 @@ export const ChatRowContent = ({ }: ChatRowContentProps) => { const { t } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState() + const { + mcpServers, + alwaysAllowMcp, + currentCheckpoint, + mode, + apiConfiguration, + clineMessages, + autoApprovalEnabled, + } = useExtensionState() const { info: model } = useSelectedModel(apiConfiguration) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState("") @@ -402,6 +411,13 @@ export const ChatRowContent = ({ + {!autoApprovalEnabled && !message.partial && ( + + )} ) } @@ -438,6 +454,13 @@ export const ChatRowContent = ({ diffStats={tool.diffStats} /> + {message.type === "ask" && !autoApprovalEnabled && !message.partial && ( + + )} ) case "insertContent": @@ -476,6 +499,13 @@ export const ChatRowContent = ({ diffStats={tool.diffStats} /> + {message.type === "ask" && !autoApprovalEnabled && !message.partial && ( + + )} ) case "searchAndReplace": @@ -510,6 +540,13 @@ export const ChatRowContent = ({ diffStats={tool.diffStats} /> + {message.type === "ask" && !autoApprovalEnabled && !message.partial && ( + + )} ) case "codebaseSearch": { @@ -571,6 +608,13 @@ export const ChatRowContent = ({ diffStats={tool.diffStats} /> + {message.type === "ask" && !autoApprovalEnabled && !message.partial && ( + + )} ) case "readFile": @@ -593,6 +637,13 @@ export const ChatRowContent = ({ }} ts={message?.ts} /> + {!autoApprovalEnabled && !message.partial && ( + + )} ) } @@ -633,6 +684,9 @@ export const ChatRowContent = ({ + {message.type === "ask" && !autoApprovalEnabled && !message.partial && ( + + )} ) case "fetchInstructions": diff --git a/webview-ui/src/components/chat/ToolApprovalButtons.tsx b/webview-ui/src/components/chat/ToolApprovalButtons.tsx new file mode 100644 index 0000000000..52c4a36970 --- /dev/null +++ b/webview-ui/src/components/chat/ToolApprovalButtons.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react" +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { Check, X } from "lucide-react" +import { vscode } from "@src/utils/vscode" + +interface ToolApprovalButtonsProps { + messageTs: number + isProtected?: boolean + disabled?: boolean + onResponse?: () => void +} + +export const ToolApprovalButtons: React.FC = ({ + messageTs: _messageTs, // Prefixed with _ to indicate intentionally unused + isProtected = false, + disabled = false, + onResponse, +}) => { + const [hasResponded, setHasResponded] = useState(false) + + const handleApprove = () => { + if (hasResponded) return + setHasResponded(true) + vscode.postMessage({ + type: "askResponse", + askResponse: "yesButtonClicked", + }) + onResponse?.() + } + + const handleReject = () => { + if (hasResponded) return + setHasResponded(true) + vscode.postMessage({ + type: "askResponse", + askResponse: "noButtonClicked", + }) + onResponse?.() + } + + if (hasResponded) { + return null // Hide buttons after response + } + + return ( +
+ + + {isProtected ? "Approve (Protected File)" : "Approve"} + + + + Reject + +
+ ) +}