diff --git a/src/cortex-tui/src/app/methods.rs b/src/cortex-tui/src/app/methods.rs index d29c936..c60f8e3 100644 --- a/src/cortex-tui/src/app/methods.rs +++ b/src/cortex-tui/src/app/methods.rs @@ -326,6 +326,8 @@ impl AppState { /// Update tool call result pub fn update_tool_result(&mut self, id: &str, output: String, success: bool, summary: String) { if let Some(call) = self.tool_calls.iter_mut().find(|c| c.id == id) { + // Clear live output when tool completes (replaced by result summary) + call.clear_live_output(); call.set_result(ToolResultDisplay { output, success, diff --git a/src/cortex-tui/src/runner/event_loop/input.rs b/src/cortex-tui/src/runner/event_loop/input.rs index 126b0d4..074d16b 100644 --- a/src/cortex-tui/src/runner/event_loop/input.rs +++ b/src/cortex-tui/src/runner/event_loop/input.rs @@ -669,8 +669,14 @@ impl EventLoop { self.app_state.streaming.current_tool = None; } - AppEvent::ToolProgress { name: _, status: _ } => { - // Tool progress updates are handled by stream controller + AppEvent::ToolProgress { name, status } => { + // Forward output to tool call's live_output buffer for real-time display + // Note: `name` here is actually the call_id from ExecCommandOutputDeltaEvent + for line in status.lines() { + if !line.is_empty() { + self.app_state.append_tool_output(&name, line.to_string()); + } + } } AppEvent::ToolApproved(_) | AppEvent::ToolRejected(_) => { diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index 6c8c972..4acd02a 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -221,26 +221,67 @@ pub fn render_tool_call( .add_modifier(Modifier::BOLD), ), Span::raw(" "), - Span::styled(summary_truncated, Style::default().fg(colors.text_dim)), + Span::styled(summary_truncated.clone(), Style::default().fg(colors.text_dim)), ])); + // For Execute tool: show command on second line with double-tab indentation when running + let is_execute = call.name.to_lowercase() == "execute" || call.name.to_lowercase() == "bash"; + if is_execute && call.status == ToolStatus::Running { + // Show the command being executed (extracted from summary which starts with "$ ") + let cmd_display = if summary_truncated.starts_with("$ ") { + summary_truncated.clone() + } else if let Some(cmd) = call.arguments.get("command") { + if let Some(s) = cmd.as_str() { + format!("$ {}", s) + } else if let Some(arr) = cmd.as_array() { + let cmd_str: String = arr + .iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" "); + format!("$ {}", cmd_str) + } else { + "$ ...".to_string() + } + } else { + "$ ...".to_string() + }; + // Truncate command if too long + let cmd_truncated = if cmd_display.len() > line_width { + format!( + "{}...", + &cmd_display + .chars() + .take(line_width.saturating_sub(3)) + .collect::() + ) + } else { + cmd_display + }; + lines.push(Line::from(vec![ + Span::raw(" "), // 8 spaces = 2x tabs + Span::styled(cmd_truncated, Style::default().fg(colors.text)), + ])); + } + // Live output lines (for Running status with output) if call.status == ToolStatus::Running && !call.live_output.is_empty() { for output_line in &call.live_output { - // Truncate long lines to fit terminal width - let truncated = if output_line.len() > line_width { + // Truncate long lines to fit terminal width (accounting for 8-char indent) + let output_width = (width as usize).saturating_sub(10); + let truncated = if output_line.len() > output_width { format!( "{}...", &output_line .chars() - .take(line_width.saturating_sub(3)) + .take(output_width.saturating_sub(3)) .collect::() ) } else { output_line.clone() }; lines.push(Line::from(vec![ - Span::styled(" │ ", Style::default().fg(colors.text_muted)), + Span::raw(" "), // 8 spaces = 2x tabs for output lines Span::styled(truncated, Style::default().fg(colors.text_dim)), ])); } diff --git a/src/cortex-tui/src/views/tool_call.rs b/src/cortex-tui/src/views/tool_call.rs index e69516e..1656dee 100644 --- a/src/cortex-tui/src/views/tool_call.rs +++ b/src/cortex-tui/src/views/tool_call.rs @@ -142,10 +142,20 @@ pub fn format_tool_summary(name: &str, args: &Value) -> String { format_first_arg(args) } "execute" | "bash" => { - if let Some(cmd) = args.get("command") - && let Some(cmd_str) = cmd.as_str() - { - let truncated = truncate_str(cmd_str, 50); + if let Some(cmd) = args.get("command") { + // Handle both string and array formats + let cmd_str = if let Some(s) = cmd.as_str() { + s.to_string() + } else if let Some(arr) = cmd.as_array() { + // Join array elements with spaces + arr.iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" ") + } else { + return format_first_arg(args); + }; + let truncated = truncate_str(&cmd_str, 50); return format!("$ {truncated}"); } format_first_arg(args) @@ -364,6 +374,37 @@ mod tests { assert!(display.result.as_ref().unwrap().success); } + #[test] + fn test_append_output_keeps_last_3_lines() { + let mut display = + ToolCallDisplay::new("test-id".to_string(), "execute".to_string(), json!({}), 0); + + // Append 5 lines - should only keep last 3 + display.append_output("line 1".to_string()); + display.append_output("line 2".to_string()); + display.append_output("line 3".to_string()); + display.append_output("line 4".to_string()); + display.append_output("line 5".to_string()); + + assert_eq!(display.live_output.len(), 3); + assert_eq!(display.live_output[0], "line 3"); + assert_eq!(display.live_output[1], "line 4"); + assert_eq!(display.live_output[2], "line 5"); + } + + #[test] + fn test_clear_live_output() { + let mut display = + ToolCallDisplay::new("test-id".to_string(), "execute".to_string(), json!({}), 0); + + display.append_output("line 1".to_string()); + display.append_output("line 2".to_string()); + assert_eq!(display.live_output.len(), 2); + + display.clear_live_output(); + assert!(display.live_output.is_empty()); + } + #[test] fn test_format_tool_summary_read() { let args = json!({"file_path": "/home/user/projects/myapp/src/main.rs"}); @@ -378,6 +419,14 @@ mod tests { assert_eq!(summary, "$ cargo build --release"); } + #[test] + fn test_format_tool_summary_execute_array() { + // Execute tool receives command as array from LLM + let args = json!({"command": ["cargo", "build", "--release"]}); + let summary = format_tool_summary("execute", &args); + assert_eq!(summary, "$ cargo build --release"); + } + #[test] fn test_format_tool_summary_websearch() { let args = json!({"query": "rust async programming"});