Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 3b99b93

Browse files
committed
feat: add write_stdin tool for interactive terminal support
This commit implements the interactive terminal feature from the terminal integration specification (plans/extract-terminal-integration.md). Changes: - Add write_stdin to toolNames in packages/types/src/tool.ts - Create write_stdin native tool schema in src/core/prompts/tools/native-tools/ - Create WriteStdinTool handler in src/core/tools/ - Add ProcessManager to track running processes by session_id - Modify ExecuteCommandTool to register processes when still running - Add write_stdin to tool routing in presentAssistantMessage.ts - Add write_stdin to NativeToolCallParser for streaming support - Add tests for ProcessManager The write_stdin tool enables the LLM to: - Send input to running terminal processes (y/n prompts, passwords) - Send control characters like Ctrl+C (\x03) - Poll for new output from long-running processes When execute_command starts a process that's still running after the yield time, it registers the process with ProcessManager and returns a session_id. The LLM can then use write_stdin with that session_id to interact with the process.
1 parent d6aab9f commit 3b99b93

10 files changed

Lines changed: 976 additions & 1 deletion

File tree

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type ToolGroup = z.infer<typeof toolGroupsSchema>
1616

1717
export const toolNames = [
1818
"execute_command",
19+
"write_stdin",
1920
"read_file",
2021
"read_command_output",
2122
"write_to_file",

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,17 @@ export class NativeToolCallParser {
400400
}
401401
break
402402

403+
case "write_stdin":
404+
if (partialArgs.session_id !== undefined) {
405+
nativeArgs = {
406+
session_id: partialArgs.session_id,
407+
chars: partialArgs.chars,
408+
yield_time_ms: partialArgs.yield_time_ms,
409+
max_output_tokens: partialArgs.max_output_tokens,
410+
}
411+
}
412+
break
413+
403414
case "write_to_file":
404415
if (partialArgs.path || partialArgs.content) {
405416
nativeArgs = {
@@ -687,6 +698,17 @@ export class NativeToolCallParser {
687698
}
688699
break
689700

701+
case "write_stdin":
702+
if (args.session_id !== undefined) {
703+
nativeArgs = {
704+
session_id: args.session_id,
705+
chars: args.chars,
706+
yield_time_ms: args.yield_time_ms,
707+
max_output_tokens: args.max_output_tokens,
708+
} as NativeArgsFor<TName>
709+
}
710+
break
711+
690712
case "apply_diff":
691713
if (args.path !== undefined && args.diff !== undefined) {
692714
nativeArgs = {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { applyPatchTool } from "../tools/ApplyPatchTool"
2626
import { searchFilesTool } from "../tools/SearchFilesTool"
2727
import { browserActionTool } from "../tools/BrowserActionTool"
2828
import { executeCommandTool } from "../tools/ExecuteCommandTool"
29+
import { writeStdinTool } from "../tools/WriteStdinTool"
2930
import { useMcpToolTool } from "../tools/UseMcpToolTool"
3031
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
3132
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
@@ -856,6 +857,13 @@ export async function presentAssistantMessage(cline: Task) {
856857
pushToolResult,
857858
})
858859
break
860+
case "write_stdin":
861+
await writeStdinTool.handle(cline, block as ToolUse<"write_stdin">, {
862+
askApproval,
863+
handleError,
864+
pushToolResult,
865+
})
866+
break
859867
case "use_mcp_tool":
860868
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
861869
askApproval,

src/core/prompts/tools/native-tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import edit_file from "./edit_file"
2020
import searchFiles from "./search_files"
2121
import switchMode from "./switch_mode"
2222
import updateTodoList from "./update_todo_list"
23+
import writeStdin from "./write_stdin"
2324
import writeToFile from "./write_to_file"
2425

2526
export { getMcpServerTools } from "./mcp_server"
@@ -75,6 +76,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
7576
searchFiles,
7677
switchMode,
7778
updateTodoList,
79+
writeStdin,
7880
writeToFile,
7981
] satisfies OpenAI.Chat.ChatCompletionTool[]
8082
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type OpenAI from "openai"
2+
3+
/**
4+
* Native tool definition for write_stdin.
5+
*
6+
* This tool allows the LLM to write characters to an existing terminal session
7+
* and receive the resulting output. It enables interactive terminal workflows
8+
* where the LLM can respond to prompts, provide input to running processes,
9+
* and monitor long-running commands.
10+
*/
11+
12+
const WRITE_STDIN_DESCRIPTION = `Writes characters to an existing exec session and returns recent output.
13+
14+
Use this tool when:
15+
1. A command started with execute_command is still running and waiting for input
16+
2. You need to respond to an interactive prompt (e.g., "Press y to continue", password prompts)
17+
3. You want to poll a long-running process for new output without sending input
18+
19+
The session_id is returned by execute_command when a process is still running.
20+
21+
Parameters:
22+
- session_id: (required) Identifier of the running exec session (returned by execute_command)
23+
- chars: (optional) Characters to write to stdin. Use empty string or omit to just poll for output.
24+
- yield_time_ms: (optional) Milliseconds to wait for output after writing (default: 250, min: 250, max: 30000)
25+
- max_output_tokens: (optional) Maximum tokens to return in the response
26+
27+
Common use cases:
28+
- Sending 'y' or 'n' to confirmation prompts
29+
- Providing input to interactive CLI tools
30+
- Sending Ctrl+C (\\x03) to terminate a process
31+
- Polling for output from a long-running process
32+
33+
Example: Responding to a confirmation prompt
34+
{ "session_id": 1234, "chars": "y\\n" }
35+
36+
Example: Sending Ctrl+C to stop a process
37+
{ "session_id": 1234, "chars": "\\x03" }
38+
39+
Example: Polling for new output (no input)
40+
{ "session_id": 1234, "chars": "", "yield_time_ms": 2000 }
41+
42+
Example: Providing password (note: prefer non-interactive approaches when possible)
43+
{ "session_id": 1234, "chars": "password\\n" }`
44+
45+
const SESSION_ID_DESCRIPTION = `Identifier of the running exec session (returned by execute_command when a process is still running)`
46+
47+
const CHARS_DESCRIPTION = `Characters to write to stdin. May be empty to just poll for output. Supports escape sequences like \\n (newline) and \\x03 (Ctrl+C).`
48+
49+
const YIELD_TIME_MS_DESCRIPTION = `Milliseconds to wait for output after writing (default: 250, range: 250-30000). Use higher values when expecting delayed output.`
50+
51+
const MAX_OUTPUT_TOKENS_DESCRIPTION = `Maximum tokens to return in the response. Excess output will be truncated with head/tail preservation.`
52+
53+
export default {
54+
type: "function",
55+
function: {
56+
name: "write_stdin",
57+
description: WRITE_STDIN_DESCRIPTION,
58+
// Note: strict mode is intentionally disabled for this tool.
59+
// With strict: true, OpenAI requires ALL properties to be in the 'required' array,
60+
// which forces the LLM to always provide explicit values (even null) for optional params.
61+
// This creates verbose tool calls and poor UX. By disabling strict mode, the LLM can
62+
// omit optional parameters entirely, making the tool easier to use.
63+
parameters: {
64+
type: "object",
65+
properties: {
66+
session_id: {
67+
type: "number",
68+
description: SESSION_ID_DESCRIPTION,
69+
},
70+
chars: {
71+
type: "string",
72+
description: CHARS_DESCRIPTION,
73+
},
74+
yield_time_ms: {
75+
type: "number",
76+
description: YIELD_TIME_MS_DESCRIPTION,
77+
},
78+
max_output_tokens: {
79+
type: "number",
80+
description: MAX_OUTPUT_TOKENS_DESCRIPTION,
81+
},
82+
},
83+
required: ["session_id"],
84+
additionalProperties: false,
85+
},
86+
},
87+
} satisfies OpenAI.Chat.ChatCompletionTool

src/core/tools/ExecuteCommandTool.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../..
1616
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1717
import { Terminal } from "../../integrations/terminal/Terminal"
1818
import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor"
19+
import { ProcessManager } from "../../integrations/terminal/ProcessManager"
1920
import { Package } from "../../shared/package"
2021
import { t } from "../../i18n"
2122
import { getTaskDirectoryPath } from "../../utils/storage"
@@ -423,12 +424,31 @@ export async function executeCommandInTerminal(
423424
`Command executed in terminal within working directory '${currentWorkingDir}'. ${exitStatus}\nOutput:\n${result}`,
424425
]
425426
} else {
427+
// Process is still running - register it with ProcessManager for write_stdin interaction
428+
let sessionId: number | undefined
429+
const currentProcess = terminal.process
430+
431+
if (currentProcess) {
432+
try {
433+
const processManager = ProcessManager.getInstance()
434+
sessionId = processManager.registerProcess(terminal, currentProcess, task.taskId, command)
435+
} catch (error) {
436+
console.warn(`[ExecuteCommandTool] Failed to register process: ${error}`)
437+
}
438+
}
439+
440+
const sessionInfo =
441+
sessionId !== undefined
442+
? `\nSession ID: ${sessionId} - Use write_stdin tool with this session_id to send input to the process.`
443+
: ""
444+
426445
return [
427446
false,
428447
[
429448
`Command is still running in terminal ${workingDir ? ` from '${workingDir.toPosix()}'` : ""}.`,
430449
result.length > 0 ? `Here's the output so far:\n${result}\n` : "\n",
431450
"You will be updated on the terminal status and new output in the future.",
451+
sessionInfo,
432452
].join("\n"),
433453
]
434454
}

0 commit comments

Comments
 (0)