mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
/plugins: remove marketplace (#19843)
This PR adds marketplace removal to the /plugins menu, giving users a way to remove user-configured plugin marketplaces. It adds a `Ctrl+R` shortcut to remove selected marketplace tabs, a confirmation prompt, loading and error states, and the app-server request flow needed to perform marketplace/remove. After a successful removal, the TUI refreshes config, plugin mentions, user config, and plugin data so the removed marketplace disappears from the menu and other surfaces in the TUI. - Add `Ctrl+R` removal option for user-configured marketplace tabs - Show marketplace removal confirmation, loading, and error states - Route `marketplace/remove` through the TUI background request flow - Refresh config, plugin mentions, and plugin data after successful removal - Adds reusable per-tab footer hints so removal guidance only appears on applicable tabs - Add test coverage for `Ctrl+R` behavior while plugin search is active Steps to test: - Add a marketplace using the TUI /plugins menu - Use Ctrl+R to remove the marketplace - Accept the confirmation prompt - Confirm the marketplace is removed when the process completes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||
use codex_features::Stage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -283,6 +284,15 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
let marketplace_root = plugins_test_absolute_path("marketplaces/debug");
|
||||
let marketplace_path =
|
||||
plugins_test_absolute_path("marketplaces/debug/.agents/plugins/marketplace.json");
|
||||
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::<TomlValue>(
|
||||
"[marketplaces.debug]\nsource_type = \"git\"\nsource = \"https://github.com/owner/debug.git\"\n",
|
||||
)
|
||||
.expect("marketplace config"),
|
||||
);
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(Vec::new())]),
|
||||
@@ -347,6 +357,117 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
||||
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::<TomlValue>(
|
||||
"[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 repo_tab = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
repo_tab.contains("Repo Marketplace.")
|
||||
&& repo_tab.contains("ctrl + r remove marketplace")
|
||||
&& repo_tab.contains("Debug Plugin"),
|
||||
"expected removable user-configured marketplace tab, got:\n{repo_tab}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
|
||||
let confirmation = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
confirmation.contains("Remove Repo Marketplace marketplace?")
|
||||
&& confirmation.contains("Remove marketplace")
|
||||
&& confirmation.contains("Back to plugins"),
|
||||
"expected marketplace removal confirmation, got:\n{confirmation}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"plugins_popup_marketplace_remove_confirmation",
|
||||
confirmation
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
let marketplace_display_name = match rx.try_recv() {
|
||||
Ok(AppEvent::OpenMarketplaceRemoveLoading {
|
||||
marketplace_display_name,
|
||||
}) => marketplace_display_name,
|
||||
other => panic!("expected OpenMarketplaceRemoveLoading event, got {other:?}"),
|
||||
};
|
||||
assert_eq!(marketplace_display_name, "Repo Marketplace");
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::FetchMarketplaceRemove {
|
||||
cwd: event_cwd,
|
||||
marketplace_name,
|
||||
marketplace_display_name,
|
||||
}) => {
|
||||
assert_eq!(event_cwd, cwd);
|
||||
assert_eq!(marketplace_name, "repo");
|
||||
assert_eq!(marketplace_display_name, "Repo Marketplace");
|
||||
}
|
||||
other => panic!("expected FetchMarketplaceRemove event, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.open_marketplace_remove_loading_popup(&marketplace_display_name);
|
||||
let loading = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
loading.contains("Removing Repo Marketplace...")
|
||||
&& loading.contains("Removing marketplace..."),
|
||||
"expected marketplace removal loading state, got:\n{loading}"
|
||||
);
|
||||
|
||||
chat.on_marketplace_remove_loaded(
|
||||
cwd.clone(),
|
||||
"repo".to_string(),
|
||||
marketplace_display_name,
|
||||
Ok(MarketplaceRemoveResponse {
|
||||
marketplace_name: "repo".to_string(),
|
||||
installed_root: Some(plugins_test_absolute_path("marketplaces/repo")),
|
||||
}),
|
||||
);
|
||||
chat.on_plugins_loaded(
|
||||
cwd,
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
])),
|
||||
);
|
||||
|
||||
let refreshed = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
refreshed.contains("Browse plugins from available marketplaces.")
|
||||
&& !refreshed.contains("Repo Marketplace")
|
||||
&& !refreshed.contains("Debug Plugin")
|
||||
&& !refreshed.contains("ctrl + r remove marketplace"),
|
||||
"expected refreshed plugin list without removed marketplace, got:\n{refreshed}"
|
||||
);
|
||||
}
|
||||
|
||||
#[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