From 19702e190ebf16f789617ca5f16bfc373c238fe7 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Sun, 22 Mar 2026 00:17:48 -0700 Subject: [PATCH] [apps] Improve app tools loading for TUI. (#15376) - [x] Remove the app tools copy in TUI and reference the core tools instead, this reduces tools/list calls from 4 to just 1. --- codex-rs/tui/src/chatwidget.rs | 99 +++++++++++++++++++++++----- codex-rs/tui/src/chatwidget/tests.rs | 1 + 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c8d9e3b61..3729778b1 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -720,6 +720,7 @@ pub(crate) struct ChatWidget { connectors_partial_snapshot: Option, connectors_prefetch_in_flight: bool, connectors_force_refetch_pending: bool, + pending_mcp_output_requests: usize, plugins_cache: PluginsCacheState, plugins_fetch_state: PluginListFetchState, // Queue of interruptive UI events deferred during an active write cycle @@ -1456,9 +1457,6 @@ impl ChatWidget { cwds: Vec::new(), force_reload: true, }); - if self.connectors_enabled() { - self.prefetch_connectors(); - } if let Some(user_message) = self.initial_user_message.take() { self.submit_user_message(user_message); } @@ -2184,6 +2182,7 @@ impl ChatWidget { } fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) { + let codex_apps_ready = ev.ready.iter().any(|server| server == "codex_apps"); let mut parts = Vec::new(); if !ev.failed.is_empty() { let failed_servers: Vec<_> = ev.failed.iter().map(|f| f.server.clone()).collect(); @@ -2202,6 +2201,11 @@ impl ChatWidget { self.mcp_startup_status = None; self.update_task_running_state(); self.maybe_send_next_queued_input(); + if self.connectors_enabled() && codex_apps_ready { + // Populate `$` app mentions from the session's already-started MCP manager + // instead of doing a separate TUI-side connector prefetch. + self.submit_op(Op::ListMcpTools); + } self.request_redraw(); } @@ -3665,6 +3669,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -3866,6 +3871,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -4059,6 +4065,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -5943,10 +5950,6 @@ impl ChatWidget { self.prefetch_connectors_with_options(force_refetch); } - fn prefetch_connectors(&mut self) { - self.prefetch_connectors_with_options(/*force_refetch*/ false); - } - fn prefetch_connectors_with_options(&mut self, force_refetch: bool) { if !self.connectors_enabled() { return; @@ -8368,8 +8371,8 @@ impl ChatWidget { .is_empty() { self.add_to_history(history_cell::empty_mcp_output()); - } else { - self.submit_op(Op::ListMcpTools); + } else if self.submit_op(Op::ListMcpTools) { + self.pending_mcp_output_requests = self.pending_mcp_output_requests.saturating_add(1); } } @@ -8831,13 +8834,77 @@ impl ChatWidget { } fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) { - self.add_to_history(history_cell::new_mcp_tools_output( - &self.config, - ev.tools, - ev.resources, - ev.resource_templates, - &ev.auth_statuses, - )); + if self.connectors_enabled() { + let mut connectors_by_id: HashMap = HashMap::new(); + for tool in ev.tools.values() { + let Some(meta) = tool.meta.as_ref().and_then(serde_json::Value::as_object) else { + continue; + }; + let Some(connector_id) = meta + .get("connector_id") + .and_then(serde_json::Value::as_str) + .filter(|id| !id.is_empty()) + else { + continue; + }; + connectors_by_id + .entry(connector_id.to_string()) + .or_insert_with(|| { + let name = meta + .get("connector_name") + .or_else(|| meta.get("connector_display_name")) + .and_then(serde_json::Value::as_str) + .filter(|name| !name.trim().is_empty()) + .unwrap_or(connector_id) + .to_string(); + let description = meta + .get("connector_description") + .or_else(|| meta.get("connectorDescription")) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|description| !description.is_empty()) + .map(ToString::to_string); + connectors::AppInfo { + id: connector_id.to_string(), + name, + description, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible: true, + is_enabled: true, + plugin_display_names: Vec::new(), + } + }); + } + + let mut app_connectors = connectors_by_id.into_values().collect::>(); + app_connectors.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then_with(|| left.id.cmp(&right.id)) + }); + let app_connectors = connectors::with_app_enabled_state(app_connectors, &self.config); + self.bottom_pane + .set_connectors_snapshot(Some(ConnectorsSnapshot { + connectors: app_connectors, + })); + } + + if self.pending_mcp_output_requests > 0 { + self.pending_mcp_output_requests -= 1; + self.add_to_history(history_cell::new_mcp_tools_output( + &self.config, + ev.tools, + ev.resources, + ev.resource_templates, + &ev.auth_statuses, + )); + } } fn on_list_custom_prompts(&mut self, ev: ListCustomPromptsResponseEvent) { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a614d1361..d837bb8ab 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1895,6 +1895,7 @@ async fn make_chatwidget_manual( connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(),