tool headers working
This commit is contained in:
@@ -40,6 +40,16 @@ pub enum TuiMessage {
|
||||
caption: String,
|
||||
content: String,
|
||||
},
|
||||
ToolDetailUpdate {
|
||||
name: String,
|
||||
content: String,
|
||||
},
|
||||
ToolComplete {
|
||||
name: String,
|
||||
success: bool,
|
||||
duration_ms: u128,
|
||||
caption: String,
|
||||
},
|
||||
SystemStatus(String),
|
||||
ContextUpdate {
|
||||
used: u32,
|
||||
@@ -60,6 +70,10 @@ struct TerminalState {
|
||||
scroll_offset: usize,
|
||||
/// Cursor blink state
|
||||
cursor_blink: bool,
|
||||
/// Tool activity history (left side of activity box)
|
||||
tool_activity: Vec<String>,
|
||||
/// Tool activity scroll offset
|
||||
tool_activity_scroll: usize,
|
||||
/// Last known visible height of output area
|
||||
last_visible_height: usize,
|
||||
/// User has manually scrolled (disable auto-scroll)
|
||||
@@ -80,6 +94,8 @@ struct TerminalState {
|
||||
is_processing: bool,
|
||||
/// Should exit
|
||||
should_exit: bool,
|
||||
/// Track the last tool header line index for updating it
|
||||
last_tool_header_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl TerminalState {
|
||||
@@ -96,6 +112,8 @@ impl TerminalState {
|
||||
],
|
||||
scroll_offset: 0,
|
||||
cursor_blink: true,
|
||||
tool_activity: Vec::new(),
|
||||
tool_activity_scroll: 0,
|
||||
last_visible_height: 0, // Will be set on first draw
|
||||
manual_scroll: false,
|
||||
last_blink: Instant::now(),
|
||||
@@ -106,82 +124,31 @@ impl TerminalState {
|
||||
last_status_blink: Instant::now(),
|
||||
is_processing: false,
|
||||
should_exit: false,
|
||||
last_tool_header_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format tool call output with a box
|
||||
/// Format tool call output
|
||||
fn format_tool_output(&mut self, tool_name: &str, caption: &str, content: &str) {
|
||||
// Calculate box width (use a reasonable width, accounting for terminal size)
|
||||
let box_width = 80;
|
||||
let border_char = "─";
|
||||
let corner_tl = "┌";
|
||||
let corner_tr = "┐";
|
||||
let corner_bl = "└";
|
||||
let corner_br = "┘";
|
||||
let vertical = "│";
|
||||
|
||||
// Add top border
|
||||
self.output_history.push(format!(
|
||||
"{}{}{}",
|
||||
corner_tl,
|
||||
border_char.repeat(box_width - 2),
|
||||
corner_tr
|
||||
));
|
||||
|
||||
// Add header with tool name (will be styled with green background in draw)
|
||||
// Add tool header bar to main output
|
||||
let header_text = format!(" {} | {}", tool_name.to_uppercase(), caption);
|
||||
let padding = box_width - 2 - header_text.len();
|
||||
self.output_history.push(format!(
|
||||
"{}[TOOL_HEADER]{}{}{}",
|
||||
vertical,
|
||||
header_text,
|
||||
" ".repeat(padding),
|
||||
vertical
|
||||
));
|
||||
|
||||
// Add separator between header and content
|
||||
self.output_history.push(format!(
|
||||
"{}{}{}",
|
||||
"├",
|
||||
border_char.repeat(box_width - 2),
|
||||
"┤"
|
||||
));
|
||||
// Add marker for special styling
|
||||
self.output_history.push(format!("[TOOL_HEADER]{}", header_text));
|
||||
|
||||
// Add content lines
|
||||
// Track the index of this tool header for later updates
|
||||
self.last_tool_header_index = Some(self.output_history.len() - 1);
|
||||
|
||||
self.output_history.push(String::new()); // Empty line after header
|
||||
|
||||
// Add the actual tool content to the tool detail panel
|
||||
self.tool_activity.clear(); // Clear previous activity
|
||||
self.tool_activity.push(format!("[{}] {}", tool_name.to_uppercase(), caption));
|
||||
self.tool_activity.push(String::new());
|
||||
for line in content.lines() {
|
||||
// Wrap long lines if needed
|
||||
let max_content_width = box_width - 4; // Account for borders and padding
|
||||
if line.len() <= max_content_width {
|
||||
self.output_history.push(format!(
|
||||
"{} {:<width$} {}",
|
||||
vertical,
|
||||
line,
|
||||
vertical,
|
||||
width = max_content_width
|
||||
));
|
||||
} else {
|
||||
// Simple word wrapping for long lines
|
||||
for chunk in line.chars().collect::<Vec<_>>().chunks(max_content_width) {
|
||||
let chunk_str: String = chunk.iter().collect();
|
||||
self.output_history.push(format!(
|
||||
"{} {:<width$} {}",
|
||||
vertical,
|
||||
chunk_str,
|
||||
vertical,
|
||||
width = max_content_width
|
||||
));
|
||||
}
|
||||
}
|
||||
self.tool_activity.push(line.to_string());
|
||||
}
|
||||
|
||||
// Add bottom border
|
||||
self.output_history.push(format!(
|
||||
"{}{}{}",
|
||||
corner_bl,
|
||||
border_char.repeat(box_width - 2),
|
||||
corner_br
|
||||
));
|
||||
self.output_history.push(String::new()); // Empty line after box
|
||||
self.tool_activity_scroll = 0; // Reset scroll when new content arrives
|
||||
|
||||
// Auto-scroll to bottom only if user hasn't manually scrolled
|
||||
if !self.manual_scroll {
|
||||
@@ -203,6 +170,46 @@ impl TerminalState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update tool header with completion status and timing
|
||||
fn update_tool_completion(&mut self, name: &str, success: bool, duration_ms: u128, caption: &str) {
|
||||
// Find and update the last tool header in place
|
||||
if let Some(index) = self.last_tool_header_index {
|
||||
if index < self.output_history.len() {
|
||||
// Format the timing info
|
||||
let timing = if duration_ms < 1000 {
|
||||
format!("{}ms", duration_ms)
|
||||
} else {
|
||||
format!("{:.2}s", duration_ms as f64 / 1000.0)
|
||||
};
|
||||
|
||||
// Create the updated header with status marker and timing
|
||||
let status_marker = if success { "[SUCCESS]" } else { "[FAILED]" };
|
||||
let header_text = format!(" {} | {} | {}", name.to_uppercase(), caption, timing);
|
||||
|
||||
// Replace the existing header line with the updated one
|
||||
self.output_history[index] = format!("{}{}", status_marker, header_text);
|
||||
|
||||
// Clear the tracking index
|
||||
self.last_tool_header_index = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update tool detail panel without changing the header
|
||||
fn update_tool_detail(&mut self, name: &str, content: &str) {
|
||||
// Update the tool detail panel with the complete content
|
||||
self.tool_activity.clear();
|
||||
self.tool_activity.push(format!("[{}] Complete", name.to_uppercase()));
|
||||
self.tool_activity.push(String::new());
|
||||
|
||||
// Add all the content lines
|
||||
for line in content.lines() {
|
||||
self.tool_activity.push(line.to_string());
|
||||
}
|
||||
|
||||
self.tool_activity_scroll = 0; // Reset scroll when new content arrives
|
||||
}
|
||||
|
||||
/// Add text to output history
|
||||
fn add_output(&mut self, text: &str) {
|
||||
let mut lines = text.lines();
|
||||
@@ -319,6 +326,20 @@ impl RetroTui {
|
||||
} => {
|
||||
state.format_tool_output(&name, &caption, &content);
|
||||
}
|
||||
TuiMessage::ToolDetailUpdate {
|
||||
name,
|
||||
content,
|
||||
} => {
|
||||
state.update_tool_detail(&name, &content);
|
||||
}
|
||||
TuiMessage::ToolComplete {
|
||||
name,
|
||||
success,
|
||||
duration_ms,
|
||||
caption,
|
||||
} => {
|
||||
state.update_tool_completion(&name, success, duration_ms, &caption);
|
||||
}
|
||||
TuiMessage::SystemStatus(status) => {
|
||||
let was_processing = state.status_line == "PROCESSING";
|
||||
state.status_line = status;
|
||||
@@ -418,7 +439,8 @@ impl RetroTui {
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(5), // Header/input area
|
||||
Constraint::Min(10), // Main output area
|
||||
Constraint::Min(10), // Main output area (will be further split)
|
||||
Constraint::Length(8), // Activity area
|
||||
Constraint::Length(1), // Status bar
|
||||
])
|
||||
.split(size);
|
||||
@@ -449,10 +471,13 @@ impl RetroTui {
|
||||
// Draw main output area
|
||||
Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset);
|
||||
|
||||
// Draw activity area (tool output)
|
||||
Self::draw_activity_area(f, chunks[2], &state.tool_activity, state.tool_activity_scroll);
|
||||
|
||||
// Draw status bar
|
||||
Self::draw_status_bar(
|
||||
f,
|
||||
chunks[2],
|
||||
chunks[3],
|
||||
&state.status_line,
|
||||
state.context_info,
|
||||
&state.provider_info,
|
||||
@@ -521,10 +546,21 @@ impl RetroTui {
|
||||
.take(visible_height)
|
||||
.map(|line| {
|
||||
// Check if this is a tool header line
|
||||
if line.contains("[TOOL_HEADER]") {
|
||||
if line.starts_with("[TOOL_HEADER]") {
|
||||
// Extract the actual header text
|
||||
let cleaned = line.replace("[TOOL_HEADER]", "");
|
||||
// Style with green background and black text
|
||||
// Style with amber background and black text
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
.bg(TERMINAL_AMBER)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if line.starts_with("[SUCCESS]") {
|
||||
// Extract the actual header text
|
||||
let cleaned = line.replace("[SUCCESS]", "");
|
||||
// Style with green background for successful tool completion
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
@@ -532,6 +568,17 @@ impl RetroTui {
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if line.starts_with("[FAILED]") {
|
||||
// Extract the actual header text
|
||||
let cleaned = line.replace("[FAILED]", "");
|
||||
// Style with red background for failed tool completion
|
||||
return Line::from(Span::styled(
|
||||
format!(" {}", cleaned),
|
||||
Style::default()
|
||||
.bg(TERMINAL_RED)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
|
||||
// Check if this is a box border line
|
||||
@@ -610,6 +657,88 @@ impl RetroTui {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the activity area with tool output
|
||||
fn draw_activity_area(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
tool_activity: &[String],
|
||||
scroll_offset: usize,
|
||||
) {
|
||||
// Split the activity area into left and right halves
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(50), // Left half for tool output
|
||||
Constraint::Percentage(50), // Right half (reserved for future use)
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// Draw left half - Tool Activity
|
||||
let visible_height = chunks[0].height.saturating_sub(2) as usize; // Account for borders
|
||||
let total_lines = tool_activity.len();
|
||||
|
||||
// Calculate scroll position
|
||||
let scroll = if total_lines <= visible_height {
|
||||
0
|
||||
} else {
|
||||
scroll_offset.min(total_lines.saturating_sub(visible_height))
|
||||
};
|
||||
|
||||
// Get visible lines for tool activity
|
||||
let visible_lines: Vec<Line> = if tool_activity.is_empty() {
|
||||
vec![Line::from(Span::styled(
|
||||
" No tool activity yet",
|
||||
Style::default().fg(TERMINAL_DIM_GREEN).add_modifier(Modifier::ITALIC),
|
||||
))]
|
||||
} else {
|
||||
tool_activity
|
||||
.iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.map(|line| {
|
||||
// Style the header lines differently
|
||||
let style = if line.starts_with('[') && line.contains(']') {
|
||||
Style::default().fg(TERMINAL_CYAN).add_modifier(Modifier::BOLD)
|
||||
} else if line.is_empty() {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(TERMINAL_GREEN)
|
||||
};
|
||||
Line::from(Span::styled(format!(" {}", line), style))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let tool_output = Paragraph::new(visible_lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" TOOL DETAIL ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
|
||||
.style(Style::default().bg(TERMINAL_BG)),
|
||||
)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
f.render_widget(tool_output, chunks[0]);
|
||||
|
||||
// Draw right half - Activity
|
||||
let reserved = Paragraph::new(vec![Line::from(Span::styled(
|
||||
" Activity log will appear here",
|
||||
Style::default().fg(TERMINAL_DIM_GREEN).add_modifier(Modifier::ITALIC),
|
||||
))])
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" ACTIVITY ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(TERMINAL_DIM_GREEN))
|
||||
.style(Style::default().bg(TERMINAL_BG)),
|
||||
);
|
||||
|
||||
f.render_widget(reserved, chunks[1]);
|
||||
}
|
||||
|
||||
/// Draw the status bar
|
||||
fn draw_status_bar(
|
||||
f: &mut Frame,
|
||||
@@ -706,6 +835,24 @@ impl RetroTui {
|
||||
});
|
||||
}
|
||||
|
||||
/// Update tool detail panel without changing the header
|
||||
pub fn update_tool_detail(&self, name: &str, content: &str) {
|
||||
let _ = self.tx.send(TuiMessage::ToolDetailUpdate {
|
||||
name: name.to_string(),
|
||||
content: content.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Send tool completion status to the terminal
|
||||
pub fn tool_complete(&self, name: &str, success: bool, duration_ms: u128, caption: &str) {
|
||||
let _ = self.tx.send(TuiMessage::ToolComplete {
|
||||
name: name.to_string(),
|
||||
success,
|
||||
duration_ms,
|
||||
caption: caption.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Update system status
|
||||
pub fn status(&self, status: &str) {
|
||||
let _ = self.tx.send(TuiMessage::SystemStatus(status.to_string()));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::retro_tui::RetroTui;
|
||||
use g3_core::ui_writer::UiWriter;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Instant;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Console implementation of UiWriter that prints to stdout
|
||||
@@ -87,6 +88,8 @@ pub struct RetroTuiWriter {
|
||||
tui: RetroTui,
|
||||
current_tool_name: Mutex<Option<String>>,
|
||||
current_tool_output: Mutex<Vec<String>>,
|
||||
current_tool_start: Mutex<Option<Instant>>,
|
||||
current_tool_caption: Mutex<String>,
|
||||
}
|
||||
|
||||
impl RetroTuiWriter {
|
||||
@@ -95,6 +98,8 @@ impl RetroTuiWriter {
|
||||
tui,
|
||||
current_tool_name: Mutex::new(None),
|
||||
current_tool_output: Mutex::new(Vec::new()),
|
||||
current_tool_start: Mutex::new(None),
|
||||
current_tool_caption: Mutex::new(String::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,12 +134,16 @@ impl UiWriter for RetroTuiWriter {
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str) {
|
||||
// Start collecting tool output
|
||||
*self.current_tool_start.lock().unwrap() = Some(Instant::now());
|
||||
*self.current_tool_name.lock().unwrap() = Some(tool_name.to_string());
|
||||
self.current_tool_output.lock().unwrap().clear();
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("Tool: {}", tool_name));
|
||||
|
||||
// Initialize caption
|
||||
*self.current_tool_caption.lock().unwrap() = String::new();
|
||||
}
|
||||
|
||||
fn print_tool_arg(&self, key: &str, value: &str) {
|
||||
@@ -142,9 +151,31 @@ impl UiWriter for RetroTuiWriter {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{}: {}", key, value));
|
||||
|
||||
// Build caption from first argument (usually the most important one)
|
||||
let mut caption = self.current_tool_caption.lock().unwrap();
|
||||
if caption.is_empty() && (key == "file_path" || key == "command" || key == "path") {
|
||||
// Truncate long values for the caption
|
||||
let truncated = if value.len() > 50 { format!("{}...", &value[..47]) } else { value.to_string() };
|
||||
*caption = truncated;
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tool_output_header(&self) {
|
||||
// This is called right before tool execution starts
|
||||
// Send the initial tool header to the TUI now
|
||||
if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() {
|
||||
let caption = self.current_tool_caption.lock().unwrap().clone();
|
||||
let caption = if caption.is_empty() { "Executing...".to_string() } else { caption };
|
||||
|
||||
// Send the tool output with initial header
|
||||
self.tui.tool_output(
|
||||
tool_name,
|
||||
&caption,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
self.current_tool_output.lock().unwrap().push(String::new());
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
@@ -173,15 +204,36 @@ impl UiWriter for RetroTuiWriter {
|
||||
.unwrap()
|
||||
.push(format!("⚡️ {}", duration_str));
|
||||
|
||||
// Now send the complete tool output as a box
|
||||
// Calculate the actual duration
|
||||
let duration_ms = if let Some(start) = *self.current_tool_start.lock().unwrap() {
|
||||
start.elapsed().as_millis()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Get the tool name and caption
|
||||
if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() {
|
||||
let content = self.current_tool_output.lock().unwrap().join("\n");
|
||||
self.tui.tool_output(tool_name, "...", &content);
|
||||
let caption = self.current_tool_caption.lock().unwrap().clone();
|
||||
let caption = if caption.is_empty() { "Completed".to_string() } else { caption };
|
||||
|
||||
// Update the tool detail panel with the complete output without adding a new header
|
||||
// This keeps the original header in place to be updated by tool_complete
|
||||
self.tui.update_tool_detail(tool_name, &content);
|
||||
|
||||
// Determine success based on whether there's an error in the output
|
||||
// This is a simple heuristic - you might want to make this more sophisticated
|
||||
let success = !content.contains("error") && !content.contains("Error") && !content.contains("ERROR");
|
||||
|
||||
// Send the completion status to update the header
|
||||
self.tui.tool_complete(tool_name, success, duration_ms, &caption);
|
||||
}
|
||||
|
||||
// Clear the buffers
|
||||
*self.current_tool_name.lock().unwrap() = None;
|
||||
self.current_tool_output.lock().unwrap().clear();
|
||||
*self.current_tool_start.lock().unwrap() = None;
|
||||
*self.current_tool_caption.lock().unwrap() = String::new();
|
||||
}
|
||||
|
||||
fn print_agent_prompt(&self) {
|
||||
|
||||
Reference in New Issue
Block a user