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:
committed by
GitHub
Unverified
parent
a18de1f3b6
commit
29224d945b
@@ -644,6 +644,12 @@ impl PluginRequestProcessor {
|
||||
data.push(remote_marketplace_to_info(remote_marketplace));
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) if explicit_marketplace_kinds => {
|
||||
return Err(remote_plugin_catalog_error_to_jsonrpc(
|
||||
err,
|
||||
"list OpenAI Curated remote plugin catalog",
|
||||
));
|
||||
}
|
||||
Err(
|
||||
RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||
|
||||
@@ -2064,7 +2064,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_fail_opens_openai_curated_remote_collection_errors() -> Result<()> {
|
||||
async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_plugins_enabled_config_with_base_url(
|
||||
@@ -2103,18 +2103,17 @@ async fn plugin_list_fail_opens_openai_curated_remote_collection_errors() -> Res
|
||||
marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
let err = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(err.error.code, -32603);
|
||||
assert!(
|
||||
response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.all(|marketplace| marketplace.name != "openai-curated-remote")
|
||||
err.error
|
||||
.message
|
||||
.contains("list OpenAI Curated remote plugin catalog")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::app_event::FeedbackCategory;
|
||||
use crate::app_event::HistoryLookupResponse;
|
||||
use crate::app_event::PermissionProfileSelection;
|
||||
use crate::app_event::PluginLocation;
|
||||
use crate::app_event::PluginRemoteSectionError;
|
||||
use crate::app_event::RateLimitRefreshOrigin;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
@@ -104,8 +105,10 @@ 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::PluginListMarketplaceKind;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallParams;
|
||||
|
||||
@@ -156,13 +156,34 @@ impl App {
|
||||
}
|
||||
|
||||
pub(super) fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
|
||||
self.chat_widget.on_plugins_list_fetch_started(cwd.clone());
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let plugin_sharing_enabled = self.config.features.enabled(Feature::PluginSharing);
|
||||
let remote_plugin_enabled = self.config.features.enabled(Feature::RemotePlugin);
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_plugins_list(request_handle, cwd.clone())
|
||||
let result = fetch_plugins_list(request_handle.clone(), cwd.clone())
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::PluginsLoaded { cwd, result });
|
||||
let should_fetch_additional_remote_sections = result.is_ok();
|
||||
app_event_tx.send(AppEvent::PluginsLoaded {
|
||||
cwd: cwd.clone(),
|
||||
result,
|
||||
});
|
||||
if should_fetch_additional_remote_sections {
|
||||
let (marketplaces, section_errors) = fetch_additional_plugin_remote_sections(
|
||||
request_handle,
|
||||
cwd.clone(),
|
||||
plugin_sharing_enabled,
|
||||
remote_plugin_enabled,
|
||||
)
|
||||
.await;
|
||||
app_event_tx.send(AppEvent::PluginRemoteSectionsLoaded {
|
||||
cwd,
|
||||
marketplaces,
|
||||
section_errors,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -756,6 +777,114 @@ pub(super) async fn fetch_plugins_list(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_additional_plugin_remote_sections(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
plugin_sharing_enabled: bool,
|
||||
remote_plugin_enabled: bool,
|
||||
) -> (Vec<PluginMarketplaceEntry>, Vec<PluginRemoteSectionError>) {
|
||||
let mut marketplaces = Vec::new();
|
||||
let mut section_errors = Vec::new();
|
||||
let mut sections = Vec::new();
|
||||
if !remote_plugin_enabled {
|
||||
sections.push((
|
||||
"vertical",
|
||||
"OpenAI Curated",
|
||||
vec![PluginListMarketplaceKind::Vertical],
|
||||
));
|
||||
}
|
||||
sections.push((
|
||||
"workspace",
|
||||
"Workspace",
|
||||
vec![PluginListMarketplaceKind::WorkspaceDirectory],
|
||||
));
|
||||
if plugin_sharing_enabled {
|
||||
sections.push((
|
||||
"shared-with-me",
|
||||
"Shared with me",
|
||||
vec![PluginListMarketplaceKind::SharedWithMe],
|
||||
));
|
||||
} else {
|
||||
section_errors.push(plugin_sharing_disabled_remote_section_error());
|
||||
}
|
||||
|
||||
for (section_id, label, marketplace_kinds) in sections {
|
||||
match request_plugin_list_for_kinds(request_handle.clone(), cwd.clone(), marketplace_kinds)
|
||||
.await
|
||||
{
|
||||
Ok(mut response) => {
|
||||
hide_cli_only_plugin_marketplaces(&mut response);
|
||||
marketplaces.extend(response.marketplaces);
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("{err:#}");
|
||||
section_errors.push(PluginRemoteSectionError {
|
||||
section_id: section_id.to_string(),
|
||||
label: label.to_string(),
|
||||
message: plugin_remote_section_error_message(label, &message),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(marketplaces, section_errors)
|
||||
}
|
||||
|
||||
fn plugin_remote_section_error_message(label: &str, err: &str) -> String {
|
||||
let next_step = plugin_remote_section_error_next_step(label, err);
|
||||
if next_step.is_empty() {
|
||||
err.to_string()
|
||||
} else {
|
||||
format!("{err} {next_step}")
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_remote_section_error_next_step(label: &str, err: &str) -> &'static str {
|
||||
let err = err.to_ascii_lowercase();
|
||||
if err.contains("api key auth is not supported") {
|
||||
"Sign in with ChatGPT auth; API key auth cannot load remote plugin catalogs."
|
||||
} else if err.contains("authentication required")
|
||||
|| err.contains("not signed in")
|
||||
|| err.contains("not logged in")
|
||||
{
|
||||
"Sign in to ChatGPT, then try loading this section again."
|
||||
} else if err.contains("codex plugins are disabled")
|
||||
|| err.contains("plugin sharing is disabled")
|
||||
|| err.contains("plugin sharing is not enabled")
|
||||
|| err.contains("feature disabled")
|
||||
{
|
||||
"Ask a workspace admin to enable Codex plugins or plugin sharing."
|
||||
} else if err.contains("workspace") && (err.contains("access") || err.contains("mismatch")) {
|
||||
"Switch to the matching workspace or ask the sharer for access."
|
||||
} else if err.contains("not found") || err.contains("status 404") {
|
||||
"Check that you are signed in to the correct workspace and still have access."
|
||||
} else if err.contains("old build") || err.contains("update codex") || err.contains("stale") {
|
||||
"Update Codex, then try opening the shared plugin again."
|
||||
} else if err.contains("service unavailable")
|
||||
|| err.contains("temporarily unavailable")
|
||||
|| err.contains("status 503")
|
||||
|| err.contains("failed to send")
|
||||
|| err.contains("request")
|
||||
|| err.contains("status")
|
||||
{
|
||||
"Try again later; local plugin functionality is still available."
|
||||
} else if err.contains("disabled by admin") || err.contains("admin disabled") {
|
||||
"Ask a workspace admin to confirm plugin access."
|
||||
} else if label == "Shared with me" && err.contains("plugin") && err.contains("disabled") {
|
||||
"Ask the sharer or a workspace admin to confirm plugin access."
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_sharing_disabled_remote_section_error() -> PluginRemoteSectionError {
|
||||
PluginRemoteSectionError {
|
||||
section_id: "shared-with-me".to_string(),
|
||||
label: "Shared with me".to_string(),
|
||||
message: "Plugin sharing is disabled for this Codex session. Enable plugin sharing to load shared plugins.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
const CLI_HIDDEN_PLUGIN_MARKETPLACES: &[&str] = &["openai-bundled"];
|
||||
|
||||
pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
|
||||
@@ -767,6 +896,23 @@ pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListRespons
|
||||
pub(super) async fn request_plugin_list(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
) -> Result<PluginListResponse> {
|
||||
request_plugin_list_with_marketplace_kinds(request_handle, cwd, /*marketplace_kinds*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn request_plugin_list_for_kinds(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
marketplace_kinds: Vec<PluginListMarketplaceKind>,
|
||||
) -> Result<PluginListResponse> {
|
||||
request_plugin_list_with_marketplace_kinds(request_handle, cwd, Some(marketplace_kinds)).await
|
||||
}
|
||||
|
||||
async fn request_plugin_list_with_marketplace_kinds(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
marketplace_kinds: Option<Vec<PluginListMarketplaceKind>>,
|
||||
) -> Result<PluginListResponse> {
|
||||
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("plugin list cwd must be absolute")?;
|
||||
let request_id = RequestId::String(format!("plugin-list-{}", Uuid::new_v4()));
|
||||
@@ -775,7 +921,7 @@ pub(super) async fn request_plugin_list(
|
||||
request_id,
|
||||
params: PluginListParams {
|
||||
cwds: Some(vec![cwd]),
|
||||
marketplace_kinds: None,
|
||||
marketplace_kinds,
|
||||
},
|
||||
})
|
||||
.await
|
||||
@@ -1118,6 +1264,71 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_remote_section_error_message_adds_concrete_next_steps() {
|
||||
let cases = [
|
||||
(
|
||||
"Workspace",
|
||||
"chatgpt authentication required for remote plugin catalog",
|
||||
"Sign in to ChatGPT, then try loading this section again.",
|
||||
),
|
||||
(
|
||||
"OpenAI Curated",
|
||||
"chatgpt authentication required for remote plugin catalog; api key auth is not supported",
|
||||
"Sign in with ChatGPT auth; API key auth cannot load remote plugin catalogs.",
|
||||
),
|
||||
(
|
||||
"Shared with me",
|
||||
"remote plugin catalog request failed with status 404: missing",
|
||||
"Check that you are signed in to the correct workspace and still have access.",
|
||||
),
|
||||
(
|
||||
"Shared with me",
|
||||
"workspace access mismatch",
|
||||
"Switch to the matching workspace or ask the sharer for access.",
|
||||
),
|
||||
(
|
||||
"Shared with me",
|
||||
"old build fallback",
|
||||
"Update Codex, then try opening the shared plugin again.",
|
||||
),
|
||||
(
|
||||
"Shared with me",
|
||||
"remote service unavailable",
|
||||
"Try again later; local plugin functionality is still available.",
|
||||
),
|
||||
(
|
||||
"Workspace",
|
||||
"plugin disabled by admin",
|
||||
"Ask a workspace admin to confirm plugin access.",
|
||||
),
|
||||
(
|
||||
"Shared with me",
|
||||
"plugin sharing is not enabled",
|
||||
"Ask a workspace admin to enable Codex plugins or plugin sharing.",
|
||||
),
|
||||
];
|
||||
|
||||
for (label, err, next_step) in cases {
|
||||
assert_eq!(
|
||||
plugin_remote_section_error_message(label, err),
|
||||
format!("{err} {next_step}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_sharing_disabled_remote_section_error_targets_shared_with_me() {
|
||||
assert_eq!(
|
||||
plugin_sharing_disabled_remote_section_error(),
|
||||
PluginRemoteSectionError {
|
||||
section_id: "shared-with-me".to_string(),
|
||||
label: "Shared with me".to_string(),
|
||||
message: "Plugin sharing is disabled for this Codex session. Enable plugin sharing to load shared plugins.".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_inventory_maps_prefix_tool_names_by_server() {
|
||||
let statuses = vec![
|
||||
|
||||
@@ -507,6 +507,17 @@ impl App {
|
||||
AppEvent::PluginsLoaded { cwd, result } => {
|
||||
self.chat_widget.on_plugins_loaded(cwd, result);
|
||||
}
|
||||
AppEvent::PluginRemoteSectionsLoaded {
|
||||
cwd,
|
||||
marketplaces,
|
||||
section_errors,
|
||||
} => {
|
||||
self.chat_widget.on_plugin_remote_sections_loaded(
|
||||
cwd,
|
||||
marketplaces,
|
||||
section_errors,
|
||||
);
|
||||
}
|
||||
AppEvent::HooksLoaded { cwd, result } => {
|
||||
self.chat_widget.on_hooks_loaded(cwd, result);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
@@ -102,6 +103,13 @@ impl PluginLocation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PluginRemoteSectionError {
|
||||
pub(crate) section_id: String,
|
||||
pub(crate) label: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
/// Distinguishes why a rate-limit refresh was requested so the completion
|
||||
/// handler can route the result correctly.
|
||||
///
|
||||
@@ -406,6 +414,13 @@ pub(crate) enum AppEvent {
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Result of explicitly fetching remote-backed plugin sections.
|
||||
PluginRemoteSectionsLoaded {
|
||||
cwd: PathBuf,
|
||||
marketplaces: Vec<PluginMarketplaceEntry>,
|
||||
section_errors: Vec<PluginRemoteSectionError>,
|
||||
},
|
||||
|
||||
/// Result of fetching lifecycle hook inventory.
|
||||
HooksLoaded {
|
||||
cwd: PathBuf,
|
||||
|
||||
@@ -598,6 +598,9 @@ pub(crate) struct ChatWidget {
|
||||
ide_context: IdeContextState,
|
||||
plugins_cache: PluginsCacheState,
|
||||
plugins_fetch_state: PluginListFetchState,
|
||||
plugin_remote_sections_loading: bool,
|
||||
plugin_remote_sections_loaded: bool,
|
||||
plugin_remote_section_errors: Vec<crate::app_event::PluginRemoteSectionError>,
|
||||
plugin_install_apps_needing_auth: Vec<AppSummary>,
|
||||
plugin_install_auth_flow: Option<PluginInstallAuthFlowState>,
|
||||
plugins_active_tab_id: Option<String>,
|
||||
|
||||
@@ -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