Allow feedback to tool or command use

This commit is contained in:
Saoud Rizwan
2024-07-22 08:39:09 -04:00
parent 612db6d070
commit 30b39d6d76
2 changed files with 47 additions and 19 deletions

View File

@@ -334,7 +334,7 @@ export class ClaudeDev {
}) })
.join("") .join("")
const { response } = await this.ask( const { response, text } = await this.ask(
"tool", "tool",
JSON.stringify({ JSON.stringify({
tool: "editedExistingFile", tool: "editedExistingFile",
@@ -343,18 +343,26 @@ export class ClaudeDev {
} as ClaudeSayTool) } as ClaudeSayTool)
) )
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "This operation was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
await fs.writeFile(filePath, newContent) await fs.writeFile(filePath, newContent)
return `Changes applied to ${filePath}:\n${diffResult}` return `Changes applied to ${filePath}:\n${diffResult}`
} else { } else {
const { response } = await this.ask( const { response, text } = await this.ask(
"tool", "tool",
JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool) JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool)
) )
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "This operation was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
await fs.mkdir(path.dirname(filePath), { recursive: true }) await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, newContent) await fs.writeFile(filePath, newContent)
@@ -370,12 +378,16 @@ export class ClaudeDev {
async readFile(filePath: string): Promise<string> { async readFile(filePath: string): Promise<string> {
try { try {
const content = await fs.readFile(filePath, "utf-8") const content = await fs.readFile(filePath, "utf-8")
const { response } = await this.ask( const { response, text } = await this.ask(
"tool", "tool",
JSON.stringify({ tool: "readFile", path: filePath, content } as ClaudeSayTool) JSON.stringify({ tool: "readFile", path: filePath, content } as ClaudeSayTool)
) )
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "This operation was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
return content return content
} catch (error) { } catch (error) {
@@ -391,12 +403,16 @@ export class ClaudeDev {
const isRoot = absolutePath === root const isRoot = absolutePath === root
if (isRoot) { if (isRoot) {
if (shouldLog) { if (shouldLog) {
const { response } = await this.ask( const { response, text } = await this.ask(
"tool", "tool",
JSON.stringify({ tool: "listFiles", path: dirPath, content: root } as ClaudeSayTool) JSON.stringify({ tool: "listFiles", path: dirPath, content: root } as ClaudeSayTool)
) )
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "This operation was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
} }
return root return root
@@ -412,12 +428,16 @@ export class ClaudeDev {
const entries = await glob("*", options) const entries = await glob("*", options)
const result = entries.slice(0, 500).join("\n") // truncate to 500 entries const result = entries.slice(0, 500).join("\n") // truncate to 500 entries
if (shouldLog) { if (shouldLog) {
const { response } = await this.ask( const { response, text } = await this.ask(
"tool", "tool",
JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool) JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool)
) )
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "This operation was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
} }
return result return result
@@ -434,9 +454,13 @@ export class ClaudeDev {
} }
async executeCommand(command: string): Promise<string> { async executeCommand(command: string): Promise<string> {
const { response } = await this.ask("command", command) const { response, text } = await this.ask("command", command)
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
return "Command execution was not approved by the user." if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
} }
try { try {
let result = "" let result = ""

View File

@@ -99,18 +99,18 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
break break
case "tool": case "tool":
setTextAreaDisabled(true) setTextAreaDisabled(false)
setClaudeAsk("tool") setClaudeAsk("tool")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Approve") setPrimaryButtonText("Approve")
setSecondaryButtonText("Cancel") setSecondaryButtonText("Reject")
break break
case "command": case "command":
setTextAreaDisabled(true) setTextAreaDisabled(false)
setClaudeAsk("command") setClaudeAsk("command")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Run Command") setPrimaryButtonText("Run Command")
setSecondaryButtonText("Cancel") setSecondaryButtonText("Reject")
break break
case "completion_result": case "completion_result":
// extension waiting for feedback. but we can just present a new task button // extension waiting for feedback. but we can just present a new task button
@@ -170,6 +170,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
} else if (claudeAsk) { } else if (claudeAsk) {
switch (claudeAsk) { switch (claudeAsk) {
case "followup": case "followup":
case "tool":
case "command": // user can provide feedback to a tool or command use
case "completion_result": // if this happens then the user has feedback for the completion result case "completion_result": // if this happens then the user has feedback for the completion result
vscode.postMessage({ type: "askResponse", askResponse: "textResponse", text }) vscode.postMessage({ type: "askResponse", askResponse: "textResponse", text })
break break
@@ -210,10 +212,11 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
const handleSecondaryButtonClick = () => { const handleSecondaryButtonClick = () => {
switch (claudeAsk) { switch (claudeAsk) {
case "request_limit_reached": case "request_limit_reached":
case "tool": // TODO: for now when a user cancels, it starts a new task. But we could easily just respond to the API with a "This operation failed" and let it try again.
startNewTask() startNewTask()
break break
case "command": case "command":
case "tool":
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({ type: "askResponse", askResponse: "noButtonTapped" }) vscode.postMessage({ type: "askResponse", askResponse: "noButtonTapped" })
break break
} }
@@ -272,7 +275,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
}, [textAreaRef.current]) }, [textAreaRef.current])
useEffect(() => { useEffect(() => {
if (!isHidden && !textAreaDisabled) { if (!isHidden && !textAreaDisabled && !enableButtons) {
textAreaRef.current?.focus() textAreaRef.current?.focus()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -280,13 +283,14 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (!textAreaDisabled) { if (!textAreaDisabled && !enableButtons) {
textAreaRef.current?.focus() textAreaRef.current?.focus()
} }
}, 50) }, 50)
return () => { return () => {
clearTimeout(timer) clearTimeout(timer)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textAreaDisabled]) }, [textAreaDisabled])
return ( return (