Merge pull request #1768 from cline/celestial-vault/eng-198-accept-reject-diff-tool-with-user-feedback

Accept tool use with feedback
This commit is contained in:
pashpashpash 2025-02-12 10:21:42 -08:00 committed by GitHub
commit add2f85053
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 82 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Allowing the user to give feedback when approving a tool use.

View file

@ -1542,33 +1542,29 @@ export class Cline {
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
const { response, text, images } = await this.ask(type, partialMessage, false)
if (response !== "yesButtonClicked") {
// User did NOT approve (rejected)
if (response === "messageResponse") {
// Rejection WITH feedback
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
// ),
// })
this.didRejectTool = true
return false
}
// Rejection WITHOUT explicit feedback
pushToolResult(formatResponse.toolDenied())
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: await this.formatToolDenied(),
// })
this.didRejectTool = true
this.didRejectTool = true // Prevent further tool uses in this message
return false
}
// Handle yesButtonClicked with text (Acceptance WITH feedback)
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) // Structured feedback to model on approval
}
// User approved without feedback
return true
}
@ -1808,11 +1804,14 @@ export class Cline {
let didApprove = true
const { response, text, images } = await this.ask("tool", completeMessage, false)
if (response !== "yesButtonClicked") {
// User did NOT approve (rejected)
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
const fileDeniedNote = fileExists
? "The file was not updated, and maintains its original contents."
: "The file was not created."
if (response === "messageResponse") {
// Rejection WITH feedback
await this.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(
@ -1827,6 +1826,16 @@ export class Cline {
this.didRejectTool = true
didApprove = false
}
} else {
// User approved
// Handle yesButtonClicked with text (Acceptance WITH feedback)
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images),
)
}
}
if (!didApprove) {

View file

@ -9,6 +9,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 feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
clineIgnoreError: (path: string) =>

View file

@ -43,8 +43,9 @@ export class BrowserSession {
}
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath)))
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
}
const stats: PCRStats = chromeExecutablePath
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")

View file

@ -324,67 +324,99 @@ 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(() => {
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
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)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask])
const handlePrimaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
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":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask],
)
const handleSecondaryButtonClick = useCallback(() => {
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
const handleSecondaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask, isStreaming])
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
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",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask, isStreaming],
)
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
@ -426,10 +458,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
}
}
@ -869,7 +901,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={handlePrimaryButtonClick}>
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
@ -881,7 +913,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={handleSecondaryButtonClick}>
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}