Gracefully terminate running subprocess if user cancels task in the middle of a command

This commit is contained in:
Saoud Rizwan
2024-08-10 03:41:47 -04:00
parent c1012dcd99
commit 9f7d6d428b
2 changed files with 15 additions and 7 deletions

View File

@@ -242,8 +242,9 @@ export class ClaudeDev {
private askResponseText?: string private askResponseText?: string
private askResponseImages?: string[] private askResponseImages?: string[]
private lastMessageTs?: number private lastMessageTs?: number
private executeCommandRunningProcess?: ResultPromise
private providerRef: WeakRef<ClaudeDevProvider> private providerRef: WeakRef<ClaudeDevProvider>
abort: boolean = false private abort: boolean = false
constructor( constructor(
provider: ClaudeDevProvider, provider: ClaudeDevProvider,
@@ -382,6 +383,14 @@ export class ClaudeDev {
} }
} }
abortTask() {
this.abort = true // will stop any autonomously running promises
const runningProcessId = this.executeCommandRunningProcess?.pid
if (runningProcessId) {
treeKill(runningProcessId, "SIGTERM")
}
}
async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<ToolResponse> { async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<ToolResponse> {
switch (toolName) { switch (toolName) {
case "write_to_file": case "write_to_file":
@@ -821,6 +830,7 @@ export class ClaudeDev {
// also worth noting that execa`input` and the execa(command) have nuanced differences like the template literal version handles escaping for you, while with the function call, you need to be more careful about how arguments are passed, especially when using shell: true. // also worth noting that execa`input` and the execa(command) have nuanced differences like the template literal version handles escaping for you, while with the function call, you need to be more careful about how arguments are passed, especially when using shell: true.
// 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, cwd: cwd })`${command}` const subprocess = execa({ shell: true, cwd: cwd })`${command}`
this.executeCommandRunningProcess = subprocess
subprocess.stdout?.on("data", (data) => { subprocess.stdout?.on("data", (data) => {
if (data) { if (data) {
@@ -854,6 +864,7 @@ export class ClaudeDev {
// the correct order of messages (although the webview is smart about // the correct order of messages (although the webview is smart about
// grouping command_output messages despite any gaps anyways) // grouping command_output messages despite any gaps anyways)
await delay(100) await delay(100)
this.executeCommandRunningProcess = undefined
// 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 ""
@@ -864,6 +875,7 @@ export class ClaudeDev {
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2) let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
const errorString = `Error executing command:\n${errorMessage}` const errorString = `Error executing command:\n${errorMessage}`
this.say("error", `Error executing command:\n${errorMessage}`) // TODO: in webview show code block for command errors this.say("error", `Error executing command:\n${errorMessage}`) // TODO: in webview show code block for command errors
this.executeCommandRunningProcess = undefined
return errorString return errorString
} }
} }

View File

@@ -332,12 +332,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
} }
async clearTask() { async clearTask() {
if (this.claudeDev) { this.claudeDev?.abortTask()
this.claudeDev.abort = true // will stop any agentically running promises this.claudeDev = undefined // removes reference to it, so once promises end it will be garbage collected
this.claudeDev = undefined // removes reference to it, so once promises end it will be garbage collected
}
// this.setApiConversationHistory(undefined)
// this.setClaudeMessages(undefined)
} }
// Caching mechanism to keep track of webview messages + API conversation history per provider instance // Caching mechanism to keep track of webview messages + API conversation history per provider instance