TUI config cleanup: plugin marketplace (#24257)

## Why

Plugin and marketplace mutations are applied by the app server, but
several TUI follow-up paths still refreshed state from the TUI host
config. In remote workspace mode, that can leave plugin UI state tied to
stale client-local `config.toml` after the server has already applied
the mutation.

## What

- Stop reloading the TUI host config after app-server-owned plugin,
marketplace, skill, and app mutations.
- Use the same app-server-owned refresh path for local and remote
sessions: ask the app server to reload user config where the running
session needs it, then refetch plugin list/detail state from the app
server.
- Build plugin mention candidates from existing app-server `plugin/list`
and `plugin/read` data in both local and remote sessions instead of
TUI-host plugin config.
- Avoid the duplicate local config reload after `ReloadUserConfig` asks
the app server to reload config.

## Verification

Manually launched a local WebSocket app-server with a temp server
`CODEX_HOME`, launched the TUI with a separate temp host `CODEX_HOME`
and `--remote`, installed a sample plugin from a temp local marketplace
through `/plugins`, and confirmed the TUI refreshed to installed state
while only the server config gained `[plugins."sample@debug"]`. Trace
logs showed the TUI using app-server `plugin/list` and `plugin/read` for
the refresh path.
This commit is contained in:
Eric Traut
2026-05-27 07:22:30 -07:00
committed by GitHub
parent 61cbf3574e
commit f20904c4d6
3 changed files with 13 additions and 54 deletions

View File

@@ -400,16 +400,16 @@ impl App {
}
pub(super) fn refresh_plugin_mentions(&mut self, app_server: &AppServerSession) {
let config = self.config.clone();
let cwd = self.config.cwd.to_path_buf();
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
if !config.features.enabled(Feature::Plugins) {
if !self.config.features.enabled(Feature::Plugins) {
app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: None });
return;
}
tokio::spawn(async move {
match fetch_plugin_mentions(request_handle, config.cwd.to_path_buf()).await {
match fetch_plugin_mentions(request_handle, cwd).await {
Ok(plugins) => {
app_event_tx.send(AppEvent::PluginMentionsLoaded {
plugins: Some(plugins),

View File

@@ -506,9 +506,6 @@ 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);
}
}
@@ -516,14 +513,7 @@ impl App {
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.refresh_plugin_mentions_after_config_write();
}
self.chat_widget
.on_marketplace_upgrade_loaded(cwd.clone(), result);
@@ -558,11 +548,7 @@ impl App {
);
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.refresh_plugin_mentions_after_config_write();
self.fetch_plugins_list(app_server, cwd);
}
}
@@ -609,11 +595,7 @@ impl App {
} => {
let install_succeeded = result.is_ok();
if install_succeeded {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(error = %err, "failed to refresh config after plugin install");
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
self.refresh_plugin_mentions_after_config_write();
}
let should_refresh_plugin_detail = self.chat_widget.on_plugin_install_loaded(
cwd.clone(),
@@ -665,14 +647,7 @@ impl App {
self.pending_plugin_enabled_writes.remove(&plugin_id);
let update_succeeded = result.is_ok();
if update_succeeded {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(
error = %err,
"failed to refresh config after plugin toggle"
);
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
self.refresh_plugin_mentions_after_config_write();
}
self.chat_widget
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
@@ -1303,14 +1278,7 @@ impl App {
} => {
let uninstall_succeeded = result.is_ok();
if uninstall_succeeded {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(
error = %err,
"failed to refresh config after plugin uninstall"
);
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
self.refresh_plugin_mentions_after_config_write();
}
self.chat_widget.on_plugin_uninstall_loaded(
cwd.clone(),
@@ -1705,14 +1673,6 @@ impl App {
{
Ok(()) => {
self.chat_widget.update_skill_enabled(path, enabled);
if !app_server.uses_remote_workspace()
&& let Err(err) = self.refresh_in_memory_config_from_disk().await
{
tracing::warn!(
error = %err,
"failed to refresh config after skill toggle"
);
}
}
Err(err) => {
let path_display = path.display();
@@ -1749,11 +1709,6 @@ impl App {
{
Ok(_) => {
self.chat_widget.update_connector_enabled(&id, enabled);
if !app_server.uses_remote_workspace()
&& let Err(err) = self.refresh_in_memory_config_from_disk().await
{
tracing::warn!(error = %err, "failed to refresh config after app toggle");
}
}
Err(err) => {
self.chat_widget.add_error_message(format!(
@@ -2098,6 +2053,11 @@ impl App {
}
}
fn refresh_plugin_mentions_after_config_write(&mut self) {
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
}
async fn apply_keymap_clear(&mut self, context: String, action: String) {
let keymap_config = match crate::keymap_setup::keymap_without_custom_binding(
&self.config.tui_keymap,

View File

@@ -684,7 +684,6 @@ impl App {
}
AppCommand::ReloadUserConfig => {
app_server.reload_user_config().await?;
self.refresh_in_memory_config_from_disk().await?;
Ok(true)
}
AppCommand::OverrideTurnContext { .. } => {