TUI Plugin Sharing 2 - add remote plugin section plumbing (#26702)

This adds the background plumbing for remote-backed plugin catalog
sections while leaving the fuller directory presentation to the next PR.
The TUI can fetch section-specific remote marketplace results, keep
local plugin data available, and carry section errors forward for later
rendering.

- Fetches explicit remote marketplace kinds for curated, workspace, and
shared-with-me sections.
- Gates shared-with-me loading on the plugin sharing feature flag.
- Adds section-level error state and user-actionable error copy.
- Merges remote marketplace results into the cached plugin list without
discarding local results.
This commit is contained in:
canvrno-oai
2026-06-15 10:25:37 -07:00
committed by GitHub
parent a18de1f3b6
commit 29224d945b
9 changed files with 364 additions and 16 deletions
@@ -160,6 +160,9 @@ impl ChatWidget {
ide_context: IdeContextState::default(),
plugins_cache: PluginsCacheState::default(),
plugins_fetch_state: PluginListFetchState::default(),
plugin_remote_sections_loading: false,
plugin_remote_sections_loaded: false,
plugin_remote_section_errors: Vec::new(),
plugin_install_apps_needing_auth: Vec::new(),
plugin_install_auth_flow: None,
plugins_active_tab_id: None,
+102 -5
View File
@@ -6,6 +6,7 @@ use std::time::Instant;
use super::ChatWidget;
use crate::app_event::AppEvent;
use crate::app_event::PluginLocation;
use crate::app_event::PluginRemoteSectionError;
use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem;
@@ -37,6 +38,10 @@ use codex_app_server_protocol::PluginSource;
use codex_app_server_protocol::PluginSummary;
use codex_app_server_protocol::PluginUninstallResponse;
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
use codex_features::Feature;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -201,7 +206,9 @@ impl ChatWidget {
cwd: PathBuf,
result: Result<PluginListResponse, String>,
) {
if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) {
let request_was_in_flight =
self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path());
if request_was_in_flight {
self.plugins_fetch_state.in_flight_cwd = None;
}
@@ -210,10 +217,28 @@ impl ChatWidget {
}
let auth_flow_active = self.plugin_install_auth_flow.is_some();
let should_refresh_plugins_popup = !auth_flow_active
&& (self
.bottom_pane
.active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)
.is_some()
|| self
.bottom_pane
.selected_index_for_active_view(PLUGINS_SELECTION_VIEW_ID)
.is_some()
|| !matches!(
self.plugins_cache_for_current_cwd(),
PluginsCacheState::Ready(_)
));
match result {
Ok(response) => {
self.plugins_fetch_state.cache_cwd = Some(cwd);
self.plugin_remote_sections_loading = request_was_in_flight;
if request_was_in_flight {
self.plugin_remote_sections_loaded = false;
}
self.plugin_remote_section_errors.clear();
let active_tab_id = self
.plugins_active_tab_id
.as_deref()
@@ -229,13 +254,15 @@ impl ChatWidget {
});
self.plugins_active_tab_id = active_tab_id;
self.plugins_cache = PluginsCacheState::Ready(response.clone());
if !auth_flow_active {
if should_refresh_plugins_popup {
self.refresh_plugins_popup_if_open(&response);
}
self.newly_installed_marketplace_tab_id = None;
}
Err(err) => {
if !auth_flow_active {
self.plugin_remote_sections_loading = false;
self.plugin_remote_sections_loaded = false;
if should_refresh_plugins_popup {
self.plugins_fetch_state.cache_cwd = None;
self.plugins_cache = PluginsCacheState::Failed(err.clone());
let _ = self.bottom_pane.replace_selection_view_if_active(
@@ -247,18 +274,62 @@ impl ChatWidget {
}
}
pub(crate) fn on_plugin_remote_sections_loaded(
&mut self,
cwd: PathBuf,
marketplaces: Vec<PluginMarketplaceEntry>,
section_errors: Vec<PluginRemoteSectionError>,
) {
if self.config.cwd.as_path() != cwd.as_path() {
return;
}
let should_refresh_plugins_popup = self
.bottom_pane
.active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)
.is_some();
self.plugin_remote_sections_loading = false;
self.plugin_remote_sections_loaded = true;
let refreshed_response = match &mut self.plugins_cache {
PluginsCacheState::Ready(response)
if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) =>
{
merge_remote_marketplaces(response, marketplaces);
self.plugin_remote_section_errors = section_errors;
Some(response.clone())
}
_ => {
self.plugin_remote_section_errors = section_errors;
None
}
};
if let Some(response) = refreshed_response
&& should_refresh_plugins_popup
{
self.refresh_plugins_popup_if_open(&response);
}
}
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.on_plugins_list_fetch_started(cwd.clone());
self.app_event_tx.send(AppEvent::FetchPluginsList { cwd });
}
pub(crate) fn on_plugins_list_fetch_started(&mut self, cwd: PathBuf) {
if self.config.cwd.as_path() != 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 {
@@ -2005,6 +2076,32 @@ fn marketplace_tab_id_matching_saved_id(
})
}
fn merge_remote_marketplaces(
response: &mut PluginListResponse,
remote_marketplaces: Vec<PluginMarketplaceEntry>,
) {
let remote_names = remote_marketplaces
.iter()
.map(|marketplace| marketplace.name.clone())
.collect::<std::collections::HashSet<_>>();
response.marketplaces.retain(|marketplace| {
marketplace.path.is_some()
|| !remote_marketplace_is_remote_section(marketplace)
&& !remote_names.contains(marketplace.name.as_str())
});
response.marketplaces.extend(remote_marketplaces);
}
fn remote_marketplace_is_remote_section(marketplace: &PluginMarketplaceEntry) -> bool {
matches!(
marketplace.name.as_str(),
REMOTE_WORKSPACE_MARKETPLACE_NAME
| REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME
| REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME
| REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME
)
}
fn disambiguate_duplicate_tab_labels(labels: Vec<String>) -> Vec<String> {
let mut counts: Vec<(String, usize)> = Vec::new();
for label in &labels {