Skip to content

Commit add2f85

Browse files
authored
Merge pull request RooCodeInc#1768 from cline/celestial-vault/eng-198-accept-reject-diff-tool-with-user-feedback
Accept tool use with feedback
2 parents 9c8254c + 69ff71b commit add2f85

File tree

5 files changed

+133
-83
lines changed

5 files changed

+133
-83
lines changed

.changeset/wise-phones-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Allowing the user to give feedback when approving a tool use.

src/core/Cline.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,33 +1542,29 @@ export class Cline {
15421542
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
15431543
const { response, text, images } = await this.ask(type, partialMessage, false)
15441544
if (response !== "yesButtonClicked") {
1545+
// User did NOT approve (rejected)
15451546
if (response === "messageResponse") {
1547+
// Rejection WITH feedback
15461548
await this.say("user_feedback", text, images)
15471549
pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
1548-
// this.userMessageContent.push({
1549-
// type: "text",
1550-
// text: `${toolDescription()}`,
1551-
// })
1552-
// this.toolResults.push({
1553-
// type: "tool_result",
1554-
// tool_use_id: toolUseId,
1555-
// content: this.formatToolResponseWithImages(
1556-
// await this.formatToolDeniedFeedback(text),
1557-
// images
1558-
// ),
1559-
// })
1550+
15601551
this.didRejectTool = true
15611552
return false
15621553
}
1554+
// Rejection WITHOUT explicit feedback
15631555
pushToolResult(formatResponse.toolDenied())
1564-
// this.toolResults.push({
1565-
// type: "tool_result",
1566-
// tool_use_id: toolUseId,
1567-
// content: await this.formatToolDenied(),
1568-
// })
1569-
this.didRejectTool = true
1556+
1557+
this.didRejectTool = true // Prevent further tool uses in this message
15701558
return false
15711559
}
1560+
1561+
// Handle yesButtonClicked with text (Acceptance WITH feedback)
1562+
if (text) {
1563+
await this.say("user_feedback", text, images)
1564+
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) // Structured feedback to model on approval
1565+
}
1566+
1567+
// User approved without feedback
15721568
return true
15731569
}
15741570

@@ -1808,11 +1804,14 @@ export class Cline {
18081804
let didApprove = true
18091805
const { response, text, images } = await this.ask("tool", completeMessage, false)
18101806
if (response !== "yesButtonClicked") {
1807+
// User did NOT approve (rejected)
1808+
18111809
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
18121810
const fileDeniedNote = fileExists
18131811
? "The file was not updated, and maintains its original contents."
18141812
: "The file was not created."
18151813
if (response === "messageResponse") {
1814+
// Rejection WITH feedback
18161815
await this.say("user_feedback", text, images)
18171816
pushToolResult(
18181817
formatResponse.toolResult(
@@ -1827,6 +1826,16 @@ export class Cline {
18271826
this.didRejectTool = true
18281827
didApprove = false
18291828
}
1829+
} else {
1830+
// User approved
1831+
1832+
// Handle yesButtonClicked with text (Acceptance WITH feedback)
1833+
if (text) {
1834+
await this.say("user_feedback", text, images)
1835+
pushToolResult(
1836+
formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images),
1837+
)
1838+
}
18301839
}
18311840

18321841
if (!didApprove) {

src/core/prompts/responses.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export const formatResponse = {
99
toolDeniedWithFeedback: (feedback?: string) =>
1010
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
1111

12+
toolApprovedWithFeedback: (feedback?: string) =>
13+
`The user approved this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
14+
1215
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
1316

1417
clineIgnoreError: (path: string) =>

src/services/browser/BrowserSession.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export class BrowserSession {
4343
}
4444

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

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 96 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -324,67 +324,99 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
324324
/*
325325
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.
326326
*/
327-
const handlePrimaryButtonClick = useCallback(() => {
328-
switch (clineAsk) {
329-
case "api_req_failed":
330-
case "command":
331-
case "command_output":
332-
case "tool":
333-
case "browser_action_launch":
334-
case "use_mcp_server":
335-
case "resume_task":
336-
case "mistake_limit_reached":
337-
case "auto_approval_max_req_reached":
338-
vscode.postMessage({
339-
type: "askResponse",
340-
askResponse: "yesButtonClicked",
341-
})
342-
break
343-
case "completion_result":
344-
case "resume_completed_task":
345-
// extension waiting for feedback. but we can just present a new task button
346-
startNewTask()
347-
break
348-
}
349-
setTextAreaDisabled(true)
350-
setClineAsk(undefined)
351-
setEnableButtons(false)
352-
// setPrimaryButtonText(undefined)
353-
// setSecondaryButtonText(undefined)
354-
disableAutoScrollRef.current = false
355-
}, [clineAsk, startNewTask])
356-
357-
const handleSecondaryButtonClick = useCallback(() => {
358-
if (isStreaming) {
359-
vscode.postMessage({ type: "cancelTask" })
360-
setDidClickCancel(true)
361-
return
362-
}
327+
const handlePrimaryButtonClick = useCallback(
328+
(text?: string, images?: string[]) => {
329+
const trimmedInput = text?.trim()
330+
switch (clineAsk) {
331+
case "api_req_failed":
332+
case "command":
333+
case "command_output":
334+
case "tool":
335+
case "browser_action_launch":
336+
case "use_mcp_server":
337+
case "resume_task":
338+
case "mistake_limit_reached":
339+
case "auto_approval_max_req_reached":
340+
if (trimmedInput || (images && images.length > 0)) {
341+
vscode.postMessage({
342+
type: "askResponse",
343+
askResponse: "yesButtonClicked",
344+
text: trimmedInput,
345+
images: images,
346+
})
347+
} else {
348+
vscode.postMessage({
349+
type: "askResponse",
350+
askResponse: "yesButtonClicked",
351+
})
352+
}
353+
// Clear input state after sending
354+
setInputValue("")
355+
setSelectedImages([])
356+
break
357+
case "completion_result":
358+
case "resume_completed_task":
359+
// extension waiting for feedback. but we can just present a new task button
360+
startNewTask()
361+
break
362+
}
363+
setTextAreaDisabled(true)
364+
setClineAsk(undefined)
365+
setEnableButtons(false)
366+
// setPrimaryButtonText(undefined)
367+
// setSecondaryButtonText(undefined)
368+
disableAutoScrollRef.current = false
369+
},
370+
[clineAsk, startNewTask],
371+
)
363372

364-
switch (clineAsk) {
365-
case "api_req_failed":
366-
case "mistake_limit_reached":
367-
case "auto_approval_max_req_reached":
368-
startNewTask()
369-
break
370-
case "command":
371-
case "tool":
372-
case "browser_action_launch":
373-
case "use_mcp_server":
374-
// responds to the API with a "This operation failed" and lets it try again
375-
vscode.postMessage({
376-
type: "askResponse",
377-
askResponse: "noButtonClicked",
378-
})
379-
break
380-
}
381-
setTextAreaDisabled(true)
382-
setClineAsk(undefined)
383-
setEnableButtons(false)
384-
// setPrimaryButtonText(undefined)
385-
// setSecondaryButtonText(undefined)
386-
disableAutoScrollRef.current = false
387-
}, [clineAsk, startNewTask, isStreaming])
373+
const handleSecondaryButtonClick = useCallback(
374+
(text?: string, images?: string[]) => {
375+
const trimmedInput = text?.trim()
376+
if (isStreaming) {
377+
vscode.postMessage({ type: "cancelTask" })
378+
setDidClickCancel(true)
379+
return
380+
}
381+
382+
switch (clineAsk) {
383+
case "api_req_failed":
384+
case "mistake_limit_reached":
385+
case "auto_approval_max_req_reached":
386+
startNewTask()
387+
break
388+
case "command":
389+
case "tool":
390+
case "browser_action_launch":
391+
case "use_mcp_server":
392+
if (trimmedInput || (images && images.length > 0)) {
393+
vscode.postMessage({
394+
type: "askResponse",
395+
askResponse: "noButtonClicked",
396+
text: trimmedInput,
397+
images: images,
398+
})
399+
} else {
400+
// responds to the API with a "This operation failed" and lets it try again
401+
vscode.postMessage({
402+
type: "askResponse",
403+
askResponse: "noButtonClicked",
404+
})
405+
}
406+
// Clear input state after sending
407+
setInputValue("")
408+
setSelectedImages([])
409+
break
410+
}
411+
setTextAreaDisabled(true)
412+
setClineAsk(undefined)
413+
setEnableButtons(false)
414+
// setPrimaryButtonText(undefined)
415+
// setSecondaryButtonText(undefined)
416+
disableAutoScrollRef.current = false
417+
},
418+
[clineAsk, startNewTask, isStreaming],
419+
)
388420

389421
const handleTaskCloseButtonClick = useCallback(() => {
390422
startNewTask()
@@ -426,10 +458,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
426458
handleSendMessage(message.text ?? "", message.images ?? [])
427459
break
428460
case "primaryButtonClick":
429-
handlePrimaryButtonClick()
461+
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
430462
break
431463
case "secondaryButtonClick":
432-
handleSecondaryButtonClick()
464+
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
433465
break
434466
}
435467
}
@@ -869,7 +901,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
869901
flex: secondaryButtonText ? 1 : 2,
870902
marginRight: secondaryButtonText ? "6px" : "0",
871903
}}
872-
onClick={handlePrimaryButtonClick}>
904+
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
873905
{primaryButtonText}
874906
</VSCodeButton>
875907
)}
@@ -881,7 +913,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
881913
flex: isStreaming ? 2 : 1,
882914
marginLeft: isStreaming ? 0 : "6px",
883915
}}
884-
onClick={handleSecondaryButtonClick}>
916+
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
885917
{isStreaming ? "Cancel" : secondaryButtonText}
886918
</VSCodeButton>
887919
)}

0 commit comments

Comments
 (0)