mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
TUI config cleanup: plugin mentions (#24266)
## Summary TUI plugin mention refresh still joined app-server plugin inventory with client-local plugin config, which can diverge once plugin state is owned by the app server. This changes the TUI to mirror the GUI client: `plugin/list` is the autocomplete source, and mention candidates are plugin-level entries filtered to installed, enabled, and not disabled by admin. The TUI no longer reads local plugin config or calls `plugin/read` while refreshing plugin mention candidates. ## API shape and limitations The current app-server API does not expose effective per-session plugin capability summaries for mention autocomplete. As in the GUI, autocomplete now trusts `plugin/list` metadata rather than proving which plugin capabilities are loaded in the active session. That avoids stale client-local reads and the cwd/remote detail gaps in `plugin/read`, but intentionally accepts the same list-level tradeoff as the app: if `plugin/list` reports a remote plugin before its local bundle is materialized, the plugin can still appear as a mention candidate.
This commit is contained in:
committed by
GitHub
Unverified
parent
675cb1afbd
commit
22e45014a2
@@ -409,7 +409,7 @@ impl App {
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
match fetch_plugin_mentions(request_handle, config).await {
|
||||
match fetch_plugin_mentions(request_handle, config.cwd.to_path_buf()).await {
|
||||
Ok(plugins) => {
|
||||
app_event_tx.send(AppEvent::PluginMentionsLoaded {
|
||||
plugins: Some(plugins),
|
||||
|
||||
@@ -1,125 +1,62 @@
|
||||
//! 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.
|
||||
//! Mention inventory comes from app-server `plugin/list`, matching the GUI
|
||||
//! client. The current API exposes plugin-level mention metadata there, but not
|
||||
//! effective per-session capability summaries.
|
||||
|
||||
use super::background_requests::request_plugin_list;
|
||||
use super::*;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
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,
|
||||
cwd: PathBuf,
|
||||
) -> 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())
|
||||
let response = request_plugin_list(request_handle, cwd).await?;
|
||||
Ok(plugin_mentions_from_list_response(response))
|
||||
}
|
||||
|
||||
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(
|
||||
fn plugin_mentions_from_list_response(
|
||||
response: PluginListResponse,
|
||||
) -> Vec<PluginMentionEntry> {
|
||||
) -> Vec<PluginCapabilitySummary> {
|
||||
response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.flat_map(plugin_mention_entries_from_marketplace)
|
||||
.flat_map(|marketplace| {
|
||||
let marketplace_name = marketplace.name;
|
||||
marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.filter_map(move |plugin| plugin_mention_from_summary(&marketplace_name, plugin))
|
||||
})
|
||||
.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_is_eligible_for_mentions(plugin: &PluginSummary) -> bool {
|
||||
plugin.installed && plugin.enabled && plugin.availability != PluginAvailability::DisabledByAdmin
|
||||
}
|
||||
|
||||
fn plugin_mention_entry(
|
||||
fn plugin_mention_from_summary(
|
||||
marketplace_name: &str,
|
||||
plugin: PluginSummary,
|
||||
) -> Option<PluginMentionEntry> {
|
||||
) -> Option<PluginCapabilitySummary> {
|
||||
if !plugin_is_eligible_for_mentions(&plugin) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let config_name = plugin_mention_config_name(marketplace_name, &plugin)?;
|
||||
Some(PluginMentionEntry {
|
||||
config_name,
|
||||
Some(PluginCapabilitySummary {
|
||||
config_name: plugin.id.clone(),
|
||||
display_name: plugin_mention_display_name(&plugin),
|
||||
description: plugin_mention_description(&plugin),
|
||||
description: plugin_mention_description(marketplace_name, &plugin),
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
@@ -131,17 +68,80 @@ fn plugin_mention_display_name(plugin: &PluginSummary) -> String {
|
||||
.unwrap_or_else(|| plugin.name.clone())
|
||||
}
|
||||
|
||||
fn plugin_mention_description(plugin: &PluginSummary) -> Option<String> {
|
||||
fn plugin_mention_description(marketplace_name: &str, plugin: &PluginSummary) -> Option<String> {
|
||||
plugin
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| {
|
||||
interface
|
||||
.short_description
|
||||
.as_deref()
|
||||
.or(interface.long_description.as_deref())
|
||||
})
|
||||
.and_then(|interface| interface.short_description.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
let marketplace_name = marketplace_name.trim();
|
||||
(!marketplace_name.is_empty()).then(|| marketplace_name.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn plugin_mentions_use_plugin_list_summaries_and_gui_eligibility() {
|
||||
let active = plugin_summary("active");
|
||||
let mut disabled_by_admin = plugin_summary("disabled-by-admin");
|
||||
disabled_by_admin.availability = PluginAvailability::DisabledByAdmin;
|
||||
let mut disabled = plugin_summary("disabled");
|
||||
disabled.enabled = false;
|
||||
let mut uninstalled = plugin_summary("uninstalled");
|
||||
uninstalled.installed = false;
|
||||
|
||||
let response = PluginListResponse {
|
||||
marketplaces: vec![PluginMarketplaceEntry {
|
||||
name: "server-marketplace".to_string(),
|
||||
path: None,
|
||||
interface: None,
|
||||
plugins: vec![active, disabled_by_admin, disabled, uninstalled],
|
||||
}],
|
||||
marketplace_load_errors: Vec::new(),
|
||||
featured_plugin_ids: Vec::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
plugin_mentions_from_list_response(response),
|
||||
vec![PluginCapabilitySummary {
|
||||
config_name: "active@server-marketplace".to_string(),
|
||||
display_name: "active".to_string(),
|
||||
description: Some("server-marketplace".to_string()),
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
fn plugin_summary(name: &str) -> PluginSummary {
|
||||
PluginSummary {
|
||||
id: format!("{name}@server-marketplace"),
|
||||
remote_plugin_id: Some(format!("plugins~{name}")),
|
||||
local_version: None,
|
||||
name: name.to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Remote,
|
||||
installed: true,
|
||||
enabled: true,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: None,
|
||||
keywords: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user