Skip to content

Commit f2d68f0

Browse files
committed
feat(tui): display Execute tool command and streaming output
- Wire AppEvent::ToolProgress to live_output buffer for real-time display - Display command being executed (e.g., '$ cargo build') in tool call header - Show up to 3 lines of real-time streaming output during command execution - Clear live output when command completes, replaced by result summary - Handle both string and array formats for command arguments - Add tests for live_output buffer management and command format handling The Execute tool now shows: 1. The command being run as a header (e.g., '$ cargo build --release') 2. Up to 3 lines of real-time output during execution 3. Result summary when command completes
1 parent ac6398e commit f2d68f0

File tree

3 files changed

+63
-6
lines changed

3 files changed

+63
-6
lines changed

src/cortex-tui/src/app/methods.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ impl AppState {
326326
/// Update tool call result
327327
pub fn update_tool_result(&mut self, id: &str, output: String, success: bool, summary: String) {
328328
if let Some(call) = self.tool_calls.iter_mut().find(|c| c.id == id) {
329+
// Clear live output when tool completes (replaced by result summary)
330+
call.clear_live_output();
329331
call.set_result(ToolResultDisplay {
330332
output,
331333
success,

src/cortex-tui/src/runner/event_loop/input.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,14 @@ impl EventLoop {
669669
self.app_state.streaming.current_tool = None;
670670
}
671671

672-
AppEvent::ToolProgress { name: _, status: _ } => {
673-
// Tool progress updates are handled by stream controller
672+
AppEvent::ToolProgress { name, status } => {
673+
// Forward output to tool call's live_output buffer for real-time display
674+
// Note: `name` here is actually the call_id from ExecCommandOutputDeltaEvent
675+
for line in status.lines() {
676+
if !line.is_empty() {
677+
self.app_state.append_tool_output(&name, line.to_string());
678+
}
679+
}
674680
}
675681

676682
AppEvent::ToolApproved(_) | AppEvent::ToolRejected(_) => {

src/cortex-tui/src/views/tool_call.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,20 @@ pub fn format_tool_summary(name: &str, args: &Value) -> String {
142142
format_first_arg(args)
143143
}
144144
"execute" | "bash" => {
145-
if let Some(cmd) = args.get("command")
146-
&& let Some(cmd_str) = cmd.as_str()
147-
{
148-
let truncated = truncate_str(cmd_str, 50);
145+
if let Some(cmd) = args.get("command") {
146+
// Handle both string and array formats
147+
let cmd_str = if let Some(s) = cmd.as_str() {
148+
s.to_string()
149+
} else if let Some(arr) = cmd.as_array() {
150+
// Join array elements with spaces
151+
arr.iter()
152+
.filter_map(|v| v.as_str())
153+
.collect::<Vec<_>>()
154+
.join(" ")
155+
} else {
156+
return format_first_arg(args);
157+
};
158+
let truncated = truncate_str(&cmd_str, 50);
149159
return format!("$ {truncated}");
150160
}
151161
format_first_arg(args)
@@ -364,6 +374,37 @@ mod tests {
364374
assert!(display.result.as_ref().unwrap().success);
365375
}
366376

377+
#[test]
378+
fn test_append_output_keeps_last_3_lines() {
379+
let mut display =
380+
ToolCallDisplay::new("test-id".to_string(), "execute".to_string(), json!({}), 0);
381+
382+
// Append 5 lines - should only keep last 3
383+
display.append_output("line 1".to_string());
384+
display.append_output("line 2".to_string());
385+
display.append_output("line 3".to_string());
386+
display.append_output("line 4".to_string());
387+
display.append_output("line 5".to_string());
388+
389+
assert_eq!(display.live_output.len(), 3);
390+
assert_eq!(display.live_output[0], "line 3");
391+
assert_eq!(display.live_output[1], "line 4");
392+
assert_eq!(display.live_output[2], "line 5");
393+
}
394+
395+
#[test]
396+
fn test_clear_live_output() {
397+
let mut display =
398+
ToolCallDisplay::new("test-id".to_string(), "execute".to_string(), json!({}), 0);
399+
400+
display.append_output("line 1".to_string());
401+
display.append_output("line 2".to_string());
402+
assert_eq!(display.live_output.len(), 2);
403+
404+
display.clear_live_output();
405+
assert!(display.live_output.is_empty());
406+
}
407+
367408
#[test]
368409
fn test_format_tool_summary_read() {
369410
let args = json!({"file_path": "/home/user/projects/myapp/src/main.rs"});
@@ -378,6 +419,14 @@ mod tests {
378419
assert_eq!(summary, "$ cargo build --release");
379420
}
380421

422+
#[test]
423+
fn test_format_tool_summary_execute_array() {
424+
// Execute tool receives command as array from LLM
425+
let args = json!({"command": ["cargo", "build", "--release"]});
426+
let summary = format_tool_summary("execute", &args);
427+
assert_eq!(summary, "$ cargo build --release");
428+
}
429+
381430
#[test]
382431
fn test_format_tool_summary_websearch() {
383432
let args = json!({"query": "rust async programming"});

0 commit comments

Comments
 (0)