mirror of
https://github.com/openai/codex.git
synced 2026-05-28 15:00:16 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
147
codex-rs/tui/src/app/plugin_mentions.rs
Normal file
147
codex-rs/tui/src/app/plugin_mentions.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user