diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 530bf399..558c01b4 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 747b6b85..326b6f4f 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 3ecaf9cc..a361512e 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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. diff --git a/vendor/codex/codex-rs/tui/src/app.rs b/vendor/codex/codex-rs/tui/src/app.rs index eb2e06b1..bb76413c 100644 --- a/vendor/codex/codex-rs/tui/src/app.rs +++ b/vendor/codex/codex-rs/tui/src/app.rs @@ -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, pending_primary_events: VecDeque, 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>, } fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option { @@ -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) diff --git a/vendor/codex/codex-rs/tui/src/app/app_server_adapter.rs b/vendor/codex/codex-rs/tui/src/app/app_server_adapter.rs index fb2588d4..b27db2ce 100644 --- a/vendor/codex/codex-rs/tui/src/app/app_server_adapter.rs +++ b/vendor/codex/codex-rs/tui/src/app/app_server_adapter.rs @@ -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; } _ => {} diff --git a/vendor/codex/codex-rs/tui/src/app/background_requests.rs b/vendor/codex/codex-rs/tui/src/app/background_requests.rs index 75175524..3f391ea8 100644 --- a/vendor/codex/codex-rs/tui/src/app/background_requests.rs +++ b/vendor/codex/codex-rs/tui/src/app/background_requests.rs @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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, rollout_path: Option, @@ -621,49 +409,9 @@ pub(super) fn mcp_inventory_maps_from_statuses(statuses: Vec) - #[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![ diff --git a/vendor/codex/codex-rs/tui/src/app/config_persistence.rs b/vendor/codex/codex-rs/tui/src/app/config_persistence.rs index 20056e8d..f3260b7d 100644 --- a/vendor/codex/codex-rs/tui/src/app/config_persistence.rs +++ b/vendor/codex/codex-rs/tui/src/app/config_persistence.rs @@ -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(()) } diff --git a/vendor/codex/codex-rs/tui/src/app/event_dispatch.rs b/vendor/codex/codex-rs/tui/src/app/event_dispatch.rs index 1d767071..2b533509 100644 --- a/vendor/codex/codex-rs/tui/src/app/event_dispatch.rs +++ b/vendor/codex/codex-rs/tui/src/app/event_dispatch.rs @@ -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) diff --git a/vendor/codex/codex-rs/tui/src/app/test_support.rs b/vendor/codex/codex-rs/tui/src/app/test_support.rs index 4dc724ee..9a43c056 100644 --- a/vendor/codex/codex-rs/tui/src/app/test_support.rs +++ b/vendor/codex/codex-rs/tui/src/app/test_support.rs @@ -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(), } } diff --git a/vendor/codex/codex-rs/tui/src/app/tests.rs b/vendor/codex/codex-rs/tui/src/app/tests.rs index 4394f0b0..b527402a 100644 --- a/vendor/codex/codex-rs/tui/src/app/tests.rs +++ b/vendor/codex/codex-rs/tui/src/app/tests.rs @@ -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, diff --git a/vendor/codex/codex-rs/tui/src/app_event.rs b/vendor/codex/codex-rs/tui/src/app_event.rs index dce1e433..9c25d189 100644 --- a/vendor/codex/codex-rs/tui/src/app_event.rs +++ b/vendor/codex/codex-rs/tui/src/app_event.rs @@ -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, - }, - - /// 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, - }, - - /// 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, - }, - - /// 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, - }, - - /// 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, diff --git a/vendor/codex/codex-rs/tui/src/bottom_pane/chat_composer.rs b/vendor/codex/codex-rs/tui/src/bottom_pane/chat_composer.rs index a03c35ce..73511e68 100644 --- a/vendor/codex/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/vendor/codex/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -368,7 +368,6 @@ pub(crate) struct ChatComposer { config: ChatComposerConfig, collaboration_mode_indicator: Option, 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, diff --git a/vendor/codex/codex-rs/tui/src/bottom_pane/command_popup.rs b/vendor/codex/codex-rs/tui/src/bottom_pane/command_popup.rs index 28c749cb..9ace6ccc 100644 --- a/vendor/codex/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/vendor/codex/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -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 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, diff --git a/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs b/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs index 622a23cc..96d938e8 100644 --- a/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs +++ b/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs @@ -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 { 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, diff --git a/vendor/codex/codex-rs/tui/src/bottom_pane/slash_commands.rs b/vendor/codex/codex-rs/tui/src/bottom_pane/slash_commands.rs index 78d99e3e..1d62010a 100644 --- a/vendor/codex/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/vendor/codex/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -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, diff --git a/vendor/codex/codex-rs/tui/src/chatwidget.rs b/vendor/codex/codex-rs/tui/src/chatwidget.rs index 284cf1ea..1fde33eb 100644 --- a/vendor/codex/codex-rs/tui/src/chatwidget.rs +++ b/vendor/codex/codex-rs/tui/src/chatwidget.rs @@ -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, - in_flight_cwd: Option, -} - -#[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, connectors_prefetch_in_flight: bool, connectors_force_refetch_pending: bool, - plugins_cache: PluginsCacheState, - plugins_fetch_state: PluginListFetchState, - plugin_install_apps_needing_auth: Vec, - plugin_install_auth_flow: Option, - plugins_active_tab_id: Option, // 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 = Vec::new(); diff --git a/vendor/codex/codex-rs/tui/src/chatwidget/plugins.rs b/vendor/codex/codex-rs/tui/src/chatwidget/plugins.rs deleted file mode 100644 index 562671c3..00000000 --- a/vendor/codex/codex-rs/tui/src/chatwidget/plugins.rs +++ /dev/null @@ -1,1405 +0,0 @@ -use std::path::PathBuf; -use std::time::Duration; -use std::time::Instant; - -use super::ChatWidget; -use crate::app_event::AppEvent; -use crate::bottom_pane::ColumnWidthMode; -use crate::bottom_pane::SelectionAction; -use crate::bottom_pane::SelectionItem; -use crate::bottom_pane::SelectionRowDisplay; -use crate::bottom_pane::SelectionTab; -use crate::bottom_pane::SelectionToggle; -use crate::bottom_pane::SelectionViewParams; -use crate::history_cell; -use crate::onboarding::mark_url_hyperlink; -use crate::render::renderable::ColumnRenderable; -use crate::render::renderable::Renderable; -use crate::shimmer::shimmer_spans; -use crate::tui::FrameRequester; -use codex_app_server_protocol::PluginDetail; -use codex_app_server_protocol::PluginInstallPolicy; -use codex_app_server_protocol::PluginInstallResponse; -use codex_app_server_protocol::PluginListResponse; -use codex_app_server_protocol::PluginMarketplaceEntry; -use codex_app_server_protocol::PluginReadResponse; -use codex_app_server_protocol::PluginSummary; -use codex_app_server_protocol::PluginUninstallResponse; -use codex_features::Feature; -use codex_utils_absolute_path::AbsolutePathBuf; -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::prelude::Widget; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; -use unicode_width::UnicodeWidthStr; - -const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection"; -const ALL_PLUGINS_TAB_ID: &str = "all-plugins"; -const INSTALLED_PLUGINS_TAB_ID: &str = "installed-plugins"; -const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated"; -const OPENAI_CURATED_TAB_ID: &str = "marketplace:openai-curated"; -const PLUGIN_ROW_PREFIX_WIDTH: usize = 6; -const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1); -const LOADING_ANIMATION_INTERVAL: Duration = Duration::from_millis(100); - -struct DelayedLoadingHeader { - started_at: Instant, - frame_requester: FrameRequester, - animations_enabled: bool, - loading_text: String, - note: Option, -} - -impl DelayedLoadingHeader { - fn new( - frame_requester: FrameRequester, - animations_enabled: bool, - loading_text: String, - note: Option, - ) -> Self { - Self { - started_at: Instant::now(), - frame_requester, - animations_enabled, - loading_text, - note, - } - } -} - -impl Renderable for DelayedLoadingHeader { - fn render(&self, area: Rect, buf: &mut Buffer) { - if area.is_empty() { - return; - } - - let mut lines = Vec::with_capacity(3); - lines.push(Line::from("Plugins".bold())); - - let now = Instant::now(); - let elapsed = now.saturating_duration_since(self.started_at); - if elapsed < LOADING_ANIMATION_DELAY { - self.frame_requester - .schedule_frame_in(LOADING_ANIMATION_DELAY - elapsed); - lines.push(Line::from(self.loading_text.as_str().dim())); - } else if self.animations_enabled { - self.frame_requester - .schedule_frame_in(LOADING_ANIMATION_INTERVAL); - lines.push(Line::from(shimmer_spans(self.loading_text.as_str()))); - } else { - lines.push(Line::from(self.loading_text.as_str().dim())); - } - - if let Some(note) = &self.note { - lines.push(Line::from(note.as_str().dim())); - } - - Paragraph::new(lines).render_ref(area, buf); - } - - fn desired_height(&self, _width: u16) -> u16 { - 2 + u16::from(self.note.is_some()) - } -} - -const APPS_HELP_ARTICLE_URL: &str = "https://help.openai.com/en/articles/11487775-apps-in-chatgpt"; - -struct PluginDisclosureLine { - line: Line<'static>, -} - -impl Renderable for PluginDisclosureLine { - fn render(&self, area: Rect, buf: &mut Buffer) { - Paragraph::new(self.line.clone()) - .wrap(Wrap { trim: false }) - .render(area, buf); - mark_url_hyperlink(buf, area, APPS_HELP_ARTICLE_URL); - } - - fn desired_height(&self, width: u16) -> u16 { - Paragraph::new(self.line.clone()) - .wrap(Wrap { trim: false }) - .line_count(width) - .try_into() - .unwrap_or(u16::MAX) - } -} - -#[derive(Debug, Clone, Default)] -pub(super) enum PluginsCacheState { - #[default] - Uninitialized, - Loading, - Ready(PluginListResponse), - Failed(String), -} - -impl ChatWidget { - pub(crate) fn add_plugins_output(&mut self) { - if !self.config.features.enabled(Feature::Plugins) { - self.add_info_message( - "Plugins are disabled.".to_string(), - Some("Enable the plugins feature to use /plugins.".to_string()), - ); - return; - } - - self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string()); - self.prefetch_plugins(); - - match self.plugins_cache_for_current_cwd() { - PluginsCacheState::Ready(response) => { - self.open_plugins_popup(&response); - } - PluginsCacheState::Failed(err) => { - self.add_to_history(history_cell::new_error_event(err)); - } - PluginsCacheState::Loading | PluginsCacheState::Uninitialized => { - self.open_plugins_loading_popup(); - } - } - self.request_redraw(); - } - - pub(crate) fn on_plugins_loaded( - &mut self, - cwd: PathBuf, - result: Result, - ) { - if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) { - self.plugins_fetch_state.in_flight_cwd = None; - } - - if self.config.cwd.as_path() != cwd.as_path() { - return; - } - - let auth_flow_active = self.plugin_install_auth_flow.is_some(); - - match result { - Ok(response) => { - self.plugins_fetch_state.cache_cwd = Some(cwd); - self.plugins_cache = PluginsCacheState::Ready(response.clone()); - if !auth_flow_active { - self.refresh_plugins_popup_if_open(&response); - } - } - Err(err) => { - if !auth_flow_active { - self.plugins_fetch_state.cache_cwd = None; - self.plugins_cache = PluginsCacheState::Failed(err.clone()); - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugins_error_popup_params(&err), - ); - } - } - } - } - - fn prefetch_plugins(&mut self) { - let cwd = self.config.cwd.to_path_buf(); - if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) { - return; - } - - self.plugins_fetch_state.in_flight_cwd = Some(cwd.clone()); - if self.plugins_fetch_state.cache_cwd.as_deref() != Some(cwd.as_path()) { - self.plugins_cache = PluginsCacheState::Loading; - } - - self.app_event_tx.send(AppEvent::FetchPluginsList { cwd }); - } - - fn plugins_cache_for_current_cwd(&self) -> PluginsCacheState { - if self.plugins_fetch_state.cache_cwd.as_deref() == Some(self.config.cwd.as_path()) { - self.plugins_cache.clone() - } else { - PluginsCacheState::Uninitialized - } - } - - fn open_plugins_loading_popup(&mut self) { - if !self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugins_loading_popup_params(), - ) { - self.bottom_pane - .show_selection_view(self.plugins_loading_popup_params()); - } - } - - fn open_plugins_popup(&mut self, response: &PluginListResponse) { - self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string()); - self.bottom_pane - .show_selection_view(self.plugins_popup_params( - response, - self.plugins_active_tab_id.clone(), - /*initial_selected_idx*/ None, - )); - } - - pub(crate) fn open_plugin_detail_loading_popup(&mut self, plugin_display_name: &str) { - self.plugins_active_tab_id = self - .bottom_pane - .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID) - .map(str::to_string) - .or_else(|| self.plugins_active_tab_id.clone()); - let params = self.plugin_detail_loading_popup_params(plugin_display_name); - let _ = self - .bottom_pane - .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params); - } - - pub(crate) fn open_plugin_install_loading_popup(&mut self, plugin_display_name: &str) { - let params = self.plugin_install_loading_popup_params(plugin_display_name); - let _ = self - .bottom_pane - .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params); - } - - pub(crate) fn open_plugin_uninstall_loading_popup(&mut self, plugin_display_name: &str) { - let params = self.plugin_uninstall_loading_popup_params(plugin_display_name); - let _ = self - .bottom_pane - .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params); - } - - pub(crate) fn on_plugin_detail_loaded( - &mut self, - cwd: PathBuf, - result: Result, - ) { - if self.config.cwd.as_path() != cwd.as_path() { - return; - } - - let plugins_response = match self.plugins_cache_for_current_cwd() { - PluginsCacheState::Ready(response) => Some(response), - _ => None, - }; - - match result { - Ok(response) => { - if let Some(plugins_response) = plugins_response { - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugin_detail_popup_params(&plugins_response, &response.plugin), - ); - } - } - Err(err) => { - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()), - ); - } - } - } - - pub(crate) fn on_plugin_install_loaded( - &mut self, - cwd: PathBuf, - _marketplace_path: AbsolutePathBuf, - _plugin_name: String, - plugin_display_name: String, - result: Result, - ) -> bool { - if self.config.cwd.as_path() != cwd.as_path() { - return true; - } - - match result { - Ok(response) => { - self.plugin_install_apps_needing_auth = response.apps_needing_auth; - self.plugin_install_auth_flow = None; - if self.plugin_install_apps_needing_auth.is_empty() { - self.add_info_message( - format!("Installed {plugin_display_name} plugin."), - Some("No additional app authentication is required.".to_string()), - ); - true - } else { - let app_names = self - .plugin_install_apps_needing_auth - .iter() - .map(|app| app.name.as_str()) - .collect::>() - .join(", "); - self.add_info_message( - format!("Installed {plugin_display_name} plugin."), - Some(format!( - "{} app(s) still need authentication: {app_names}", - self.plugin_install_apps_needing_auth.len() - )), - ); - self.plugin_install_auth_flow = Some(super::PluginInstallAuthFlowState { - plugin_display_name, - next_app_index: 0, - }); - self.open_plugin_install_auth_popup(); - false - } - } - Err(err) => { - self.plugin_install_apps_needing_auth.clear(); - self.plugin_install_auth_flow = None; - let plugins_response = match self.plugins_cache_for_current_cwd() { - PluginsCacheState::Ready(response) => Some(response), - _ => None, - }; - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()), - ); - true - } - } - } - - pub(crate) fn on_plugin_enabled_set( - &mut self, - cwd: PathBuf, - plugin_id: String, - enabled: bool, - result: Result<(), String>, - ) { - if self.config.cwd.as_path() != cwd.as_path() { - return; - } - - if let Err(err) = result { - self.add_error_message(format!( - "Failed to update plugin config for {plugin_id}: {err}" - )); - if let PluginsCacheState::Ready(response) = self.plugins_cache_for_current_cwd() { - self.refresh_plugins_popup_if_open(&response); - } - return; - } - - let refreshed_response = match &mut self.plugins_cache { - PluginsCacheState::Ready(response) - if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) => - { - for plugin in response - .marketplaces - .iter_mut() - .flat_map(|marketplace| marketplace.plugins.iter_mut()) - .filter(|plugin| plugin.id == plugin_id) - { - plugin.enabled = enabled; - } - Some(response.clone()) - } - _ => None, - }; - - if let Some(response) = refreshed_response { - self.refresh_plugins_popup_if_open(&response); - } - } - - pub(crate) fn on_plugin_uninstall_loaded( - &mut self, - cwd: PathBuf, - plugin_display_name: String, - result: Result, - ) { - if self.config.cwd.as_path() != cwd.as_path() { - return; - } - - match result { - Ok(_response) => { - self.plugin_install_apps_needing_auth.clear(); - self.plugin_install_auth_flow = None; - self.add_info_message( - format!("Uninstalled {plugin_display_name} plugin."), - Some("Bundled apps remain installed.".to_string()), - ); - } - Err(err) => { - let plugins_response = match self.plugins_cache_for_current_cwd() { - PluginsCacheState::Ready(response) => Some(response), - _ => None, - }; - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugin_detail_error_popup_params(&err, plugins_response.as_ref()), - ); - } - } - } - - pub(crate) fn advance_plugin_install_auth_flow(&mut self) { - let should_finish = { - let Some(flow) = self.plugin_install_auth_flow.as_mut() else { - return; - }; - flow.next_app_index += 1; - flow.next_app_index >= self.plugin_install_apps_needing_auth.len() - }; - - if should_finish { - self.finish_plugin_install_auth_flow(/*abandoned*/ false); - return; - } - - self.open_plugin_install_auth_popup(); - } - - pub(crate) fn abandon_plugin_install_auth_flow(&mut self) { - self.finish_plugin_install_auth_flow(/*abandoned*/ true); - } - - fn open_plugin_install_auth_popup(&mut self) { - let Some(params) = self.plugin_install_auth_popup_params() else { - self.finish_plugin_install_auth_flow(/*abandoned*/ false); - return; - }; - if !self - .bottom_pane - .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params) - && let Some(params) = self.plugin_install_auth_popup_params() - { - self.bottom_pane.show_selection_view(params); - } - } - - fn plugin_install_auth_popup_params(&self) -> Option { - let flow = self.plugin_install_auth_flow.as_ref()?; - let app = self - .plugin_install_apps_needing_auth - .get(flow.next_app_index)?; - let total = self.plugin_install_apps_needing_auth.len(); - let current = flow.next_app_index + 1; - let is_installed = self.plugin_install_auth_app_is_installed(app.id.as_str()); - let status_label = if is_installed { - "Already installed in this session." - } else { - "Install the required Apps in ChatGPT to continue:" - }; - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("{} plugin installed.", flow.plugin_display_name).bold(), - )); - header.push(Line::from( - format!("App setup {current}/{total}: {}", app.name).dim(), - )); - header.push(Line::from(status_label.dim())); - - let mut items = Vec::new(); - - if let Some(install_url) = app.install_url.clone() { - let install_label = if is_installed { - "Manage on ChatGPT" - } else { - "Install on ChatGPT" - }; - items.push(SelectionItem { - name: install_label.to_string(), - description: Some("Open the ChatGPT app management page".to_string()), - selected_description: Some("Open the app page in your browser.".to_string()), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::OpenUrlInBrowser { - url: install_url.clone(), - }); - })], - ..Default::default() - }); - } else { - items.push(SelectionItem { - name: "ChatGPT apps link unavailable".to_string(), - description: Some("This app did not provide an install/manage URL.".to_string()), - is_disabled: true, - ..Default::default() - }); - } - - if is_installed { - items.push(SelectionItem { - name: "Continue".to_string(), - description: Some("This app is already installed.".to_string()), - selected_description: Some("Advance to the next app.".to_string()), - actions: vec![Box::new(|tx| { - tx.send(AppEvent::PluginInstallAuthAdvance { - refresh_connectors: false, - }); - })], - ..Default::default() - }); - } else { - items.push(SelectionItem { - name: "I've installed it".to_string(), - description: Some( - "Trust your confirmation and continue to the next app.".to_string(), - ), - selected_description: Some( - "Continue without waiting for refresh to complete.".to_string(), - ), - actions: vec![Box::new(|tx| { - tx.send(AppEvent::PluginInstallAuthAdvance { - refresh_connectors: true, - }); - })], - ..Default::default() - }); - } - - items.push(SelectionItem { - name: "Skip remaining app setup".to_string(), - description: Some("Stop this follow-up flow for this plugin.".to_string()), - selected_description: Some("Abandon remaining required app setup.".to_string()), - actions: vec![Box::new(|tx| { - tx.send(AppEvent::PluginInstallAuthAbandon); - })], - ..Default::default() - }); - - Some(SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - footer_hint: Some(plugin_detail_hint_line()), - items, - col_width_mode: ColumnWidthMode::AutoAllRows, - ..Default::default() - }) - } - - fn plugin_install_auth_app_is_installed(&self, app_id: &str) -> bool { - self.connectors_for_mentions().is_some_and(|connectors| { - connectors - .iter() - .any(|connector| connector.id == app_id && connector.is_accessible) - }) - } - - fn finish_plugin_install_auth_flow(&mut self, abandoned: bool) { - let Some(flow) = self.plugin_install_auth_flow.take() else { - return; - }; - self.plugin_install_apps_needing_auth.clear(); - if abandoned { - self.add_info_message( - format!( - "Skipped remaining app setup for {} plugin.", - flow.plugin_display_name - ), - Some("The plugin may not be usable until required apps are installed.".to_string()), - ); - } else { - self.add_info_message( - format!( - "Completed app setup flow for {} plugin.", - flow.plugin_display_name - ), - Some("You can now continue managing plugins from /plugins.".to_string()), - ); - } - - let plugins_response = match self.plugins_cache_for_current_cwd() { - PluginsCacheState::Ready(response) => Some(response), - _ => None, - }; - if let Some(plugins_response) = plugins_response { - let tab_id = self.plugins_active_tab_id.clone(); - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugins_popup_params( - &plugins_response, - tab_id, - /*initial_selected_idx*/ None, - ), - ); - } - } - - fn refresh_plugins_popup_if_open(&mut self, response: &PluginListResponse) { - let active_tab_id = self - .bottom_pane - .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID) - .map(str::to_string) - .or_else(|| self.plugins_active_tab_id.clone()); - let selected_idx = self - .bottom_pane - .selected_index_for_active_view(PLUGINS_SELECTION_VIEW_ID); - self.plugins_active_tab_id = active_tab_id.clone(); - let _ = self.bottom_pane.replace_selection_view_if_active( - PLUGINS_SELECTION_VIEW_ID, - self.plugins_popup_params(response, active_tab_id, selected_idx), - ); - } - - fn plugins_loading_popup_params(&self) -> SelectionViewParams { - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(DelayedLoadingHeader::new( - self.frame_requester.clone(), - self.config.animations, - "Loading available plugins...".to_string(), - Some("This updates when the marketplace list is ready.".to_string()), - )), - items: vec![SelectionItem { - name: "Loading plugins...".to_string(), - description: Some("This updates when the marketplace list is ready.".to_string()), - is_disabled: true, - ..Default::default() - }], - ..Default::default() - } - } - - fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams { - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(DelayedLoadingHeader::new( - self.frame_requester.clone(), - self.config.animations, - format!("Loading details for {plugin_display_name}..."), - /*note*/ None, - )), - items: vec![SelectionItem { - name: "Loading plugin details...".to_string(), - description: Some("This updates when plugin details load.".to_string()), - is_disabled: true, - ..Default::default() - }], - ..Default::default() - } - } - - fn plugin_install_loading_popup_params( - &self, - plugin_display_name: &str, - ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Installing {plugin_display_name}...").dim(), - )); - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - items: vec![SelectionItem { - name: "Installing plugin...".to_string(), - description: Some("This updates when plugin installation completes.".to_string()), - is_disabled: true, - ..Default::default() - }], - ..Default::default() - } - } - - fn plugin_uninstall_loading_popup_params( - &self, - plugin_display_name: &str, - ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("Uninstalling {plugin_display_name}...").dim(), - )); - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - items: vec![SelectionItem { - name: "Uninstalling plugin...".to_string(), - description: Some("This updates when the plugin removal completes.".to_string()), - is_disabled: true, - ..Default::default() - }], - ..Default::default() - } - } - - fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to load plugins.".dim())); - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - items: vec![SelectionItem { - name: "Plugin marketplace unavailable".to_string(), - description: Some(err.to_string()), - is_disabled: true, - ..Default::default() - }], - ..Default::default() - } - } - - fn plugin_detail_error_popup_params( - &self, - err: &str, - plugins_response: Option<&PluginListResponse>, - ) -> SelectionViewParams { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from("Failed to load plugin details.".dim())); - - let mut items = vec![SelectionItem { - name: "Plugin detail unavailable".to_string(), - description: Some(err.to_string()), - is_disabled: true, - ..Default::default() - }]; - if let Some(plugins_response) = plugins_response.cloned() { - let cwd = self.config.cwd.to_path_buf(); - items.push(SelectionItem { - name: "Back to plugins".to_string(), - description: Some("Return to the plugin list.".to_string()), - selected_description: Some("Return to the plugin list.".to_string()), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::PluginsLoaded { - cwd: cwd.clone(), - result: Ok(plugins_response.clone()), - }); - })], - ..Default::default() - }); - } - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - footer_hint: Some(plugin_detail_hint_line()), - items, - ..Default::default() - } - } - - fn plugins_popup_params( - &self, - response: &PluginListResponse, - active_tab_id: Option, - initial_selected_idx: Option, - ) -> SelectionViewParams { - let marketplaces: Vec<&PluginMarketplaceEntry> = response.marketplaces.iter().collect(); - - let total: usize = marketplaces - .iter() - .map(|marketplace| marketplace.plugins.len()) - .sum(); - let installed = marketplaces - .iter() - .flat_map(|marketplace| marketplace.plugins.iter()) - .filter(|plugin| plugin.installed) - .count(); - - let all_entries = plugin_entries_for_marketplaces(marketplaces.iter().copied()); - let name_column_width = all_entries - .iter() - .map(|(_, _, display_name)| { - PLUGIN_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(display_name.as_str()) - }) - .max(); - let installed_entries = all_entries - .iter() - .filter(|(_, plugin, _)| plugin.installed) - .cloned() - .collect(); - - let mut tabs = Vec::new(); - tabs.push(SelectionTab { - id: ALL_PLUGINS_TAB_ID.to_string(), - label: "All Plugins".to_string(), - header: plugins_header( - "Browse plugins from available marketplaces.".to_string(), - format!("Installed {installed} of {total} available plugins."), - ), - items: self.plugin_selection_items( - all_entries, - /*include_marketplace_names*/ true, - "No marketplace plugins available", - "No plugins are available in the discovered marketplaces.", - ), - }); - - tabs.push(SelectionTab { - id: INSTALLED_PLUGINS_TAB_ID.to_string(), - label: format!("Installed ({installed})"), - header: plugins_header( - "Installed plugins.".to_string(), - format!("Showing {installed} installed plugins."), - ), - items: self.plugin_selection_items( - installed_entries, - /*include_marketplace_names*/ true, - "No installed plugins", - "No installed plugins.", - ), - }); - - let curated_marketplace = marketplaces - .iter() - .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) - .copied(); - let curated_entries = curated_marketplace - .map(|marketplace| plugin_entries_for_marketplaces([marketplace])) - .unwrap_or_default(); - let curated_total = curated_entries.len(); - let curated_installed = curated_entries - .iter() - .filter(|(_, plugin, _)| plugin.installed) - .count(); - tabs.push(SelectionTab { - id: OPENAI_CURATED_TAB_ID.to_string(), - label: "OpenAI Curated".to_string(), - header: plugins_header( - "OpenAI Curated marketplace.".to_string(), - format!("Installed {curated_installed} of {curated_total} OpenAI Curated plugins."), - ), - items: self.plugin_selection_items( - curated_entries, - /*include_marketplace_names*/ false, - "No OpenAI Curated plugins available", - "No OpenAI Curated plugins available.", - ), - }); - - let mut additional_marketplaces: Vec<&PluginMarketplaceEntry> = marketplaces - .iter() - .copied() - .filter(|marketplace| marketplace.name != OPENAI_CURATED_MARKETPLACE_NAME) - .collect(); - additional_marketplaces.sort_by(|left, right| { - marketplace_display_name(left) - .to_ascii_lowercase() - .cmp(&marketplace_display_name(right).to_ascii_lowercase()) - .then_with(|| marketplace_display_name(left).cmp(&marketplace_display_name(right))) - .then_with(|| left.name.cmp(&right.name)) - }); - - let labels = disambiguate_duplicate_tab_labels( - additional_marketplaces - .iter() - .map(|marketplace| marketplace_display_name(marketplace)) - .collect(), - ); - for (marketplace, label) in additional_marketplaces.into_iter().zip(labels) { - let entries = plugin_entries_for_marketplaces([marketplace]); - let marketplace_total = entries.len(); - let marketplace_installed = entries - .iter() - .filter(|(_, plugin, _)| plugin.installed) - .count(); - tabs.push(SelectionTab { - id: marketplace_tab_id(marketplace), - label: label.clone(), - header: plugins_header( - format!("{label}."), - format!( - "Installed {marketplace_installed} of {marketplace_total} {label} plugins." - ), - ), - items: self.plugin_selection_items( - entries, - /*include_marketplace_names*/ false, - "No plugins available in this marketplace", - "No plugins available in this marketplace.", - ), - }); - } - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(()), - footer_hint: Some(plugins_popup_hint_line()), - tabs, - initial_tab_id: active_tab_id, - is_searchable: true, - search_placeholder: Some("Type to search plugins".to_string()), - col_width_mode: ColumnWidthMode::AutoAllRows, - row_display: SelectionRowDisplay::SingleLine, - name_column_width, - initial_selected_idx, - ..Default::default() - } - } - - fn plugin_detail_popup_params( - &self, - plugins_response: &PluginListResponse, - plugin: &PluginDetail, - ) -> SelectionViewParams { - let marketplace_label = plugin.marketplace_name.clone(); - let display_name = plugin_display_name(&plugin.summary); - let detail_status_label = if plugin.summary.installed { - if plugin.summary.enabled { - "Installed" - } else { - "Disabled" - } - } else { - match plugin.summary.install_policy { - PluginInstallPolicy::NotAvailable => "Not installable", - PluginInstallPolicy::Available => "Can be installed", - PluginInstallPolicy::InstalledByDefault => "Available by default", - } - }; - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from( - format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(), - )); - if !plugin.summary.installed { - header.push(PluginDisclosureLine { - line: Line::from(vec![ - "Data shared with this app is subject to the app's ".into(), - "terms of service".bold(), - " and ".into(), - "privacy policy".bold(), - ". ".into(), - "Learn more".cyan().underlined(), - ".".into(), - ]), - }); - } - if let Some(description) = plugin_detail_description(plugin) { - header.push(Line::from(description.dim())); - } - - let cwd = self.config.cwd.to_path_buf(); - let plugins_response = plugins_response.clone(); - let mut items = vec![SelectionItem { - name: "Back to plugins".to_string(), - description: Some("Return to the plugin list.".to_string()), - selected_description: Some("Return to the plugin list.".to_string()), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::PluginsLoaded { - cwd: cwd.clone(), - result: Ok(plugins_response.clone()), - }); - })], - ..Default::default() - }]; - - if plugin.summary.installed { - let uninstall_cwd = self.config.cwd.to_path_buf(); - let plugin_id = plugin.summary.id.clone(); - let plugin_display_name = display_name; - items.push(SelectionItem { - name: "Uninstall plugin".to_string(), - description: Some("Remove this plugin now.".to_string()), - selected_description: Some("Remove this plugin now.".to_string()), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::OpenPluginUninstallLoading { - plugin_display_name: plugin_display_name.clone(), - }); - tx.send(AppEvent::FetchPluginUninstall { - cwd: uninstall_cwd.clone(), - plugin_id: plugin_id.clone(), - plugin_display_name: plugin_display_name.clone(), - }); - })], - ..Default::default() - }); - } else if plugin.summary.install_policy == PluginInstallPolicy::NotAvailable { - items.push(SelectionItem { - name: "Install plugin".to_string(), - description: Some( - "This plugin is not installable from this marketplace.".to_string(), - ), - is_disabled: true, - ..Default::default() - }); - } else if let Some(marketplace_path) = plugin.marketplace_path.clone() { - let install_cwd = self.config.cwd.to_path_buf(); - let plugin_name = plugin.summary.name.clone(); - let plugin_display_name = display_name; - items.push(SelectionItem { - name: "Install plugin".to_string(), - description: Some("Install this plugin now.".to_string()), - selected_description: Some("Install this plugin now.".to_string()), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::OpenPluginInstallLoading { - plugin_display_name: plugin_display_name.clone(), - }); - tx.send(AppEvent::FetchPluginInstall { - cwd: install_cwd.clone(), - marketplace_path: marketplace_path.clone(), - plugin_name: plugin_name.clone(), - plugin_display_name: plugin_display_name.clone(), - }); - })], - ..Default::default() - }); - } else { - items.push(SelectionItem { - name: "Install plugin".to_string(), - description: Some("Installing remote plugins is not supported yet.".to_string()), - is_disabled: true, - ..Default::default() - }); - } - - items.push(SelectionItem { - name: "Skills".to_string(), - description: Some(plugin_skill_summary(plugin)), - is_disabled: true, - ..Default::default() - }); - items.push(SelectionItem { - name: "Apps".to_string(), - description: Some(plugin_app_summary(plugin)), - is_disabled: true, - ..Default::default() - }); - items.push(SelectionItem { - name: "MCP Servers".to_string(), - description: Some(plugin_mcp_summary(plugin)), - is_disabled: true, - ..Default::default() - }); - - SelectionViewParams { - view_id: Some(PLUGINS_SELECTION_VIEW_ID), - header: Box::new(header), - footer_hint: Some(plugin_detail_hint_line()), - items, - col_width_mode: ColumnWidthMode::AutoAllRows, - ..Default::default() - } - } - - fn plugin_selection_items<'a>( - &self, - mut plugin_entries: Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)>, - include_marketplace_names: bool, - empty_name: &str, - empty_description: &str, - ) -> Vec { - sort_plugin_entries(&mut plugin_entries); - let status_label_width = plugin_entries - .iter() - .map(|(_, plugin, _)| plugin_status_label(plugin).chars().count()) - .max() - .unwrap_or(0); - - let mut items: Vec = Vec::new(); - for (marketplace, plugin, display_name) in plugin_entries { - let marketplace_label = marketplace_display_name(marketplace); - let status_label = plugin_status_label(plugin); - let description = if include_marketplace_names { - plugin_brief_description(plugin, &marketplace_label, status_label_width) - } else { - plugin_brief_description_without_marketplace(plugin, status_label_width) - }; - let can_view_details = marketplace.path.is_some(); - let selected_status_label = format!("{status_label: = if let Some(marketplace_path) = marketplace_path { - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenPluginDetailLoading { - plugin_display_name: plugin_display_name.clone(), - }); - tx.send(AppEvent::FetchPluginDetail { - cwd: cwd.clone(), - params: codex_app_server_protocol::PluginReadParams { - marketplace_path: Some(marketplace_path.clone()), - remote_marketplace_name: None, - plugin_name: plugin_name.clone(), - }, - }); - })] - } else { - Vec::new() - }; - let is_disabled = !can_view_details && !plugin.installed; - let disabled_reason = - is_disabled.then(|| "remote plugin details are not available yet".to_string()); - - items.push(SelectionItem { - name: display_name, - toggle, - toggle_placeholder: (!plugin.installed).then_some("[-] "), - description: Some(description), - selected_description: Some(selected_description), - search_value: Some(search_value), - actions, - is_disabled, - disabled_reason, - ..Default::default() - }); - } - - if items.is_empty() { - items.push(SelectionItem { - name: empty_name.to_string(), - description: Some(empty_description.to_string()), - is_disabled: true, - ..Default::default() - }); - } - items - } -} - -fn plugins_popup_hint_line() -> Line<'static> { - Line::from("space enable/disable · ←/→ select marketplace · enter view details · esc close") -} - -fn plugin_detail_hint_line() -> Line<'static> { - Line::from("Press esc to close.") -} - -fn plugins_header(subtitle: String, count_line: String) -> Box { - let mut header = ColumnRenderable::new(); - header.push(Line::from("Plugins".bold())); - header.push(Line::from(subtitle.dim())); - header.push(Line::from(count_line.dim())); - Box::new(header) -} - -fn plugin_entries_for_marketplaces<'a>( - marketplaces: impl IntoIterator, -) -> Vec<(&'a PluginMarketplaceEntry, &'a PluginSummary, String)> { - marketplaces - .into_iter() - .flat_map(|marketplace| { - marketplace - .plugins - .iter() - .map(move |plugin| (marketplace, plugin, plugin_display_name(plugin))) - }) - .collect() -} - -fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary, String)]) { - entries.sort_by(|left, right| { - right - .1 - .installed - .cmp(&left.1.installed) - .then_with(|| { - left.2 - .to_ascii_lowercase() - .cmp(&right.2.to_ascii_lowercase()) - }) - .then_with(|| left.2.cmp(&right.2)) - .then_with(|| left.1.name.cmp(&right.1.name)) - .then_with(|| left.1.id.cmp(&right.1.id)) - }); -} - -fn marketplace_tab_id(marketplace: &PluginMarketplaceEntry) -> String { - match marketplace.path.as_ref() { - Some(path) => format!("marketplace:{}", path.display()), - None => format!("marketplace:{}", marketplace.name), - } -} - -fn disambiguate_duplicate_tab_labels(labels: Vec) -> Vec { - let mut counts: Vec<(String, usize)> = Vec::new(); - for label in &labels { - if let Some((_, count)) = counts.iter_mut().find(|(existing, _)| existing == label) { - *count += 1; - } else { - counts.push((label.clone(), 1)); - } - } - - let mut seen: Vec<(String, usize)> = Vec::new(); - labels - .into_iter() - .map(|label| { - let total = counts - .iter() - .find(|(existing, _)| existing == &label) - .map(|(_, count)| *count) - .unwrap_or(1); - if total == 1 { - return label; - } - - let current = if let Some((_, seen_count)) = - seen.iter_mut().find(|(existing, _)| existing == &label) - { - *seen_count += 1; - *seen_count - } else { - seen.push((label.clone(), 1)); - 1 - }; - format!("{label} ({current}/{total})") - }) - .collect() -} - -fn marketplace_display_name(marketplace: &PluginMarketplaceEntry) -> String { - marketplace - .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(|| marketplace.name.clone()) -} - -fn plugin_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_brief_description( - plugin: &PluginSummary, - marketplace_label: &str, - status_label_width: usize, -) -> String { - let status_label = plugin_status_label(plugin); - let status_label = format!("{status_label: format!("{status_label} · {marketplace_label} · {description}"), - None => format!("{status_label} · {marketplace_label}"), - } -} - -fn plugin_brief_description_without_marketplace( - plugin: &PluginSummary, - status_label_width: usize, -) -> String { - let status_label = plugin_status_label(plugin); - let status_label = format!("{status_label: format!("{status_label} · {description}"), - None => status_label, - } -} - -fn plugin_status_label(plugin: &PluginSummary) -> &'static str { - if plugin.installed { - if plugin.enabled { - "Installed" - } else { - "Disabled" - } - } else { - match plugin.install_policy { - PluginInstallPolicy::NotAvailable => "Not installable", - PluginInstallPolicy::Available => "Available", - PluginInstallPolicy::InstalledByDefault => "Available", - } - } -} - -fn plugin_description(plugin: &PluginSummary) -> Option { - plugin - .interface - .as_ref() - .and_then(|interface| { - interface - .short_description - .as_deref() - .or(interface.long_description.as_deref()) - }) - .map(str::trim) - .filter(|description| !description.is_empty()) - .map(str::to_string) -} - -fn plugin_detail_description(plugin: &PluginDetail) -> Option { - plugin - .description - .as_deref() - .or_else(|| { - plugin - .summary - .interface - .as_ref() - .and_then(|interface| interface.long_description.as_deref()) - }) - .or_else(|| { - plugin - .summary - .interface - .as_ref() - .and_then(|interface| interface.short_description.as_deref()) - }) - .map(str::trim) - .filter(|description| !description.is_empty()) - .map(str::to_string) -} - -fn plugin_skill_summary(plugin: &PluginDetail) -> String { - if plugin.skills.is_empty() { - "No plugin skills.".to_string() - } else { - plugin - .skills - .iter() - .map(|skill| skill.name.as_str()) - .collect::>() - .join(", ") - } -} - -fn plugin_app_summary(plugin: &PluginDetail) -> String { - if plugin.apps.is_empty() { - "No plugin apps.".to_string() - } else { - plugin - .apps - .iter() - .map(|app| app.name.as_str()) - .collect::>() - .join(", ") - } -} - -fn plugin_mcp_summary(plugin: &PluginDetail) -> String { - if plugin.mcp_servers.is_empty() { - "No plugin MCP servers.".to_string() - } else { - plugin.mcp_servers.join(", ") - } -} diff --git a/vendor/codex/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/vendor/codex/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 6e041b83..3f5738d9 100644 --- a/vendor/codex/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/vendor/codex/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -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 diff --git a/vendor/codex/codex-rs/tui/src/chatwidget/tests.rs b/vendor/codex/codex-rs/tui/src/chatwidget/tests.rs index bc3d2fc2..b6efce13 100644 --- a/vendor/codex/codex-rs/tui/src/chatwidget/tests.rs +++ b/vendor/codex/codex-rs/tui/src/chatwidget/tests.rs @@ -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!( diff --git a/vendor/codex/codex-rs/tui/src/chatwidget/tests/helpers.rs b/vendor/codex/codex-rs/tui/src/chatwidget/tests/helpers.rs index dad32f30..b5dc88f5 100644 --- a/vendor/codex/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/vendor/codex/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -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, -) -> 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) -> 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, -) -> 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, diff --git a/vendor/codex/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/vendor/codex/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 776741d0..3d9d8bc1 100644 --- a/vendor/codex/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/vendor/codex/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -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; diff --git a/vendor/codex/codex-rs/tui/src/onboarding/mod.rs b/vendor/codex/codex-rs/tui/src/onboarding/mod.rs index 0db5204d..98f6582d 100644 --- a/vendor/codex/codex-rs/tui/src/onboarding/mod.rs +++ b/vendor/codex/codex-rs/tui/src/onboarding/mod.rs @@ -1,5 +1,4 @@ mod auth; pub(crate) mod onboarding_screen; mod trust_directory; -pub(crate) use auth::mark_url_hyperlink; mod welcome; diff --git a/vendor/codex/codex-rs/tui/src/slash_command.rs b/vendor/codex/codex-rs/tui/src/slash_command.rs index 4c362399..ceb53e07 100644 --- a/vendor/codex/codex-rs/tui/src/slash_command.rs +++ b/vendor/codex/codex-rs/tui/src/slash_command.rs @@ -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