/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:
canvrno-oai authored and GitHub committed 2026-04-30 10:25:07 -07:00
1 parent c02814c106
commit a85d265097
9 files changed
+593 -9

No files matched your search

@@ -7,6 +7,8 @@
use super::*;
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_utils_absolute_path::AbsolutePathBuf;
impl App {
@@ -130,6 +132,30 @@ impl App {
});
}
pub(super) fn fetch_marketplace_remove(
&mut self,
app_server: &AppServerSession,
cwd: PathBuf,
marketplace_name: String,
marketplace_display_name: 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 marketplace_name_for_event = marketplace_name.clone();
let result = fetch_marketplace_remove(request_handle, marketplace_name)
.await
.map_err(|err| format!("Failed to remove marketplace: {err}"));
app_event_tx.send(AppEvent::MarketplaceRemoveLoaded {
cwd: cwd_for_event,
marketplace_name: marketplace_name_for_event,
marketplace_display_name,
result,
});
});
}
pub(super) fn fetch_plugin_install(
&mut self,
app_server: &AppServerSession,
@@ -582,6 +608,19 @@ fn marketplace_add_source_for_request(cwd: &std::path::Path, source: String) ->
source
}
pub(super) async fn fetch_marketplace_remove(
request_handle: AppServerRequestHandle,
marketplace_name: String,
) -> Result<MarketplaceRemoveResponse> {
let request_id = RequestId::String(format!("marketplace-remove-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::MarketplaceRemove {
request_id,
params: MarketplaceRemoveParams { marketplace_name },
})
.await
.wrap_err("marketplace/remove failed in TUI")
}
pub(super) async fn fetch_plugin_install(
request_handle: AppServerRequestHandle,
marketplace_path: AbsolutePathBuf,
+53
View File
@@ -390,6 +390,21 @@ impl App {
AppEvent::OpenMarketplaceAddLoading { source } => {
self.chat_widget.open_marketplace_add_loading_popup(&source);
}
AppEvent::OpenMarketplaceRemoveConfirm {
marketplace_name,
marketplace_display_name,
} => {
self.chat_widget.open_marketplace_remove_confirmation(
marketplace_name,
marketplace_display_name,
);
}
AppEvent::OpenMarketplaceRemoveLoading {
marketplace_display_name,
} => {
self.chat_widget
.open_marketplace_remove_loading_popup(&marketplace_display_name);
}
AppEvent::OpenPluginDetailLoading {
plugin_display_name,
} => {
@@ -423,6 +438,44 @@ impl App {
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() {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(error = %err, "failed to refresh config after marketplace add");
}
self.fetch_plugins_list(app_server, cwd);
}
}
AppEvent::FetchMarketplaceRemove {
cwd,
marketplace_name,
marketplace_display_name,
} => {
self.fetch_marketplace_remove(
app_server,
cwd,
marketplace_name,
marketplace_display_name,
);
}
AppEvent::MarketplaceRemoveLoaded {
cwd,
marketplace_name,
marketplace_display_name,
result,
} => {
let remove_succeeded = result.is_ok();
self.chat_widget.on_marketplace_remove_loaded(
cwd.clone(),
marketplace_name,
marketplace_display_name,
result,
);
if remove_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()
{
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(error = %err, "failed to refresh config after marketplace remove");
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
self.fetch_plugins_list(app_server, cwd);
}
}