mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user