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
This commit is contained in:
Roo Code 2025-12-02 06:07:53 +00:00
parent 906c6f0de4
commit e3e4bc2251
2 changed files with 120 additions and 1 deletions

View file

@ -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 = ({
</span>
</div>
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
{!autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={false}
disabled={isStreaming}
/>
)}
</>
)
}
@ -438,6 +454,13 @@ export const ChatRowContent = ({
diffStats={tool.diffStats}
/>
</div>
{message.type === "ask" && !autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={tool.isProtected}
disabled={isStreaming}
/>
)}
</>
)
case "insertContent":
@ -476,6 +499,13 @@ export const ChatRowContent = ({
diffStats={tool.diffStats}
/>
</div>
{message.type === "ask" && !autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={tool.isProtected}
disabled={isStreaming}
/>
)}
</>
)
case "searchAndReplace":
@ -510,6 +540,13 @@ export const ChatRowContent = ({
diffStats={tool.diffStats}
/>
</div>
{message.type === "ask" && !autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={tool.isProtected}
disabled={isStreaming}
/>
)}
</>
)
case "codebaseSearch": {
@ -571,6 +608,13 @@ export const ChatRowContent = ({
diffStats={tool.diffStats}
/>
</div>
{message.type === "ask" && !autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={tool.isProtected}
disabled={isStreaming}
/>
)}
</>
)
case "readFile":
@ -593,6 +637,13 @@ export const ChatRowContent = ({
}}
ts={message?.ts}
/>
{!autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons
messageTs={message.ts}
isProtected={false}
disabled={isStreaming}
/>
)}
</>
)
}
@ -633,6 +684,9 @@ export const ChatRowContent = ({
</ToolUseBlockHeader>
</ToolUseBlock>
</div>
{message.type === "ask" && !autoApprovalEnabled && !message.partial && (
<ToolApprovalButtons messageTs={message.ts} isProtected={false} disabled={isStreaming} />
)}
</>
)
case "fetchInstructions":

View file

@ -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<ToolApprovalButtonsProps> = ({
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 (
<div className="flex gap-2 mt-3 ml-6">
<VSCodeButton
appearance="primary"
onClick={handleApprove}
disabled={disabled}
style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<Check className="w-4 h-4" />
{isProtected ? "Approve (Protected File)" : "Approve"}
</VSCodeButton>
<VSCodeButton
appearance="secondary"
onClick={handleReject}
disabled={disabled}
style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<X className="w-4 h-4" />
Reject
</VSCodeButton>
</div>
)
}