diff --git a/codex-rs/tui_app_server/src/chatwidget.rs b/codex-rs/tui_app_server/src/chatwidget.rs index b31cbbbb76..c320c95bf2 100644 --- a/codex-rs/tui_app_server/src/chatwidget.rs +++ b/codex-rs/tui_app_server/src/chatwidget.rs @@ -2798,6 +2798,15 @@ impl ChatWidget { ) { let mut activated_pending_round = false; let startup_status = if self.mcp_startup_ignore_updates_until_next_start { + if matches!(status, McpStartupStatus::Starting) + && !self + .mcp_startup_pending_next_round + .values() + .any(|state| matches!(state, McpStartupStatus::Starting)) + { + self.mcp_startup_pending_next_round.clear(); + self.mcp_startup_allow_terminal_only_next_round = false; + } self.mcp_startup_pending_next_round.insert(server, status); let Some(expected_servers) = &self.mcp_startup_expected_servers else { return; @@ -2938,6 +2947,7 @@ impl ChatWidget { pub(crate) fn finish_mcp_startup_after_lag(&mut self) { if self.mcp_startup_ignore_updates_until_next_start { + self.mcp_startup_pending_next_round.clear(); self.mcp_startup_allow_terminal_only_next_round = true; } diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index e1f576b7f1..29fee16dbc 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -11801,6 +11801,93 @@ async fn app_server_mcp_startup_after_lag_can_settle_without_starting_updates() assert!(!chat.bottom_pane.is_task_running()); } +#[tokio::test] +async fn app_server_mcp_startup_next_round_discards_stale_terminal_updates() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.show_welcome_banner = false; + chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]); + + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "alpha".to_string(), + status: McpServerStartupState::Starting, + error: None, + }), + /*replay_kind*/ None, + ); + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "alpha".to_string(), + status: McpServerStartupState::Failed, + error: Some("MCP client for `alpha` failed to start: handshake failed".to_string()), + }), + /*replay_kind*/ None, + ); + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "beta".to_string(), + status: McpServerStartupState::Starting, + error: None, + }), + /*replay_kind*/ None, + ); + let _ = drain_insert_history(&mut rx); + + chat.finish_mcp_startup_after_lag(); + let _ = drain_insert_history(&mut rx); + assert!(!chat.bottom_pane.is_task_running()); + + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "alpha".to_string(), + status: McpServerStartupState::Failed, + error: Some( + "MCP client for `alpha` failed to start: stale handshake failed".to_string(), + ), + }), + /*replay_kind*/ None, + ); + assert!(drain_insert_history(&mut rx).is_empty()); + + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "beta".to_string(), + status: McpServerStartupState::Starting, + error: None, + }), + /*replay_kind*/ None, + ); + assert!(drain_insert_history(&mut rx).is_empty()); + assert!(!chat.bottom_pane.is_task_running()); + + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "alpha".to_string(), + status: McpServerStartupState::Ready, + error: None, + }), + /*replay_kind*/ None, + ); + assert!(drain_insert_history(&mut rx).is_empty()); + assert!(chat.bottom_pane.is_task_running()); + + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "beta".to_string(), + status: McpServerStartupState::Ready, + error: None, + }), + /*replay_kind*/ None, + ); + + let summary_text = drain_insert_history(&mut rx) + .iter() + .map(|lines| lines_to_single_string(lines)) + .collect::(); + assert!(summary_text.is_empty()); + assert!(!chat.bottom_pane.is_task_running()); +} + #[tokio::test] async fn app_server_mcp_startup_next_round_after_lag_can_settle_without_starting_updates() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -11837,6 +11924,18 @@ async fn app_server_mcp_startup_next_round_after_lag_can_settle_without_starting let _ = drain_insert_history(&mut rx); assert!(!chat.bottom_pane.is_task_running()); + chat.handle_server_notification( + ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification { + name: "alpha".to_string(), + status: McpServerStartupState::Failed, + error: Some( + "MCP client for `alpha` failed to start: stale handshake failed".to_string(), + ), + }), + /*replay_kind*/ None, + ); + assert!(drain_insert_history(&mut rx).is_empty()); + chat.finish_mcp_startup_after_lag(); chat.handle_server_notification(