mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
22e45014a2
## 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.
148 lines
5.1 KiB
Rust
148 lines
5.1 KiB
Rust
//! Plugin mention capability enrichment for the TUI.
|
|
//!
|
|
//! 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::PluginSummary;
|
|
use codex_plugin::PluginCapabilitySummary;
|
|
|
|
pub(super) async fn fetch_plugin_mentions(
|
|
request_handle: AppServerRequestHandle,
|
|
cwd: PathBuf,
|
|
) -> Result<Vec<PluginCapabilitySummary>> {
|
|
let response = request_plugin_list(request_handle, cwd).await?;
|
|
Ok(plugin_mentions_from_list_response(response))
|
|
}
|
|
|
|
fn plugin_mentions_from_list_response(
|
|
response: PluginListResponse,
|
|
) -> Vec<PluginCapabilitySummary> {
|
|
response
|
|
.marketplaces
|
|
.into_iter()
|
|
.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_is_eligible_for_mentions(plugin: &PluginSummary) -> bool {
|
|
plugin.installed && plugin.enabled && plugin.availability != PluginAvailability::DisabledByAdmin
|
|
}
|
|
|
|
fn plugin_mention_from_summary(
|
|
marketplace_name: &str,
|
|
plugin: PluginSummary,
|
|
) -> Option<PluginCapabilitySummary> {
|
|
if !plugin_is_eligible_for_mentions(&plugin) {
|
|
return None;
|
|
}
|
|
|
|
Some(PluginCapabilitySummary {
|
|
config_name: plugin.id.clone(),
|
|
display_name: plugin_mention_display_name(&plugin),
|
|
description: plugin_mention_description(marketplace_name, &plugin),
|
|
has_skills: false,
|
|
mcp_server_names: Vec::new(),
|
|
app_connector_ids: Vec::new(),
|
|
})
|
|
}
|
|
|
|
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(marketplace_name: &str, plugin: &PluginSummary) -> Option<String> {
|
|
plugin
|
|
.interface
|
|
.as_ref()
|
|
.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(),
|
|
}
|
|
}
|
|
}
|