mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
Queue plugin toggle writes and let Space extend active search
This commit is contained in:
+48
-11
@@ -1049,6 +1049,9 @@ pub(crate) struct App {
|
||||
primary_session_configured: Option<ThreadSessionState>,
|
||||
pending_primary_events: VecDeque<ThreadBufferedEvent>,
|
||||
pending_app_server_requests: PendingAppServerRequests,
|
||||
// Serialize plugin enablement writes per plugin so stale completions cannot
|
||||
// overwrite a newer toggle.
|
||||
pending_plugin_enabled_writes: HashMap<(PathBuf, String), Option<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -2183,6 +2186,23 @@ impl App {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
) {
|
||||
let key = (cwd.clone(), plugin_id.clone());
|
||||
if let Some(queued_enabled) = self.pending_plugin_enabled_writes.get_mut(&key) {
|
||||
*queued_enabled = Some(enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_plugin_enabled_writes.insert(key, None);
|
||||
self.spawn_plugin_enabled_write(app_server, cwd, plugin_id, enabled);
|
||||
}
|
||||
|
||||
fn spawn_plugin_enabled_write(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
enabled: bool,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
@@ -4049,6 +4069,7 @@ impl App {
|
||||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
pending_app_server_requests: PendingAppServerRequests::default(),
|
||||
pending_plugin_enabled_writes: HashMap::new(),
|
||||
};
|
||||
if let Some(started) = initial_started_thread {
|
||||
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||
@@ -5313,19 +5334,35 @@ impl App {
|
||||
enabled,
|
||||
result,
|
||||
} => {
|
||||
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"
|
||||
);
|
||||
let key = (cwd.clone(), plugin_id.clone());
|
||||
let queued_enabled = self
|
||||
.pending_plugin_enabled_writes
|
||||
.get_mut(&key)
|
||||
.and_then(Option::take);
|
||||
let should_apply_result = if let Some(queued_enabled) = queued_enabled
|
||||
&& (result.is_err() || queued_enabled != enabled)
|
||||
{
|
||||
self.spawn_plugin_enabled_write(app_server, cwd, plugin_id, queued_enabled);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if should_apply_result {
|
||||
self.pending_plugin_enabled_writes.remove(&key);
|
||||
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.chat_widget.refresh_plugin_mentions();
|
||||
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
||||
self.chat_widget
|
||||
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
|
||||
}
|
||||
self.chat_widget
|
||||
.on_plugin_enabled_set(cwd, plugin_id, enabled, result);
|
||||
}
|
||||
AppEvent::RefreshPluginMentions => {
|
||||
self.refresh_plugin_mentions();
|
||||
|
||||
@@ -798,7 +798,11 @@ impl BottomPaneView for ListSelectionView {
|
||||
code: KeyCode::Char(' '),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} if self.selected_item_has_toggle() => self.toggle_selected(),
|
||||
} if self.selected_item_has_toggle()
|
||||
&& (!self.is_searchable || self.search_query.is_empty()) =>
|
||||
{
|
||||
self.toggle_selected()
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(' '),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -1482,6 +1486,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_appends_to_active_search_instead_of_toggling_selected_item() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = ListSelectionView::new(
|
||||
SelectionViewParams {
|
||||
items: vec![SelectionItem {
|
||||
name: "Plugin".to_string(),
|
||||
toggle: Some(SelectionToggle {
|
||||
is_on: false,
|
||||
action: Box::new(|_enabled, tx: &_| {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
}),
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
is_searchable: true,
|
||||
..Default::default()
|
||||
},
|
||||
tx,
|
||||
);
|
||||
view.set_search_query("plugin".to_string());
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(view.search_query, "plugin ");
|
||||
assert!(
|
||||
!view.active_items()[0]
|
||||
.toggle
|
||||
.as_ref()
|
||||
.is_some_and(|toggle| toggle.is_on),
|
||||
"expected Space to leave the toggle state unchanged while search is active"
|
||||
);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"expected Space with an active search query to avoid firing the toggle action"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_with_no_matches_triggers_cancel_callback() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -663,6 +663,46 @@ async fn plugins_popup_space_on_uninstalled_row_does_not_start_search() {
|
||||
assert_eq!(after, before);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_space_with_active_search_does_not_toggle_installed_plugin() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
plugins_test_summary(
|
||||
"plugin-calendar",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
Some("Schedule management."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
plugins_test_summary(
|
||||
"plugin-drive",
|
||||
"drive",
|
||||
Some("Drive"),
|
||||
Some("Document access."),
|
||||
/*installed*/ true,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
),
|
||||
])]),
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
type_plugins_search_query(&mut chat, "dr");
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"did not expect Space with an active plugin search to emit a toggle event"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_search_filters_visible_rows_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user