refactor(tui/history): consolidate merges + introduce ExecAction enum

- Add ExecAction enum and action_enum_from_parsed()
- Switch exec_tools to use enum instead of string tags
- Add ChatWidget::history_maybe_merge_tool_with_previous()
- Adopt merge helper in web_search_complete
- Keep behavior identical; centralize merge pathways

Validated with ./build-fast.sh (no warnings).
This commit is contained in:
James Peter
2025-09-02 19:38:37 +10:00
parent b3696d0f46
commit 7dc0cbfc7d
4 changed files with 66 additions and 31 deletions
+38
View File
@@ -1605,6 +1605,44 @@ impl ChatWidget<'_> {
crate::chatwidget::exec_tools::try_merge_completed_exec_at(self, idx);
}
// Merge adjacent tool cells with the same header (e.g., successive Web Search blocks)
fn history_maybe_merge_tool_with_previous(&mut self, idx: usize) {
if idx == 0 || idx >= self.history_cells.len() { return; }
let new_lines = self.history_cells[idx].display_lines();
let new_header = new_lines
.first()
.and_then(|l| l.spans.get(0))
.map(|s| s.content.clone().to_string())
.unwrap_or_default();
if new_header.is_empty() { return; }
let prev_lines = self.history_cells[idx - 1].display_lines();
let prev_header = prev_lines
.first()
.and_then(|l| l.spans.get(0))
.map(|s| s.content.clone().to_string())
.unwrap_or_default();
if new_header != prev_header { return; }
let mut combined = prev_lines.clone();
while combined
.last()
.map(|l| crate::render::line_utils::is_blank_line_trim(l))
.unwrap_or(false)
{ combined.pop(); }
let mut body: Vec<ratatui::text::Line<'static>> = new_lines.into_iter().skip(1).collect();
while body.first().map(|l| crate::render::line_utils::is_blank_line_trim(l)).unwrap_or(false) { body.remove(0); }
while body.last().map(|l| crate::render::line_utils::is_blank_line_trim(l)).unwrap_or(false) { body.pop(); }
if let Some(first_line) = body.first_mut() {
if let Some(first_span) = first_line.spans.get_mut(0) {
if first_span.content == " └ " || first_span.content == "└ " {
first_span.content = " ".into();
}
}
}
combined.extend(body);
self.history_replace_at(idx - 1, Box::new(crate::history_cell::PlainHistoryCell { lines: combined, kind: crate::history_cell::HistoryCellType::Plain }));
self.history_remove_at(idx);
}
/// Clean up faded-out animation cells
fn process_animation_cleanup(&mut self) {
// With trait-based cells, we can't easily detect and clean up specific cell types
+5 -5
View File
@@ -301,10 +301,10 @@ pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBe
if chat.ended_call_ids.contains(&ev.call_id) { return; }
for cell in &chat.history_cells { cell.trigger_fade(); }
let parsed_command = ev.parsed_cmd.clone();
let action = history_cell::action_from_parsed(&parsed_command);
let action = history_cell::action_enum_from_parsed(&parsed_command);
chat.height_manager.borrow_mut().record_event(HeightEvent::RunBegin);
if action == "read" {
if matches!(action, history_cell::ExecAction::Read) {
chat.exec.running_commands.insert(
ev.call_id.clone(),
super::RunningCommand { command: ev.command.clone(), parsed: parsed_command.clone(), history_index: None },
@@ -379,13 +379,13 @@ pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndE
.map(|cmd| (cmd.command, cmd.parsed, cmd.history_index))
.unwrap_or_else(|| (vec![call_id.clone()], vec![], None));
let action = history_cell::action_from_parsed(&parsed);
if action == "read" {
let action = history_cell::action_enum_from_parsed(&parsed);
if matches!(action, history_cell::ExecAction::Read) {
let any_read_running = chat
.exec
.running_commands
.values()
.any(|rc| history_cell::action_from_parsed(&rc.parsed) == "read");
.any(|rc| matches!(history_cell::action_enum_from_parsed(&rc.parsed), history_cell::ExecAction::Read));
if !any_read_running {
if let Some(idx) = chat.exec.running_read_agg_index.take() {
if idx < chat.history_cells.len() {
+1 -26
View File
@@ -49,32 +49,7 @@ pub(super) fn web_search_complete(chat: &mut ChatWidget<'_>, call_id: String, qu
let final_query = query.or(maybe_query);
let completed = rt.finalize_web_search(true, final_query);
chat.history_replace_at(i, Box::new(completed));
// Merge adjacent Web Search blocks with same header
if i > 0 {
let new_lines = chat.history_cells[i].display_lines();
let new_header = new_lines.first().and_then(|l| l.spans.get(0)).map(|s| s.content.clone().to_string()).unwrap_or_default();
let prev_lines = chat.history_cells[i - 1].display_lines();
let prev_header = prev_lines.first().and_then(|l| l.spans.get(0)).map(|s| s.content.clone().to_string()).unwrap_or_default();
if !new_header.is_empty() && new_header == prev_header {
let mut combined = prev_lines.clone();
while combined.last().map(|l| crate::render::line_utils::is_blank_line_trim(l)).unwrap_or(false) { combined.pop(); }
let mut body: Vec<ratatui::text::Line<'static>> = new_lines.into_iter().skip(1).collect();
while body.first().map(|l| crate::render::line_utils::is_blank_line_trim(l)).unwrap_or(false) { body.remove(0); }
while body.last().map(|l| crate::render::line_utils::is_blank_line_trim(l)).unwrap_or(false) { body.pop(); }
if let Some(first_line) = body.first_mut() {
if let Some(first_span) = first_line.spans.get_mut(0) {
if first_span.content == " └ " || first_span.content == "└ " {
first_span.content = " ".into();
}
}
}
combined.extend(body);
chat.history_replace_at(i - 1, Box::new(history_cell::PlainHistoryCell { lines: combined, kind: history_cell::HistoryCellType::Plain }));
chat.history_remove_at(i);
}
}
// history_replace_at already invalidates and redraws
chat.history_maybe_merge_tool_with_previous(i);
}
}
}
+22
View File
@@ -81,6 +81,28 @@ pub(crate) enum ExecKind {
Run,
}
// Unified action classification for exec commands
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExecAction {
Read,
Search,
List,
Run,
}
pub(crate) fn action_enum_from_parsed(parsed: &Vec<codex_core::parse_command::ParsedCommand>) -> ExecAction {
use codex_core::parse_command::ParsedCommand;
for p in parsed {
match p {
ParsedCommand::Read { .. } => return ExecAction::Read,
ParsedCommand::Search { .. } => return ExecAction::Search,
ParsedCommand::ListFiles { .. } => return ExecAction::List,
_ => {}
}
}
ExecAction::Run
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExecStatus {
Running,