mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
fix(tui/history): collapse wait tool output into exec cell
This commit is contained in:
@@ -204,6 +204,9 @@ struct RunningCommand {
|
||||
explore_entry: Option<(usize, usize)>,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
wait_total: Option<Duration>,
|
||||
wait_active: bool,
|
||||
wait_note: Option<(String, bool)>,
|
||||
}
|
||||
|
||||
pub(crate) struct ChatWidget<'a> {
|
||||
@@ -488,6 +491,12 @@ fn wait_target_from_params(params: Option<&String>, call_id: &str) -> String {
|
||||
format!("call {}", call_id)
|
||||
}
|
||||
|
||||
fn wait_exec_call_id_from_params(params: Option<&String>) -> Option<ExecCallId> {
|
||||
params
|
||||
.and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
|
||||
.and_then(|json| json.get("call_id").and_then(|v| v.as_str()).map(|s| ExecCallId(s.to_string())))
|
||||
}
|
||||
|
||||
fn line_text(line: &Line<'static>) -> String {
|
||||
line.spans
|
||||
.iter()
|
||||
@@ -2027,6 +2036,7 @@ impl ChatWidget<'_> {
|
||||
tools_state: ToolState {
|
||||
running_custom_tools: HashMap::new(),
|
||||
running_web_search: HashMap::new(),
|
||||
running_wait_tools: HashMap::new(),
|
||||
},
|
||||
// Use max width to disable wrapping during streaming
|
||||
// Text will be properly wrapped when displayed based on terminal width
|
||||
@@ -2217,6 +2227,7 @@ impl ChatWidget<'_> {
|
||||
tools_state: ToolState {
|
||||
running_custom_tools: HashMap::new(),
|
||||
running_web_search: HashMap::new(),
|
||||
running_wait_tools: HashMap::new(),
|
||||
},
|
||||
live_builder: RowBuilder::new(usize::MAX),
|
||||
pending_images: HashMap::new(),
|
||||
@@ -4423,6 +4434,37 @@ impl ChatWidget<'_> {
|
||||
self.flush_interrupt_queue();
|
||||
// Show an active entry immediately for all custom tools so the user sees progress
|
||||
let params_string = parameters.map(|p| p.to_string());
|
||||
if tool_name == "wait" {
|
||||
if let Some(exec_call_id) =
|
||||
wait_exec_call_id_from_params(params_string.as_ref())
|
||||
{
|
||||
self.tools_state
|
||||
.running_wait_tools
|
||||
.insert(ToolCallId(call_id.clone()), exec_call_id.clone());
|
||||
|
||||
if let Some(running) = self.exec.running_commands.get_mut(&exec_call_id) {
|
||||
running.wait_active = true;
|
||||
running.wait_note = None;
|
||||
let history_index = running.history_index;
|
||||
if let Some(idx) = history_index {
|
||||
if idx < self.history_cells.len() {
|
||||
if let Some(exec_cell) = self.history_cells[idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
{
|
||||
exec_cell.set_waiting(true);
|
||||
exec_cell.set_wait_note(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for command".to_string());
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Animated running cell with live timer and formatted args
|
||||
let cell = if tool_name.starts_with("browser_") {
|
||||
history_cell::new_running_browser_tool_call(
|
||||
@@ -4494,6 +4536,92 @@ impl ChatWidget<'_> {
|
||||
Ok(content) => (true, content),
|
||||
Err(error) => (false, error),
|
||||
};
|
||||
if tool_name == "wait" {
|
||||
if let Some(exec_call_id) = self
|
||||
.tools_state
|
||||
.running_wait_tools
|
||||
.remove(&ToolCallId(call_id.clone()))
|
||||
{
|
||||
let trimmed = content.trim();
|
||||
let wait_still_pending = !success && trimmed != "Cancelled by user.";
|
||||
let wait_note = if trimmed == "Cancelled by user." {
|
||||
Some(("Wait cancelled".to_string(), true))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut history_index: Option<usize> = None;
|
||||
if let Some(running) = self.exec.running_commands.get_mut(&exec_call_id) {
|
||||
let base = running.wait_total.unwrap_or_default();
|
||||
let total = base.saturating_add(duration);
|
||||
running.wait_total = Some(total);
|
||||
history_index = running.history_index;
|
||||
running.wait_active = wait_still_pending;
|
||||
running.wait_note = wait_note.clone();
|
||||
}
|
||||
|
||||
let mut updated = false;
|
||||
if let Some(idx) = history_index {
|
||||
if idx < self.history_cells.len() {
|
||||
if let Some(exec_cell) = self.history_cells[idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
{
|
||||
let total = exec_cell
|
||||
.wait_total()
|
||||
.unwrap_or_default()
|
||||
.saturating_add(duration);
|
||||
exec_cell.set_wait_total(Some(total));
|
||||
if wait_still_pending {
|
||||
exec_cell.set_waiting(true);
|
||||
} else {
|
||||
exec_cell.set_waiting(false);
|
||||
}
|
||||
exec_cell.set_wait_note(wait_note.clone());
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
if let Some(exec_cell) = self
|
||||
.history_cells
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|cell| {
|
||||
cell.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExecCell>()
|
||||
})
|
||||
{
|
||||
let total = exec_cell
|
||||
.wait_total()
|
||||
.unwrap_or_default()
|
||||
.saturating_add(duration);
|
||||
exec_cell.set_wait_total(Some(total));
|
||||
if wait_still_pending {
|
||||
exec_cell.set_waiting(true);
|
||||
} else {
|
||||
exec_cell.set_waiting(false);
|
||||
}
|
||||
exec_cell.set_wait_note(wait_note.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
self.remove_background_completion_message(&call_id);
|
||||
self.bottom_pane
|
||||
.update_status_text("responding".to_string());
|
||||
self.maybe_hide_spinner();
|
||||
} else if trimmed == "Cancelled by user." {
|
||||
self.bottom_pane
|
||||
.update_status_text("wait cancelled".to_string());
|
||||
} else {
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for command".to_string());
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let running_entry = self
|
||||
.tools_state
|
||||
.running_custom_tools
|
||||
@@ -14187,6 +14315,7 @@ impl RunningToolEntry {
|
||||
struct ToolState {
|
||||
running_custom_tools: HashMap<ToolCallId, RunningToolEntry>,
|
||||
running_web_search: HashMap<ToolCallId, (usize, Option<String>)>,
|
||||
running_wait_tools: HashMap<ToolCallId, ExecCallId>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct StreamState {
|
||||
|
||||
@@ -508,6 +508,9 @@ pub(super) fn handle_exec_begin_now(
|
||||
explore_entry: Some((idx, entry_idx)),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
wait_total: None,
|
||||
wait_active: false,
|
||||
wait_note: None,
|
||||
},
|
||||
);
|
||||
chat.invalidate_height_cache();
|
||||
@@ -539,6 +542,9 @@ pub(super) fn handle_exec_begin_now(
|
||||
explore_entry: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
wait_total: None,
|
||||
wait_active: false,
|
||||
wait_note: None,
|
||||
},
|
||||
);
|
||||
if !chat.tools_state.running_web_search.is_empty() {
|
||||
@@ -598,15 +604,17 @@ pub(super) fn handle_exec_end_now(
|
||||
chat.height_manager
|
||||
.borrow_mut()
|
||||
.record_event(HeightEvent::RunEnd);
|
||||
let (command, parsed, history_index, explore_entry) = match cmd {
|
||||
let (command, parsed, history_index, explore_entry, wait_total, wait_note) = match cmd {
|
||||
Some(super::RunningCommand {
|
||||
command,
|
||||
parsed,
|
||||
history_index,
|
||||
explore_entry,
|
||||
wait_total,
|
||||
wait_note,
|
||||
..
|
||||
}) => (command, parsed, history_index, explore_entry),
|
||||
None => (vec![call_id.clone()], vec![], None, None),
|
||||
}) => (command, parsed, history_index, explore_entry, wait_total, wait_note),
|
||||
None => (vec![call_id.clone()], vec![], None, None, None, None),
|
||||
};
|
||||
|
||||
if let Some((agg_idx, entry_idx)) = explore_entry {
|
||||
@@ -674,6 +682,11 @@ pub(super) fn handle_exec_end_now(
|
||||
stderr,
|
||||
},
|
||||
));
|
||||
if let Some(ref cell) = completed_opt {
|
||||
cell.set_wait_total(wait_total);
|
||||
cell.set_wait_note(wait_note.clone());
|
||||
cell.set_waiting(false);
|
||||
}
|
||||
|
||||
let mut replaced = false;
|
||||
if let Some(idx) = history_index {
|
||||
|
||||
@@ -1322,6 +1322,19 @@ impl HistoryCell for PlanUpdateCell {
|
||||
|
||||
// ==================== ExecCell ====================
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct ExecWaitNote {
|
||||
text: String,
|
||||
is_error: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ExecWaitState {
|
||||
total_wait: Option<Duration>,
|
||||
waiting: bool,
|
||||
note: Option<ExecWaitNote>,
|
||||
}
|
||||
|
||||
pub(crate) struct ExecCell {
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed: Vec<ParsedCommand>,
|
||||
@@ -1336,6 +1349,7 @@ pub(crate) struct ExecCell {
|
||||
// Cached per-width layout (wrapped rows + totals) while content is stable
|
||||
cached_layout: std::cell::RefCell<Option<Rc<ExecLayoutCache>>>,
|
||||
has_bold_command: bool,
|
||||
wait_state: std::cell::RefCell<ExecWaitState>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1814,6 +1828,9 @@ impl HistoryCell for ExecCell {
|
||||
fn has_custom_render(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn is_animating(&self) -> bool {
|
||||
self.output.is_none() && self.start_time.is_some()
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let (pre_total, _out_block_total, out_total_with_status) = self.ensure_wrap_totals(width);
|
||||
pre_total.saturating_add(out_total_with_status)
|
||||
@@ -1942,6 +1959,82 @@ impl HistoryCell for ExecCell {
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
fn invalidate_render_caches(&self) {
|
||||
self.cached_display_lines.borrow_mut().take();
|
||||
self.cached_pre_lines.borrow_mut().take();
|
||||
self.cached_out_lines.borrow_mut().take();
|
||||
self.cached_layout.borrow_mut().take();
|
||||
self.stream_status_line.borrow_mut().take();
|
||||
}
|
||||
|
||||
pub(crate) fn set_waiting(&self, waiting: bool) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.waiting != waiting {
|
||||
state.waiting = waiting;
|
||||
drop(state);
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_wait_total(&self, total: Option<Duration>) {
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.total_wait != total {
|
||||
state.total_wait = total;
|
||||
drop(state);
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_total(&self) -> Option<Duration> {
|
||||
self.wait_state.borrow().total_wait
|
||||
}
|
||||
|
||||
pub(crate) fn set_wait_note(&self, note: Option<(String, bool)>) {
|
||||
let new_note = note.map(|(text, is_error)| ExecWaitNote { text, is_error });
|
||||
let mut state = self.wait_state.borrow_mut();
|
||||
if state.note != new_note {
|
||||
state.note = new_note;
|
||||
drop(state);
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_note_line(&self, state: &ExecWaitState) -> Option<Line<'static>> {
|
||||
let note = state.note.as_ref()?;
|
||||
let mut line = Line::from(note.text.clone());
|
||||
let mut style = Style::default().fg(if note.is_error {
|
||||
crate::colors::error()
|
||||
} else {
|
||||
crate::colors::text_dim()
|
||||
});
|
||||
if note.is_error {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
for span in line.spans.iter_mut() {
|
||||
span.style = style;
|
||||
}
|
||||
Some(line)
|
||||
}
|
||||
|
||||
fn wait_state_snapshot(&self) -> ExecWaitState {
|
||||
self.wait_state.borrow().clone()
|
||||
}
|
||||
|
||||
fn wait_summary_line(&self, state: &ExecWaitState) -> Option<Line<'static>> {
|
||||
if state.waiting {
|
||||
return None;
|
||||
}
|
||||
let total = state.total_wait?;
|
||||
if total.is_zero() {
|
||||
return None;
|
||||
}
|
||||
let text = format!("Waited {}", format_duration(total));
|
||||
Some(Line::styled(
|
||||
text,
|
||||
Style::default().fg(crate::colors::text_dim()),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn has_bold_command(&self) -> bool {
|
||||
self.has_bold_command
|
||||
@@ -1955,11 +2048,7 @@ impl ExecCell {
|
||||
self.command = command;
|
||||
self.parsed = parsed;
|
||||
self.has_bold_command = command_has_bold_token(&self.command);
|
||||
self.cached_display_lines.borrow_mut().take();
|
||||
self.cached_pre_lines.borrow_mut().take();
|
||||
self.cached_out_lines.borrow_mut().take();
|
||||
self.cached_layout.borrow_mut().take();
|
||||
self.stream_status_line.borrow_mut().take();
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
/// Compute wrapped row totals for the preamble and the output at the given width.
|
||||
/// Delegates to the per-width layout cache to avoid redundant reflow work.
|
||||
@@ -1973,10 +2062,14 @@ impl ExecCell {
|
||||
}
|
||||
|
||||
fn ensure_layout(&self, width: u16) -> Rc<ExecLayoutCache> {
|
||||
if let Some(layout) = self.cached_layout.borrow().as_ref() {
|
||||
if layout.width == width {
|
||||
return layout.clone();
|
||||
if self.output.is_some() {
|
||||
if let Some(layout) = self.cached_layout.borrow().as_ref() {
|
||||
if layout.width == width {
|
||||
return layout.clone();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.cached_layout.borrow_mut().take();
|
||||
}
|
||||
|
||||
let (pre_lines_raw, out_lines_raw, status_line_opt) = self.exec_render_parts();
|
||||
@@ -2020,7 +2113,9 @@ impl ExecCell {
|
||||
out_block_total,
|
||||
out_total_with_status,
|
||||
});
|
||||
*self.cached_layout.borrow_mut() = Some(layout.clone());
|
||||
if self.output.is_some() {
|
||||
*self.cached_layout.borrow_mut() = Some(layout.clone());
|
||||
}
|
||||
layout
|
||||
}
|
||||
// Build separate segments: (preamble lines, output lines)
|
||||
@@ -2040,12 +2135,15 @@ impl ExecCell {
|
||||
return (pre.clone(), out.clone(), None);
|
||||
}
|
||||
|
||||
let (pre, out, status) = if self.parsed.is_empty() {
|
||||
let wait_state = self.wait_state_snapshot();
|
||||
let status_label = if wait_state.waiting { "Waiting" } else { "Running" };
|
||||
let (pre, mut out, status) = if self.parsed.is_empty() {
|
||||
exec_render_parts_generic(
|
||||
&self.command,
|
||||
self.output.as_ref(),
|
||||
self.stream_preview.as_ref(),
|
||||
self.start_time,
|
||||
status_label,
|
||||
)
|
||||
} else {
|
||||
exec_render_parts_parsed(
|
||||
@@ -2053,10 +2151,59 @@ impl ExecCell {
|
||||
self.output.as_ref(),
|
||||
self.stream_preview.as_ref(),
|
||||
self.start_time,
|
||||
status_label,
|
||||
)
|
||||
};
|
||||
|
||||
if self.output.is_some() {
|
||||
let mut extra_lines: Vec<Line<'static>> = Vec::new();
|
||||
if let Some(summary_line) = self.wait_summary_line(&wait_state) {
|
||||
extra_lines.push(summary_line);
|
||||
}
|
||||
if let Some(note_line) = self.wait_note_line(&wait_state) {
|
||||
extra_lines.push(note_line);
|
||||
}
|
||||
if !extra_lines.is_empty() {
|
||||
let is_blank_line = |line: &Line<'static>| {
|
||||
line.spans
|
||||
.iter()
|
||||
.all(|span| span.content.as_ref().trim().is_empty())
|
||||
};
|
||||
let is_error_line = |line: &Line<'static>| {
|
||||
line.spans
|
||||
.first()
|
||||
.map(|span| span.content.as_ref().starts_with("Error (exit code"))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
if let Some(pos) = out.iter().position(is_error_line) {
|
||||
let mut insert_at = pos;
|
||||
if insert_at > 0 && !is_blank_line(&out[insert_at - 1]) {
|
||||
out.insert(insert_at, Line::from(""));
|
||||
insert_at += 1;
|
||||
}
|
||||
for line in extra_lines.iter() {
|
||||
out.insert(insert_at, line.clone());
|
||||
insert_at += 1;
|
||||
out.insert(insert_at, Line::from(""));
|
||||
insert_at += 1;
|
||||
}
|
||||
} else {
|
||||
let mut insert_at = out.len();
|
||||
if insert_at > 0 && is_blank_line(&out[insert_at.saturating_sub(1)]) {
|
||||
insert_at = insert_at.saturating_sub(1);
|
||||
}
|
||||
if insert_at > 0 && !is_blank_line(&out[insert_at - 1]) {
|
||||
out.insert(insert_at, Line::from(""));
|
||||
insert_at += 1;
|
||||
}
|
||||
for line in extra_lines.iter() {
|
||||
out.insert(insert_at, line.clone());
|
||||
insert_at += 1;
|
||||
out.insert(insert_at, Line::from(""));
|
||||
insert_at += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
*self.cached_pre_lines.borrow_mut() = Some(pre.clone());
|
||||
*self.cached_out_lines.borrow_mut() = Some(out.clone());
|
||||
self.stream_status_line.borrow_mut().take();
|
||||
@@ -2079,11 +2226,7 @@ impl ExecCell {
|
||||
stderr: stderr.to_string(),
|
||||
});
|
||||
}
|
||||
self.cached_display_lines.borrow_mut().take();
|
||||
self.cached_pre_lines.borrow_mut().take();
|
||||
self.cached_out_lines.borrow_mut().take();
|
||||
self.cached_layout.borrow_mut().take();
|
||||
self.stream_status_line.borrow_mut().take();
|
||||
self.invalidate_render_caches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3072,6 +3215,7 @@ fn exec_render_parts_generic(
|
||||
output: Option<&CommandOutput>,
|
||||
stream_preview: Option<&CommandOutput>,
|
||||
start_time: Option<Instant>,
|
||||
status_label: &str,
|
||||
) -> (
|
||||
Vec<Line<'static>>,
|
||||
Vec<Line<'static>>,
|
||||
@@ -3106,7 +3250,7 @@ fn exec_render_parts_generic(
|
||||
let has_output = !trim_empty_lines(out.clone()).is_empty();
|
||||
|
||||
if render_running_header {
|
||||
let mut message = "Running...".to_string();
|
||||
let mut message = format!("{}...", status_label);
|
||||
if let Some(start) = start_time {
|
||||
let elapsed = start.elapsed();
|
||||
message = format!("{message} ({})", format_duration(elapsed));
|
||||
@@ -3143,6 +3287,7 @@ fn exec_render_parts_parsed(
|
||||
output: Option<&CommandOutput>,
|
||||
stream_preview: Option<&CommandOutput>,
|
||||
start_time: Option<Instant>,
|
||||
status_label: &str,
|
||||
) -> (
|
||||
Vec<Line<'static>>,
|
||||
Vec<Line<'static>>,
|
||||
@@ -3170,8 +3315,8 @@ fn exec_render_parts_parsed(
|
||||
)),
|
||||
ExecAction::Run => {
|
||||
let mut message = match &ctx_path {
|
||||
Some(p) => format!("Running... in {p}"),
|
||||
None => "Running...".to_string(),
|
||||
Some(p) => format!("{}... in {p}", status_label),
|
||||
None => format!("{}...", status_label),
|
||||
};
|
||||
if let Some(start) = start_time {
|
||||
let elapsed = start.elapsed();
|
||||
@@ -6065,6 +6210,7 @@ fn new_exec_cell(
|
||||
cached_out_lines: std::cell::RefCell::new(None),
|
||||
cached_layout: std::cell::RefCell::new(None),
|
||||
has_bold_command,
|
||||
wait_state: std::cell::RefCell::new(ExecWaitState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user