From 96e3c907f75e993e73e9418aead20c8a3f05d8a3 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 16:00:00 -0700 Subject: [PATCH 01/11] tui: polish remote plugin catalog rows --- .../src/bottom_pane/list_selection_view.rs | 98 +++++++++++-------- codex-rs/tui/src/bottom_pane/mod.rs | 2 + codex-rs/tui/src/chatwidget/plugin_catalog.rs | 73 +++++++++----- ...ts__plugins_popup_curated_marketplace.snap | 12 +-- .../chatwidget/tests/popups_and_settings.rs | 52 ++++++++-- 5 files changed, 164 insertions(+), 73 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 7af981d7ded5..4455a6b67074 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -281,6 +281,63 @@ pub(crate) struct ListSelectionView { keymap: ListKeymap, } +const SELECTION_TOGGLE_ON_PREFIX: &str = "[*] "; +const SELECTION_TOGGLE_OFF_PREFIX: &str = "[ ] "; +pub(crate) const SELECTION_TOGGLE_UNAVAILABLE_PREFIX: &str = "[-] "; +pub(crate) const SELECTION_TOGGLE_BLOCKED_PREFIX: &str = "[!] "; + +fn selection_toggle_prefix(toggle: &SelectionToggle) -> &'static str { + if toggle.is_on { + SELECTION_TOGGLE_ON_PREFIX + } else { + SELECTION_TOGGLE_OFF_PREFIX + } +} + +fn selection_item_toggle_prefix(item: &SelectionItem) -> Option<&'static str> { + item.toggle + .as_ref() + .map(selection_toggle_prefix) + .or(item.toggle_placeholder) +} + +impl ListSelectionView { + fn selected_item_has_toggle(&self) -> bool { + self.selected_actual_idx() + .and_then(|actual_idx| self.active_items().get(actual_idx)) + .is_some_and(|item| item.toggle.is_some() && Self::item_is_enabled(item)) + } + + fn selected_item_has_toggle_placeholder(&self) -> bool { + self.selected_actual_idx() + .and_then(|actual_idx| self.active_items().get(actual_idx)) + .is_some_and(|item| { + item.toggle.is_none() + && item.toggle_placeholder.is_some() + && Self::item_is_enabled(item) + }) + } + + fn toggle_selected(&mut self) { + let Some(actual_idx) = self.selected_actual_idx() else { + return; + }; + let app_event_tx = self.app_event_tx.clone(); + let Some(item) = self.active_items_mut().get_mut(actual_idx) else { + return; + }; + if !Self::item_is_enabled(item) { + return; + } + let Some(toggle) = item.toggle.as_mut() else { + return; + }; + + toggle.is_on = !toggle.is_on; + (toggle.action)(toggle.is_on, &app_event_tx); + } +} + impl ListSelectionView { /// Create a selection popup view with filtering, scrolling, and callbacks wired. /// @@ -533,10 +590,8 @@ impl ListSelectionView { let wrap_prefix_width = UnicodeWidthStr::width(wrap_prefix.as_str()); let mut name_prefix_spans = Vec::new(); name_prefix_spans.push(wrap_prefix.into()); - if let Some(toggle) = &item.toggle { - name_prefix_spans.push(if toggle.is_on { "[*] " } else { "[ ] " }.into()); - } else if let Some(placeholder) = item.toggle_placeholder { - name_prefix_spans.push(placeholder.into()); + if let Some(toggle_prefix) = selection_item_toggle_prefix(item) { + name_prefix_spans.push(toggle_prefix.into()); } name_prefix_spans.extend(item.name_prefix_spans.clone()); let description = is_selected @@ -611,22 +666,6 @@ impl ListSelectionView { item.disabled_reason.is_none() && !item.is_disabled } - fn selected_item_has_toggle(&self) -> bool { - self.selected_actual_idx() - .and_then(|actual_idx| self.active_items().get(actual_idx)) - .is_some_and(|item| item.toggle.is_some() && Self::item_is_enabled(item)) - } - - fn selected_item_has_toggle_placeholder(&self) -> bool { - self.selected_actual_idx() - .and_then(|actual_idx| self.active_items().get(actual_idx)) - .is_some_and(|item| { - item.toggle.is_none() - && item.toggle_placeholder.is_some() - && Self::item_is_enabled(item) - }) - } - fn actual_idx_for_enabled_number(&self, number: usize) -> Option { if number == 0 { return None; @@ -640,25 +679,6 @@ impl ListSelectionView { .map(|(idx, _)| idx) } - fn toggle_selected(&mut self) { - let Some(actual_idx) = self.selected_actual_idx() else { - return; - }; - let app_event_tx = self.app_event_tx.clone(); - let Some(item) = self.active_items_mut().get_mut(actual_idx) else { - return; - }; - if !Self::item_is_enabled(item) { - return; - } - let Some(toggle) = item.toggle.as_mut() else { - return; - }; - - toggle.is_on = !toggle.is_on; - (toggle.action)(toggle.is_on, &app_event_tx); - } - fn move_up(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 7fffdeb98e41..ad30dedf5a73 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -190,6 +190,8 @@ use crate::status_indicator_widget::StatusDetailsCapitalization; use crate::status_indicator_widget::StatusIndicatorWidget; pub(crate) use experimental_features_view::ExperimentalFeatureItem; pub(crate) use experimental_features_view::ExperimentalFeaturesView; +pub(crate) use list_selection_view::SELECTION_TOGGLE_BLOCKED_PREFIX; +pub(crate) use list_selection_view::SELECTION_TOGGLE_UNAVAILABLE_PREFIX; pub(crate) use list_selection_view::SelectionAction; pub(crate) use list_selection_view::SelectionItem; diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index a19ede3ab5ab..acb479e73c77 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -12,6 +12,8 @@ use crate::app_event::AppEvent; use crate::app_event::PluginLocation; use crate::app_event::PluginRemoteSectionError; use crate::bottom_pane::ColumnWidthMode; +use crate::bottom_pane::SELECTION_TOGGLE_BLOCKED_PREFIX; +use crate::bottom_pane::SELECTION_TOGGLE_UNAVAILABLE_PREFIX; use crate::bottom_pane::SelectionAction; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionRowDisplay; @@ -730,7 +732,7 @@ impl ChatWidget { let total = all_entries.len(); let installed = all_entries .iter() - .filter(|(_, plugin, _)| plugin.installed) + .filter(|(_, plugin, _)| plugin_shows_as_installed(plugin)) .count(); let name_column_width = all_entries .iter() @@ -741,7 +743,7 @@ impl ChatWidget { .max(); let installed_entries = all_entries .iter() - .filter(|(_, plugin, _)| plugin.installed) + .filter(|(_, plugin, _)| plugin_shows_as_installed(plugin)) .cloned() .collect(); @@ -788,7 +790,7 @@ impl ChatWidget { let curated_total = curated_entries.len(); let curated_installed = curated_entries .iter() - .filter(|(_, plugin, _)| plugin.installed) + .filter(|(_, plugin, _)| plugin_shows_as_installed(plugin)) .count(); let curated_has_entries = !curated_entries.is_empty(); let curated_loading = self.plugin_remote_sections_loading @@ -876,7 +878,7 @@ impl ChatWidget { let marketplace_total = entries.len(); let marketplace_installed = entries .iter() - .filter(|(_, plugin, _)| plugin.installed) + .filter(|(_, plugin, _)| plugin_shows_as_installed(plugin)) .count(); let tab_id = marketplace_tab_id(marketplace); let can_remove_marketplace = @@ -1060,6 +1062,13 @@ impl ChatWidget { is_disabled: true, ..Default::default() }); + } else if plugin.summary.install_policy == PluginInstallPolicy::InstalledByDefault { + items.push(SelectionItem { + name: "Installed by admin".to_string(), + description: Some("This plugin is installed by your workspace admin.".to_string()), + is_disabled: true, + ..Default::default() + }); } else if plugin.summary.install_policy == PluginInstallPolicy::NotAvailable { items.push(SelectionItem { name: "Install plugin".to_string(), @@ -1164,7 +1173,8 @@ impl ChatWidget { plugin_detail_request_for_entry(marketplace, plugin, preferred_local_sources); let can_view_details = plugin_detail_request.is_some(); let disabled_by_admin = plugin.availability == PluginAvailability::DisabledByAdmin; - let can_toggle_plugin = plugin.installed && !disabled_by_admin; + let shows_as_installed = plugin_shows_as_installed(plugin); + let can_toggle_plugin = shows_as_installed && !disabled_by_admin; let selected_status_label = format!("{status_label: bool { - if candidate.1.installed != existing.1.installed { - return candidate.1.installed; + let candidate_shows_as_installed = plugin_shows_as_installed(candidate.1); + let existing_shows_as_installed = plugin_shows_as_installed(existing.1); + if candidate_shows_as_installed != existing_shows_as_installed { + return candidate_shows_as_installed; } let candidate_is_local_share = @@ -1360,6 +1384,9 @@ fn plugin_detail_status_label(plugin: &PluginSummary) -> &'static str { if plugin.availability == PluginAvailability::DisabledByAdmin { return "Disabled by admin"; } + if plugin.install_policy == PluginInstallPolicy::InstalledByDefault { + return "Installed by admin"; + } if plugin.installed { if plugin.enabled { "Installed" @@ -1370,11 +1397,15 @@ fn plugin_detail_status_label(plugin: &PluginSummary) -> &'static str { match plugin.install_policy { PluginInstallPolicy::NotAvailable => "Not installable", PluginInstallPolicy::Available => "Can be installed", - PluginInstallPolicy::InstalledByDefault => "Available by default", + PluginInstallPolicy::InstalledByDefault => "Installed by admin", } } } +fn plugin_shows_as_installed(plugin: &PluginSummary) -> bool { + plugin.installed || plugin.install_policy == PluginInstallPolicy::InstalledByDefault +} + fn plugin_metadata_items(plugin: &PluginDetail) -> Vec { let mut items = Vec::new(); items.push(SelectionItem { @@ -1517,10 +1548,8 @@ fn plugin_entries_for_marketplaces<'a>( fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary, String)]) { entries.sort_by(|left, right| { - right - .1 - .installed - .cmp(&left.1.installed) + plugin_shows_as_installed(right.1) + .cmp(&plugin_shows_as_installed(left.1)) .then_with(|| { left.2 .to_ascii_lowercase() @@ -1816,9 +1845,9 @@ fn plugin_brief_description_without_marketplace( fn plugin_status_label(plugin: &PluginSummary) -> &'static str { if plugin.availability == PluginAvailability::DisabledByAdmin { - return "Disabled by admin"; + return "Disabled"; } - if plugin.installed { + if plugin_shows_as_installed(plugin) { if plugin.enabled { "Installed" } else { @@ -1828,7 +1857,7 @@ fn plugin_status_label(plugin: &PluginSummary) -> &'static str { match plugin.install_policy { PluginInstallPolicy::NotAvailable => "Not installable", PluginInstallPolicy::Available => "Available", - PluginInstallPolicy::InstalledByDefault => "Available by default", + PluginInstallPolicy::InstalledByDefault => "Installed", } } } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap index 2ec91937fbb3..024b90e49e0d 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap @@ -4,14 +4,14 @@ expression: popup --- Plugins Browse plugins from available marketplaces. - Installed 1 of 4 available plugins. + Installed 2 of 4 available plugins. - [All Plugins] Installed (1) OpenAI Curated Workspace Shared with me Repo Marketplace Add Marketplace + [All Plugins] Installed (2) OpenAI Curated Workspace Shared with me Repo Marketplace Add Marketplace Type to search plugins -› [ ] Alpha Sync Disabled Space to enable; Enter view details. - [-] Bravo Search Available · OpenAI Curated · Search docs and tickets. - [-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins. - [-] Starter Available by default · OpenAI Curated · Included by default. +› [ ] Alpha Sync Disabled Space to enable; Enter view details. + [*] Starter Installed · OpenAI Curated · Included by default. + [-] Bravo Search Available · OpenAI Curated · Search docs and tickets. + [-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins. space enable/disable · ←/→ select marketplace · enter view details · esc close diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index f97d93a0c877..4178cc121f6c 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -209,12 +209,12 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_ ); assert!( plugins_test_popup_row_position(&popup, "Alpha Sync") - < plugins_test_popup_row_position(&popup, "Bravo Search") + < plugins_test_popup_row_position(&popup, "Starter") + && plugins_test_popup_row_position(&popup, "Starter") + < 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}" + < plugins_test_popup_row_position(&popup, "Hidden Repo Plugin"), + "expected /plugins rows to sort installed and default plugins first, then alphabetically, got:\n{popup}" ); } @@ -1076,8 +1076,10 @@ async fn plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint() { let popup = render_bottom_popup(&chat, /*width*/ 120); assert!( - popup.contains("Disabled by admin") + popup.contains("[!] Admin Blocked") + && popup.contains("Disabled") && popup.contains("Press Enter to view plugin details.") + && !popup.contains("Disabled by admin") && !popup.contains("Space to disable"), "expected admin-disabled installed row to omit toggle hint, got:\n{popup}" ); @@ -1685,6 +1687,44 @@ async fn plugins_popup_search_filters_visible_rows_snapshot() { ); } +#[tokio::test] +async fn plugins_popup_search_matches_plugin_descriptions() { + 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-drive", + "drive", + Some("Drive"), + Some("Document access."), + /*installed*/ false, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + ])]), + ); + + type_plugins_search_query(&mut chat, "document"); + + let popup = render_bottom_popup(&chat, /*width*/ 100); + assert!( + popup.contains("Drive") && !popup.contains("Calendar"), + "expected plugin search to match descriptions, 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; From de358c16d8d8f8bab73fd1f153ae476d02ad5a81 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 16:09:44 -0700 Subject: [PATCH 02/11] Hide empty shared plugin section --- codex-rs/tui/src/chatwidget/plugin_catalog.rs | 5 +++++ ..._plugins_popup_empty_shared_section_hidden.snap | 14 ++++++++++++++ .../src/chatwidget/tests/popups_and_settings.rs | 11 ++++++----- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index acb479e73c77..f88d038d95c3 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -156,6 +156,7 @@ struct RemoteMarketplaceSection { label: &'static str, loading_tab_id: &'static str, marketplace_names: &'static [&'static str], + show_empty_tab: bool, empty_item_name: &'static str, empty_item_description: &'static str, tab_order: u8, @@ -167,6 +168,7 @@ const REMOTE_MARKETPLACE_SECTIONS: [RemoteMarketplaceSection; 2] = [ label: "Workspace", loading_tab_id: "workspace-loading", marketplace_names: &[REMOTE_WORKSPACE_MARKETPLACE_NAME], + show_empty_tab: true, empty_item_name: "No workspace plugins available", empty_item_description: "No workspace directory plugins are available.", tab_order: WORKSPACE_SECTION_TAB_ORDER, @@ -180,6 +182,7 @@ const REMOTE_MARKETPLACE_SECTIONS: [RemoteMarketplaceSection; 2] = [ REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME, REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME, ], + show_empty_tab: false, empty_item_name: "No shared plugins available", empty_item_description: "No plugins have been shared with you.", tab_order: SHARED_WITH_ME_SECTION_TAB_ORDER, @@ -206,6 +209,8 @@ impl RemoteMarketplaceSection { } else if remote_sections_loaded { if let Some(section_error) = plugin_remote_section_error(section_errors, self.id) { remote_section_error_tab(section_error) + } else if !self.show_empty_tab { + return None; } else { remote_section_empty_tab( self.id, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap new file mode 100644 index 000000000000..146d02a5a21a --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_empty_shared_section_hidden.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/chatwidget/tests/popups_and_settings.rs +expression: loaded_popup +--- + Plugins + Workspace. + This section loaded successfully. + + All Plugins Installed (0) OpenAI Curated [Workspace] Add Marketplace + + Type to search plugins +› No workspace plugins available No workspace directory plugins are available. + + space enable/disable · ←/→ select marketplace · enter view details · esc close diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 4178cc121f6c..e6639e7c707f 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1174,7 +1174,12 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { let workspace_loading_popup = select_tab_containing(&mut chat, "Loading Workspace plugins."); chat.on_plugin_remote_sections_loaded(cwd.to_path_buf(), Vec::new(), Vec::new()); - let shared_empty_popup = select_tab_containing(&mut chat, "Shared with me."); + let loaded_popup = render_bottom_popup(&chat, /*width*/ 100); + assert_chatwidget_snapshot!("plugins_popup_empty_shared_section_hidden", loaded_popup); + assert!( + !loaded_popup.contains("Shared with me"), + "expected empty shared section to stay hidden, got:\n{loaded_popup}" + ); chat.on_plugin_remote_sections_loaded( cwd.to_path_buf(), @@ -1205,7 +1210,6 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { [ remote_section_state(&curated_loading_popup), remote_section_state(&workspace_loading_popup), - remote_section_state(&shared_empty_popup), remote_section_state(&workspace_error_popup), remote_section_state(&remote_curated_empty_popup), ] @@ -1217,9 +1221,6 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { Loading Workspace plugins. Loading Workspace plugins... This section updates when app-server returns it. - Shared with me. - No shared plugins available No plugins have been shared with you. - Workspace unavailable. Workspace unavailable Sign in to ChatGPT to load workspace plugins. From 85809cfd7a201e3df5c1c0b125c4f4f78a4f2f28 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 16:42:04 -0700 Subject: [PATCH 03/11] snapshots --- ...i__chatwidget__tests__plugins_popup_curated_marketplace.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap index 024b90e49e0d..fdb4fe9c17c8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap @@ -6,7 +6,7 @@ expression: popup Browse plugins from available marketplaces. Installed 2 of 4 available plugins. - [All Plugins] Installed (2) OpenAI Curated Workspace Shared with me Repo Marketplace Add Marketplace + [All Plugins] Installed (2) OpenAI Curated Workspace Repo Marketplace Add Marketplace Type to search plugins › [ ] Alpha Sync Disabled Space to enable; Enter view details. From 83a0f494c3fc8568dfda5b77d0fbc96c4a6c2b57 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 17:29:44 -0700 Subject: [PATCH 04/11] Polish remote plugin loading copy --- codex-rs/tui/src/chatwidget/plugin_catalog.rs | 4 ++-- codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index f88d038d95c3..69f5c88902a2 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -806,7 +806,7 @@ impl ChatWidget { if curated_loading && !curated_has_entries { ( "Loading OpenAI Curated plugins...", - "This section updates when app-server returns it.", + "This updates when these plugins finish loading.", ) } else if let Some(section_error) = by_openai_section_error && !curated_has_entries @@ -1680,7 +1680,7 @@ fn is_personal_marketplace_path(marketplace_path: &AbsolutePathBuf) -> bool { fn remote_section_loading_item(label: &str) -> SelectionItem { SelectionItem { name: format!("Loading {label} plugins..."), - description: Some("This section updates when app-server returns it.".to_string()), + description: Some("This updates when these plugins finish loading.".to_string()), is_disabled: true, ..Default::default() } diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index e6639e7c707f..eb461b22480e 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1216,10 +1216,10 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { .join("\n\n"), @r###" OpenAI Curated marketplace. - Loading OpenAI Curated plugins... This section updates when app-server returns it. + Loading OpenAI Curated plugins... This updates when these plugins finish loading. Loading Workspace plugins. - Loading Workspace plugins... This section updates when app-server returns it. + Loading Workspace plugins... This updates when these plugins finish loading. Workspace unavailable. Workspace unavailable Sign in to ChatGPT to load workspace plugins. From fc3a69b20eacde6ac7dc974d5b0e38436b79bb30 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 17:35:47 -0700 Subject: [PATCH 05/11] Polish PR5 plugin catalog copy --- codex-rs/tui/src/chatwidget/plugin_catalog.rs | 4 ++-- .../tui/src/chatwidget/tests/popups_and_settings.rs | 4 ++-- codex-rs/tui/src/custom_terminal.rs | 11 ++++++++++- codex-rs/tui/src/tui/test_support.rs | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index 69f5c88902a2..e2bfa2fa8cba 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -806,7 +806,7 @@ impl ChatWidget { if curated_loading && !curated_has_entries { ( "Loading OpenAI Curated plugins...", - "This updates when these plugins finish loading.", + "This updates when shared plugins finish loading.", ) } else if let Some(section_error) = by_openai_section_error && !curated_has_entries @@ -1680,7 +1680,7 @@ fn is_personal_marketplace_path(marketplace_path: &AbsolutePathBuf) -> bool { fn remote_section_loading_item(label: &str) -> SelectionItem { SelectionItem { name: format!("Loading {label} plugins..."), - description: Some("This updates when these plugins finish loading.".to_string()), + description: Some("This updates when shared plugins finish loading.".to_string()), is_disabled: true, ..Default::default() } diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index eb461b22480e..fac28452884f 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1216,10 +1216,10 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { .join("\n\n"), @r###" OpenAI Curated marketplace. - Loading OpenAI Curated plugins... This updates when these plugins finish loading. + Loading OpenAI Curated plugins... This updates when shared plugins finish loading. Loading Workspace plugins. - Loading Workspace plugins... This updates when these plugins finish loading. + Loading Workspace plugins... This updates when shared plugins finish loading. Workspace unavailable. Workspace unavailable Sign in to ChatGPT to load workspace plugins. diff --git a/codex-rs/tui/src/custom_terminal.rs b/codex-rs/tui/src/custom_terminal.rs index 480010391501..fed3e5456f72 100644 --- a/codex-rs/tui/src/custom_terminal.rs +++ b/codex-rs/tui/src/custom_terminal.rs @@ -223,7 +223,7 @@ where )) } - pub(crate) fn with_screen_size_and_cursor_position( + fn with_screen_size_and_cursor_position( backend: B, screen_size: Size, cursor_pos: Position, @@ -245,6 +245,15 @@ where } } + #[cfg(test)] + pub(crate) fn with_screen_size_and_cursor_position_for_test( + backend: B, + screen_size: Size, + cursor_pos: Position, + ) -> Self { + Self::with_screen_size_and_cursor_position(backend, screen_size, cursor_pos) + } + /// Get a Frame object which provides a consistent view into the terminal state for rendering. pub fn get_frame(&mut self) -> Frame<'_> { Frame { diff --git a/codex-rs/tui/src/tui/test_support.rs b/codex-rs/tui/src/tui/test_support.rs index 5c1f30eec028..c3f9f6bc3bf4 100644 --- a/codex-rs/tui/src/tui/test_support.rs +++ b/codex-rs/tui/src/tui/test_support.rs @@ -11,7 +11,7 @@ use crate::custom_terminal::Terminal; pub(crate) fn make_test_tui() -> io::Result { let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::with_screen_size_and_cursor_position( + let terminal = Terminal::with_screen_size_and_cursor_position_for_test( backend, Size { width: 80, From 786deae73aa77d53035fd2f89b5a3cae6aea4859 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 17:47:01 -0700 Subject: [PATCH 06/11] snapshots --- ...get__tests__plugins_popup_newly_installed_marketplace.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap index 803ff9fbe67c..60ebee6f9a1f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap @@ -1,13 +1,13 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs +assertion_line: 473 expression: popup --- Plugins Debug Marketplace installed successfully. Select the plugins you want to use and press Enter to install or view details. - All Plugins Installed (0) OpenAI Curated Workspace Shared with me [Debug Marketplace] - Add Marketplace + All Plugins Installed (0) OpenAI Curated Workspace [Debug Marketplace] Add Marketplace Type to search plugins › [-] Debug Plugin Available Press Enter to install or view plugin details. From be926d406aebad88e0d15bb0174924295f4d591d Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 17:54:03 -0700 Subject: [PATCH 07/11] snapshots --- ...tui__chatwidget__tests__plugins_popup_search_filtered.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap index bfb0affb21b2..3a2e455f6dd5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap @@ -1,13 +1,13 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 767 +assertion_line: 1684 expression: popup --- Plugins Browse plugins from available marketplaces. Installed 0 of 3 available plugins. - [All Plugins] Installed (0) OpenAI Curated Workspace Shared with me Add Marketplace + [All Plugins] Installed (0) OpenAI Curated Workspace Add Marketplace sla › [-] Slack Available Press Enter to install or view plugin details. From 13fc32ccc034e865cb6864b3bfe3188c47a63dc2 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 22 Jun 2026 18:20:06 -0700 Subject: [PATCH 08/11] Fix default plugin install actions --- codex-rs/tui/src/chatwidget/plugin_catalog.rs | 22 ++++++++------ ..._tests__plugin_detail_popup_installed.snap | 18 +++++------ ...lugins_popup_admin_disabled_installed.snap | 14 +++++++++ ...ts__plugins_popup_curated_marketplace.snap | 2 +- .../chatwidget/tests/popups_and_settings.rs | 30 +++++++++++-------- 5 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_admin_disabled_installed.snap diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index e2bfa2fa8cba..e0ab744d4a32 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -1031,7 +1031,16 @@ impl ChatWidget { }]; if plugin.summary.installed { - if let Some(plugin_id) = plugin_uninstall_id(&plugin.summary) { + if plugin.summary.install_policy == PluginInstallPolicy::InstalledByDefault { + items.push(SelectionItem { + name: "Installed by admin".to_string(), + description: Some( + "This plugin is installed by your workspace admin.".to_string(), + ), + is_disabled: true, + ..Default::default() + }); + } else if let Some(plugin_id) = plugin_uninstall_id(&plugin.summary) { let uninstall_cwd = self.config.cwd.to_path_buf(); let plugin_display_name = display_name; items.push(SelectionItem { @@ -1067,13 +1076,6 @@ impl ChatWidget { is_disabled: true, ..Default::default() }); - } else if plugin.summary.install_policy == PluginInstallPolicy::InstalledByDefault { - items.push(SelectionItem { - name: "Installed by admin".to_string(), - description: Some("This plugin is installed by your workspace admin.".to_string()), - is_disabled: true, - ..Default::default() - }); } else if plugin.summary.install_policy == PluginInstallPolicy::NotAvailable { items.push(SelectionItem { name: "Install plugin".to_string(), @@ -1179,7 +1181,9 @@ impl ChatWidget { let can_view_details = plugin_detail_request.is_some(); let disabled_by_admin = plugin.availability == PluginAvailability::DisabledByAdmin; let shows_as_installed = plugin_shows_as_installed(plugin); - let can_toggle_plugin = shows_as_installed && !disabled_by_admin; + let can_toggle_plugin = plugin.installed + && plugin.install_policy != PluginInstallPolicy::InstalledByDefault + && !disabled_by_admin; let selected_status_label = format!("{status_label: