Fix issue where sending message to stdin during non-interactive long-running process would not relinquish control back over the exit command button

This commit is contained in:
Saoud Rizwan
2024-08-03 21:23:33 -04:00
parent bbdf66b45c
commit 09050b8800
2 changed files with 35 additions and 25 deletions

View File

@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import defaultShell from "default-shell"
import * as diff from "diff"
import { execa, ExecaError } from "execa"
import { execa, ExecaError, ResultPromise } from "execa"
import fs from "fs/promises"
import os from "os"
import osName from "os-name"
@@ -631,20 +631,10 @@ export class ClaudeDev {
}
return "The user denied this operation."
}
try {
let result = ""
// execa by default tries to convert bash into javascript, so need to specify `shell: true` to use sh on unix or cmd.exe on windows
// 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
const subprocess = execa({ shell: true })`${command}`
const sendCommandOutput = async (subprocess: ResultPromise, line: string): Promise<void> => {
try {
for await (const chunk of subprocess) {
const line = chunk.toString()
// stream output to user in realtime
// do not await as we are not waiting for a response
this.ask("command_output", line)
.then(({ response, text }) => {
const { response, text } = await this.ask("command_output", line)
// 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") {
// SIGINT is typically what's sent when a user interrupts a process (like pressing Ctrl+C)
@@ -661,11 +651,27 @@ export class ClaudeDev {
// if the user sent some input, we send it to the command stdin
// add newline as cli programs expect a newline after each input
subprocess.stdin?.write(text + "\n")
// Recurse with an empty string to continue listening for more input
sendCommandOutput(subprocess, "") // empty strings are effectively ignored by the webview, this is done solely to relinquish control over the exit command button
}
})
.catch(() => {
// this can only happen if this ask promise was ignored, so ignore this error
})
} catch {
// This can only happen if this ask promise was ignored, so ignore this error
}
}
try {
let result = ""
// execa by default tries to convert bash into javascript, so need to specify `shell: true` to use sh on unix or cmd.exe on windows
// 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
const subprocess = execa({ shell: true })`${command}`
try {
for await (const chunk of subprocess) {
const line = chunk.toString()
// stream output to user in realtime
// do not await as we are not waiting for a response
sendCommandOutput(subprocess, line)
result += `${line}\n`
}
} catch (e) {

View File

@@ -41,7 +41,11 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
combinedText += `\n${COMMAND_OUTPUT_STRING}`
didAddOutput = true
}
combinedText += "\n" + (messages[j].text || "")
// handle cases where we receive empty command_output (ie when extension is relinquishing control over exit command button)
const output = messages[j].text || ""
if (output.length > 0) {
combinedText += "\n" + output
}
}
j++
}