mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
marketplace/add support in plugins menu v2
This commit is contained in:
@@ -82,6 +82,8 @@ use codex_app_server_protocol::FeedbackUploadResponse;
|
||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::ListMcpServerStatusParams;
|
||||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::MarketplaceAddParams;
|
||||
use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::McpServerStatusDetail;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
@@ -2086,6 +2088,28 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
fn fetch_marketplace_add(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
source: String,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let cwd_for_event = cwd.clone();
|
||||
let source_for_event = source.clone();
|
||||
let result = fetch_marketplace_add(request_handle, source)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to add marketplace: {err}"));
|
||||
app_event_tx.send(AppEvent::MarketplaceAddLoaded {
|
||||
cwd: cwd_for_event,
|
||||
source: source_for_event,
|
||||
result,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn fetch_plugin_detail(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
@@ -4644,6 +4668,37 @@ impl App {
|
||||
AppEvent::FetchPluginsList { cwd } => {
|
||||
self.fetch_plugins_list(app_server, cwd);
|
||||
}
|
||||
AppEvent::OpenMarketplaceAddPrompt => {
|
||||
self.chat_widget.open_marketplace_add_prompt();
|
||||
}
|
||||
AppEvent::OpenMarketplaceAddLoading { source } => {
|
||||
self.chat_widget.open_marketplace_add_loading_popup(&source);
|
||||
}
|
||||
AppEvent::FetchMarketplaceAdd { cwd, source } => {
|
||||
self.fetch_marketplace_add(app_server, cwd, source);
|
||||
}
|
||||
AppEvent::MarketplaceAddLoaded {
|
||||
cwd,
|
||||
source,
|
||||
result,
|
||||
} => {
|
||||
let add_succeeded = result.is_ok();
|
||||
if add_succeeded {
|
||||
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to refresh config after marketplace add"
|
||||
);
|
||||
}
|
||||
self.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
}
|
||||
self.chat_widget
|
||||
.on_marketplace_add_loaded(cwd.clone(), source, result);
|
||||
if add_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() {
|
||||
self.fetch_plugins_list(app_server, cwd);
|
||||
}
|
||||
}
|
||||
AppEvent::OpenPluginDetailLoading {
|
||||
plugin_display_name,
|
||||
} => {
|
||||
@@ -6470,6 +6525,24 @@ fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
|
||||
.retain(|marketplace| !CLI_HIDDEN_PLUGIN_MARKETPLACES.contains(&marketplace.name.as_str()));
|
||||
}
|
||||
|
||||
async fn fetch_marketplace_add(
|
||||
request_handle: AppServerRequestHandle,
|
||||
source: String,
|
||||
) -> Result<MarketplaceAddResponse> {
|
||||
let request_id = RequestId::String(format!("marketplace-add-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::MarketplaceAdd {
|
||||
request_id,
|
||||
params: MarketplaceAddParams {
|
||||
source,
|
||||
ref_name: None,
|
||||
sparse_paths: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("marketplace/add failed")
|
||||
}
|
||||
|
||||
async fn fetch_plugin_detail(
|
||||
request_handle: AppServerRequestHandle,
|
||||
params: PluginReadParams,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
@@ -212,6 +213,27 @@ pub(crate) enum AppEvent {
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Open the prompt for adding a marketplace source.
|
||||
OpenMarketplaceAddPrompt,
|
||||
|
||||
/// Replace the plugins popup with a marketplace-add loading state.
|
||||
OpenMarketplaceAddLoading {
|
||||
source: String,
|
||||
},
|
||||
|
||||
/// Add a marketplace from the provided source.
|
||||
FetchMarketplaceAdd {
|
||||
cwd: PathBuf,
|
||||
source: String,
|
||||
},
|
||||
|
||||
/// Result of adding a marketplace.
|
||||
MarketplaceAddLoaded {
|
||||
cwd: PathBuf,
|
||||
source: String,
|
||||
result: Result<MarketplaceAddResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with a plugin-detail loading state.
|
||||
OpenPluginDetailLoading {
|
||||
plugin_display_name: String,
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionTab;
|
||||
use crate::bottom_pane::SelectionToggle;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::history_cell;
|
||||
use crate::legacy_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use crate::onboarding::mark_url_hyperlink;
|
||||
@@ -16,6 +17,7 @@ use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use crate::tui::FrameRequester;
|
||||
use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
use codex_app_server_protocol::PluginDetail;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
@@ -40,6 +42,7 @@ const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection";
|
||||
const ALL_PLUGINS_TAB_ID: &str = "all-plugins";
|
||||
const INSTALLED_PLUGINS_TAB_ID: &str = "installed-plugins";
|
||||
const OPENAI_CURATED_TAB_ID: &str = "marketplace:openai-curated";
|
||||
const ADD_MARKETPLACE_TAB_ID: &str = "add-marketplace";
|
||||
const PLUGIN_MENU_ROW_PREFIX_WIDTH: usize = 6;
|
||||
const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1);
|
||||
const LOADING_ANIMATION_INTERVAL: Duration = Duration::from_millis(100);
|
||||
@@ -241,6 +244,43 @@ impl ChatWidget {
|
||||
));
|
||||
}
|
||||
|
||||
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();
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
let view = CustomPromptView::new(
|
||||
"Add marketplace".to_string(),
|
||||
"owner/repo, git URL, or local marketplace path".to_string(),
|
||||
Some("Examples: owner/repo, git URL, ./marketplace".to_string()),
|
||||
Box::new(move |source: String| {
|
||||
let source = source.trim().to_string();
|
||||
if source.is_empty() {
|
||||
return;
|
||||
}
|
||||
tx.send(AppEvent::OpenMarketplaceAddLoading {
|
||||
source: source.clone(),
|
||||
});
|
||||
tx.send(AppEvent::FetchMarketplaceAdd {
|
||||
cwd: cwd.clone(),
|
||||
source,
|
||||
});
|
||||
}),
|
||||
);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
}
|
||||
|
||||
pub(crate) fn open_marketplace_add_loading_popup(&mut self, source: &str) {
|
||||
self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());
|
||||
let params = self.marketplace_add_loading_popup_params(source);
|
||||
if !self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)
|
||||
{
|
||||
self.bottom_pane
|
||||
.show_selection_view(self.marketplace_add_loading_popup_params(source));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_plugin_detail_loading_popup(&mut self, plugin_display_name: &str) {
|
||||
self.plugins_active_tab_id = self
|
||||
.bottom_pane
|
||||
@@ -267,6 +307,51 @@ impl ChatWidget {
|
||||
.replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params);
|
||||
}
|
||||
|
||||
pub(crate) fn on_marketplace_add_loaded(
|
||||
&mut self,
|
||||
cwd: PathBuf,
|
||||
source: String,
|
||||
result: Result<MarketplaceAddResponse, String>,
|
||||
) {
|
||||
if self.config.cwd.as_path() != cwd.as_path() {
|
||||
return;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
self.plugins_active_tab_id =
|
||||
Some(marketplace_tab_id_from_path(&response.installed_root));
|
||||
let message = if response.already_added {
|
||||
format!(
|
||||
"Marketplace {} is already added.",
|
||||
response.marketplace_name
|
||||
)
|
||||
} else {
|
||||
format!("Added marketplace {}.", response.marketplace_name)
|
||||
};
|
||||
self.add_info_message(
|
||||
message,
|
||||
Some(format!(
|
||||
"Marketplace root: {}",
|
||||
response.installed_root.as_path().display()
|
||||
)),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.plugins_active_tab_id = Some(ADD_MARKETPLACE_TAB_ID.to_string());
|
||||
let params = self.marketplace_add_error_popup_params(&source, &err);
|
||||
if !self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)
|
||||
{
|
||||
self.bottom_pane.show_selection_view(
|
||||
self.marketplace_add_error_popup_params(&source, &err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_plugin_detail_loaded(
|
||||
&mut self,
|
||||
cwd: PathBuf,
|
||||
@@ -653,6 +738,27 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn marketplace_add_loading_popup_params(&self, source: &str) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
self.config.animations,
|
||||
"Adding marketplace...".to_string(),
|
||||
Some(source.to_string()),
|
||||
)),
|
||||
items: vec![SelectionItem {
|
||||
name: "Adding marketplace...".to_string(),
|
||||
description: Some(
|
||||
"This updates when marketplace installation completes.".to_string(),
|
||||
),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
@@ -736,6 +842,54 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn marketplace_add_error_popup_params(&self, source: &str, err: &str) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from("Failed to add marketplace.".dim()));
|
||||
|
||||
let mut items = vec![
|
||||
SelectionItem {
|
||||
name: "Marketplace add failed".to_string(),
|
||||
description: Some(err.to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Try again".to_string(),
|
||||
description: Some(format!("Enter a marketplace source. Last source: {source}")),
|
||||
selected_description: Some("Enter a marketplace source.".to_string()),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenMarketplaceAddPrompt);
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
if let PluginsCacheState::Ready(plugins_response) = self.plugins_cache_for_current_cwd() {
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
items.push(SelectionItem {
|
||||
name: "Back to plugins".to_string(),
|
||||
description: Some("Return to the plugin list.".to_string()),
|
||||
selected_description: Some("Return to the plugin list.".to_string()),
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::PluginsLoaded {
|
||||
cwd: cwd.clone(),
|
||||
result: Ok(plugins_response.clone()),
|
||||
});
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
SelectionViewParams {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_detail_error_popup_params(
|
||||
&self,
|
||||
err: &str,
|
||||
@@ -800,6 +954,7 @@ impl ChatWidget {
|
||||
.map(|(_, _, display_name)| {
|
||||
PLUGIN_MENU_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(display_name.as_str())
|
||||
})
|
||||
.chain([UnicodeWidthStr::width("Add marketplace")])
|
||||
.max();
|
||||
let installed_entries = all_entries
|
||||
.iter()
|
||||
@@ -913,6 +1068,8 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
tabs.push(self.marketplace_add_tab());
|
||||
|
||||
SelectionViewParams {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(()),
|
||||
@@ -928,6 +1085,31 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn marketplace_add_tab(&self) -> SelectionTab {
|
||||
SelectionTab {
|
||||
id: ADD_MARKETPLACE_TAB_ID.to_string(),
|
||||
label: "Add Marketplace".to_string(),
|
||||
header: plugins_header(
|
||||
"Add a marketplace from a Git repo or local root.".to_string(),
|
||||
"Enter a source to make its plugins available in this menu.".to_string(),
|
||||
/*remote_sync_error*/ None,
|
||||
),
|
||||
items: vec![SelectionItem {
|
||||
name: "Add marketplace".to_string(),
|
||||
description: Some(
|
||||
"Enter owner/repo, a Git URL, or a local marketplace path.".to_string(),
|
||||
),
|
||||
selected_description: Some(
|
||||
"Press Enter to enter a marketplace source.".to_string(),
|
||||
),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenMarketplaceAddPrompt);
|
||||
})],
|
||||
..Default::default()
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_detail_popup_params(
|
||||
&self,
|
||||
plugins_response: &PluginListResponse,
|
||||
@@ -1210,7 +1392,11 @@ fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary,
|
||||
}
|
||||
|
||||
fn marketplace_tab_id(marketplace: &PluginMarketplaceEntry) -> String {
|
||||
format!("marketplace:{}", marketplace.path.display())
|
||||
marketplace_tab_id_from_path(&marketplace.path)
|
||||
}
|
||||
|
||||
fn marketplace_tab_id_from_path(path: &AbsolutePathBuf) -> String {
|
||||
format!("marketplace:{}", path.display())
|
||||
}
|
||||
|
||||
fn disambiguate_duplicate_tab_labels(labels: Vec<String>) -> Vec<String> {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ expression: popup
|
||||
Installed 1 of 4 available plugins.
|
||||
Using cached marketplace data: remote sync timed out
|
||||
|
||||
[All Plugins] Installed (1) OpenAI Curated Repo Marketplace
|
||||
[All Plugins] Installed (1) OpenAI Curated Repo Marketplace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ expression: popup
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 0 of 3 available plugins.
|
||||
|
||||
[All Plugins] Installed (0) OpenAI Curated
|
||||
[All Plugins] Installed (0) OpenAI Curated Add Marketplace
|
||||
|
||||
sla
|
||||
› [-] Slack Available Press Enter to view plugin details.
|
||||
|
||||
@@ -68,6 +68,7 @@ pub(super) use codex_app_server_protocol::ItemCompletedNotification;
|
||||
pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewCompletedNotification;
|
||||
pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotification;
|
||||
pub(super) use codex_app_server_protocol::ItemStartedNotification;
|
||||
pub(super) use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
pub(super) use codex_app_server_protocol::MarketplaceInterface;
|
||||
pub(super) use codex_app_server_protocol::McpServerStartupState;
|
||||
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
|
||||
@@ -163,6 +163,122 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_add_marketplace_tab_opens_prompt_and_submits_source() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let cwd = chat.config.cwd.to_path_buf();
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(Vec::new())]),
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
for _ in 0..3 {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Add a marketplace from a Git repo or local root."),
|
||||
"expected Add Marketplace tab, got:\n{popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::OpenMarketplaceAddPrompt) => {}
|
||||
other => panic!("expected OpenMarketplaceAddPrompt event, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.open_marketplace_add_prompt();
|
||||
let prompt = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
prompt.contains("owner/repo, git URL, or local marketplace path"),
|
||||
"expected marketplace source prompt, got:\n{prompt}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::OpenMarketplaceAddLoading { source }) => {
|
||||
assert_eq!(source, "owner/repo");
|
||||
}
|
||||
other => panic!("expected OpenMarketplaceAddLoading event, got {other:?}"),
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::FetchMarketplaceAdd {
|
||||
cwd: event_cwd,
|
||||
source,
|
||||
}) => {
|
||||
assert_eq!(event_cwd, cwd);
|
||||
assert_eq!(source, "owner/repo");
|
||||
}
|
||||
other => panic!("expected FetchMarketplaceAdd event, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let cwd = chat.config.cwd.to_path_buf();
|
||||
let marketplace_path = plugins_test_absolute_path("marketplaces/debug");
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(Vec::new())]),
|
||||
);
|
||||
chat.open_marketplace_add_loading_popup("owner/repo");
|
||||
chat.on_marketplace_add_loaded(
|
||||
cwd.clone(),
|
||||
"owner/repo".to_string(),
|
||||
Ok(MarketplaceAddResponse {
|
||||
marketplace_name: "debug".to_string(),
|
||||
installed_root: marketplace_path.clone(),
|
||||
already_added: false,
|
||||
}),
|
||||
);
|
||||
chat.on_plugins_loaded(
|
||||
cwd,
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
PluginMarketplaceEntry {
|
||||
name: "debug".to_string(),
|
||||
path: marketplace_path,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Debug Marketplace".to_string()),
|
||||
}),
|
||||
plugins: vec![plugins_test_summary(
|
||||
"plugin-debug",
|
||||
"debug",
|
||||
Some("Debug Plugin"),
|
||||
Some("Debug marketplace plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)],
|
||||
},
|
||||
])),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
popup.contains("Debug Marketplace.") && popup.contains("Debug Plugin"),
|
||||
"expected marketplace add refresh to switch to the new marketplace tab, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summaries() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user