Use plugin/list to get list of plugins for mentions (#22375)

This switches TUI plugin mentions to use app-server `plugin/list` for
plugin inventory and metadata instead of `PluginManager`, while keeping
the same mention-eligibility filters as before.

Same filters as before:
- Only plugins in the current config / cwd scope.
- Only installed and enabled plugins.
- Only plugins that actually expose a capability, meaning at least one
skill, MCP server, or app connector.
- Uses `plugin/list` for the mention names/descriptions
This commit is contained in:
canvrno-oai
2026-05-13 14:11:10 -07:00
committed by GitHub
parent 14473c216f
commit 16592f593d
4 changed files with 182 additions and 23 deletions

View File

@@ -127,7 +127,6 @@ use codex_app_server_protocol::TurnStatus;
use codex_config::ConfigLayerStackOrdering;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::ModelAvailabilityNuxConfig;
use codex_core_plugins::PluginsManager;
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_model_provider::create_model_provider;
@@ -196,6 +195,7 @@ mod loaded_threads;
mod pending_interactive_replay;
mod pets;
mod platform_actions;
mod plugin_mentions;
mod replay_filter;
mod resize_reflow;
mod session_lifecycle;

View File

@@ -4,6 +4,7 @@
//! limits, add-credit nudges, and feedback uploads. Results are routed back through `AppEvent` so
//! the main event loop remains single-threaded.
use super::plugin_mentions::fetch_plugin_mentions;
use super::*;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
@@ -358,8 +359,9 @@ impl App {
});
}
pub(super) fn refresh_plugin_mentions(&mut self) {
pub(super) fn refresh_plugin_mentions(&mut self, app_server: &AppServerSession) {
let config = self.config.clone();
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
if !config.features.enabled(Feature::Plugins) {
app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: None });
@@ -367,15 +369,16 @@ impl App {
}
tokio::spawn(async move {
let plugins_input = config.plugins_config_input();
let plugins = PluginsManager::new(config.codex_home.to_path_buf())
.plugins_for_config(&plugins_input)
.await
.capability_summaries()
.to_vec();
app_event_tx.send(AppEvent::PluginMentionsLoaded {
plugins: Some(plugins),
});
match fetch_plugin_mentions(request_handle, config).await {
Ok(plugins) => {
app_event_tx.send(AppEvent::PluginMentionsLoaded {
plugins: Some(plugins),
});
}
Err(err) => {
tracing::warn!(error = %err, "plugin/list failed while refreshing plugin mention candidates");
}
}
});
}
@@ -635,18 +638,9 @@ pub(super) async fn fetch_plugins_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
) -> Result<PluginListResponse> {
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("plugin list cwd must be absolute")?;
let request_id = RequestId::String(format!("plugin-list-{}", Uuid::new_v4()));
let mut response = request_handle
.request_typed(ClientRequest::PluginList {
request_id,
params: PluginListParams {
cwds: Some(vec![cwd]),
marketplace_kinds: None,
},
})
let mut response = request_plugin_list(request_handle, cwd)
.await
.wrap_err("plugin/list failed in TUI")?;
.wrap_err("plugin/list failed while loading the plugins menu")?;
hide_cli_only_plugin_marketplaces(&mut response);
Ok(response)
}
@@ -659,6 +653,24 @@ pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListRespons
.retain(|marketplace| !CLI_HIDDEN_PLUGIN_MARKETPLACES.contains(&marketplace.name.as_str()));
}
pub(super) async fn request_plugin_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
) -> Result<PluginListResponse> {
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("plugin list cwd must be absolute")?;
let request_id = RequestId::String(format!("plugin-list-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::PluginList {
request_id,
params: PluginListParams {
cwds: Some(vec![cwd]),
marketplace_kinds: None,
},
})
.await
.wrap_err("plugin/list failed in TUI")
}
pub(super) async fn fetch_plugin_detail(
request_handle: AppServerRequestHandle,
params: PluginReadParams,

View File

@@ -1253,7 +1253,7 @@ impl App {
}
}
AppEvent::RefreshPluginMentions => {
self.refresh_plugin_mentions();
self.refresh_plugin_mentions(app_server);
}
AppEvent::PluginMentionsLoaded { mut plugins } => {
if !self.config.features.enabled(Feature::Plugins) {

View File

@@ -0,0 +1,147 @@
//! Plugin mention capability enrichment for the TUI.
//!
//! Mention inventory comes from app-server `plugin/list`, while mention eligibility still reuses
//! the older local bulk capability summaries. That keeps the feature app-server-shaped without
//! paying for a `plugin/read` per plugin.
use super::background_requests::request_plugin_list;
use super::*;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::PluginMarketplaceEntry;
use codex_app_server_protocol::PluginSummary;
use codex_core_plugins::PluginsManager;
use codex_plugin::PluginCapabilitySummary;
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct PluginMentionEntry {
config_name: String,
display_name: String,
description: Option<String>,
}
impl PluginMentionEntry {
fn capability_summary(
self,
capabilities_by_config_name: &HashMap<String, PluginCapabilitySummary>,
) -> Option<PluginCapabilitySummary> {
let capabilities = capabilities_by_config_name.get(&self.config_name)?;
Some(PluginCapabilitySummary {
config_name: self.config_name,
display_name: self.display_name,
description: self.description,
has_skills: capabilities.has_skills,
mcp_server_names: capabilities.mcp_server_names.clone(),
app_connector_ids: capabilities.app_connector_ids.clone(),
})
}
}
pub(super) async fn fetch_plugin_mentions(
request_handle: AppServerRequestHandle,
config: crate::legacy_core::config::Config,
) -> Result<Vec<PluginCapabilitySummary>> {
let response = request_plugin_list(request_handle, config.cwd.to_path_buf()).await?;
let mention_entries = plugin_mention_entries_from_list_response(response);
let capabilities_by_config_name = load_plugin_mention_capabilities(&config).await;
Ok(mention_entries
.into_iter()
.filter_map(|entry| entry.capability_summary(&capabilities_by_config_name))
.collect())
}
async fn load_plugin_mention_capabilities(
config: &crate::legacy_core::config::Config,
) -> HashMap<String, PluginCapabilitySummary> {
let plugins_input = config.plugins_config_input();
PluginsManager::new(config.codex_home.to_path_buf())
.plugins_for_config(&plugins_input)
.await
.capability_summaries()
.iter()
.cloned()
.map(|summary| (summary.config_name.clone(), summary))
.collect()
}
fn plugin_mention_entries_from_list_response(
response: PluginListResponse,
) -> Vec<PluginMentionEntry> {
response
.marketplaces
.into_iter()
.flat_map(plugin_mention_entries_from_marketplace)
.collect()
}
fn plugin_mention_entries_from_marketplace(
marketplace: PluginMarketplaceEntry,
) -> Vec<PluginMentionEntry> {
let marketplace_name = marketplace.name;
marketplace
.plugins
.into_iter()
.filter_map(|plugin| plugin_mention_entry(&marketplace_name, plugin))
.collect()
}
fn plugin_mention_entry(
marketplace_name: &str,
plugin: PluginSummary,
) -> Option<PluginMentionEntry> {
if !plugin_is_eligible_for_mentions(&plugin) {
return None;
}
let config_name = plugin_mention_config_name(marketplace_name, &plugin)?;
Some(PluginMentionEntry {
config_name,
display_name: plugin_mention_display_name(&plugin),
description: plugin_mention_description(&plugin),
})
}
fn plugin_is_eligible_for_mentions(plugin: &PluginSummary) -> bool {
plugin.installed && plugin.enabled
}
fn plugin_mention_config_name(marketplace_name: &str, plugin: &PluginSummary) -> Option<String> {
codex_plugin::PluginId::new(plugin.name.clone(), marketplace_name.to_string())
.map(|plugin_id| plugin_id.as_key())
.map_err(|err| {
tracing::warn!(
plugin_name = plugin.name,
marketplace_name,
error = %err,
"skipping plugin mention with invalid identity"
);
})
.ok()
}
fn plugin_mention_display_name(plugin: &PluginSummary) -> String {
plugin
.interface
.as_ref()
.and_then(|interface| interface.display_name.as_deref())
.map(str::trim)
.filter(|display_name| !display_name.is_empty())
.map(str::to_string)
.unwrap_or_else(|| plugin.name.clone())
}
fn plugin_mention_description(plugin: &PluginSummary) -> Option<String> {
plugin
.interface
.as_ref()
.and_then(|interface| {
interface
.short_description
.as_deref()
.or(interface.long_description.as_deref())
})
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
}