mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
tui: running state for custom + MCP tools with live timers; replace-in-place on completion
- Add RunningToolCallCell with live elapsed time - Browser_*: present-participle titles (Clicking…, Typing…, etc.) + formatted args - Custom tools: generic running titles; MCP: Working… with provider.tool(args) - Track running entries by call_id and replace with completed cell on end - Keep bottom status updates consistent
This commit is contained in:
@@ -139,6 +139,8 @@ pub(crate) struct ChatWidget<'a> {
|
||||
// Track the ID of the current streaming message to prevent duplicates
|
||||
// Track the ID of the current streaming reasoning to prevent duplicates
|
||||
running_commands: HashMap<String, RunningCommand>,
|
||||
// Track active custom tool cells by call_id so we can replace them on completion
|
||||
running_custom_tools: HashMap<String, usize>,
|
||||
live_builder: RowBuilder,
|
||||
// Store pending image paths keyed by their placeholder text
|
||||
pending_images: HashMap<String, PathBuf>,
|
||||
@@ -506,36 +508,36 @@ impl ChatWidget<'_> {
|
||||
for cell in &self.history_cells {
|
||||
cell.trigger_fade();
|
||||
}
|
||||
let McpToolCallBeginEvent {
|
||||
call_id: _,
|
||||
invocation,
|
||||
} = ev;
|
||||
|
||||
// Add active MCP tool call to history
|
||||
self.add_to_history(history_cell::new_active_mcp_tool_call(invocation));
|
||||
let McpToolCallBeginEvent { call_id, invocation } = ev;
|
||||
// Add animated running MCP tool call to history and track index
|
||||
let cell = history_cell::new_running_mcp_tool_call(invocation);
|
||||
self.add_to_history(cell);
|
||||
if let Some(last_idx) = self.history_cells.len().checked_sub(1) {
|
||||
self.running_custom_tools.insert(call_id, last_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle MCP tool call end immediately
|
||||
fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
|
||||
let McpToolCallEndEvent {
|
||||
call_id: _,
|
||||
duration,
|
||||
invocation,
|
||||
result,
|
||||
} = ev;
|
||||
|
||||
let McpToolCallEndEvent { call_id, duration, invocation, result } = ev;
|
||||
// Determine success from result
|
||||
let success = !result.as_ref().map(|r| r.is_error.unwrap_or(false)).unwrap_or(false);
|
||||
|
||||
// Add completed MCP tool call to history
|
||||
self.add_to_history(history_cell::new_completed_mcp_tool_call(
|
||||
80, // TODO: Use actual terminal width
|
||||
let completed = history_cell::new_completed_mcp_tool_call(
|
||||
80, // TODO: use actual terminal width
|
||||
invocation,
|
||||
duration,
|
||||
success,
|
||||
result,
|
||||
));
|
||||
// MCP tool calls are added directly to history, no active cell to move
|
||||
);
|
||||
if let Some(idx) = self.running_custom_tools.remove(&call_id) {
|
||||
if idx < self.history_cells.len() {
|
||||
self.history_cells[idx] = completed;
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.add_to_history(completed);
|
||||
}
|
||||
|
||||
/// Handle patch apply end immediately
|
||||
@@ -828,6 +830,7 @@ impl ChatWidget<'_> {
|
||||
content_buffer: String::new(),
|
||||
last_assistant_message: None,
|
||||
running_commands: HashMap::new(),
|
||||
running_custom_tools: HashMap::new(),
|
||||
// Use max width to disable wrapping during streaming
|
||||
// Text will be properly wrapped when displayed based on terminal width
|
||||
live_builder: RowBuilder::new(usize::MAX),
|
||||
@@ -1943,7 +1946,7 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
EventMsg::CustomToolCallBegin(CustomToolCallBeginEvent {
|
||||
call_id: _,
|
||||
call_id,
|
||||
tool_name,
|
||||
parameters,
|
||||
}) => {
|
||||
@@ -1952,12 +1955,18 @@ impl ChatWidget<'_> {
|
||||
self.finalize_active_stream();
|
||||
// Flush any queued interrupts when streaming ends
|
||||
self.flush_interrupt_queue();
|
||||
// For browser_* and agent_* tools, suppress the separate begin entry to show a single combined section
|
||||
if !(tool_name.starts_with("browser_") || tool_name.starts_with("agent_")) {
|
||||
let params_string = parameters.map(|p| p.to_string());
|
||||
self.add_to_history(history_cell::new_active_custom_tool_call(
|
||||
tool_name.clone(), params_string,
|
||||
));
|
||||
// Show an active entry immediately for all custom tools so the user sees progress
|
||||
let params_string = parameters.map(|p| p.to_string());
|
||||
// Animated running cell with live timer and formatted args
|
||||
let cell = if tool_name.starts_with("browser_") {
|
||||
history_cell::new_running_browser_tool_call(tool_name.clone(), params_string.clone())
|
||||
} else {
|
||||
history_cell::new_running_custom_tool_call(tool_name.clone(), params_string.clone())
|
||||
};
|
||||
self.add_to_history(cell);
|
||||
// Track index so we can replace it on completion
|
||||
if let Some(last_idx) = self.history_cells.len().checked_sub(1) {
|
||||
self.running_custom_tools.insert(call_id.clone(), last_idx);
|
||||
}
|
||||
|
||||
// Update border status based on tool
|
||||
@@ -1971,7 +1980,7 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
EventMsg::CustomToolCallEnd(CustomToolCallEndEvent {
|
||||
call_id: _,
|
||||
call_id,
|
||||
tool_name,
|
||||
parameters,
|
||||
duration,
|
||||
@@ -1984,13 +1993,24 @@ impl ChatWidget<'_> {
|
||||
Ok(content) => (true, content),
|
||||
Err(error) => (false, error),
|
||||
};
|
||||
self.add_to_history(history_cell::new_completed_custom_tool_call(
|
||||
tool_name,
|
||||
params_string,
|
||||
duration,
|
||||
success,
|
||||
let completed = history_cell::new_completed_custom_tool_call(
|
||||
tool_name,
|
||||
params_string,
|
||||
duration,
|
||||
success,
|
||||
content,
|
||||
));
|
||||
);
|
||||
if let Some(idx) = self.running_custom_tools.remove(&call_id) {
|
||||
if idx < self.history_cells.len() {
|
||||
self.history_cells[idx] = Box::new(completed);
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
} else {
|
||||
self.add_to_history(completed);
|
||||
}
|
||||
} else {
|
||||
self.add_to_history(completed);
|
||||
}
|
||||
|
||||
// After tool completes, likely transitioning to response
|
||||
self.bottom_pane.update_status_text("responding".to_string());
|
||||
|
||||
@@ -573,6 +573,32 @@ impl ToolCallCell {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RunningToolCallCell (animated) ====================
|
||||
|
||||
pub(crate) struct RunningToolCallCell {
|
||||
title: String,
|
||||
start_time: Instant,
|
||||
arg_lines: Vec<Line<'static>>,
|
||||
}
|
||||
|
||||
impl HistoryCell for RunningToolCallCell {
|
||||
fn as_any(&self) -> &dyn std::any::Any { self }
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
|
||||
fn kind(&self) -> HistoryCellType { HistoryCellType::Tool { status: ToolStatus::Running } }
|
||||
fn is_animating(&self) -> bool { true }
|
||||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||||
let elapsed = self.start_time.elapsed();
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::styled(
|
||||
format!("{} ({})", self.title, format_duration(elapsed)),
|
||||
Style::default().fg(crate::colors::primary()).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
lines.extend(self.arg_lines.clone());
|
||||
lines.push(Line::from(""));
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CollapsibleReasoningCell ====================
|
||||
// For reasoning content that can be collapsed to show only summary
|
||||
|
||||
@@ -1500,7 +1526,7 @@ pub(crate) fn new_active_custom_tool_call(tool_name: String, args: Option<String
|
||||
} else {
|
||||
format!("{}()", tool_name)
|
||||
};
|
||||
|
||||
|
||||
let lines: Vec<Line> = vec![
|
||||
title_line,
|
||||
Line::styled(
|
||||
@@ -1514,6 +1540,95 @@ pub(crate) fn new_active_custom_tool_call(tool_name: String, args: Option<String
|
||||
ToolCallCell { lines, state: ToolState::Running }
|
||||
}
|
||||
|
||||
// Friendly present-participle titles for running browser tools
|
||||
fn browser_running_title(tool_name: &str) -> &'static str {
|
||||
match tool_name {
|
||||
"browser_click" => "Clicking...",
|
||||
"browser_type" => "Typing...",
|
||||
"browser_key" => "Sending key...",
|
||||
"browser_javascript" => "Running JavaScript...",
|
||||
"browser_scroll" => "Scrolling...",
|
||||
"browser_open" => "Opening...",
|
||||
"browser_close" => "Closing...",
|
||||
"browser_status" => "Checking status...",
|
||||
"browser_history" => "Navigating...",
|
||||
"browser_inspect" => "Inspecting...",
|
||||
"browser_console" => "Reading console...",
|
||||
"browser_move" => "Moving...",
|
||||
_ => "Working...",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_running_browser_tool_call(tool_name: String, args: Option<String>) -> RunningToolCallCell {
|
||||
// Parse args JSON and format like completed cells
|
||||
let mut arg_lines: Vec<Line<'static>> = Vec::new();
|
||||
if let Some(args_str) = args {
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&args_str) {
|
||||
arg_lines.extend(format_browser_args_line(&json));
|
||||
}
|
||||
}
|
||||
RunningToolCallCell {
|
||||
title: browser_running_title(&tool_name).to_string(),
|
||||
start_time: Instant::now(),
|
||||
arg_lines,
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_tool_running_title(tool_name: &str) -> String {
|
||||
if tool_name.starts_with("agent_") {
|
||||
// Reuse agent title and append ellipsis
|
||||
format!("{}...", agent_tool_title(tool_name))
|
||||
} else if tool_name.starts_with("browser_") {
|
||||
browser_running_title(tool_name).to_string()
|
||||
} else {
|
||||
// TitleCase from snake_case and append ellipsis
|
||||
let pretty = tool_name
|
||||
.split('_')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(f) => format!("{}{}", f.to_uppercase(), chars.as_str()),
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
format!("{}...", pretty)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_running_custom_tool_call(tool_name: String, args: Option<String>) -> RunningToolCallCell {
|
||||
// Parse args JSON and format as key/value lines
|
||||
let mut arg_lines: Vec<Line<'static>> = Vec::new();
|
||||
if let Some(args_str) = args {
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&args_str) {
|
||||
arg_lines.extend(format_browser_args_line(&json));
|
||||
} else {
|
||||
// Fallback to showing raw args string
|
||||
arg_lines.push(Line::from(vec![
|
||||
Span::styled("└ args: ", Style::default().fg(crate::colors::text_dim())),
|
||||
Span::styled(args_str, Style::default().fg(crate::colors::text())),
|
||||
]));
|
||||
}
|
||||
}
|
||||
RunningToolCallCell {
|
||||
title: custom_tool_running_title(&tool_name),
|
||||
start_time: Instant::now(),
|
||||
arg_lines,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_running_mcp_tool_call(invocation: McpInvocation) -> RunningToolCallCell {
|
||||
// Represent as provider.tool(...) on one dim line beneath a generic running header with timer
|
||||
let line = format_mcp_invocation(invocation);
|
||||
RunningToolCallCell {
|
||||
title: "Working...".to_string(),
|
||||
start_time: Instant::now(),
|
||||
arg_lines: vec![line],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_completed_custom_tool_call(
|
||||
tool_name: String,
|
||||
args: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user