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

View File

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

View File

@@ -337,56 +337,96 @@ 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. 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(
switch (clineAsk) { (text?: string, images?: string[]) => {
case "api_req_failed": const trimmedInput = text?.trim()
case "command": switch (clineAsk) {
case "command_output": case "api_req_failed":
case "tool": case "command":
case "browser_action_launch": case "command_output":
case "use_mcp_server": case "tool":
case "resume_task": case "browser_action_launch":
case "mistake_limit_reached": case "use_mcp_server":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) case "resume_task":
break case "mistake_limit_reached":
case "completion_result": // Only send text/images if they exist
case "resume_completed_task": if (trimmedInput || (images && images.length > 0)) {
// extension waiting for feedback. but we can just present a new task button vscode.postMessage({
startNewTask() type: "askResponse",
break askResponse: "yesButtonClicked",
} text: trimmedInput,
setTextAreaDisabled(true) images: images,
setClineAsk(undefined) })
setEnableButtons(false) } else {
disableAutoScrollRef.current = false vscode.postMessage({
}, [clineAsk, startNewTask]) type: "askResponse",
askResponse: "yesButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
case "completion_result":
case "resume_completed_task":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask],
)
const handleSecondaryButtonClick = useCallback(() => { const handleSecondaryButtonClick = useCallback(
if (isStreaming) { (text?: string, images?: string[]) => {
vscode.postMessage({ type: "cancelTask" }) const trimmedInput = text?.trim()
setDidClickCancel(true) if (isStreaming) {
return vscode.postMessage({ type: "cancelTask" })
} setDidClickCancel(true)
return
}
switch (clineAsk) { switch (clineAsk) {
case "api_req_failed": case "api_req_failed":
case "mistake_limit_reached": case "mistake_limit_reached":
case "resume_task": case "resume_task":
startNewTask() startNewTask()
break break
case "command": case "command":
case "tool": case "tool":
case "browser_action_launch": case "browser_action_launch":
case "use_mcp_server": case "use_mcp_server":
// responds to the API with a "This operation failed" and lets it try again // Only send text/images if they exist
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" }) if (trimmedInput || (images && images.length > 0)) {
break vscode.postMessage({
} type: "askResponse",
setTextAreaDisabled(true) askResponse: "noButtonClicked",
setClineAsk(undefined) text: trimmedInput,
setEnableButtons(false) images: images,
disableAutoScrollRef.current = false })
}, [clineAsk, startNewTask, isStreaming]) } else {
// responds to the API with a "This operation failed" and lets it try again
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],
)
const handleTaskCloseButtonClick = useCallback(() => { const handleTaskCloseButtonClick = useCallback(() => {
startNewTask() startNewTask()
@@ -430,10 +470,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
handleSendMessage(message.text ?? "", message.images ?? []) handleSendMessage(message.text ?? "", message.images ?? [])
break break
case "primaryButtonClick": case "primaryButtonClick":
handlePrimaryButtonClick() handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
break break
case "secondaryButtonClick": case "secondaryButtonClick":
handleSecondaryButtonClick() handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
break break
} }
} }
@@ -1038,7 +1078,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2, flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0", marginRight: secondaryButtonText ? "6px" : "0",
}} }}
onClick={handlePrimaryButtonClick}> onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText} {primaryButtonText}
</VSCodeButton> </VSCodeButton>
)} )}
@@ -1050,7 +1090,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1, flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px", marginLeft: isStreaming ? 0 : "6px",
}} }}
onClick={handleSecondaryButtonClick}> onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText} {isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton> </VSCodeButton>
)} )}