From ae03d073b3e0d4227b8646e09872baa157408cdf Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 18 May 2026 11:34:31 -0700 Subject: [PATCH] TUI: replay in-progress MCP calls as started (#23236) Fixes #22300. ## Summary MCP tool calls can appear in thread history while still in progress. During replay, `handle_thread_item` routed every `ThreadItem::McpToolCall` to the completion handler, so an in-progress item with no result or error was rendered as `MCP tool call completed without a result`. This updates replay handling to mirror command executions: `InProgress` MCP calls go through `on_mcp_tool_call_started`, while completed and failed calls continue through the completion path. ## Validation - `cargo test -p codex-tui replayed_in_progress_mcp_tool_call_stays_active` --- codex-rs/tui/src/chatwidget/replay.rs | 4 +++ .../src/chatwidget/tests/history_replay.rs | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/codex-rs/tui/src/chatwidget/replay.rs b/codex-rs/tui/src/chatwidget/replay.rs index 294e07e33..f0211aaca 100644 --- a/codex-rs/tui/src/chatwidget/replay.rs +++ b/codex-rs/tui/src/chatwidget/replay.rs @@ -133,6 +133,10 @@ impl ChatWidget { .. } => {} item @ ThreadItem::FileChange { .. } => self.on_file_change_completed(item), + item @ ThreadItem::McpToolCall { + status: codex_app_server_protocol::McpToolCallStatus::InProgress, + .. + } => self.on_mcp_tool_call_started(item), item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_completed(item), ThreadItem::WebSearch { id, query, action } => { self.on_web_search_begin(id.clone()); diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 36b3e0189..628c5abb4 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -826,6 +826,33 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { assert!(rendered.contains("Raw reasoning")); } +#[tokio::test] +async fn replayed_in_progress_mcp_tool_call_stays_active() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let _ = drain_insert_history(&mut rx); + + chat.replay_thread_item( + AppServerThreadItem::McpToolCall { + id: "mcp-1".to_string(), + server: "copilot-bridge".to_string(), + tool: "copilot".to_string(), + status: codex_app_server_protocol::McpToolCallStatus::InProgress, + arguments: json!({"action": "wait"}), + mcp_app_resource_uri: None, + result: None, + error: None, + duration_ms: None, + }, + "turn-1".to_string(), + ReplayKind::ThreadSnapshot, + ); + + assert!(drain_insert_history(&mut rx).is_empty()); + let active = active_blob(&chat); + assert!(active.contains("Calling")); + assert!(!active.contains("MCP tool call completed without a result")); +} + #[tokio::test] async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;