2
0
Эх сурвалжийг харах

Merge pull request #638 from napter/approval-feedback

Allow the user to send context with approval or rejection
Matt Rubens 11 сар өмнө
parent
commit
81a5e805b2

+ 9 - 21
src/core/Cline.ts

@@ -1093,35 +1093,23 @@ 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
-							// 	),
-							// })
-							this.didRejectTool = true
-							return false
+						} else {
+							pushToolResult(formatResponse.toolDenied())
 						}
-						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
 				}
 

+ 3 - 0
src/core/prompts/responses.ts

@@ -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: () =>

+ 93 - 53
webview-ui/src/components/chat/ChatView.tsx

@@ -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.
 	*/
-	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":
-				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)
-		disableAutoScrollRef.current = false
-	}, [clineAsk, startNewTask])
-
-	const handleSecondaryButtonClick = useCallback(() => {
-		if (isStreaming) {
-			vscode.postMessage({ type: "cancelTask" })
-			setDidClickCancel(true)
-			return
-		}
+	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":
+					// 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":
+					// 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],
+	)
 
-		switch (clineAsk) {
-			case "api_req_failed":
-			case "mistake_limit_reached":
-			case "resume_task":
-				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)
-		disableAutoScrollRef.current = false
-	}, [clineAsk, startNewTask, isStreaming])
+	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 "resume_task":
+					startNewTask()
+					break
+				case "command":
+				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",
+						})
+					}
+					// Clear input state after sending
+					setInputValue("")
+					setSelectedImages([])
+					break
+			}
+			setTextAreaDisabled(true)
+			setClineAsk(undefined)
+			setEnableButtons(false)
+			disableAutoScrollRef.current = false
+		},
+		[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>
 							)}