Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 48 additions & 52 deletions src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2029,8 +2029,8 @@ impl PromptState {
if let Some(active_command) = self.active_commands.get_mut(&call_id) {
let data_str = String::from_utf8_lossy(&chunk).to_string();

let update = if client.supports_terminal_output(active_command) {
ToolCallUpdate::new(
if client.supports_terminal_output(active_command) {
let update = ToolCallUpdate::new(
active_command.tool_call_id.clone(),
ToolCallUpdateFields::new(),
)
Expand All @@ -2040,27 +2040,15 @@ impl PromptState {
"terminal_id": call_id,
"data": data_str
}),
)]))
)]));
client.send_tool_call_update(update);
} else {
// Fallback path (no terminal_output capability): accumulate locally
// and emit a single ToolCallUpdate at exec_command_end. Resending the
// entire accumulated buffer per chunk is O(N²) memory and crashes the
// process on large outputs (issue #225).
active_command.output.push_str(&data_str);
let content = match active_command.file_extension.as_deref() {
Some("md") => active_command.output.clone(),
Some(ext) => format!(
"```{ext}\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
None => format!(
"```sh\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
};
ToolCallUpdate::new(
active_command.tool_call_id.clone(),
ToolCallUpdateFields::new().content(vec![content.into()]),
)
};

client.send_tool_call_update(update);
}
}
}

Expand Down Expand Up @@ -2092,15 +2080,35 @@ impl PromptState {
ExecCommandStatus::Failed | ExecCommandStatus::Declined => ToolCallStatus::Failed,
};

let supports_terminal = client.supports_terminal_output(&active_command);

let mut fields = ToolCallUpdateFields::new()
.status(status)
.raw_output(raw_output);

// For the non-terminal fallback path the per-chunk delta handler now
// accumulates silently (see exec_command_output_delta). Emit the full
// buffer here, exactly once, as a single content block. Skip the emission
// entirely when the command produced no output, so we don't surface an
// empty fenced code block to the client.
if !supports_terminal && !active_command.output.is_empty() {
let content = match active_command.file_extension.as_deref() {
Some("md") => active_command.output.clone(),
Some(ext) => format!(
"```{ext}\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
None => format!(
"```sh\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
};
fields = fields.content(vec![content.into()]);
}

client.send_tool_call_update(
ToolCallUpdate::new(
active_command.tool_call_id.clone(),
ToolCallUpdateFields::new()
.status(status)
.raw_output(raw_output),
)
.meta(client.supports_terminal_output(&active_command).then(
|| {
ToolCallUpdate::new(active_command.tool_call_id.clone(), fields).meta(
supports_terminal.then(|| {
Meta::from_iter([(
"terminal_exit".into(),
serde_json::json!({
Expand All @@ -2109,8 +2117,8 @@ impl PromptState {
"signal": null
}),
)])
},
)),
}),
),
);
}
}
Expand All @@ -2125,8 +2133,8 @@ impl PromptState {
let stdin = format!("\n{stdin}\n");
// Stream output bytes to the display-only terminal via ToolCallUpdate meta.
if let Some(active_command) = self.active_commands.get_mut(&call_id) {
let update = if client.supports_terminal_output(active_command) {
ToolCallUpdate::new(
if client.supports_terminal_output(active_command) {
let update = ToolCallUpdate::new(
active_command.tool_call_id.clone(),
ToolCallUpdateFields::new(),
)
Expand All @@ -2136,27 +2144,15 @@ impl PromptState {
"terminal_id": call_id,
"data": stdin
}),
)]))
)]));
client.send_tool_call_update(update);
} else {
// Fallback path: accumulate stdin into the active command buffer and
// defer emission to exec_command_end. Emitting per stdin event would
// re-send the entire output+stdin buffer each time and reintroduce the
// O(N²) growth fixed in the delta path.
active_command.output.push_str(&stdin);
let content = match active_command.file_extension.as_deref() {
Some("md") => active_command.output.clone(),
Some(ext) => format!(
"```{ext}\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
None => format!(
"```sh\n{}\n```\n",
active_command.output.trim_end_matches('\n')
),
};
ToolCallUpdate::new(
active_command.tool_call_id.clone(),
ToolCallUpdateFields::new().content(vec![content.into()]),
)
};

client.send_tool_call_update(update);
}
}
}

Expand Down