mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
TUI Plugin Sharing 3 - render remote plugin catalog sections (#26703)
## Summary [#26701](https://github.com/openai/codex/pull/26701) added remote plugin identity support, [#26702](https://github.com/openai/codex/pull/26702) added remote-section fetching and state, and [#28768](https://github.com/openai/codex/pull/28768) extracted the catalog rendering module. This PR builds the product-facing `/plugins` catalog on that foundation so remote records appear as OpenAI Curated, Workspace, and Shared with me sections rather than backend marketplace implementation details. Plugin details remain read-only for sharing metadata. This PR does not add share-authoring actions or change the app-server protocol. ## Changes - Renders OpenAI Curated, Workspace, and Shared with me sections with loading, empty, and error states. - Preserves section selection and stable tab ordering as remote sections transition between fallback and populated states. - Shows OpenAI Curated loading only when the explicit vertical fallback request was issued. - Centralizes remote marketplace identity matching around the existing marketplace constants. - Uses product labels for remote marketplaces and identifies the personal marketplace as Local by its path. - Shows read-only source, authentication, version, and sharing metadata in plugin detail views. - Applies narrow display deduplication for local and remote records sharing a remote plugin ID: - installed records take precedence; - local mapped sources are preferred for details only when their installed state matches the selected record. - Returns from detail and confirmation views through the current plugin cache so newly loaded remote sections are not overwritten by an older captured response. - Keeps admin-disabled plugins view-only and labels default-installed plugins as Available by default. ## Tests New tests: - `plugins_popup_admin_disabled_available_plugin_has_view_only_hint` - `plugins_popup_remote_section_fallback_states_snapshot` - `plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share_is_uninstalled` Updated existing plugin catalog tests and snapshots for product labels, detail metadata, personal-marketplace labeling, and stable tab ordering. Verification: - `cargo clippy -p codex-tui --all-targets -- -D warnings` ## Follow-ups - Local/remote duplicate normalization should eventually move into app-server. This PR intentionally keeps the compatibility behavior narrow and display-only. - PR5 will sanitize sensitive components before displaying Git source URLs.
This commit is contained in:
committed by
GitHub
Unverified
parent
44dbae90eb
commit
f6fa259312
@@ -507,6 +507,9 @@ impl App {
|
||||
AppEvent::PluginsLoaded { cwd, result } => {
|
||||
self.chat_widget.on_plugins_loaded(cwd, result);
|
||||
}
|
||||
AppEvent::OpenPluginsList { cwd, response } => {
|
||||
self.chat_widget.open_plugins_list(cwd, response);
|
||||
}
|
||||
AppEvent::PluginRemoteSectionsLoaded {
|
||||
cwd,
|
||||
marketplaces,
|
||||
|
||||
@@ -450,6 +450,12 @@ pub(crate) enum AppEvent {
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Open the plugin list from an already cached response.
|
||||
OpenPluginsList {
|
||||
cwd: PathBuf,
|
||||
response: PluginListResponse,
|
||||
},
|
||||
|
||||
/// Result of explicitly fetching remote-backed plugin sections.
|
||||
PluginRemoteSectionsLoaded {
|
||||
cwd: PathBuf,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ pub(super) const ADD_MARKETPLACE_TAB_ID: &str = "add-marketplace";
|
||||
pub(super) struct PluginListFetchState {
|
||||
pub(super) cache_cwd: Option<PathBuf>,
|
||||
pub(super) in_flight_cwd: Option<PathBuf>,
|
||||
pub(super) vertical_section_requested: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -147,6 +148,7 @@ impl ChatWidget {
|
||||
Err(err) => {
|
||||
self.plugin_remote_sections_loading = false;
|
||||
self.plugin_remote_sections_loaded = false;
|
||||
self.plugins_fetch_state.vertical_section_requested = false;
|
||||
if should_refresh_plugins_popup {
|
||||
self.plugins_fetch_state.cache_cwd = None;
|
||||
self.plugins_cache = PluginsCacheState::Failed(err.clone());
|
||||
@@ -175,6 +177,7 @@ impl ChatWidget {
|
||||
.is_some();
|
||||
self.plugin_remote_sections_loading = false;
|
||||
self.plugin_remote_sections_loaded = true;
|
||||
self.plugins_fetch_state.vertical_section_requested = false;
|
||||
let refreshed_response = match &mut self.plugins_cache {
|
||||
PluginsCacheState::Ready(response)
|
||||
if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) =>
|
||||
@@ -212,6 +215,8 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
self.plugins_fetch_state.in_flight_cwd = Some(cwd.clone());
|
||||
self.plugins_fetch_state.vertical_section_requested =
|
||||
!self.config.features.enabled(Feature::RemotePlugin);
|
||||
if self.plugins_fetch_state.cache_cwd.as_deref() != Some(cwd.as_path()) {
|
||||
self.plugins_cache = PluginsCacheState::Loading;
|
||||
}
|
||||
@@ -245,6 +250,36 @@ impl ChatWidget {
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn open_plugins_list(&mut self, cwd: PathBuf, response: PluginListResponse) {
|
||||
if self.config.cwd.as_path() != cwd.as_path() {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match self.plugins_cache_for_current_cwd() {
|
||||
PluginsCacheState::Ready(current_response) => current_response,
|
||||
PluginsCacheState::Uninitialized
|
||||
| PluginsCacheState::Loading
|
||||
| PluginsCacheState::Failed(_) => response,
|
||||
};
|
||||
self.plugins_fetch_state.cache_cwd = Some(cwd);
|
||||
self.plugins_cache = PluginsCacheState::Ready(response.clone());
|
||||
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())
|
||||
.or_else(|| Some(ALL_PLUGINS_TAB_ID.to_string()));
|
||||
self.plugins_active_tab_id = active_tab_id.clone();
|
||||
let params =
|
||||
self.plugins_popup_params(&response, active_tab_id, /*initial_selected_idx*/ None);
|
||||
if !self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)
|
||||
{
|
||||
self.open_plugins_popup(&response);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_marketplace_add_prompt(&mut self) {
|
||||
self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());
|
||||
let tx = self.app_event_tx.clone();
|
||||
|
||||
+3
-1
@@ -3,13 +3,15 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: strip_osc8_for_snapshot(&popup)
|
||||
---
|
||||
Plugins
|
||||
Figma · Can be installed · ChatGPT Marketplace
|
||||
Figma · Can be installed · Local
|
||||
Data shared with this app is subject to the app's terms of service and privacy policy. Learn
|
||||
more.
|
||||
Turn Figma files into implementation context.
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Install plugin Install this plugin now.
|
||||
Source Local
|
||||
Auth Auth on install
|
||||
Skills design-review, extract-copy
|
||||
Hooks PreToolUse (1), Stop (2)
|
||||
Apps Figma, Slack
|
||||
|
||||
+2
@@ -8,6 +8,8 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Uninstall plugin Remove this plugin now.
|
||||
Source Local
|
||||
Auth Auth on install
|
||||
Skills design-review, extract-copy
|
||||
Hooks PreToolUse (1), Stop (2)
|
||||
Apps Figma, Slack
|
||||
|
||||
+4
-4
@@ -9,9 +9,9 @@ expression: popup
|
||||
[All Plugins] Installed (1) OpenAI Curated Repo Marketplace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||
[-] Bravo Search Available · ChatGPT Marketplace · Search docs and tickets.
|
||||
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins.
|
||||
[-] Starter Available · ChatGPT Marketplace · Included by default.
|
||||
› [ ] 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 /plugi…
|
||||
[-] Starter Available by default · OpenAI Curated · Included by default.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 339
|
||||
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 [Debug Marketplace] Add Marketplace
|
||||
All Plugins Installed (0) OpenAI Curated Workspace Shared with me [Debug Marketplace]
|
||||
Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [-] Debug Plugin Available Press Enter to install or view plugin details.
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_app_server_protocol::HooksListEntry;
|
||||
use codex_app_server_protocol::HooksListResponse;
|
||||
use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginShareContext;
|
||||
use codex_features::Stage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -250,7 +251,7 @@ async fn plugins_popup_truncates_long_descriptions_in_list_rows() {
|
||||
.expect("expected verbose plugin row in popup");
|
||||
insta::assert_snapshot!(
|
||||
verbose_row,
|
||||
@" [-] Verbose Plugin Available · ChatGPT Marketplace · This descri…"
|
||||
@" [-] Verbose Plugin Available · OpenAI Curated · This description…"
|
||||
);
|
||||
assert!(
|
||||
!popup
|
||||
@@ -459,11 +460,20 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
chat.add_plugins_output();
|
||||
for _ in 0..3 {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
let reopened_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let reopened_popup = (0..8)
|
||||
.find_map(|_| {
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
if popup.contains("[Debug Marketplace]") {
|
||||
Some(popup)
|
||||
} else {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
panic!("expected Debug Marketplace tab after reopening, got:\n{popup}");
|
||||
});
|
||||
assert!(
|
||||
reopened_popup.contains("Installed 0 of 1 Debug Marketplace plugins.")
|
||||
&& !reopened_popup.contains("installed successfully"),
|
||||
@@ -584,10 +594,17 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summaries() {
|
||||
async fn plugin_detail_popup_snapshot_labels_personal_marketplace_as_local() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let marketplace_name = "personal-marketplace";
|
||||
let personal_marketplace_path = AbsolutePathBuf::try_from(
|
||||
dirs::home_dir()
|
||||
.expect("home directory")
|
||||
.join(".agents/plugins/marketplace.json"),
|
||||
)
|
||||
.expect("absolute personal marketplace path");
|
||||
let summary = plugins_test_summary(
|
||||
"plugin-figma",
|
||||
"figma",
|
||||
@@ -597,28 +614,29 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
);
|
||||
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
summary.clone(),
|
||||
])]);
|
||||
let response = plugins_test_response(vec![PluginMarketplaceEntry {
|
||||
name: marketplace_name.to_string(),
|
||||
path: Some(personal_marketplace_path.clone()),
|
||||
interface: None,
|
||||
plugins: 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"],
|
||||
&[
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
),
|
||||
}),
|
||||
let mut plugin = plugins_test_detail(
|
||||
summary,
|
||||
Some("Turn Figma files into implementation context."),
|
||||
&["design-review", "extract-copy"],
|
||||
&[
|
||||
(codex_app_server_protocol::HookEventName::PreToolUse, 1),
|
||||
(codex_app_server_protocol::HookEventName::Stop, 2),
|
||||
],
|
||||
&["Figma", "Slack"],
|
||||
&["figma-mcp", "docs-mcp"],
|
||||
);
|
||||
plugin.marketplace_name = marketplace_name.to_string();
|
||||
plugin.marketplace_path = Some(personal_marketplace_path);
|
||||
chat.on_plugin_detail_loaded(cwd.to_path_buf(), Ok(PluginReadResponse { plugin }));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert_chatwidget_snapshot!(
|
||||
@@ -1022,6 +1040,222 @@ async fn plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint() {
|
||||
assert_eq!(after, before);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_admin_disabled_available_plugin_has_view_only_hint() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = PluginSummary {
|
||||
availability: PluginAvailability::DisabledByAdmin,
|
||||
..plugins_test_summary(
|
||||
"plugin-admin-blocked",
|
||||
"admin-blocked",
|
||||
Some("Admin Blocked"),
|
||||
Some("Blocked by policy."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)
|
||||
};
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![summary])]),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let admin_blocked_row = popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Admin Blocked"))
|
||||
.expect("expected admin-disabled plugin row");
|
||||
assert!(
|
||||
admin_blocked_row.contains("Press Enter to view plugin details.")
|
||||
&& !admin_blocked_row.contains("install or view"),
|
||||
"expected admin-disabled available plugin to stay view-only, got:\n{admin_blocked_row}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_remote_section_fallback_states_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let select_tab_containing = |chat: &mut ChatWidget, visible_text: &str| -> String {
|
||||
for _ in 0..8 {
|
||||
let popup = render_bottom_popup(chat, /*width*/ 100);
|
||||
if popup.contains(visible_text) {
|
||||
return popup;
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
let popup = render_bottom_popup(chat, /*width*/ 100);
|
||||
panic!("expected plugins tab containing {visible_text:?}, got:\n{popup}");
|
||||
};
|
||||
let remote_section_state = |popup: &str| -> String {
|
||||
let header = popup
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.nth(1)
|
||||
.expect("expected remote section header");
|
||||
let item = popup
|
||||
.lines()
|
||||
.find_map(|line| line.trim_start().strip_prefix('›'))
|
||||
.expect("expected selected remote section item")
|
||||
.trim();
|
||||
format!("{header}\n{item}")
|
||||
};
|
||||
|
||||
chat.add_plugins_output();
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
])),
|
||||
);
|
||||
let curated_loading_popup =
|
||||
select_tab_containing(&mut chat, "Loading OpenAI Curated plugins...");
|
||||
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.");
|
||||
|
||||
chat.on_plugin_remote_sections_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Vec::new(),
|
||||
vec![crate::app_event::PluginRemoteSectionError {
|
||||
section_id: "workspace".to_string(),
|
||||
label: "Workspace".to_string(),
|
||||
message: "Sign in to ChatGPT to load workspace plugins.".to_string(),
|
||||
}],
|
||||
);
|
||||
let workspace_error_popup = select_tab_containing(&mut chat, "Workspace unavailable.");
|
||||
|
||||
let (mut remote_chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
remote_chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
remote_chat.set_feature_enabled(Feature::RemotePlugin, /*enabled*/ true);
|
||||
remote_chat.add_plugins_output();
|
||||
let remote_cwd = remote_chat.config.cwd.clone();
|
||||
remote_chat.on_plugins_loaded(
|
||||
remote_cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
])),
|
||||
);
|
||||
let remote_curated_empty_popup =
|
||||
select_tab_containing(&mut remote_chat, "No OpenAI Curated plugins available");
|
||||
|
||||
insta::assert_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),
|
||||
]
|
||||
.join("\n\n"),
|
||||
@r###"
|
||||
OpenAI Curated marketplace.
|
||||
Loading OpenAI Curated plugins... This section updates when app-server returns it.
|
||||
|
||||
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.
|
||||
|
||||
OpenAI Curated marketplace.
|
||||
No OpenAI Curated plugins available No OpenAI Curated plugins available.
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share_is_uninstalled() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let remote_plugin_id = "plugins~Plugin_docs";
|
||||
let remote_marketplace_name = "workspace-shared-with-me-private";
|
||||
let local_summary = PluginSummary {
|
||||
share_context: Some(PluginShareContext {
|
||||
remote_plugin_id: remote_plugin_id.to_string(),
|
||||
remote_version: None,
|
||||
discoverability: None,
|
||||
share_url: None,
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_principals: None,
|
||||
}),
|
||||
..plugins_test_summary(
|
||||
"plugin-docs",
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Local editable docs plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)
|
||||
};
|
||||
let popup = render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![local_summary]),
|
||||
PluginMarketplaceEntry {
|
||||
name: remote_marketplace_name.to_string(),
|
||||
path: None,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Shared with me".to_string()),
|
||||
}),
|
||||
plugins: vec![plugins_test_remote_summary(
|
||||
remote_plugin_id,
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Shared docs plugin."),
|
||||
/*installed*/ true,
|
||||
)],
|
||||
},
|
||||
]),
|
||||
);
|
||||
let all_plugins_row = popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Docs"))
|
||||
.expect("expected all-plugins row");
|
||||
assert!(
|
||||
popup.contains("Installed 1 of 1 available plugins.")
|
||||
&& all_plugins_row.contains("Installed")
|
||||
&& !all_plugins_row.contains("Available"),
|
||||
"expected installed remote duplicate to win over local mapped share, got:\n{popup}"
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::OpenPluginDetailLoading {
|
||||
plugin_display_name,
|
||||
}) => {
|
||||
assert_eq!(plugin_display_name, "Docs");
|
||||
}
|
||||
other => panic!("expected OpenPluginDetailLoading event, got {other:?}"),
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::FetchPluginDetail { params, .. }) => {
|
||||
assert_eq!(params.marketplace_path, None);
|
||||
assert_eq!(
|
||||
params.remote_marketplace_name,
|
||||
Some(remote_marketplace_name.to_string())
|
||||
);
|
||||
assert_eq!(params.plugin_name, remote_plugin_id);
|
||||
}
|
||||
other => panic!("expected FetchPluginDetail event, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_error_popup_skips_disabled_row_numbering() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user