From dffed22f454166da28c1f102c0f436c2cd48c434 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 10 Jan 2026 00:04:31 +0100 Subject: [PATCH] acp: Add keybindings for session config option categories (#46484) Adds the normal mode/model keybindings for the first config option of a given category Release Notes: - N/A --- crates/agent_ui/src/acp/config_options.rs | 117 +++++++++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 52 ++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/config_options.rs b/crates/agent_ui/src/acp/config_options.rs index c9310350c19e2ec08cf5e1b36d55fc9d2c37cfa4..387069cd1671fa811ad3933d943f5d691d848b37 100644 --- a/crates/agent_ui/src/acp/config_options.rs +++ b/crates/agent_ui/src/acp/config_options.rs @@ -8,7 +8,7 @@ use collections::HashSet; use fs::Fs; use fuzzy::StringMatchCandidate; use gpui::{ - BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*, + App, BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*, }; use ordered_float::OrderedFloat; use picker::popover_menu::PickerPopoverMenu; @@ -67,6 +67,113 @@ impl ConfigOptionsView { } } + pub fn toggle_category_picker( + &mut self, + category: acp::SessionConfigOptionCategory, + window: &mut Window, + cx: &mut Context, + ) -> bool { + let Some(config_id) = self.first_config_option_id(category) else { + return false; + }; + + let Some(selector) = self.selector_for_config_id(&config_id, cx) else { + return false; + }; + + selector.update(cx, |selector, cx| { + selector.toggle_picker(window, cx); + }); + + true + } + + pub fn cycle_category_option( + &mut self, + category: acp::SessionConfigOptionCategory, + favorites_only: bool, + cx: &mut Context, + ) -> bool { + let Some(config_id) = self.first_config_option_id(category) else { + return false; + }; + + let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else { + return false; + }; + + let task = self + .config_options + .set_config_option(config_id, next_value, cx); + + cx.spawn(async move |_, _| { + if let Err(err) = task.await { + log::error!("Failed to set config option: {:?}", err); + } + }) + .detach(); + + true + } + + fn first_config_option_id( + &self, + category: acp::SessionConfigOptionCategory, + ) -> Option { + self.config_options + .config_options() + .into_iter() + .find(|option| option.category.as_ref() == Some(&category)) + .map(|option| option.id) + } + + fn selector_for_config_id( + &self, + config_id: &acp::SessionConfigId, + cx: &App, + ) -> Option> { + self.selectors + .iter() + .find(|selector| selector.read(cx).config_id() == config_id) + .cloned() + } + + fn next_value_for_config( + &self, + config_id: &acp::SessionConfigId, + favorites_only: bool, + cx: &mut Context, + ) -> Option { + let mut options = extract_options(&self.config_options, config_id); + if options.is_empty() { + return None; + } + + if favorites_only { + let favorites = self + .agent_server + .favorite_config_option_value_ids(config_id, cx); + options.retain(|option| favorites.contains(&option.value)); + if options.is_empty() { + return None; + } + } + + let current_value = get_current_value(&self.config_options, config_id); + let current_index = current_value + .as_ref() + .and_then(|current| options.iter().position(|option| &option.value == current)) + .unwrap_or(usize::MAX); + + let next_index = if current_index == usize::MAX { + 0 + } else { + (current_index + 1) % options.len() + }; + + Some(options[next_index].value.clone()) + } + fn config_option_ids( config_options: &Rc, ) -> Vec { @@ -206,6 +313,14 @@ impl ConfigOptionSelector { .find(|opt| opt.id == self.config_id) } + fn config_id(&self) -> &acp::SessionConfigId { + &self.config_id + } + + fn toggle_picker(&self, window: &mut Window, cx: &mut Context) { + self.picker_handle.toggle(window, cx); + } + fn current_value_name(&self) -> String { let Some(option) = self.current_option() else { return "Unknown".to_string(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 547b0d4dde98cc79d5becda6c9ee6daa428e25d5..c7cb6860e4097562abd6f06729c9b0472caeb067 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -6953,6 +6953,19 @@ impl Render for AcpThreadView { cx.notify(); })) .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { + if let Some(config_options_view) = this.config_options_view.as_ref() { + let handled = config_options_view.update(cx, |view, cx| { + view.toggle_category_picker( + acp::SessionConfigOptionCategory::Mode, + window, + cx, + ) + }); + if handled { + return; + } + } + if let Some(profile_selector) = this.profile_selector.as_ref() { profile_selector.read(cx).menu_handle().toggle(window, cx); } else if let Some(mode_selector) = this.mode_selector() { @@ -6960,6 +6973,19 @@ impl Render for AcpThreadView { } })) .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| { + if let Some(config_options_view) = this.config_options_view.as_ref() { + let handled = config_options_view.update(cx, |view, cx| { + view.cycle_category_option( + acp::SessionConfigOptionCategory::Mode, + false, + cx, + ) + }); + if handled { + return; + } + } + if let Some(profile_selector) = this.profile_selector.as_ref() { profile_selector.update(cx, |profile_selector, cx| { profile_selector.cycle_profile(cx); @@ -6971,12 +6997,38 @@ impl Render for AcpThreadView { } })) .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { + if let Some(config_options_view) = this.config_options_view.as_ref() { + let handled = config_options_view.update(cx, |view, cx| { + view.toggle_category_picker( + acp::SessionConfigOptionCategory::Model, + window, + cx, + ) + }); + if handled { + return; + } + } + if let Some(model_selector) = this.model_selector.as_ref() { model_selector .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); } })) .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| { + if let Some(config_options_view) = this.config_options_view.as_ref() { + let handled = config_options_view.update(cx, |view, cx| { + view.cycle_category_option( + acp::SessionConfigOptionCategory::Model, + true, + cx, + ) + }); + if handled { + return; + } + } + if let Some(model_selector) = this.model_selector.as_ref() { model_selector.update(cx, |model_selector, cx| { model_selector.cycle_favorite_models(window, cx);