Gracefully handle when user exits command early

This commit is contained in:
Saoud Rizwan
2024-07-29 17:44:49 -04:00
parent cf2ab16538
commit d324afdf14
4 changed files with 39 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk" import { Anthropic } from "@anthropic-ai/sdk"
import defaultShell from "default-shell" import defaultShell from "default-shell"
import * as diff from "diff" import * as diff from "diff"
import { execa } from "execa" import { execa, ExecaError } from "execa"
import fs from "fs/promises" import fs from "fs/promises"
import { globby } from "globby" import { globby } from "globby"
import os from "os" import os from "os"
@@ -29,7 +29,7 @@ CAPABILITIES
- You have access to tools that let you analyze software projects, execute CLI commands on the user's computer, list files in a directory, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more. - You have access to tools that let you analyze software projects, execute CLI commands on the user's computer, list files in a directory, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
- For example, when asked to make edits or improvements you might use the analyze_project and read_file tools to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to implement changes. - For example, when asked to make edits or improvements you might use the analyze_project and read_file tools to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to implement changes.
- You can use the analyze_project tool to get a comprehensive overview of a software project's file structure and source code definitions. This can be particularly useful when you need to understand the broader context and relationships between different parts of the code, as well as the overall organization of files and directories. - You can use the analyze_project tool to get a comprehensive overview of a software project's file structure and source code definitions. This can be particularly useful when you need to understand the broader context and relationships between different parts of the code, as well as the overall organization of files and directories.
- The execute_command tool lets you run commands on the user's computer and should be used whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. - The execute_command tool lets you run commands on the user's computer and should be used whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the user has the ability to send input to stdin and terminate the command on their own if needed.
==== ====
@@ -523,14 +523,17 @@ export class ClaudeDev {
// execa returns a promise-like object that is both a promise and a Subprocess that has properties like stdin // execa returns a promise-like object that is both a promise and a Subprocess that has properties like stdin
const subprocess = execa({ shell: true })`${command}` const subprocess = execa({ shell: true })`${command}`
try {
for await (const chunk of subprocess) { for await (const chunk of subprocess) {
const line = chunk.toString() const line = chunk.toString()
// stream output to user in realtime // stream output to user in realtime
// do not await as we are not waiting for a response
this.ask("command_output", line) this.ask("command_output", line)
.then(({ response, text }) => { .then(({ response, text }) => {
// if this ask promise is not ignored, that means the user responded to it somehow either by clicking primary button or by typing text // if this ask promise is not ignored, that means the user responded to it somehow either by clicking primary button or by typing text
if (response === "yesButtonTapped") { if (response === "yesButtonTapped") {
subprocess.kill() // Will result in for loop throwing error, so claude will know command failed // SIGINT is typically what's sent when a user interrupts a process (like pressing Ctrl+C)
subprocess.kill("SIGINT") // will result in for loop throwing error
} else { } else {
// if the user sent some input, we send it to the command stdin // if the user sent some input, we send it to the command stdin
// add newline as cli programs expect a newline after each input // add newline as cli programs expect a newline after each input
@@ -542,11 +545,20 @@ export class ClaudeDev {
}) })
result += `${line}\n` result += `${line}\n`
} }
} catch (e) {
if ((e as ExecaError).signal === "SIGINT") {
const line = `\nUser exited command early...`
await this.say("command_output", line)
result += line
} else {
throw e // if the command was not terminated by user, let outer catch handle it as a real error
}
}
// for attemptCompletion, we don't want to return the command output // for attemptCompletion, we don't want to return the command output
if (returnEmptyStringOnSuccess) { if (returnEmptyStringOnSuccess) {
return "" return ""
} }
return `Command executed successfully. Output:\n${result}` return `Command Output:\n${result}`
} catch (e) { } catch (e) {
const error = e as any const error = e as any
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2) let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)

View File

@@ -42,6 +42,7 @@ export type ClaudeSay =
| "completion_result" | "completion_result"
| "user_feedback" | "user_feedback"
| "api_req_retried" | "api_req_retried"
| "command_output"
export interface ClaudeSayTool { export interface ClaudeSayTool {
tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles" | "analyzeProject" tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles" | "analyzeProject"

View File

@@ -138,6 +138,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
break break
case "text": case "text":
break break
case "command_output":
break
case "completion_result": case "completion_result":
break break
} }
@@ -324,7 +326,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
const placeholderText = useMemo(() => { const placeholderText = useMemo(() => {
if (messages.at(-1)?.ask === "command_output") { if (messages.at(-1)?.ask === "command_output") {
return "Type input for command stdin..." return "Type input to command stdin..."
} }
return task ? "Type a message..." : "Type your task here..." return task ? "Type a message..." : "Type your task here..."
}, [task, messages]) }, [task, messages])

View File

@@ -35,7 +35,7 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
// Stop if we encounter the next command // Stop if we encounter the next command
break break
} }
if (messages[j].type === "ask" && messages[j].ask === "command_output") { if (messages[j].ask === "command_output" || messages[j].say === "command_output") {
if (!didAddOutput) { if (!didAddOutput) {
// Add a newline before the first output // Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}` combinedText += `\n${COMMAND_OUTPUT_STRING}`
@@ -57,7 +57,7 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
// Second pass: remove command_outputs and replace original commands with combined ones // Second pass: remove command_outputs and replace original commands with combined ones
return messages return messages
.filter((msg) => !(msg.type === "ask" && msg.ask === "command_output")) .filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
.map((msg) => { .map((msg) => {
if (msg.type === "ask" && msg.ask === "command") { if (msg.type === "ask" && msg.ask === "command") {
const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts) const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts)