Remove TUI plugin marketplace surface
This commit is contained in:
parent
b8a7a52107
commit
e8c66f32a7
23 changed files with 15 additions and 2936 deletions
|
|
@ -868,6 +868,14 @@ mentions, and `codex-tui` no longer depends on `codex-plugin` or
|
|||
`codex-utils-plugins`. The remaining TUI marketplace code is a product UI
|
||||
compatibility stump to cut next, not auth/model spine machinery.
|
||||
|
||||
That stump is now cut from TUI. `/plugins` is no longer a slash command, the
|
||||
`chatwidget/plugins.rs` marketplace popup module is deleted, plugin AppEvent
|
||||
variants and background RPC helpers are gone, plugin enablement write queuing
|
||||
is gone, and the plugin marketplace popup tests/helpers were deleted. TUI still
|
||||
has ordinary app connector fields whose protocol shape mentions historical
|
||||
`plugin_display_names`; those are connector metadata compatibility, not plugin
|
||||
runtime authority.
|
||||
|
||||
The Phase 6 freshness slice is landed. It exposes read-only
|
||||
`thread/epiphany/freshness` from live retrieval summaries plus graph
|
||||
frontier/churn state and, for loaded threads, watcher-backed invalidation
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -306,8 +306,9 @@ current_status:
|
|||
composition no longer owns plugin mention state, emits structured plugin
|
||||
mentions, refreshes plugin mentions through `AppEvent`, decodes legacy
|
||||
`[@plugin](plugin://...)` mention links, or depends on `codex-plugin` /
|
||||
`codex-utils-plugins`; the remaining TUI plugin marketplace code is a
|
||||
product UI compatibility stump, not prompt/runtime authority. MCP remains
|
||||
JSON-RPC at the edge; user-declared MCP servers are the only runtime MCP
|
||||
authority.
|
||||
`codex-utils-plugins`. The TUI `/plugins` command and marketplace organ are
|
||||
now deleted too: no slash command, plugin popup module, plugin AppEvent
|
||||
variants, plugin background RPC helpers, plugin enablement queue, or
|
||||
marketplace popup tests remain in `codex-tui`. MCP remains JSON-RPC at the
|
||||
edge; user-declared MCP servers are the only runtime MCP authority.
|
||||
next_action: Continue starving coordinator/runtime execution and operator surfaces off app-server JSON-RPC and out of `codex_message_processor.rs`; keep the vestigial Codex organ narrowed to subscription auth/model transport.
|
||||
|
|
|
|||
16
vendor/codex/codex-rs/tui/src/app.rs
vendored
16
vendor/codex/codex-rs/tui/src/app.rs
vendored
|
|
@ -81,8 +81,6 @@ use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
|||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::ConfigWriteResponse;
|
||||
use codex_app_server_protocol::FeedbackUploadParams;
|
||||
use codex_app_server_protocol::FeedbackUploadResponse;
|
||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
|
|
@ -90,15 +88,6 @@ use codex_app_server_protocol::ListMcpServerStatusParams;
|
|||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::McpServerStatusDetail;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallParams;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SendAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
|
|
@ -555,10 +544,6 @@ pub(crate) struct App {
|
|||
primary_session_configured: Option<ThreadSessionState>,
|
||||
pending_primary_events: VecDeque<ThreadBufferedEvent>,
|
||||
pending_app_server_requests: PendingAppServerRequests,
|
||||
// Serialize plugin enablement writes per plugin so stale completions cannot
|
||||
// overwrite a newer toggle, even if the plugin is toggled from different
|
||||
// cwd contexts.
|
||||
pending_plugin_enabled_writes: HashMap<String, Option<bool>>,
|
||||
}
|
||||
|
||||
fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option<AppServerTurnError> {
|
||||
|
|
@ -916,7 +901,6 @@ impl App {
|
|||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
pending_app_server_requests: PendingAppServerRequests::default(),
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
};
|
||||
if let Some(started) = initial_started_thread {
|
||||
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ impl App {
|
|||
|
||||
async fn handle_server_notification_event(
|
||||
&mut self,
|
||||
app_server_client: &AppServerSession,
|
||||
_app_server_client: &AppServerSession,
|
||||
notification: ServerNotification,
|
||||
) {
|
||||
match ¬ification {
|
||||
|
|
@ -190,16 +190,13 @@ impl App {
|
|||
return;
|
||||
}
|
||||
ServerNotification::ExternalAgentConfigImportCompleted(_) => {
|
||||
let cwd = self.chat_widget.config_ref().cwd.to_path_buf();
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to refresh config after external agent config import"
|
||||
);
|
||||
}
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
self.fetch_plugins_list(app_server_client, cwd);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Background app-server requests launched by the TUI app.
|
||||
//!
|
||||
//! This module owns fire-and-forget fetch/write helpers for MCP inventory, skills, plugins, rate
|
||||
//! This module owns fire-and-forget fetch/write helpers for MCP inventory, skills, rate
|
||||
//! limits, add-credit nudges, and feedback uploads. Results are routed back through `AppEvent` so
|
||||
//! the main event loop remains single-threaded.
|
||||
|
||||
|
|
@ -78,126 +78,6 @@ impl App {
|
|||
});
|
||||
}
|
||||
|
||||
pub(super) fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_plugins_list(request_handle, cwd.clone())
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::PluginsLoaded { cwd, result });
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn fetch_plugin_detail(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
params: PluginReadParams,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_plugin_detail(request_handle, params)
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::PluginDetailLoaded { cwd, result });
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn fetch_plugin_install(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
plugin_display_name: String,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let cwd_for_event = cwd.clone();
|
||||
let marketplace_path_for_event = marketplace_path.clone();
|
||||
let plugin_name_for_event = plugin_name.clone();
|
||||
let result = fetch_plugin_install(request_handle, marketplace_path, plugin_name)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to install plugin: {err}"));
|
||||
app_event_tx.send(AppEvent::PluginInstallLoaded {
|
||||
cwd: cwd_for_event,
|
||||
marketplace_path: marketplace_path_for_event,
|
||||
plugin_name: plugin_name_for_event,
|
||||
plugin_display_name,
|
||||
result,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn fetch_plugin_uninstall(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let cwd_for_event = cwd.clone();
|
||||
let plugin_id_for_event = plugin_id.clone();
|
||||
let result = fetch_plugin_uninstall(request_handle, plugin_id)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to uninstall plugin: {err}"));
|
||||
app_event_tx.send(AppEvent::PluginUninstallLoaded {
|
||||
cwd: cwd_for_event,
|
||||
plugin_id: plugin_id_for_event,
|
||||
plugin_display_name,
|
||||
result,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn set_plugin_enabled(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
) {
|
||||
if let Some(queued_enabled) = self.pending_plugin_enabled_writes.get_mut(&plugin_id) {
|
||||
*queued_enabled = Some(enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_plugin_enabled_writes
|
||||
.insert(plugin_id.clone(), None);
|
||||
self.spawn_plugin_enabled_write(app_server, cwd, plugin_id, enabled);
|
||||
}
|
||||
|
||||
pub(super) fn spawn_plugin_enabled_write(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let cwd_for_event = cwd.clone();
|
||||
let plugin_id_for_event = plugin_id.clone();
|
||||
let result = write_plugin_enabled(request_handle, plugin_id, enabled)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("Failed to update plugin config: {err}"));
|
||||
app_event_tx.send(AppEvent::PluginEnabledSet {
|
||||
cwd: cwd_for_event,
|
||||
plugin_id: plugin_id_for_event,
|
||||
enabled,
|
||||
result,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn submit_feedback(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
|
|
@ -451,98 +331,6 @@ pub(super) async fn fetch_skills_list(
|
|||
.wrap_err("skills/list failed in TUI")
|
||||
}
|
||||
|
||||
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]),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("plugin/list failed in TUI")?;
|
||||
hide_cli_only_plugin_marketplaces(&mut response);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
const CLI_HIDDEN_PLUGIN_MARKETPLACES: &[&str] = &["openai-bundled"];
|
||||
|
||||
pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
|
||||
response
|
||||
.marketplaces
|
||||
.retain(|marketplace| !CLI_HIDDEN_PLUGIN_MARKETPLACES.contains(&marketplace.name.as_str()));
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_plugin_detail(
|
||||
request_handle: AppServerRequestHandle,
|
||||
params: PluginReadParams,
|
||||
) -> Result<PluginReadResponse> {
|
||||
let request_id = RequestId::String(format!("plugin-read-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::PluginRead { request_id, params })
|
||||
.await
|
||||
.wrap_err("plugin/read failed in TUI")
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_plugin_install(
|
||||
request_handle: AppServerRequestHandle,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
) -> Result<PluginInstallResponse> {
|
||||
let request_id = RequestId::String(format!("plugin-install-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::PluginInstall {
|
||||
request_id,
|
||||
params: PluginInstallParams {
|
||||
marketplace_path: Some(marketplace_path),
|
||||
remote_marketplace_name: None,
|
||||
plugin_name,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("plugin/install failed in TUI")
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_plugin_uninstall(
|
||||
request_handle: AppServerRequestHandle,
|
||||
plugin_id: String,
|
||||
) -> Result<PluginUninstallResponse> {
|
||||
let request_id = RequestId::String(format!("plugin-uninstall-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::PluginUninstall {
|
||||
request_id,
|
||||
params: PluginUninstallParams { plugin_id },
|
||||
})
|
||||
.await
|
||||
.wrap_err("plugin/uninstall failed in TUI")
|
||||
}
|
||||
|
||||
pub(super) async fn write_plugin_enabled(
|
||||
request_handle: AppServerRequestHandle,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
) -> Result<ConfigWriteResponse> {
|
||||
let request_id = RequestId::String(format!("plugin-enable-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::ConfigValueWrite {
|
||||
request_id,
|
||||
params: ConfigValueWriteParams {
|
||||
key_path: format!("plugins.{plugin_id}"),
|
||||
value: serde_json::json!({ "enabled": enabled }),
|
||||
merge_strategy: MergeStrategy::Upsert,
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("config/value/write failed while updating plugin enablement in TUI")
|
||||
}
|
||||
|
||||
pub(super) fn build_feedback_upload_params(
|
||||
origin_thread_id: Option<ThreadId>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
|
|
@ -621,49 +409,9 @@ pub(super) fn mcp_inventory_maps_from_statuses(statuses: Vec<McpServerStatus>) -
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_protocol::mcp::Tool;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn test_absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(path)).expect("absolute test path")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_cli_only_plugin_marketplaces_removes_openai_bundled() {
|
||||
let mut response = PluginListResponse {
|
||||
marketplaces: vec![
|
||||
PluginMarketplaceEntry {
|
||||
name: "openai-bundled".to_string(),
|
||||
path: Some(test_absolute_path("/marketplaces/openai-bundled")),
|
||||
interface: None,
|
||||
plugins: Vec::new(),
|
||||
},
|
||||
PluginMarketplaceEntry {
|
||||
name: "openai-curated".to_string(),
|
||||
path: Some(test_absolute_path("/marketplaces/openai-curated")),
|
||||
interface: None,
|
||||
plugins: Vec::new(),
|
||||
},
|
||||
],
|
||||
marketplace_load_errors: Vec::new(),
|
||||
featured_plugin_ids: Vec::new(),
|
||||
};
|
||||
|
||||
hide_cli_only_plugin_marketplaces(&mut response);
|
||||
|
||||
assert_eq!(
|
||||
response.marketplaces,
|
||||
vec![PluginMarketplaceEntry {
|
||||
name: "openai-curated".to_string(),
|
||||
path: Some(test_absolute_path("/marketplaces/openai-curated")),
|
||||
interface: None,
|
||||
plugins: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_inventory_maps_prefix_tool_names_by_server() {
|
||||
let statuses = vec![
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ impl App {
|
|||
.await?;
|
||||
self.apply_runtime_policy_overrides(&mut config);
|
||||
self.config = config;
|
||||
self.chat_widget.sync_plugin_mentions_config(&self.config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
173
vendor/codex/codex-rs/tui/src/app/event_dispatch.rs
vendored
173
vendor/codex/codex-rs/tui/src/app/event_dispatch.rs
vendored
|
|
@ -303,151 +303,6 @@ impl App {
|
|||
AppEvent::RefreshConnectors { force_refetch } => {
|
||||
self.chat_widget.refresh_connectors(force_refetch);
|
||||
}
|
||||
AppEvent::PluginInstallAuthAdvance { refresh_connectors } => {
|
||||
if refresh_connectors {
|
||||
self.chat_widget.refresh_connectors(/*force_refetch*/ true);
|
||||
}
|
||||
self.chat_widget.advance_plugin_install_auth_flow();
|
||||
}
|
||||
AppEvent::PluginInstallAuthAbandon => {
|
||||
self.chat_widget.abandon_plugin_install_auth_flow();
|
||||
}
|
||||
AppEvent::FetchPluginsList { cwd } => {
|
||||
self.fetch_plugins_list(app_server, cwd);
|
||||
}
|
||||
AppEvent::OpenPluginDetailLoading {
|
||||
plugin_display_name,
|
||||
} => {
|
||||
self.chat_widget
|
||||
.open_plugin_detail_loading_popup(&plugin_display_name);
|
||||
}
|
||||
AppEvent::OpenPluginInstallLoading {
|
||||
plugin_display_name,
|
||||
} => {
|
||||
self.chat_widget
|
||||
.open_plugin_install_loading_popup(&plugin_display_name);
|
||||
}
|
||||
AppEvent::OpenPluginUninstallLoading {
|
||||
plugin_display_name,
|
||||
} => {
|
||||
self.chat_widget
|
||||
.open_plugin_uninstall_loading_popup(&plugin_display_name);
|
||||
}
|
||||
AppEvent::PluginsLoaded { cwd, result } => {
|
||||
self.chat_widget.on_plugins_loaded(cwd, result);
|
||||
}
|
||||
AppEvent::FetchPluginDetail { cwd, params } => {
|
||||
self.fetch_plugin_detail(app_server, cwd, params);
|
||||
}
|
||||
AppEvent::PluginDetailLoaded { cwd, result } => {
|
||||
self.chat_widget.on_plugin_detail_loaded(cwd, result);
|
||||
}
|
||||
AppEvent::FetchPluginInstall {
|
||||
cwd,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
plugin_display_name,
|
||||
} => {
|
||||
self.fetch_plugin_install(
|
||||
app_server,
|
||||
cwd,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
plugin_display_name,
|
||||
);
|
||||
}
|
||||
AppEvent::FetchPluginUninstall {
|
||||
cwd,
|
||||
plugin_id,
|
||||
plugin_display_name,
|
||||
} => {
|
||||
self.fetch_plugin_uninstall(app_server, cwd, plugin_id, plugin_display_name);
|
||||
}
|
||||
AppEvent::SetPluginEnabled {
|
||||
cwd,
|
||||
plugin_id,
|
||||
enabled,
|
||||
} => {
|
||||
self.set_plugin_enabled(app_server, cwd, plugin_id, enabled);
|
||||
}
|
||||
AppEvent::PluginInstallLoaded {
|
||||
cwd,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
plugin_display_name,
|
||||
result,
|
||||
} => {
|
||||
let install_succeeded = result.is_ok();
|
||||
if install_succeeded {
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(error = %err, "failed to refresh config after plugin install");
|
||||
}
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
}
|
||||
let should_refresh_plugin_detail = self.chat_widget.on_plugin_install_loaded(
|
||||
cwd.clone(),
|
||||
marketplace_path.clone(),
|
||||
plugin_name.clone(),
|
||||
plugin_display_name,
|
||||
result,
|
||||
);
|
||||
if install_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()
|
||||
{
|
||||
self.fetch_plugins_list(app_server, cwd.clone());
|
||||
if should_refresh_plugin_detail {
|
||||
self.fetch_plugin_detail(
|
||||
app_server,
|
||||
cwd,
|
||||
PluginReadParams {
|
||||
marketplace_path: Some(marketplace_path),
|
||||
remote_marketplace_name: None,
|
||||
plugin_name,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::PluginEnabledSet {
|
||||
cwd,
|
||||
plugin_id,
|
||||
enabled,
|
||||
result,
|
||||
} => {
|
||||
let queued_enabled = self
|
||||
.pending_plugin_enabled_writes
|
||||
.get_mut(&plugin_id)
|
||||
.and_then(Option::take);
|
||||
let should_apply_result = if let Some(queued_enabled) = queued_enabled
|
||||
&& (result.is_err() || queued_enabled != enabled)
|
||||
{
|
||||
self.spawn_plugin_enabled_write(
|
||||
app_server,
|
||||
cwd.clone(),
|
||||
plugin_id.clone(),
|
||||
queued_enabled,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if should_apply_result {
|
||||
self.pending_plugin_enabled_writes.remove(&plugin_id);
|
||||
let update_succeeded = result.is_ok();
|
||||
if update_succeeded {
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to refresh config after plugin toggle"
|
||||
);
|
||||
}
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
}
|
||||
self.chat_widget
|
||||
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
|
||||
}
|
||||
}
|
||||
AppEvent::FetchMcpInventory { detail } => {
|
||||
self.fetch_mcp_inventory(app_server, detail);
|
||||
}
|
||||
|
|
@ -954,34 +809,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
}
|
||||
AppEvent::PluginUninstallLoaded {
|
||||
cwd,
|
||||
plugin_id: _plugin_id,
|
||||
plugin_display_name,
|
||||
result,
|
||||
} => {
|
||||
let uninstall_succeeded = result.is_ok();
|
||||
if uninstall_succeeded {
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to refresh config after plugin uninstall"
|
||||
);
|
||||
}
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
}
|
||||
self.chat_widget.on_plugin_uninstall_loaded(
|
||||
cwd.clone(),
|
||||
plugin_display_name,
|
||||
result,
|
||||
);
|
||||
if uninstall_succeeded
|
||||
&& self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()
|
||||
{
|
||||
self.fetch_plugins_list(app_server, cwd);
|
||||
}
|
||||
}
|
||||
AppEvent::PersistPersonalitySelection { personality } => {
|
||||
let profile = self.active_profile.as_deref();
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ pub(super) async fn make_test_app() -> App {
|
|||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
pending_app_server_requests: PendingAppServerRequests::default(),
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
vendor/codex/codex-rs/tui/src/app/tests.rs
vendored
2
vendor/codex/codex-rs/tui/src/app/tests.rs
vendored
|
|
@ -3674,7 +3674,6 @@ async fn make_test_app() -> App {
|
|||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
pending_app_server_requests: PendingAppServerRequests::default(),
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3731,7 +3730,6 @@ async fn make_test_app_with_channels() -> (
|
|||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
pending_app_server_requests: PendingAppServerRequests::default(),
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
},
|
||||
rx,
|
||||
op_rx,
|
||||
|
|
|
|||
98
vendor/codex/codex-rs/tui/src/app_event.rs
vendored
98
vendor/codex/codex-rs/tui/src/app_event.rs
vendored
|
|
@ -15,11 +15,6 @@ use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
|||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::McpServerStatusDetail;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::ThreadId;
|
||||
|
|
@ -232,99 +227,6 @@ pub(crate) enum AppEvent {
|
|||
force_refetch: bool,
|
||||
},
|
||||
|
||||
/// Fetch plugin marketplace state for the provided working directory.
|
||||
FetchPluginsList {
|
||||
cwd: PathBuf,
|
||||
},
|
||||
|
||||
/// Result of fetching plugin marketplace state.
|
||||
PluginsLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with a plugin-detail loading state.
|
||||
OpenPluginDetailLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Fetch detail for a specific plugin from a marketplace.
|
||||
FetchPluginDetail {
|
||||
cwd: PathBuf,
|
||||
params: PluginReadParams,
|
||||
},
|
||||
|
||||
/// Result of fetching plugin detail.
|
||||
PluginDetailLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PluginReadResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with an install loading state.
|
||||
OpenPluginInstallLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with an uninstall loading state.
|
||||
OpenPluginUninstallLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Install a specific plugin from a marketplace.
|
||||
FetchPluginInstall {
|
||||
cwd: PathBuf,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Result of installing a plugin.
|
||||
PluginInstallLoaded {
|
||||
cwd: PathBuf,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
plugin_display_name: String,
|
||||
result: Result<PluginInstallResponse, String>,
|
||||
},
|
||||
|
||||
/// Uninstall a specific plugin by canonical plugin id.
|
||||
FetchPluginUninstall {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Result of uninstalling a plugin.
|
||||
PluginUninstallLoaded {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
result: Result<PluginUninstallResponse, String>,
|
||||
},
|
||||
|
||||
/// Enable or disable an installed plugin.
|
||||
SetPluginEnabled {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
},
|
||||
|
||||
/// Result of enabling or disabling a plugin.
|
||||
PluginEnabledSet {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
result: Result<(), String>,
|
||||
},
|
||||
|
||||
/// Advance the post-install plugin app-auth flow.
|
||||
PluginInstallAuthAdvance {
|
||||
refresh_connectors: bool,
|
||||
},
|
||||
|
||||
/// Abandon the post-install plugin app-auth flow.
|
||||
PluginInstallAuthAbandon,
|
||||
|
||||
/// Fetch MCP inventory via app-server RPCs and render it into history.
|
||||
FetchMcpInventory {
|
||||
detail: McpServerStatusDetail,
|
||||
|
|
|
|||
|
|
@ -368,7 +368,6 @@ pub(crate) struct ChatComposer {
|
|||
config: ChatComposerConfig,
|
||||
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
|
||||
connectors_enabled: bool,
|
||||
plugins_command_enabled: bool,
|
||||
fast_command_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
realtime_conversation_enabled: bool,
|
||||
|
|
@ -428,7 +427,6 @@ impl ChatComposer {
|
|||
BuiltinCommandFlags {
|
||||
collaboration_modes_enabled: self.collaboration_modes_enabled,
|
||||
connectors_enabled: self.connectors_enabled,
|
||||
plugins_command_enabled: self.plugins_command_enabled,
|
||||
fast_command_enabled: self.fast_command_enabled,
|
||||
personality_command_enabled: self.personality_command_enabled,
|
||||
realtime_conversation_enabled: self.realtime_conversation_enabled,
|
||||
|
|
@ -512,7 +510,6 @@ impl ChatComposer {
|
|||
config,
|
||||
collaboration_mode_indicator: None,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
|
|
@ -550,10 +547,6 @@ impl ChatComposer {
|
|||
self.sync_popups();
|
||||
}
|
||||
|
||||
pub fn set_plugins_command_enabled(&mut self, enabled: bool) {
|
||||
self.plugins_command_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Toggle composer-side image paste handling.
|
||||
///
|
||||
/// This only affects whether image-like paste content is converted into attachments; the
|
||||
|
|
@ -3455,7 +3448,6 @@ impl ChatComposer {
|
|||
if is_editing_slash_command_name {
|
||||
let collaboration_modes_enabled = self.collaboration_modes_enabled;
|
||||
let connectors_enabled = self.connectors_enabled;
|
||||
let plugins_command_enabled = self.plugins_command_enabled;
|
||||
let fast_command_enabled = self.fast_command_enabled;
|
||||
let personality_command_enabled = self.personality_command_enabled;
|
||||
let realtime_conversation_enabled = self.realtime_conversation_enabled;
|
||||
|
|
@ -3463,7 +3455,6 @@ impl ChatComposer {
|
|||
let mut command_popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
plugins_command_enabled,
|
||||
fast_command_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ pub(crate) struct CommandPopup {
|
|||
pub(crate) struct CommandPopupFlags {
|
||||
pub(crate) collaboration_modes_enabled: bool,
|
||||
pub(crate) connectors_enabled: bool,
|
||||
pub(crate) plugins_command_enabled: bool,
|
||||
pub(crate) fast_command_enabled: bool,
|
||||
pub(crate) personality_command_enabled: bool,
|
||||
pub(crate) realtime_conversation_enabled: bool,
|
||||
|
|
@ -46,7 +45,6 @@ impl From<CommandPopupFlags> for slash_commands::BuiltinCommandFlags {
|
|||
Self {
|
||||
collaboration_modes_enabled: value.collaboration_modes_enabled,
|
||||
connectors_enabled: value.connectors_enabled,
|
||||
plugins_command_enabled: value.plugins_command_enabled,
|
||||
fast_command_enabled: value.fast_command_enabled,
|
||||
personality_command_enabled: value.personality_command_enabled,
|
||||
realtime_conversation_enabled: value.realtime_conversation_enabled,
|
||||
|
|
@ -355,7 +353,6 @@ mod tests {
|
|||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
|
|
@ -376,7 +373,6 @@ mod tests {
|
|||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
|
|
@ -397,7 +393,6 @@ mod tests {
|
|||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
|
|
@ -425,7 +420,6 @@ mod tests {
|
|||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
|
|
@ -446,7 +440,6 @@ mod tests {
|
|||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: false,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
|
|
|
|||
15
vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs
vendored
15
vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs
vendored
|
|
@ -90,8 +90,6 @@ mod skills_toggle_view;
|
|||
pub(crate) mod slash_commands;
|
||||
pub(crate) use footer::CollaborationModeIndicator;
|
||||
pub(crate) use list_selection_view::ColumnWidthMode;
|
||||
pub(crate) use list_selection_view::SelectionRowDisplay;
|
||||
pub(crate) use list_selection_view::SelectionToggle;
|
||||
pub(crate) use list_selection_view::SelectionViewParams;
|
||||
pub(crate) use list_selection_view::SideContentWidth;
|
||||
pub(crate) use list_selection_view::popup_content_width;
|
||||
|
|
@ -124,7 +122,6 @@ mod selection_tabs;
|
|||
mod textarea;
|
||||
mod unified_exec_footer;
|
||||
pub(crate) use feedback_view::FeedbackNoteView;
|
||||
pub(crate) use selection_tabs::SelectionTab;
|
||||
|
||||
/// How long the "press again to quit" hint stays visible.
|
||||
///
|
||||
|
|
@ -274,11 +271,6 @@ impl BottomPane {
|
|||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn set_plugins_command_enabled(&mut self, enabled: bool) {
|
||||
self.composer.set_plugins_command_enabled(enabled);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn take_mention_bindings(&mut self) -> Vec<MentionBinding> {
|
||||
self.composer.take_mention_bindings()
|
||||
}
|
||||
|
|
@ -861,13 +853,6 @@ impl BottomPane {
|
|||
.and_then(|view| view.selected_index())
|
||||
}
|
||||
|
||||
pub(crate) fn active_tab_id_for_active_view(&self, view_id: &'static str) -> Option<&str> {
|
||||
self.view_stack
|
||||
.last()
|
||||
.filter(|view| view.view_id() == Some(view_id))
|
||||
.and_then(|view| view.active_tab_id())
|
||||
}
|
||||
|
||||
/// Update the pending-input preview shown above the composer.
|
||||
pub(crate) fn set_pending_input_preview(
|
||||
&mut self,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use crate::slash_command::built_in_slash_commands;
|
|||
pub(crate) struct BuiltinCommandFlags {
|
||||
pub(crate) collaboration_modes_enabled: bool,
|
||||
pub(crate) connectors_enabled: bool,
|
||||
pub(crate) plugins_command_enabled: bool,
|
||||
pub(crate) fast_command_enabled: bool,
|
||||
pub(crate) personality_command_enabled: bool,
|
||||
pub(crate) realtime_conversation_enabled: bool,
|
||||
|
|
@ -33,7 +32,6 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st
|
|||
|| !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan)
|
||||
})
|
||||
.filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps)
|
||||
.filter(|(_, cmd)| flags.plugins_command_enabled || *cmd != SlashCommand::Plugins)
|
||||
.filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast)
|
||||
.filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
|
||||
.filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
|
||||
|
|
@ -73,7 +71,6 @@ mod tests {
|
|||
BuiltinCommandFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: true,
|
||||
plugins_command_enabled: true,
|
||||
fast_command_enabled: true,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
|
|
|
|||
47
vendor/codex/codex-rs/tui/src/chatwidget.rs
vendored
47
vendor/codex/codex-rs/tui/src/chatwidget.rs
vendored
|
|
@ -81,7 +81,6 @@ use crate::version::CODEX_CLI_VERSION;
|
|||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollabAgentState as AppServerCollabAgentState;
|
||||
use codex_app_server_protocol::CollabAgentStatus as AppServerCollabAgentStatus;
|
||||
|
|
@ -371,8 +370,6 @@ mod slash_dispatch;
|
|||
use self::skills::collect_tool_mentions;
|
||||
use self::skills::find_app_mentions;
|
||||
use self::skills::find_skill_mentions_with_tool_mentions;
|
||||
mod plugins;
|
||||
use self::plugins::PluginsCacheState;
|
||||
mod plan_implementation;
|
||||
use self::plan_implementation::PLAN_IMPLEMENTATION_TITLE;
|
||||
mod realtime;
|
||||
|
|
@ -608,18 +605,6 @@ enum ConnectorsCacheState {
|
|||
Failed(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct PluginListFetchState {
|
||||
cache_cwd: Option<PathBuf>,
|
||||
in_flight_cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PluginInstallAuthFlowState {
|
||||
plugin_display_name: String,
|
||||
next_app_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RateLimitErrorKind {
|
||||
ServerOverloaded,
|
||||
|
|
@ -862,11 +847,6 @@ pub(crate) struct ChatWidget {
|
|||
connectors_partial_snapshot: Option<ConnectorsSnapshot>,
|
||||
connectors_prefetch_in_flight: bool,
|
||||
connectors_force_refetch_pending: bool,
|
||||
plugins_cache: PluginsCacheState,
|
||||
plugins_fetch_state: PluginListFetchState,
|
||||
plugin_install_apps_needing_auth: Vec<AppSummary>,
|
||||
plugin_install_auth_flow: Option<PluginInstallAuthFlowState>,
|
||||
plugins_active_tab_id: Option<String>,
|
||||
// Queue of interruptive UI events deferred during an active write cycle
|
||||
interrupts: InterruptManager,
|
||||
// Accumulates the current reasoning block text to extract a header
|
||||
|
|
@ -2157,8 +2137,6 @@ impl ChatWidget {
|
|||
self.refresh_status_surfaces();
|
||||
self.sync_fast_command_enabled();
|
||||
self.sync_personality_command_enabled();
|
||||
self.sync_plugins_command_enabled();
|
||||
self.refresh_plugin_mentions();
|
||||
if display == SessionConfiguredDisplay::Normal {
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let show_fast_status =
|
||||
|
|
@ -5173,11 +5151,6 @@ impl ChatWidget {
|
|||
connectors_partial_snapshot: None,
|
||||
connectors_prefetch_in_flight: false,
|
||||
connectors_force_refetch_pending: false,
|
||||
plugins_cache: PluginsCacheState::default(),
|
||||
plugins_fetch_state: PluginListFetchState::default(),
|
||||
plugin_install_apps_needing_auth: Vec::new(),
|
||||
plugin_install_auth_flow: None,
|
||||
plugins_active_tab_id: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
|
|
@ -5257,7 +5230,6 @@ impl ChatWidget {
|
|||
.set_collaboration_modes_enabled(/*enabled*/ true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget.sync_plugins_command_enabled();
|
||||
widget
|
||||
.bottom_pane
|
||||
.set_queued_message_edit_binding(widget.queued_message_edit_binding);
|
||||
|
|
@ -9723,10 +9695,6 @@ impl ChatWidget {
|
|||
if feature == Feature::Personality {
|
||||
self.sync_personality_command_enabled();
|
||||
}
|
||||
if feature == Feature::Plugins {
|
||||
self.sync_plugins_command_enabled();
|
||||
self.refresh_plugin_mentions();
|
||||
}
|
||||
if feature == Feature::PreventIdleSleep {
|
||||
self.turn_sleep_inhibitor = SleepInhibitor::new(enabled);
|
||||
self.turn_sleep_inhibitor
|
||||
|
|
@ -9965,11 +9933,6 @@ impl ChatWidget {
|
|||
.set_personality_command_enabled(self.config.features.enabled(Feature::Personality));
|
||||
}
|
||||
|
||||
fn sync_plugins_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_plugins_command_enabled(self.config.features.enabled(Feature::Plugins));
|
||||
}
|
||||
|
||||
fn current_model_supports_personality(&self) -> bool {
|
||||
let model = self.current_model();
|
||||
self.model_catalog
|
||||
|
|
@ -10859,7 +10822,6 @@ impl ChatWidget {
|
|||
|
||||
fn on_list_skills(&mut self, ev: ListSkillsResponseEvent) {
|
||||
self.set_skills_from_response(&ev);
|
||||
self.refresh_plugin_mentions();
|
||||
}
|
||||
|
||||
pub(crate) fn on_connectors_loaded(
|
||||
|
|
@ -10956,15 +10918,6 @@ impl ChatWidget {
|
|||
self.bottom_pane.set_connectors_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_plugin_mentions(&mut self) {
|
||||
}
|
||||
|
||||
pub(crate) fn sync_plugin_mentions_config(&mut self, config: &Config) {
|
||||
self.config.features = config.features.clone();
|
||||
self.config.config_layer_stack = config.config_layer_stack.clone();
|
||||
self.config.realtime = config.realtime.clone();
|
||||
self.config.memories = config.memories.clone();
|
||||
}
|
||||
|
||||
pub(crate) fn open_review_popup(&mut self) {
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
|
|
|
|||
1405
vendor/codex/codex-rs/tui/src/chatwidget/plugins.rs
vendored
1405
vendor/codex/codex-rs/tui/src/chatwidget/plugins.rs
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -364,9 +364,6 @@ impl ChatWidget {
|
|||
SlashCommand::Apps => {
|
||||
self.add_connectors_output();
|
||||
}
|
||||
SlashCommand::Plugins => {
|
||||
self.add_plugins_output();
|
||||
}
|
||||
SlashCommand::Rollout => {
|
||||
if let Some(path) = self.rollout_path() {
|
||||
self.add_info_message(
|
||||
|
|
@ -702,7 +699,6 @@ impl ChatWidget {
|
|||
slash_commands::BuiltinCommandFlags {
|
||||
collaboration_modes_enabled: self.collaboration_modes_enabled(),
|
||||
connectors_enabled: self.connectors_enabled(),
|
||||
plugins_command_enabled: self.config.features.enabled(Feature::Plugins),
|
||||
fast_command_enabled: self.fast_mode_enabled(),
|
||||
personality_command_enabled: self.config.features.enabled(Feature::Personality),
|
||||
realtime_conversation_enabled: self.realtime_conversation_enabled(),
|
||||
|
|
@ -726,7 +722,6 @@ impl ChatWidget {
|
|||
| SlashCommand::MemoryUpdate
|
||||
| SlashCommand::Mcp
|
||||
| SlashCommand::Apps
|
||||
| SlashCommand::Plugins
|
||||
| SlashCommand::Rollout
|
||||
| SlashCommand::Copy
|
||||
| SlashCommand::Diff
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ pub(super) use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
|||
pub(super) use codex_app_server_protocol::AdditionalFileSystemPermissions as AppServerAdditionalFileSystemPermissions;
|
||||
pub(super) use codex_app_server_protocol::AdditionalNetworkPermissions as AppServerAdditionalNetworkPermissions;
|
||||
pub(super) use codex_app_server_protocol::AdditionalPermissionProfile as AppServerAdditionalPermissionProfile;
|
||||
pub(super) use codex_app_server_protocol::AppSummary;
|
||||
pub(super) use codex_app_server_protocol::AutoReviewDecisionSource as AppServerGuardianApprovalReviewDecisionSource;
|
||||
pub(super) use codex_app_server_protocol::CollabAgentState as AppServerCollabAgentState;
|
||||
pub(super) use codex_app_server_protocol::CollabAgentStatus as AppServerCollabAgentStatus;
|
||||
|
|
@ -65,25 +64,14 @@ pub(super) use codex_app_server_protocol::ItemCompletedNotification;
|
|||
pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewCompletedNotification;
|
||||
pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotification;
|
||||
pub(super) use codex_app_server_protocol::ItemStartedNotification;
|
||||
pub(super) use codex_app_server_protocol::MarketplaceInterface;
|
||||
pub(super) use codex_app_server_protocol::McpServerStartupState;
|
||||
pub(super) use codex_app_server_protocol::McpServerStatusDetail;
|
||||
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
pub(super) use codex_app_server_protocol::PatchApplyStatus as AppServerPatchApplyStatus;
|
||||
pub(super) use codex_app_server_protocol::PatchChangeKind;
|
||||
pub(super) use codex_app_server_protocol::PermissionsRequestApprovalParams as AppServerPermissionsRequestApprovalParams;
|
||||
pub(super) use codex_app_server_protocol::PluginAuthPolicy;
|
||||
pub(super) use codex_app_server_protocol::PluginDetail;
|
||||
pub(super) use codex_app_server_protocol::PluginInstallPolicy;
|
||||
pub(super) use codex_app_server_protocol::PluginInterface;
|
||||
pub(super) use codex_app_server_protocol::PluginListResponse;
|
||||
pub(super) use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
pub(super) use codex_app_server_protocol::PluginReadResponse;
|
||||
pub(super) use codex_app_server_protocol::PluginSource;
|
||||
pub(super) use codex_app_server_protocol::PluginSummary;
|
||||
pub(super) use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification;
|
||||
pub(super) use codex_app_server_protocol::ServerNotification;
|
||||
pub(super) use codex_app_server_protocol::SkillSummary;
|
||||
pub(super) use codex_app_server_protocol::ThreadClosedNotification;
|
||||
pub(super) use codex_app_server_protocol::ThreadItem as AppServerThreadItem;
|
||||
pub(super) use codex_app_server_protocol::Turn as AppServerTurn;
|
||||
|
|
@ -225,7 +213,6 @@ pub(super) use tokio::sync::mpsc::error::TryRecvError;
|
|||
pub(super) use tokio::sync::mpsc::unbounded_channel;
|
||||
pub(super) use toml::Value as TomlValue;
|
||||
|
||||
pub(super) const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated";
|
||||
|
||||
pub(super) fn chatwidget_snapshot_dir() -> PathBuf {
|
||||
let snapshot_file = codex_utils_cargo_bin::find_resource!(
|
||||
|
|
|
|||
|
|
@ -237,13 +237,8 @@ pub(super) async fn make_chatwidget_manual(
|
|||
mcp_startup_pending_next_round_saw_starting: false,
|
||||
connectors_cache: ConnectorsCacheState::default(),
|
||||
connectors_partial_snapshot: None,
|
||||
plugin_install_apps_needing_auth: Vec::new(),
|
||||
plugin_install_auth_flow: None,
|
||||
plugins_active_tab_id: None,
|
||||
connectors_prefetch_in_flight: false,
|
||||
connectors_force_refetch_pending: false,
|
||||
plugins_cache: PluginsCacheState::default(),
|
||||
plugins_fetch_state: PluginListFetchState::default(),
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
|
|
@ -845,162 +840,6 @@ pub(super) fn strip_osc8_for_snapshot(text: &str) -> String {
|
|||
|
||||
stripped
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
std::env::temp_dir()
|
||||
.join("codex-plugin-menu-tests")
|
||||
.join(path)
|
||||
.abs()
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_interface(
|
||||
display_name: Option<&str>,
|
||||
short_description: Option<&str>,
|
||||
long_description: Option<&str>,
|
||||
) -> PluginInterface {
|
||||
PluginInterface {
|
||||
display_name: display_name.map(str::to_string),
|
||||
short_description: short_description.map(str::to_string),
|
||||
long_description: long_description.map(str::to_string),
|
||||
developer_name: None,
|
||||
category: None,
|
||||
capabilities: Vec::new(),
|
||||
website_url: None,
|
||||
privacy_policy_url: None,
|
||||
terms_of_service_url: None,
|
||||
default_prompt: None,
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_url: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_summary(
|
||||
id: &str,
|
||||
name: &str,
|
||||
display_name: Option<&str>,
|
||||
description: Option<&str>,
|
||||
installed: bool,
|
||||
enabled: bool,
|
||||
install_policy: PluginInstallPolicy,
|
||||
) -> PluginSummary {
|
||||
PluginSummary {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
source: PluginSource::Local {
|
||||
path: plugins_test_absolute_path(&format!("plugins/{name}")),
|
||||
},
|
||||
installed,
|
||||
enabled,
|
||||
install_policy,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
interface: Some(plugins_test_interface(
|
||||
display_name,
|
||||
description,
|
||||
/*long_description*/ None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_curated_marketplace(
|
||||
plugins: Vec<PluginSummary>,
|
||||
) -> PluginMarketplaceEntry {
|
||||
PluginMarketplaceEntry {
|
||||
name: OPENAI_CURATED_MARKETPLACE_NAME.to_string(),
|
||||
path: Some(plugins_test_absolute_path("marketplaces/chatgpt")),
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("ChatGPT Marketplace".to_string()),
|
||||
}),
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_repo_marketplace(plugins: Vec<PluginSummary>) -> PluginMarketplaceEntry {
|
||||
PluginMarketplaceEntry {
|
||||
name: "repo".to_string(),
|
||||
path: Some(plugins_test_absolute_path("marketplaces/repo")),
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Repo Marketplace".to_string()),
|
||||
}),
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_response(
|
||||
marketplaces: Vec<PluginMarketplaceEntry>,
|
||||
) -> PluginListResponse {
|
||||
PluginListResponse {
|
||||
marketplaces,
|
||||
marketplace_load_errors: Vec::new(),
|
||||
featured_plugin_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn render_loaded_plugins_popup(
|
||||
chat: &mut ChatWidget,
|
||||
response: PluginListResponse,
|
||||
) -> String {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
render_bottom_popup(chat, /*width*/ 100)
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_detail(
|
||||
summary: PluginSummary,
|
||||
description: Option<&str>,
|
||||
skills: &[&str],
|
||||
apps: &[(&str, bool)],
|
||||
mcp_servers: &[&str],
|
||||
) -> PluginDetail {
|
||||
PluginDetail {
|
||||
marketplace_name: "ChatGPT Marketplace".to_string(),
|
||||
marketplace_path: Some(plugins_test_absolute_path("marketplaces/chatgpt")),
|
||||
summary,
|
||||
description: description.map(str::to_string),
|
||||
skills: skills
|
||||
.iter()
|
||||
.map(|name| SkillSummary {
|
||||
name: (*name).to_string(),
|
||||
description: format!("{name} description"),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
path: Some(plugins_test_absolute_path(&format!(
|
||||
"skills/{name}/SKILL.md"
|
||||
))),
|
||||
enabled: true,
|
||||
})
|
||||
.collect(),
|
||||
apps: apps
|
||||
.iter()
|
||||
.map(|(name, needs_auth)| AppSummary {
|
||||
id: format!("{name}-id"),
|
||||
name: (*name).to_string(),
|
||||
description: Some(format!("{name} app")),
|
||||
install_url: Some(format!("https://example.test/{name}")),
|
||||
needs_auth: *needs_auth,
|
||||
})
|
||||
.collect(),
|
||||
mcp_servers: mcp_servers.iter().map(|name| (*name).to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_popup_row_position(popup: &str, needle: &str) -> usize {
|
||||
popup
|
||||
.find(needle)
|
||||
.unwrap_or_else(|| panic!("expected popup to contain {needle}: {popup}"))
|
||||
}
|
||||
|
||||
pub(super) fn type_plugins_search_query(chat: &mut ChatWidget, query: &str) {
|
||||
for ch in query.chars() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn assert_hook_events_snapshot(
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
run_id: &str,
|
||||
|
|
|
|||
|
|
@ -86,721 +86,6 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
|
|||
assert_eq!(chat.current_model(), resolved_model);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_loading_state_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
chat.add_plugins_output();
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Loading available plugins..."),
|
||||
"expected /plugins to open in a loading state before the marketplace arrives, got:\n{popup}"
|
||||
);
|
||||
assert_chatwidget_snapshot!("plugins_popup_loading_state", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_name() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let response = plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-bravo",
|
||||
"bravo",
|
||||
Some("Bravo Search"),
|
||||
Some("Search docs and tickets."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-alpha",
|
||||
"alpha",
|
||||
Some("Alpha Sync"),
|
||||
Some("Already installed but disabled."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ false,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-starter",
|
||||
"starter",
|
||||
Some("Starter"),
|
||||
Some("Included by default."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::InstalledByDefault,
|
||||
),
|
||||
]),
|
||||
plugins_test_repo_marketplace(vec![plugins_test_summary(
|
||||
"plugin-hidden",
|
||||
"hidden",
|
||||
Some("Hidden Repo Plugin"),
|
||||
Some("Should not be shown in /plugins."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)]),
|
||||
]);
|
||||
let popup = render_loaded_plugins_popup(&mut chat, response);
|
||||
assert_chatwidget_snapshot!("plugins_popup_curated_marketplace", popup);
|
||||
assert!(
|
||||
popup.contains("Hidden Repo Plugin"),
|
||||
"expected /plugins to include non-curated marketplaces, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
plugins_test_popup_row_position(&popup, "Alpha Sync")
|
||||
< plugins_test_popup_row_position(&popup, "Bravo Search")
|
||||
&& plugins_test_popup_row_position(&popup, "Bravo Search")
|
||||
< plugins_test_popup_row_position(&popup, "Hidden Repo Plugin")
|
||||
&& plugins_test_popup_row_position(&popup, "Hidden Repo Plugin")
|
||||
< plugins_test_popup_row_position(&popup, "Starter"),
|
||||
"expected /plugins rows to sort installed plugins first, then alphabetically, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summaries() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = plugins_test_summary(
|
||||
"plugin-figma",
|
||||
"figma",
|
||||
Some("Figma"),
|
||||
Some("Design handoff."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
);
|
||||
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
summary.clone(),
|
||||
])]);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: plugins_test_detail(
|
||||
summary,
|
||||
Some("Turn Figma files into implementation context."),
|
||||
&["design-review", "extract-copy"],
|
||||
&[("Figma", true), ("Slack", false)],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert_chatwidget_snapshot!(
|
||||
"plugin_detail_popup_installable",
|
||||
strip_osc8_for_snapshot(&popup)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = plugins_test_summary(
|
||||
"plugin-figma",
|
||||
"figma",
|
||||
Some("Figma"),
|
||||
Some("Design handoff."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
);
|
||||
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
summary.clone(),
|
||||
])]);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: plugins_test_detail(
|
||||
summary,
|
||||
Some("Turn Figma files into implementation context."),
|
||||
&["design-review", "extract-copy"],
|
||||
&[("Figma", true), ("Slack", false)],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
!popup.contains("Data shared with this app is subject to the app's"),
|
||||
"expected installed plugin details to hide the disclosure line, got:\n{popup}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"plugin_detail_popup_installed",
|
||||
strip_osc8_for_snapshot(&popup)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_refresh_preserves_selected_row_position() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let initial = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-notion",
|
||||
"notion",
|
||||
Some("Notion"),
|
||||
Some("Workspace docs."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-slack",
|
||||
"slack",
|
||||
Some("Slack"),
|
||||
Some("Team chat."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]);
|
||||
render_loaded_plugins_popup(&mut chat, initial);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
|
||||
let before = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
before.contains("› [-] Slack"),
|
||||
"expected Slack to be selected before refresh, got:\n{before}"
|
||||
);
|
||||
|
||||
let refreshed = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-airtable",
|
||||
"airtable",
|
||||
Some("Airtable"),
|
||||
Some("Structured records."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-notion",
|
||||
"notion",
|
||||
Some("Notion"),
|
||||
Some("Workspace docs."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-slack",
|
||||
"slack",
|
||||
Some("Slack"),
|
||||
Some("Team chat."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(refreshed));
|
||||
|
||||
let after = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
after.contains("› [-] Notion"),
|
||||
"expected refresh to preserve the selected row position, got:\n{after}"
|
||||
);
|
||||
assert!(
|
||||
after.contains("Airtable"),
|
||||
"expected refreshed popup to include the updated plugin list, got:\n{after}"
|
||||
);
|
||||
assert!(
|
||||
after.contains("Slack"),
|
||||
"expected refreshed popup to include the updated plugin list, got:\n{after}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_refreshes_installed_counts_after_install() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let initial = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]);
|
||||
let before = render_loaded_plugins_popup(&mut chat, initial);
|
||||
assert!(
|
||||
before.contains("Installed 1 of 2 available plugins."),
|
||||
"expected initial installed count before refresh, got:\n{before}"
|
||||
);
|
||||
assert!(
|
||||
before.contains("Available"),
|
||||
"expected pre-install popup copy before refresh, got:\n{before}"
|
||||
);
|
||||
|
||||
let refreshed = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(refreshed));
|
||||
|
||||
let after = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
after.contains("Installed 2 of 2 available plugins."),
|
||||
"expected /plugins to refresh installed counts after install, got:\n{after}"
|
||||
);
|
||||
assert!(
|
||||
after.contains("Installed Space to disable; Enter view details."),
|
||||
"expected refreshed selected row copy to reflect the installed plugin state, got:\n{after}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_space_toggles_installed_plugin_from_list() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let cwd = chat.config.cwd.to_path_buf();
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SetPluginEnabled {
|
||||
cwd: event_cwd,
|
||||
plugin_id,
|
||||
enabled,
|
||||
}) => {
|
||||
assert_eq!(event_cwd, cwd);
|
||||
assert_eq!(plugin_id, "plugin-drive");
|
||||
assert!(!enabled);
|
||||
}
|
||||
other => panic!("expected SetPluginEnabled event, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.on_plugin_enabled_set(
|
||||
cwd,
|
||||
"plugin-drive".to_string(),
|
||||
/*enabled*/ false,
|
||||
Ok(()),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("› [ ] Drive"),
|
||||
"expected selected plugin row to stay selected after refresh, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_space_on_uninstalled_row_does_not_start_search() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
let before = render_bottom_popup(&chat, /*width*/ 100);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
let after = render_bottom_popup(&chat, /*width*/ 100);
|
||||
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"did not expect Space on an uninstalled plugin to emit an event"
|
||||
);
|
||||
assert_eq!(after, before);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_space_with_active_search_does_not_toggle_installed_plugin() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
type_plugins_search_query(&mut chat, "dr");
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"did not expect Space with an active plugin search to emit a toggle event"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_search_filters_visible_rows_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-slack",
|
||||
"slack",
|
||||
Some("Slack"),
|
||||
Some("Team chat."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
type_plugins_search_query(&mut chat, "sla");
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert_chatwidget_snapshot!("plugins_popup_search_filtered", popup);
|
||||
assert!(
|
||||
!popup.contains("Calendar") && !popup.contains("Drive"),
|
||||
"expected search to leave only matching rows visible, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_installed_tab_filters_rows_and_clears_search() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-slack",
|
||||
"slack",
|
||||
Some("Slack"),
|
||||
Some("Team chat."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
type_plugins_search_query(&mut chat, "sla");
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Installed plugins.") && popup.contains("Showing 1 installed plugins."),
|
||||
"expected Installed tab header, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Calendar") && !popup.contains("Slack"),
|
||||
"expected Installed tab to show only installed plugins, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
!popup.contains("sla"),
|
||||
"expected tab switch to clear search query, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_openai_curated_tab_omits_marketplace_in_rows() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)]),
|
||||
plugins_test_repo_marketplace(vec![plugins_test_summary(
|
||||
"plugin-repo",
|
||||
"repo",
|
||||
Some("Repo Plugin"),
|
||||
Some("Repo-only plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)]),
|
||||
]),
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("OpenAI Curated marketplace."),
|
||||
"expected OpenAI Curated tab header, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Calendar") && !popup.contains("Repo Plugin"),
|
||||
"expected OpenAI Curated tab to show only official marketplace plugins, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
!popup.contains("ChatGPT Marketplace ·"),
|
||||
"expected marketplace-specific rows to omit marketplace labels, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_refresh_preserves_duplicate_marketplace_tab_by_path() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let response = plugins_test_response(vec![
|
||||
PluginMarketplaceEntry {
|
||||
name: "duplicate".to_string(),
|
||||
path: Some(plugins_test_absolute_path(
|
||||
"marketplaces/home/marketplace.json",
|
||||
)),
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Duplicate Marketplace".to_string()),
|
||||
}),
|
||||
plugins: vec![plugins_test_summary(
|
||||
"plugin-home",
|
||||
"home",
|
||||
Some("Home Plugin"),
|
||||
Some("Home marketplace plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)],
|
||||
},
|
||||
PluginMarketplaceEntry {
|
||||
name: "duplicate".to_string(),
|
||||
path: Some(plugins_test_absolute_path(
|
||||
"marketplaces/repo/marketplace.json",
|
||||
)),
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Duplicate Marketplace".to_string()),
|
||||
}),
|
||||
plugins: vec![plugins_test_summary(
|
||||
"plugin-repo",
|
||||
"repo",
|
||||
Some("Repo Plugin"),
|
||||
Some("Repo marketplace plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)],
|
||||
},
|
||||
]);
|
||||
let cwd = chat.config.cwd.to_path_buf();
|
||||
chat.on_plugins_loaded(cwd.clone(), Ok(response.clone()));
|
||||
chat.add_plugins_output();
|
||||
|
||||
for _ in 0..4 {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
chat.on_plugins_loaded(cwd, Ok(response));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Duplicate Marketplace (2/2)."),
|
||||
"expected refresh to preserve the second duplicate marketplace tab, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Repo Plugin") && !popup.contains("Home Plugin"),
|
||||
"expected second duplicate marketplace rows after refresh, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_search_no_matches_and_backspace_restores_results() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-slack",
|
||||
"slack",
|
||||
Some("Slack"),
|
||||
Some("Team chat."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
type_plugins_search_query(&mut chat, "zzz");
|
||||
|
||||
let no_matches = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
no_matches.contains("zzz"),
|
||||
"expected popup to show the typed search query, got:\n{no_matches}"
|
||||
);
|
||||
assert!(
|
||||
no_matches.contains("no matches"),
|
||||
"expected popup to render the no-matches UX, got:\n{no_matches}"
|
||||
);
|
||||
|
||||
for _ in 0..3 {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Backspace));
|
||||
}
|
||||
|
||||
let restored = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
restored.contains("Calendar") && restored.contains("Slack"),
|
||||
"expected clearing the query to restore the plugin rows, got:\n{restored}"
|
||||
);
|
||||
assert!(
|
||||
!restored.contains("no matches"),
|
||||
"did not expect the no-matches state after clearing the query, got:\n{restored}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_popup_stays_loading_until_final_snapshot_updates() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
mod auth;
|
||||
pub(crate) mod onboarding_screen;
|
||||
mod trust_directory;
|
||||
pub(crate) use auth::mark_url_hyperlink;
|
||||
mod welcome;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ pub enum SlashCommand {
|
|||
Theme,
|
||||
Mcp,
|
||||
Apps,
|
||||
Plugins,
|
||||
Logout,
|
||||
Quit,
|
||||
Exit,
|
||||
|
|
@ -117,7 +116,6 @@ impl SlashCommand {
|
|||
SlashCommand::Memories => "configure memory use and generation",
|
||||
SlashCommand::Mcp => "list configured MCP tools; use /mcp verbose for details",
|
||||
SlashCommand::Apps => "manage apps",
|
||||
SlashCommand::Plugins => "browse plugins",
|
||||
SlashCommand::Logout => "log out of Codex",
|
||||
SlashCommand::Rollout => "print the rollout file path",
|
||||
SlashCommand::TestApproval => "test approval request",
|
||||
|
|
@ -188,7 +186,6 @@ impl SlashCommand {
|
|||
| SlashCommand::Stop
|
||||
| SlashCommand::Mcp
|
||||
| SlashCommand::Apps
|
||||
| SlashCommand::Plugins
|
||||
| SlashCommand::Feedback
|
||||
| SlashCommand::Quit
|
||||
| SlashCommand::Exit
|
||||
|
|
|
|||
Reference in a new issue