feat: add stdin support for ExecaTerminalProcess

- Add writeStdin method to RooTerminalProcess interface
- Add abstract writeStdin to BaseTerminalProcess
- Implement writeStdin in ExecaTerminalProcess using stdin pipe
- Implement writeStdin in TerminalProcess using terminal.sendText
- Change ExecaTerminalProcess from stdin: 'ignore' to stdin: 'pipe'
- Update WriteStdinTool to use unified process.writeStdin() method

This enables write_stdin tool to work with both VSCode terminals and
Execa terminals, providing consistent interactive terminal support.
This commit is contained in:
Hannes Rudolph 2026-01-27 18:30:56 -07:00
parent c9af762114
commit cb28f75fff
5 changed files with 62 additions and 15 deletions

View file

@ -4,7 +4,6 @@ import { Task } from "../task/Task"
import { ToolUse } from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { ProcessManager } from "../../integrations/terminal/ProcessManager"
import { Terminal } from "../../integrations/terminal/Terminal"
import { t } from "../../i18n"
import { BaseTool, ToolCallbacks } from "./BaseTool"
@ -122,22 +121,16 @@ export class WriteStdinTool extends BaseTool<"write_stdin"> {
// Process escape sequences in input
const processedChars = this.processEscapeSequences(chars)
// Write to stdin
// Write to stdin using the unified writeStdin interface
const { terminal, process } = entry
let writeSuccess = false
try {
if (terminal instanceof Terminal) {
// VSCode terminal - use sendText
// Note: sendText automatically adds a newline by default, so we pass false
// to prevent double newlines when the input already ends with \n
terminal.terminal.sendText(processedChars, false)
writeSuccess = true
} else {
// Execa terminal - would need stdin pipe support
// For now, we'll indicate this isn't supported for execa
// TODO: Implement stdin support for ExecaTerminalProcess
const errorMsg = `Session ${session_id} is using a non-interactive terminal. Interactive stdin is only supported for VSCode terminals.`
// Use the process writeStdin method which works for both VSCode and Execa terminals
writeSuccess = process.writeStdin(processedChars)
if (!writeSuccess) {
const errorMsg = `Failed to write to session ${session_id}: stdin is not available`
await task.say("error", errorMsg)
pushToolResult(`Error: ${errorMsg}`)
return

View file

@ -137,6 +137,13 @@ export abstract class BaseTerminalProcess extends EventEmitter<RooTerminalProces
*/
abstract getUnretrievedOutput(): string
/**
* Write characters to stdin.
* @param chars The characters to write
* @returns true if write was successful, false if stdin is not available
*/
abstract writeStdin(chars: string): boolean
/**
* Clears the internal output buffer when all content has been retrieved.
*

View file

@ -42,8 +42,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
shell: true,
cwd: this.terminal.getCurrentWorkingDirectory(),
all: true,
// Ignore stdin to ensure non-interactive mode and prevent hanging
stdin: "ignore",
// Use pipe for stdin to allow interactive input via writeStdin
stdin: "pipe",
env: {
...process.env,
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
@ -241,6 +241,26 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
return output.slice(0, index)
}
/**
* Write characters to stdin of the running process.
* @param chars The characters to write
* @returns true if write was successful
*/
public override writeStdin(chars: string): boolean {
if (!this.subprocess?.stdin) {
console.warn("[ExecaTerminalProcess#writeStdin] No stdin available")
return false
}
try {
this.subprocess.stdin.write(chars)
return true
} catch (error) {
console.error("[ExecaTerminalProcess#writeStdin] Failed to write:", error)
return false
}
}
private emitRemainingBufferIfListening() {
if (!this.isListening) {
return

View file

@ -314,6 +314,28 @@ export class TerminalProcess extends BaseTerminalProcess {
return this.removeEscapeSequences(outputToProcess)
}
/**
* Write characters to stdin of the running process.
* For VSCode terminals, this uses sendText which sends to the terminal.
* @param chars The characters to write
* @returns true if write was successful
*/
public override writeStdin(chars: string): boolean {
try {
const vsceTerminal = this.terminal.terminal
if (!vsceTerminal) {
console.warn("[TerminalProcess#writeStdin] No VSCode terminal available")
return false
}
// sendText with addNewline=false to avoid double newlines
vsceTerminal.sendText(chars, false)
return true
} catch (error) {
console.error("[TerminalProcess#writeStdin] Failed to write:", error)
return false
}
}
private emitRemainingBufferIfListening() {
if (this.isListening) {
const remainingBuffer = this.getUnretrievedOutput()

View file

@ -37,6 +37,11 @@ export interface RooTerminalProcess extends EventEmitter<RooTerminalProcessEvent
hasUnretrievedOutput: () => boolean
getUnretrievedOutput: () => string
trimRetrievedOutput: () => void
/**
* Write characters to stdin. Returns true if write was successful.
* May return false if stdin is not available (e.g., stdin: "ignore").
*/
writeStdin: (chars: string) => boolean
}
export type RooTerminalProcessResultPromise = RooTerminalProcess & Promise<void>