Merge pull request #638 from napter/approval-feedback

Allow the user to send context with approval or rejection
This commit is contained in:
Matt Rubens
2025-01-30 23:36:43 -05:00
committed by GitHub
3 changed files with 104 additions and 73 deletions

View File

@@ -1093,34 +1093,22 @@ export class Cline {
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
const { response, text, images } = await this.ask(type, partialMessage, false)
if (response !== "yesButtonClicked") {
if (response === "messageResponse") {
// Handle both messageResponse and noButtonClicked with text
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
)
// this.userMessageContent.push({
// type: "text",
// text: `${toolDescription()}`,
// })
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: this.formatToolResponseWithImages(
// await this.formatToolDeniedFeedback(text),
// images
// ),
// })
} else {
pushToolResult(formatResponse.toolDenied())
}
this.didRejectTool = true
return false
}
pushToolResult(formatResponse.toolDenied())
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: await this.formatToolDenied(),
// })
this.didRejectTool = true
return false
// Handle yesButtonClicked with text
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
}
return true
}

View File

@@ -8,6 +8,9 @@ export const formatResponse = {
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolApprovedWithFeedback: (feedback?: string) =>
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
noToolsUsed: () =>

View File

@@ -337,7 +337,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
/*
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
*/
const handlePrimaryButtonClick = useCallback(() => {
const handlePrimaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
switch (clineAsk) {
case "api_req_failed":
case "command":
@@ -347,7 +349,23 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
// Only send text/images if they exist
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: trimmedInput,
images: images,
})
} else {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
case "completion_result":
case "resume_completed_task":
@@ -359,9 +377,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask])
},
[clineAsk, startNewTask],
)
const handleSecondaryButtonClick = useCallback(() => {
const handleSecondaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
@@ -378,15 +400,33 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// Only send text/images if they exist
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
text: trimmedInput,
images: images,
})
} else {
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask, isStreaming])
},
[clineAsk, startNewTask, isStreaming],
)
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
@@ -430,10 +470,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
handleSendMessage(message.text ?? "", message.images ?? [])
break
case "primaryButtonClick":
handlePrimaryButtonClick()
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
break
case "secondaryButtonClick":
handleSecondaryButtonClick()
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
break
}
}
@@ -1038,7 +1078,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={handlePrimaryButtonClick}>
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
@@ -1050,7 +1090,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={handleSecondaryButtonClick}>
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}