mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
feat: Add 'Add & Run' button to command approval UI
- Add third button option to command approval dialog - Implement streamlined whitelisting workflow that adds command to whitelist and executes it - Update UI to show three buttons: 'Run Command', 'Add & Run', and 'Reject' - Add backend logic to detect 'Add & Run' selection and update allowedCommands setting - Enhance message passing system to support tertiary button interactions Fixes #5290
This commit is contained in:
parent
3a8ba27615
commit
95b6c2a8d5
5 changed files with 222 additions and 66 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const [enableButtons, setEnableButtons] = useState<boolean>(false)
|
||||
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
|
||||
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
|
||||
const [tertiaryButtonText, setTertiaryButtonText] = useState<string | undefined>(undefined)
|
||||
const [didClickCancel, setDidClickCancel] = useState(false)
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
||||
|
|
@ -312,7 +313,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setClineAsk("command")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText(t("chat:runCommand.title"))
|
||||
setSecondaryButtonText(t("chat:reject.title"))
|
||||
setSecondaryButtonText("Add & Run")
|
||||
setTertiaryButtonText(t("chat:reject.title"))
|
||||
break
|
||||
case "command_output":
|
||||
setSendingDisabled(false)
|
||||
|
|
@ -609,6 +611,22 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
// For command case, secondary button is "Add & Run"
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 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<ChatViewRef, ChatViewPro
|
|||
[clineAsk, startNewTask, isStreaming],
|
||||
)
|
||||
|
||||
const handleTertiaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
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<ChatViewRef, ChatViewPro
|
|||
case "secondaryButtonClick":
|
||||
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "tertiaryButtonClick":
|
||||
handleTertiaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
}
|
||||
break
|
||||
case "condenseTaskContextResponse":
|
||||
|
|
@ -716,6 +767,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleSetChatBoxMessage,
|
||||
handlePrimaryButtonClick,
|
||||
handleSecondaryButtonClick,
|
||||
handleTertiaryButtonClick,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -1490,7 +1542,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`flex ${
|
||||
className={`${
|
||||
primaryButtonText || secondaryButtonText || isStreaming ? "px-[15px] pt-[10px]" : "p-0"
|
||||
} ${
|
||||
primaryButtonText || secondaryButtonText || isStreaming
|
||||
|
|
@ -1498,59 +1550,120 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
? "opacity-100"
|
||||
: "opacity-50"
|
||||
: "opacity-0"
|
||||
}`}>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
primaryButtonText === t("chat:retry.title")
|
||||
? t("chat:retry.tooltip")
|
||||
: primaryButtonText === t("chat:save.title")
|
||||
? t("chat:save.tooltip")
|
||||
: primaryButtonText === t("chat:approve.title")
|
||||
? t("chat:approve.tooltip")
|
||||
: primaryButtonText === t("chat:runCommand.title")
|
||||
} ${tertiaryButtonText ? "flex flex-col gap-[6px]" : "flex"}`}>
|
||||
{/* Three-button layout for command approval */}
|
||||
{tertiaryButtonText && !isStreaming ? (
|
||||
<>
|
||||
{/* Top row: Run and Add & Run buttons */}
|
||||
<div className="flex gap-[6px]">
|
||||
{primaryButtonText && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
primaryButtonText === t("chat:runCommand.title")
|
||||
? t("chat:runCommand.tooltip")
|
||||
: primaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: primaryButtonText === t("chat:resumeTask.title")
|
||||
? t("chat:resumeTask.tooltip")
|
||||
: primaryButtonText === t("chat:proceedAnyways.title")
|
||||
? t("chat:proceedAnyways.tooltip")
|
||||
: primaryButtonText ===
|
||||
t("chat:proceedWhileRunning.title")
|
||||
? t("chat:proceedWhileRunning.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{(secondaryButtonText || isStreaming) && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
isStreaming
|
||||
? t("chat:cancel.tooltip")
|
||||
: secondaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: secondaryButtonText === t("chat:reject.title")
|
||||
? t("chat:reject.tooltip")
|
||||
: secondaryButtonText === t("chat:terminate.title")
|
||||
? t("chat:terminate.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
|
||||
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
className="flex-1"
|
||||
onClick={() =>
|
||||
handlePrimaryButtonClick(inputValue, selectedImages)
|
||||
}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{secondaryButtonText && (
|
||||
<StandardTooltip content="Add command to whitelist and run it">
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons}
|
||||
className="flex-1"
|
||||
onClick={() =>
|
||||
handleSecondaryButtonClick(inputValue, selectedImages)
|
||||
}>
|
||||
{secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
{/* Bottom row: Reject button */}
|
||||
<div className="flex">
|
||||
<StandardTooltip
|
||||
content={
|
||||
tertiaryButtonText === t("chat:reject.title")
|
||||
? t("chat:reject.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons}
|
||||
className="flex-1"
|
||||
onClick={() => handleTertiaryButtonClick(inputValue, selectedImages)}>
|
||||
{tertiaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Two-button layout for other cases */
|
||||
<>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
primaryButtonText === t("chat:retry.title")
|
||||
? t("chat:retry.tooltip")
|
||||
: primaryButtonText === t("chat:save.title")
|
||||
? t("chat:save.tooltip")
|
||||
: primaryButtonText === t("chat:approve.title")
|
||||
? t("chat:approve.tooltip")
|
||||
: primaryButtonText === t("chat:runCommand.title")
|
||||
? t("chat:runCommand.tooltip")
|
||||
: primaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: primaryButtonText === t("chat:resumeTask.title")
|
||||
? t("chat:resumeTask.tooltip")
|
||||
: primaryButtonText ===
|
||||
t("chat:proceedAnyways.title")
|
||||
? t("chat:proceedAnyways.tooltip")
|
||||
: primaryButtonText ===
|
||||
t("chat:proceedWhileRunning.title")
|
||||
? t("chat:proceedWhileRunning.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{(secondaryButtonText || isStreaming) && !tertiaryButtonText && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
isStreaming
|
||||
? t("chat:cancel.tooltip")
|
||||
: secondaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: secondaryButtonText === t("chat:reject.title")
|
||||
? t("chat:reject.tooltip")
|
||||
: secondaryButtonText === t("chat:terminate.title")
|
||||
? t("chat:terminate.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
|
||||
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue