diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 95bdea006..36155fb33 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -9,7 +9,11 @@ use codex_app_server_protocol::MarketplaceAddParams; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceRemoveParams; use codex_app_server_protocol::MarketplaceRemoveResponse; +use codex_app_server_protocol::MarketplaceUpgradeParams; +use codex_app_server_protocol::MarketplaceUpgradeResponse; + use codex_app_server_protocol::RequestId; + use codex_utils_absolute_path::AbsolutePathBuf; impl App { @@ -168,6 +172,26 @@ impl App { }); } + pub(super) fn fetch_marketplace_upgrade( + &mut self, + app_server: &AppServerSession, + cwd: PathBuf, + marketplace_name: Option, + ) { + 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 result = fetch_marketplace_upgrade(request_handle, marketplace_name) + .await + .map_err(|err| format!("Failed to upgrade marketplace: {err}")); + app_event_tx.send(AppEvent::MarketplaceUpgradeLoaded { + cwd: cwd_for_event, + result, + }); + }); + } + pub(super) fn fetch_plugin_install( &mut self, app_server: &AppServerSession, @@ -685,6 +709,20 @@ pub(super) async fn fetch_marketplace_remove( .await .wrap_err("marketplace/remove failed in TUI") } + +pub(super) async fn fetch_marketplace_upgrade( + request_handle: AppServerRequestHandle, + marketplace_name: Option, +) -> Result { + let request_id = RequestId::String(format!("marketplace-upgrade-{}", Uuid::new_v4())); + request_handle + .request_typed(ClientRequest::MarketplaceUpgrade { + request_id, + params: MarketplaceUpgradeParams { marketplace_name }, + }) + .await + .wrap_err("marketplace/upgrade failed in TUI") +} pub(super) async fn fetch_plugin_install( request_handle: AppServerRequestHandle, marketplace_path: AbsolutePathBuf, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index b2cd1e3e0..6bdc41372 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -408,6 +408,10 @@ impl App { self.chat_widget .open_marketplace_remove_loading_popup(&marketplace_display_name); } + AppEvent::OpenMarketplaceUpgradeLoading { marketplace_name } => { + self.chat_widget + .open_marketplace_upgrade_loading_popup(marketplace_name.as_deref()); + } AppEvent::OpenPluginDetailLoading { plugin_display_name, } => { @@ -435,6 +439,12 @@ impl App { AppEvent::FetchMarketplaceAdd { cwd, source } => { self.fetch_marketplace_add(app_server, cwd, source); } + AppEvent::FetchMarketplaceUpgrade { + cwd, + marketplace_name, + } => { + self.fetch_marketplace_upgrade(app_server, cwd, marketplace_name); + } AppEvent::MarketplaceAddLoaded { cwd, source, @@ -450,6 +460,25 @@ impl App { self.fetch_plugins_list(app_server, cwd); } } + AppEvent::MarketplaceUpgradeLoaded { cwd, result } => { + let marketplace_contents_changed = + matches!(&result, Ok(response) if !response.upgraded_roots.is_empty()); + if marketplace_contents_changed { + if let Err(err) = self.refresh_in_memory_config_from_disk().await { + tracing::warn!( + error = %err, + "failed to refresh config after marketplace upgrade" + ); + } + self.chat_widget.refresh_plugin_mentions(); + self.chat_widget.submit_op(AppCommand::reload_user_config()); + } + self.chat_widget + .on_marketplace_upgrade_loaded(cwd.clone(), result); + if self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() { + self.fetch_plugins_list(app_server, cwd); + } + } AppEvent::FetchMarketplaceRemove { cwd, marketplace_name, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 5ae99e088..5e45bf38e 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -15,6 +15,7 @@ use codex_app_server_protocol::AddCreditsNudgeEmailStatus; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceRemoveResponse; +use codex_app_server_protocol::MarketplaceUpgradeResponse; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; use codex_app_server_protocol::PluginInstallResponse; @@ -354,6 +355,23 @@ pub(crate) enum AppEvent { result: Result, }, + /// Replace the plugins popup with a marketplace-upgrade loading state. + OpenMarketplaceUpgradeLoading { + marketplace_name: Option, + }, + + /// Upgrade configured Git marketplaces. + FetchMarketplaceUpgrade { + cwd: PathBuf, + marketplace_name: Option, + }, + + /// Result of upgrading configured Git marketplaces. + MarketplaceUpgradeLoaded { + cwd: PathBuf, + result: Result, + }, + /// Replace the plugins popup with a plugin-detail loading state. OpenPluginDetailLoading { plugin_display_name: String, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 60dec4c92..c9621199e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5032,6 +5032,7 @@ impl ChatWidget { } if modifiers.contains(KeyModifiers::CONTROL) && c.eq_ignore_ascii_case(&'c') ) && !key_hint::ctrl(KeyCode::Char('r')).is_press(key_event) + && !key_hint::ctrl(KeyCode::Char('u')).is_press(key_event) { self.bottom_pane.handle_key_event(key_event); if self.bottom_pane.no_modal_or_popup_active() { @@ -10521,6 +10522,9 @@ impl ChatWidget { &mut self, plugins: Option>, ) { + if self.bottom_pane.plugins() == plugins.as_ref() { + return; + } self.bottom_pane.set_plugin_mentions(plugins); } diff --git a/codex-rs/tui/src/chatwidget/plugins.rs b/codex-rs/tui/src/chatwidget/plugins.rs index 6c5fe6c15..3bca3d5d8 100644 --- a/codex-rs/tui/src/chatwidget/plugins.rs +++ b/codex-rs/tui/src/chatwidget/plugins.rs @@ -24,6 +24,7 @@ use crate::render::renderable::Renderable; use crate::tui::FrameRequester; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceRemoveResponse; +use codex_app_server_protocol::MarketplaceUpgradeResponse; use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginInstallResponse; @@ -37,6 +38,7 @@ use codex_features::Feature; use codex_utils_absolute_path::AbsolutePathBuf; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; @@ -311,6 +313,26 @@ impl ChatWidget { } } + pub(crate) fn open_marketplace_upgrade_loading_popup( + &mut self, + marketplace_name: Option<&str>, + ) { + self.plugins_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()); + let params = self.marketplace_upgrade_loading_popup_params(marketplace_name); + if !self + .bottom_pane + .replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params) + { + self.bottom_pane.show_selection_view( + self.marketplace_upgrade_loading_popup_params(marketplace_name), + ); + } + } + pub(crate) fn open_marketplace_remove_confirmation( &mut self, marketplace_name: String, @@ -569,8 +591,101 @@ impl ChatWidget { } } + pub(crate) fn on_marketplace_upgrade_loaded( + &mut self, + cwd: PathBuf, + result: Result, + ) { + if self.config.cwd.as_path() != cwd.as_path() { + return; + } + + match result { + Ok(response) => { + if response.upgraded_roots.len() == 1 { + self.plugins_active_tab_id = + Some(marketplace_tab_id_from_path(&response.upgraded_roots[0])); + } + + let selected_count = response.selected_marketplaces.len(); + let upgraded_count = response.upgraded_roots.len(); + let error_count = response.errors.len(); + if selected_count == 0 { + self.add_info_message( + "No configured Git marketplaces to upgrade.".to_string(), + Some("Only configured Git marketplaces can be upgraded.".to_string()), + ); + return; + } + + if upgraded_count == 0 && error_count == 0 { + let message = if selected_count == 1 { + format!( + "Marketplace {} is already up to date.", + response.selected_marketplaces[0] + ) + } else { + format!( + "Checked {selected_count} marketplaces; all are already up to date." + ) + }; + self.add_info_message( + message, + Some(format!( + "Checked: {}", + response.selected_marketplaces.join(", ") + )), + ); + return; + } + + if upgraded_count > 0 { + let noun = if upgraded_count == 1 { + "marketplace" + } else { + "marketplaces" + }; + self.add_info_message( + format!("Upgraded {upgraded_count} {noun}."), + Some(format!( + "Updated roots: {}", + response + .upgraded_roots + .iter() + .map(|root| root.as_path().display().to_string()) + .collect::>() + .join(", ") + )), + ); + } + + if error_count > 0 { + let noun = if error_count == 1 { + "marketplace" + } else { + "marketplaces" + }; + self.add_error_message(format!( + "Failed to upgrade {error_count} {noun}: {}", + response + .errors + .iter() + .map(|err| format!("{}: {}", err.marketplace_name, err.message)) + .collect::>() + .join("; ") + )); + } + } + Err(err) => { + self.add_error_message(err); + } + } + } + pub(crate) fn handle_plugins_popup_key_event(&mut self, key_event: KeyEvent) -> bool { - if !key_hint::ctrl(KeyCode::Char('r')).is_press(key_event) { + let remove_marketplace = key_hint::ctrl(KeyCode::Char('r')).is_press(key_event); + let upgrade_marketplace = key_hint::ctrl(KeyCode::Char('u')).is_press(key_event); + if !remove_marketplace && !upgrade_marketplace { return false; } @@ -591,10 +706,33 @@ impl ChatWidget { return false; }; - self.open_marketplace_remove_confirmation( - marketplace.name.clone(), - marketplace_display_name(marketplace), - ); + if remove_marketplace { + self.open_marketplace_remove_confirmation( + marketplace.name.clone(), + marketplace_display_name(marketplace), + ); + return true; + } + if marketplace.path.is_none() + || !marketplace_is_user_configured_git(&self.config, &marketplace.name) + { + return false; + } + if key_event.kind != KeyEventKind::Press { + return true; + } + + let cwd = self.config.cwd.to_path_buf(); + let marketplace_name = Some(marketplace.name.clone()); + self.open_marketplace_upgrade_loading_popup(marketplace_name.as_deref()); + self.app_event_tx + .send(AppEvent::OpenMarketplaceUpgradeLoading { + marketplace_name: marketplace_name.clone(), + }); + self.app_event_tx.send(AppEvent::FetchMarketplaceUpgrade { + cwd, + marketplace_name, + }); true } @@ -1010,6 +1148,31 @@ impl ChatWidget { } } + fn marketplace_upgrade_loading_popup_params( + &self, + marketplace_name: Option<&str>, + ) -> SelectionViewParams { + let loading_text = marketplace_name + .map(|name| format!("Upgrading {name} marketplace...")) + .unwrap_or_else(|| "Upgrading marketplaces...".to_string()); + SelectionViewParams { + view_id: Some(PLUGINS_SELECTION_VIEW_ID), + header: Box::new(DelayedLoadingHeader::new( + self.frame_requester.clone(), + self.config.animations, + loading_text.clone(), + /*note*/ None, + )), + items: vec![SelectionItem { + name: loading_text, + description: Some("This updates when marketplace upgrade 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), @@ -1358,10 +1521,17 @@ impl ChatWidget { .filter(|(_, plugin, _)| plugin.installed) .count(); let tab_id = marketplace_tab_id(marketplace); - if marketplace_is_user_configured(&self.config, &marketplace.name) { + let can_remove_marketplace = + marketplace_is_user_configured(&self.config, &marketplace.name); + let can_upgrade_marketplace = marketplace.path.is_some() + && marketplace_is_user_configured_git(&self.config, &marketplace.name); + if can_remove_marketplace || can_upgrade_marketplace { tab_footer_hints.push(( tab_id.clone(), - plugins_popup_hint_line(/*can_remove_marketplace*/ true), + plugins_popup_hint_line( + /*can_remove_marketplace*/ can_remove_marketplace, + /*can_upgrade_marketplace*/ can_upgrade_marketplace, + ), )); } let header = if self.newly_installed_marketplace_tab_id.as_deref() == Some(&tab_id) { @@ -1397,7 +1567,7 @@ impl ChatWidget { view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(()), footer_hint: Some(plugins_popup_hint_line( - /*can_remove_marketplace*/ false, + /*can_remove_marketplace*/ false, /*can_upgrade_marketplace*/ false, )), tab_footer_hints, tabs, @@ -1687,13 +1857,23 @@ impl ChatWidget { } } -fn plugins_popup_hint_line(can_remove_marketplace: bool) -> Line<'static> { - if can_remove_marketplace { - Line::from( - "space enable/disable · ←/→ select marketplace · enter view details · ctrl + r remove marketplace · esc close", - ) - } else { - Line::from("space enable/disable · ←/→ select marketplace · enter view details · esc close") +fn plugins_popup_hint_line( + can_remove_marketplace: bool, + can_upgrade_marketplace: bool, +) -> Line<'static> { + match (can_remove_marketplace, can_upgrade_marketplace) { + (true, true) => Line::from( + "ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close", + ), + (true, false) => { + Line::from("ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close") + } + (false, true) => { + Line::from("ctrl + u upgrade · space toggle · ←/→ tabs · enter details · esc close") + } + (false, false) => Line::from( + "space enable/disable · ←/→ select marketplace · enter view details · esc close", + ), } } @@ -1833,6 +2013,19 @@ fn marketplace_is_user_configured(config: &Config, marketplace_name: &str) -> bo .is_some_and(|marketplaces| marketplaces.contains_key(marketplace_name)) } +fn marketplace_is_user_configured_git(config: &Config, marketplace_name: &str) -> bool { + config + .config_layer_stack + .get_user_layer() + .and_then(|user_layer| user_layer.config.get("marketplaces")) + .and_then(toml::Value::as_table) + .and_then(|marketplaces| marketplaces.get(marketplace_name)) + .and_then(toml::Value::as_table) + .and_then(|marketplace| marketplace.get("source_type")) + .and_then(toml::Value::as_str) + .is_some_and(|source_type| source_type == "git") +} + fn plugin_display_name(plugin: &PluginSummary) -> String { plugin .interface diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap index 6957ef2da..515b70092 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap @@ -12,4 +12,4 @@ expression: popup Type to search plugins › [-] Debug Plugin Available Press Enter to install or view plugin details. - space enable/disable · ←/→ select marketplace · enter view details · ctrl + r remove marketplace · + ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 77717faad..323d0b749 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -77,6 +77,8 @@ pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotif 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::MarketplaceUpgradeErrorInfo; +pub(super) use codex_app_server_protocol::MarketplaceUpgradeResponse; pub(super) use codex_app_server_protocol::McpServerStartupState; pub(super) use codex_app_server_protocol::McpServerStatusDetail; pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification; diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 6f7a50e27..f575349dd 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -108,6 +108,61 @@ async fn plugins_popup_loading_state_snapshot() { assert_chatwidget_snapshot!("plugins_popup_loading_state", popup); } +#[tokio::test] +async fn marketplace_upgrade_loading_popup_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); + + chat.open_marketplace_upgrade_loading_popup(Some("debug")); + + let popup = render_bottom_popup(&chat, /*width*/ 100); + let upgrade_lines = popup + .lines() + .map(str::trim) + .filter(|line| line.contains("Upgrading")) + .collect::>() + .join(" | "); + insta::assert_snapshot!( + upgrade_lines, + @"Upgrading debug marketplace... | › Upgrading debug marketplace... This updates when marketplace upgrade completes." + ); +} + +#[tokio::test] +async fn marketplace_upgrade_failure_includes_backend_messages_snapshot() { + 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.clone(); + + chat.on_marketplace_upgrade_loaded( + cwd.to_path_buf(), + Ok(MarketplaceUpgradeResponse { + selected_marketplaces: vec!["debug".to_string(), "tools".to_string()], + upgraded_roots: Vec::new(), + errors: vec![ + MarketplaceUpgradeErrorInfo { + marketplace_name: "debug".to_string(), + message: "git ls-remote marketplace source failed with status 128: authentication failed".to_string(), + }, + MarketplaceUpgradeErrorInfo { + marketplace_name: "tools".to_string(), + message: "failed to validate upgraded marketplace root: marketplace root does not contain a supported manifest".to_string(), + }, + ], + }), + ); + + let rendered = drain_insert_history(&mut rx) + .iter() + .map(|lines| lines_to_single_string(lines)) + .collect::>() + .join("\n"); + insta::assert_snapshot!( + rendered.trim(), + @"■ Failed to upgrade 2 marketplaces: debug: git ls-remote marketplace source failed with status 128: authentication failed; tools: failed to validate upgraded marketplace root: marketplace root does not contain a supported manifest" + ); +} + #[tokio::test] async fn hooks_popup_shows_list_diagnostics() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -305,6 +360,78 @@ async fn plugins_popup_add_marketplace_tab_opens_prompt_and_submits_source() { } } +#[tokio::test] +async fn plugins_popup_upgrades_user_configured_git_marketplace_from_marketplace_tab() { + 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(); + let temp = tempdir().expect("tempdir"); + let config_toml_path = temp.path().join("config.toml").abs(); + chat.config.config_layer_stack = ConfigLayerStack::default().with_user_config( + &config_toml_path, + toml::from_str::( + "[marketplaces.repo]\nsource_type = \"git\"\nsource = \"https://github.com/owner/repo.git\"\n", + ) + .expect("marketplace config"), + ); + + render_loaded_plugins_popup( + &mut chat, + plugins_test_response(vec![ + plugins_test_curated_marketplace(Vec::new()), + plugins_test_repo_marketplace(vec![plugins_test_summary( + "plugin-debug", + "debug", + Some("Debug Plugin"), + Some("Debug marketplace plugin."), + /*installed*/ false, + /*enabled*/ true, + PluginInstallPolicy::Available, + )]), + ]), + ); + + 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("Repo Marketplace.") + && popup.contains("ctrl + u upgrade") + && popup.contains("ctrl + r remove") + && popup.contains("Debug Plugin"), + "expected upgradeable user-configured marketplace tab, got:\n{popup}" + ); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL)); + chat.handle_key_event(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL)); + + match rx.try_recv() { + Ok(AppEvent::OpenMarketplaceUpgradeLoading { marketplace_name }) => { + assert_eq!(marketplace_name, Some("repo".to_string())); + } + other => panic!("expected OpenMarketplaceUpgradeLoading event, got {other:?}"), + } + match rx.try_recv() { + Ok(AppEvent::FetchMarketplaceUpgrade { + cwd: event_cwd, + marketplace_name, + }) => { + assert_eq!(event_cwd, cwd); + assert_eq!(marketplace_name, Some("repo".to_string())); + } + other => panic!("expected FetchMarketplaceUpgrade event, got {other:?}"), + } + let no_more_events = rx.try_recv(); + assert!( + no_more_events.is_err(), + "expected no duplicate marketplace upgrade events, got {no_more_events:?}" + ); +} + #[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; @@ -369,6 +496,8 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() { assert_chatwidget_snapshot!("plugins_popup_newly_installed_marketplace", popup); assert!( popup.contains("Debug Marketplace installed successfully.") + && popup.contains("ctrl + u upgrade") + && popup.contains("ctrl + r remove") && popup.contains("Debug Plugin"), "expected marketplace add refresh to switch to the new marketplace tab, got:\n{popup}" ); @@ -425,7 +554,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() { let repo_tab = render_bottom_popup(&chat, /*width*/ 100); assert!( repo_tab.contains("Repo Marketplace.") - && repo_tab.contains("ctrl + r remove marketplace") + && repo_tab.contains("ctrl + u upgrade") + && repo_tab.contains("ctrl + r remove") && repo_tab.contains("Debug Plugin"), "expected removable user-configured marketplace tab, got:\n{repo_tab}" ); @@ -493,7 +623,7 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() { refreshed.contains("Browse plugins from available marketplaces.") && !refreshed.contains("Repo Marketplace") && !refreshed.contains("Debug Plugin") - && !refreshed.contains("ctrl + r remove marketplace"), + && !refreshed.contains("ctrl + r remove"), "expected refreshed plugin list without removed marketplace, got:\n{refreshed}" ); }