Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions src/cortex-tui/src/app/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions src/cortex-tui/src/runner/event_loop/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_) => {
Expand Down
51 changes: 46 additions & 5 deletions src/cortex-tui/src/views/minimal_session/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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::<String>()
)
} 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::<String>()
)
} 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)),
]));
}
Expand Down
57 changes: 53 additions & 4 deletions src/cortex-tui/src/views/tool_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.join(" ")
} else {
return format_first_arg(args);
};
let truncated = truncate_str(&cmd_str, 50);
return format!("$ {truncated}");
}
format_first_arg(args)
Expand Down Expand Up @@ -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"});
Expand All @@ -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"});
Expand Down
Loading