From 7da74bd9d8b2f716c13ac6902b3902e3477f86db Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 11 Feb 2025 13:20:54 -0800 Subject: [PATCH 1/4] add accept feedback --- src/core/Cline.ts | 13 +- src/core/prompts/responses.ts | 3 + webview-ui/src/components/chat/ChatView.tsx | 158 ++++++++++++-------- 3 files changed, 110 insertions(+), 64 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 7d40127edf..faa04cf110 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1542,7 +1542,9 @@ 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({ @@ -1560,15 +1562,24 @@ export class Cline { 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 } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 623e3d8806..8a14acc02b 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -9,6 +9,9 @@ export const formatResponse = { toolDeniedWithFeedback: (feedback?: string) => `The user denied this operation and provided the following feedback:\n\n${feedback}\n`, + toolApprovedWithFeedback: (feedback?: string) => + `The user approved this operation and provided the following feedback:\n\n${feedback}\n`, + toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, clineIgnoreError: (path: string) => diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index cad6e84aff..8b4290cd2a 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -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} )} @@ -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} )} From 96540ddf1e64b7fccf708014be95640e4c0e47ef Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 11 Feb 2025 13:21:22 -0800 Subject: [PATCH 2/4] fix linting error --- src/services/browser/BrowserSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 14b6ecef8b..183979d92a 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -43,8 +43,9 @@ export class BrowserSession { } const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get("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") From f096d73813a947e94cb81e9424b01ff91d5a7747 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 12 Feb 2025 08:46:23 -0800 Subject: [PATCH 3/4] approve with feedback for write_to_file/replace_in_file --- src/core/Cline.ts | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index faa04cf110..4a6609d84c 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1547,28 +1547,13 @@ export class Cline { // 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 // Prevent further tool uses in this message return false } @@ -1819,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( @@ -1838,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) { From 69ff71b051f1d967abf5cd05ec54a4aa2ac3a5c5 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 12 Feb 2025 08:56:29 -0800 Subject: [PATCH 4/4] add changeset --- .changeset/wise-phones-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-phones-mate.md diff --git a/.changeset/wise-phones-mate.md b/.changeset/wise-phones-mate.md new file mode 100644 index 0000000000..7be57b25e9 --- /dev/null +++ b/.changeset/wise-phones-mate.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Allowing the user to give feedback when approving a tool use.