mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core/tui: non-blocking MCP startup (#6334)
This makes MCP startup not block TUI startup. Messages sent while MCPs are booting will be queued. https://github.com/user-attachments/assets/96e1d234-5d8f-4932-a935-a675d35c05e0 Fixes #6317 --------- Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
co-authored by
pakrym-oai
parent
ae2a084fae
commit
03ffe4d595
@@ -279,20 +279,23 @@ impl BottomPane {
|
||||
// esc_backtrack_hint_visible removed; hints are controlled internally.
|
||||
|
||||
pub fn set_task_running(&mut self, running: bool) {
|
||||
let was_running = self.is_task_running;
|
||||
self.is_task_running = running;
|
||||
self.composer.set_task_running(running);
|
||||
|
||||
if running {
|
||||
if self.status.is_none() {
|
||||
self.status = Some(StatusIndicatorWidget::new(
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
));
|
||||
if !was_running {
|
||||
if self.status.is_none() {
|
||||
self.status = Some(StatusIndicatorWidget::new(
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
));
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
status.set_interrupt_hint_visible(true);
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
status.set_interrupt_hint_visible(true);
|
||||
}
|
||||
self.request_redraw();
|
||||
} else {
|
||||
// Hide the status indicator when a task completes, but keep other modal views.
|
||||
self.hide_status_indicator();
|
||||
|
||||
@@ -27,6 +27,9 @@ use codex_core::protocol::ExecCommandSource;
|
||||
use codex_core::protocol::ExitedReviewModeEvent;
|
||||
use codex_core::protocol::ListCustomPromptsResponseEvent;
|
||||
use codex_core::protocol::McpListToolsResponseEvent;
|
||||
use codex_core::protocol::McpStartupCompleteEvent;
|
||||
use codex_core::protocol::McpStartupStatus;
|
||||
use codex_core::protocol::McpStartupUpdateEvent;
|
||||
use codex_core::protocol::McpToolCallBeginEvent;
|
||||
use codex_core::protocol::McpToolCallEndEvent;
|
||||
use codex_core::protocol::Op;
|
||||
@@ -259,6 +262,7 @@ pub(crate) struct ChatWidget {
|
||||
stream_controller: Option<StreamController>,
|
||||
running_commands: HashMap<String, RunningCommand>,
|
||||
task_complete_pending: bool,
|
||||
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
|
||||
// Queue of interruptive UI events deferred during an active write cycle
|
||||
interrupts: InterruptManager,
|
||||
// Accumulates the current reasoning block text to extract a header
|
||||
@@ -567,8 +571,76 @@ impl ChatWidget {
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
fn on_warning(&mut self, message: String) {
|
||||
self.add_to_history(history_cell::new_warning_event(message));
|
||||
fn on_warning(&mut self, message: impl Into<String>) {
|
||||
self.add_to_history(history_cell::new_warning_event(message.into()));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_mcp_startup_update(&mut self, ev: McpStartupUpdateEvent) {
|
||||
let mut status = self.mcp_startup_status.take().unwrap_or_default();
|
||||
if let McpStartupStatus::Failed { error } = &ev.status {
|
||||
self.on_warning(error);
|
||||
}
|
||||
status.insert(ev.server, ev.status);
|
||||
self.mcp_startup_status = Some(status);
|
||||
self.bottom_pane.set_task_running(true);
|
||||
if let Some(current) = &self.mcp_startup_status {
|
||||
let total = current.len();
|
||||
let mut starting: Vec<_> = current
|
||||
.iter()
|
||||
.filter_map(|(name, state)| {
|
||||
if matches!(state, McpStartupStatus::Starting) {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
starting.sort();
|
||||
if let Some(first) = starting.first() {
|
||||
let completed = total.saturating_sub(starting.len());
|
||||
let max_to_show = 3;
|
||||
let mut to_show: Vec<String> = starting
|
||||
.iter()
|
||||
.take(max_to_show)
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
if starting.len() > max_to_show {
|
||||
to_show.push("…".to_string());
|
||||
}
|
||||
let header = if total > 1 {
|
||||
format!(
|
||||
"Starting MCP servers ({completed}/{total}): {}",
|
||||
to_show.join(", ")
|
||||
)
|
||||
} else {
|
||||
format!("Booting MCP server: {first}")
|
||||
};
|
||||
self.set_status_header(header);
|
||||
}
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) {
|
||||
let mut parts = Vec::new();
|
||||
if !ev.failed.is_empty() {
|
||||
let failed_servers: Vec<_> = ev.failed.iter().map(|f| f.server.clone()).collect();
|
||||
parts.push(format!("failed: {}", failed_servers.join(", ")));
|
||||
}
|
||||
if !ev.cancelled.is_empty() {
|
||||
self.on_warning(format!(
|
||||
"MCP startup interrupted. The following servers were not initialized: {}",
|
||||
ev.cancelled.join(", ")
|
||||
));
|
||||
}
|
||||
if !parts.is_empty() {
|
||||
self.on_warning(format!("MCP startup incomplete ({})", parts.join("; ")));
|
||||
}
|
||||
|
||||
self.mcp_startup_status = None;
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.maybe_send_next_queued_input();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -1061,6 +1133,7 @@ impl ChatWidget {
|
||||
stream_controller: None,
|
||||
running_commands: HashMap::new(),
|
||||
task_complete_pending: false,
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
@@ -1128,6 +1201,7 @@ impl ChatWidget {
|
||||
stream_controller: None,
|
||||
running_commands: HashMap::new(),
|
||||
task_complete_pending: false,
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
@@ -1540,6 +1614,8 @@ impl ChatWidget {
|
||||
}
|
||||
EventMsg::Warning(WarningEvent { message }) => self.on_warning(message),
|
||||
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
|
||||
EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev),
|
||||
EventMsg::McpStartupComplete(ev) => self.on_mcp_startup_complete(ev),
|
||||
EventMsg::TurnAborted(ev) => match ev.reason {
|
||||
TurnAbortReason::Interrupted => {
|
||||
self.on_interrupted_turn(ev.reason);
|
||||
|
||||
@@ -58,8 +58,6 @@ use tempfile::tempdir;
|
||||
use tokio::sync::mpsc::error::TryRecvError;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
const TEST_WARNING_MESSAGE: &str = "Heads up: Long conversations and multiple compactions can cause the model to be less accurate. Start a new conversation when possible to keep conversations small and targeted.";
|
||||
|
||||
fn test_config() -> Config {
|
||||
// Use base defaults to avoid depending on host state.
|
||||
Config::load_from_base_config_with_overrides(
|
||||
@@ -268,6 +266,7 @@ fn make_chatwidget_manual() -> (
|
||||
stream_controller: None,
|
||||
running_commands: HashMap::new(),
|
||||
task_complete_pending: false,
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
@@ -2439,7 +2438,7 @@ fn warning_event_adds_warning_history_cell() {
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-1".into(),
|
||||
msg: EventMsg::Warning(WarningEvent {
|
||||
message: TEST_WARNING_MESSAGE.to_string(),
|
||||
message: "test warning message".to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2447,7 +2446,7 @@ fn warning_event_adds_warning_history_cell() {
|
||||
assert_eq!(cells.len(), 1, "expected one warning history cell");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains(TEST_WARNING_MESSAGE),
|
||||
rendered.contains("test warning message"),
|
||||
"warning cell missing content: {rendered}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1018,10 +1018,8 @@ fn try_new_completed_mcp_tool_call_with_image_output(
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
pub(crate) fn new_warning_event(message: String) -> PlainHistoryCell {
|
||||
PlainHistoryCell {
|
||||
lines: vec![vec![format!("⚠ {message}").yellow()].into()],
|
||||
}
|
||||
pub(crate) fn new_warning_event(message: String) -> PrefixedWrappedHistoryCell {
|
||||
PrefixedWrappedHistoryCell::new(message.yellow(), "⚠ ".yellow(), " ")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -145,7 +145,6 @@ impl Renderable for StatusIndicatorWidget {
|
||||
let elapsed_duration = self.elapsed_duration_at(now);
|
||||
let pretty_elapsed = fmt_elapsed_compact(elapsed_duration.as_secs());
|
||||
|
||||
// Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback.
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
spans.push(spinner(Some(self.last_resume_at)));
|
||||
spans.push(" ".into());
|
||||
|
||||
Reference in New Issue
Block a user