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}
)}