Skip to content

Commit 78869ce

Browse files
improve MCP tool call styling
1 parent 5332f6e commit 78869ce

9 files changed

+531
-144
lines changed

codex-rs/tui/src/chatwidget.rs

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ use crate::history_cell;
6666
use crate::history_cell::CommandOutput;
6767
use crate::history_cell::ExecCell;
6868
use crate::history_cell::HistoryCell;
69+
use crate::history_cell::McpToolCallCell;
6970
use crate::history_cell::PatchEventType;
7071
use crate::slash_command::SlashCommand;
7172
use crate::text_formatting::truncate_text;
@@ -114,7 +115,7 @@ pub(crate) struct ChatWidget {
114115
app_event_tx: AppEventSender,
115116
codex_op_tx: UnboundedSender<Op>,
116117
bottom_pane: BottomPane,
117-
active_exec_cell: Option<ExecCell>,
118+
active_cell: Option<Box<dyn HistoryCell>>,
118119
config: Config,
119120
auth_manager: Arc<AuthManager>,
120121
session_header: SessionHeader,
@@ -281,7 +282,7 @@ impl ChatWidget {
281282
/// and stop/clear running UI state.
282283
fn finalize_turn_with_error_message(&mut self, message: String) {
283284
// Ensure any spinner is replaced by a red ✗ and flushed into history.
284-
self.finalize_active_exec_cell_as_failed();
285+
self.finalize_active_cell_as_failed();
285286
// Emit the provided error message/history cell.
286287
self.add_to_history(history_cell::new_error_event(message));
287288
// Reset running state and clear streaming buffers.
@@ -481,8 +482,8 @@ impl ChatWidget {
481482

482483
#[inline]
483484
fn handle_streaming_delta(&mut self, delta: String) {
484-
// Before streaming agent content, flush any active exec cell group.
485-
self.flush_active_exec_cell();
485+
// Before streaming agent content, flush any active cell group.
486+
self.flush_active_cell();
486487
let sink = AppEventHistorySink(self.app_event_tx.clone());
487488
self.stream.begin(&sink);
488489
self.stream.push_and_maybe_commit(&delta, &sink);
@@ -496,16 +497,25 @@ impl ChatWidget {
496497
None => (vec![ev.call_id.clone()], Vec::new()),
497498
};
498499

499-
if self.active_exec_cell.is_none() {
500-
// This should have been created by handle_exec_begin_now, but in case it wasn't,
501-
// create it now.
502-
self.active_exec_cell = Some(history_cell::new_active_exec_command(
500+
let needs_new = self
501+
.active_cell
502+
.as_ref()
503+
.map(|cell| cell.as_any().downcast_ref::<ExecCell>().is_none())
504+
.unwrap_or(true);
505+
if needs_new {
506+
self.flush_active_cell();
507+
self.active_cell = Some(Box::new(history_cell::new_active_exec_command(
503508
ev.call_id.clone(),
504509
command,
505510
parsed,
506-
));
511+
)));
507512
}
508-
if let Some(cell) = self.active_exec_cell.as_mut() {
513+
514+
if let Some(cell) = self
515+
.active_cell
516+
.as_mut()
517+
.and_then(|c| c.as_any_mut().downcast_mut::<ExecCell>())
518+
{
509519
cell.complete_call(
510520
&ev.call_id,
511521
CommandOutput {
@@ -517,7 +527,7 @@ impl ChatWidget {
517527
ev.duration,
518528
);
519529
if cell.should_flush() {
520-
self.flush_active_exec_cell();
530+
self.flush_active_cell();
521531
}
522532
}
523533
}
@@ -584,58 +594,76 @@ impl ChatWidget {
584594
parsed_cmd: ev.parsed_cmd.clone(),
585595
},
586596
);
587-
if let Some(exec) = &self.active_exec_cell {
588-
if let Some(new_exec) = exec.with_added_call(
597+
if let Some(cell) = self
598+
.active_cell
599+
.as_mut()
600+
.and_then(|c| c.as_any_mut().downcast_mut::<ExecCell>())
601+
&& let Some(new_exec) = cell.with_added_call(
589602
ev.call_id.clone(),
590603
ev.command.clone(),
591604
ev.parsed_cmd.clone(),
592-
) {
593-
self.active_exec_cell = Some(new_exec);
594-
} else {
595-
// Make a new cell.
596-
self.flush_active_exec_cell();
597-
self.active_exec_cell = Some(history_cell::new_active_exec_command(
598-
ev.call_id.clone(),
599-
ev.command.clone(),
600-
ev.parsed_cmd,
601-
));
602-
}
605+
)
606+
{
607+
*cell = new_exec;
603608
} else {
604-
self.active_exec_cell = Some(history_cell::new_active_exec_command(
609+
self.flush_active_cell();
610+
611+
self.active_cell = Some(Box::new(history_cell::new_active_exec_command(
605612
ev.call_id.clone(),
606613
ev.command.clone(),
607614
ev.parsed_cmd,
608-
));
615+
)));
609616
}
610617

611-
// Request a redraw so the working header and command list are visible immediately.
612618
self.request_redraw();
613619
}
614620

615621
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
616622
self.flush_answer_stream_with_separator();
617-
self.add_to_history(history_cell::new_active_mcp_tool_call(ev.invocation));
623+
self.flush_active_cell();
624+
self.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
625+
ev.call_id,
626+
ev.invocation,
627+
)));
628+
self.request_redraw();
618629
}
619630
pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
620631
self.flush_answer_stream_with_separator();
621-
self.add_boxed_history(history_cell::new_completed_mcp_tool_call(
622-
80,
623-
ev.invocation,
624-
ev.duration,
625-
ev.result
626-
.as_ref()
627-
.map(|r| !r.is_error.unwrap_or(false))
628-
.unwrap_or(false),
629-
ev.result,
630-
));
632+
633+
let McpToolCallEndEvent {
634+
call_id,
635+
invocation,
636+
duration,
637+
result,
638+
} = ev;
639+
640+
let extra_cell = match self
641+
.active_cell
642+
.as_mut()
643+
.and_then(|cell| cell.as_any_mut().downcast_mut::<McpToolCallCell>())
644+
{
645+
Some(cell) if cell.call_id() == call_id => cell.complete(duration, result),
646+
_ => {
647+
self.flush_active_cell();
648+
let mut cell = history_cell::new_active_mcp_tool_call(call_id, invocation);
649+
let extra_cell = cell.complete(duration, result);
650+
self.active_cell = Some(Box::new(cell));
651+
extra_cell
652+
}
653+
};
654+
655+
self.flush_active_cell();
656+
if let Some(extra) = extra_cell {
657+
self.add_boxed_history(extra);
658+
}
631659
}
632660

633661
fn layout_areas(&self, area: Rect) -> [Rect; 3] {
634662
let bottom_min = self.bottom_pane.desired_height(area.width).min(area.height);
635663
let remaining = area.height.saturating_sub(bottom_min);
636664

637665
let active_desired = self
638-
.active_exec_cell
666+
.active_cell
639667
.as_ref()
640668
.map_or(0, |c| c.desired_height(area.width) + 1);
641669
let active_height = active_desired.min(remaining);
@@ -680,7 +708,7 @@ impl ChatWidget {
680708
placeholder_text: placeholder,
681709
disable_paste_burst: config.disable_paste_burst,
682710
}),
683-
active_exec_cell: None,
711+
active_cell: None,
684712
config: config.clone(),
685713
auth_manager,
686714
session_header: SessionHeader::new(config.model.clone()),
@@ -736,7 +764,7 @@ impl ChatWidget {
736764
placeholder_text: placeholder,
737765
disable_paste_burst: config.disable_paste_burst,
738766
}),
739-
active_exec_cell: None,
767+
active_cell: None,
740768
config: config.clone(),
741769
auth_manager,
742770
session_header: SessionHeader::new(config.model.clone()),
@@ -762,7 +790,7 @@ impl ChatWidget {
762790
pub fn desired_height(&self, width: u16) -> u16 {
763791
self.bottom_pane.desired_height(width)
764792
+ self
765-
.active_exec_cell
793+
.active_cell
766794
.as_ref()
767795
.map_or(0, |c| c.desired_height(width) + 1)
768796
}
@@ -974,10 +1002,9 @@ impl ChatWidget {
9741002
}
9751003
}
9761004

977-
fn flush_active_exec_cell(&mut self) {
978-
if let Some(active) = self.active_exec_cell.take() {
979-
self.app_event_tx
980-
.send(AppEvent::InsertHistoryCell(Box::new(active)));
1005+
fn flush_active_cell(&mut self) {
1006+
if let Some(active) = self.active_cell.take() {
1007+
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
9811008
}
9821009
}
9831010

@@ -988,7 +1015,7 @@ impl ChatWidget {
9881015
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
9891016
if !cell.display_lines(u16::MAX).is_empty() {
9901017
// Only break exec grouping if the cell renders visible lines.
991-
self.flush_active_exec_cell();
1018+
self.flush_active_cell();
9921019
}
9931020
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
9941021
}
@@ -1166,12 +1193,16 @@ impl ChatWidget {
11661193
}
11671194
}
11681195

1169-
/// Mark the active exec cell as failed (✗) and flush it into history.
1170-
fn finalize_active_exec_cell_as_failed(&mut self) {
1171-
if let Some(cell) = self.active_exec_cell.take() {
1172-
let cell = cell.into_failed();
1173-
// Insert finalized exec into history and keep grouping consistent.
1174-
self.add_to_history(cell);
1196+
/// Mark the active cell as failed (✗) and flush it into history.
1197+
fn finalize_active_cell_as_failed(&mut self) {
1198+
if let Some(mut cell) = self.active_cell.take() {
1199+
// Insert finalized cell into history and keep grouping consistent.
1200+
if let Some(exec) = cell.as_any_mut().downcast_mut::<ExecCell>() {
1201+
exec.mark_failed();
1202+
} else if let Some(tool) = cell.as_any_mut().downcast_mut::<McpToolCallCell>() {
1203+
tool.mark_failed();
1204+
}
1205+
self.add_boxed_history(cell);
11751206
}
11761207
}
11771208

@@ -1468,12 +1499,16 @@ impl WidgetRef for &ChatWidget {
14681499
let [_, active_cell_area, bottom_pane_area] = self.layout_areas(area);
14691500
(&self.bottom_pane).render(bottom_pane_area, buf);
14701501
if !active_cell_area.is_empty()
1471-
&& let Some(cell) = &self.active_exec_cell
1502+
&& let Some(cell) = &self.active_cell
14721503
{
1473-
let mut active_cell_area = active_cell_area;
1474-
active_cell_area.y = active_cell_area.y.saturating_add(1);
1475-
active_cell_area.height -= 1;
1476-
cell.render_ref(active_cell_area, buf);
1504+
let mut area = active_cell_area;
1505+
area.y = area.y.saturating_add(1);
1506+
area.height = area.height.saturating_sub(1);
1507+
if let Some(exec) = cell.as_any().downcast_ref::<ExecCell>() {
1508+
exec.render_ref(area, buf);
1509+
} else if let Some(tool) = cell.as_any().downcast_ref::<McpToolCallCell>() {
1510+
tool.render_ref(area, buf);
1511+
}
14771512
}
14781513
}
14791514
}

codex-rs/tui/src/chatwidget/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ fn make_chatwidget_manual() -> (
234234
app_event_tx,
235235
codex_op_tx: op_tx,
236236
bottom_pane: bottom,
237-
active_exec_cell: None,
237+
active_cell: None,
238238
config: cfg.clone(),
239239
auth_manager,
240240
session_header: SessionHeader::new(cfg.model.clone()),
@@ -434,9 +434,9 @@ fn end_exec(chat: &mut ChatWidget, call_id: &str, stdout: &str, stderr: &str, ex
434434

435435
fn active_blob(chat: &ChatWidget) -> String {
436436
let lines = chat
437-
.active_exec_cell
437+
.active_cell
438438
.as_ref()
439-
.expect("active exec cell present")
439+
.expect("active cell present")
440440
.display_lines(80);
441441
lines_to_single_string(&lines)
442442
}

0 commit comments

Comments
 (0)