From a27304bf9177914a0cb68179b5ce4d8469fc1502 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Mon, 9 Feb 2026 10:42:07 -0300
Subject: [PATCH] agent_ui: Refine thinking effort selector (#48790)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR adds some design refinements to the thinking effort selector (to
be generally rolled out soon):
- Improved split button UI styles and consistency with other dropdowns
in the message editor
- Stopped rendering the effort selector if thinking is turned off
- Added a keybinding to trigger the effort menu
- Added a keybinding to cycle through effort options
---
- [x] Code Reviewed
- [x] Manual QA
Release Notes:
- N/A
---
assets/keymaps/default-linux.json | 2 +
assets/keymaps/default-macos.json | 2 +
assets/keymaps/default-windows.json | 2 +
crates/agent_ui/src/acp/mode_selector.rs | 2 +-
crates/agent_ui/src/acp/thread_view.rs | 9 +-
.../src/acp/thread_view/active_thread.rs | 177 +++++++++++++++---
crates/agent_ui/src/agent_ui.rs | 4 +
crates/agent_ui/src/profile_selector.rs | 2 +-
.../ui/src/components/button/split_button.rs | 13 +-
9 files changed, 171 insertions(+), 42 deletions(-)
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index feb27bdcb017af2f8606624a863e8f6db483c41f..d877cfb41206087f555e47e01dccffeb9357b2c8 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -322,6 +322,8 @@
"alt-l": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"ctrl-alt-k": "agent::ToggleThinkingMode",
+ "ctrl-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 74a3d23c7d064b747a559eb6c0fa94430b54c504..19ce418bf13ef28e37149b3fc9dc3644f0fc782d 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -366,6 +366,8 @@
"alt-tab": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"cmd-alt-k": "agent::ToggleThinkingMode",
+ "cmd-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index 9ed8dc2e85f9c3b924ab6ad8d38723ea8185729d..5e0cbdc8afabf0c4e9901a256f44d653e294b02c 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -324,6 +324,8 @@
"alt-l": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"ctrl-alt-k": "agent::ToggleThinkingMode",
+ "ctrl-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs
index 15e2a3b768c74d4a56d906fada5fd0e992ffc8e3..1dbc05dddba849821448f74a6f98f5a72c67d684 100644
--- a/crates/agent_ui/src/acp/mode_selector.rs
+++ b/crates/agent_ui/src/acp/mode_selector.rs
@@ -190,7 +190,7 @@ impl Render for ModeSelector {
h_flex()
.gap_2()
.justify_between()
- .child(Label::new("Toggle Mode Menu"))
+ .child(Label::new("Change Mode"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 61c7301fb1e9d9b0fcc2b05144393ffea4248d9b..3dfdeccf7ec9f03b84752101502c708ecb08ac88 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -75,10 +75,11 @@ use crate::profile_selector::{ProfileProvider, ProfileSelector};
use crate::ui::{AgentNotification, AgentNotificationEvent};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue,
- CycleFavoriteModels, CycleModeSelector, EditFirstQueuedMessage, ExpandMessageEditor,
- ExternalAgentInitialContent, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff,
- OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage, SelectPermissionGranularity,
- SendImmediately, SendNextQueuedMessage, ToggleProfileSelector, ToggleThinkingMode,
+ CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage,
+ ExpandMessageEditor, ExternalAgentInitialContent, Follow, KeepAll, NewThread,
+ OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,
+ RemoveFirstQueuedMessage, SelectPermissionGranularity, SendImmediately, SendNextQueuedMessage,
+ ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode,
};
const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs
index de3b9038c715a5141023b65f7f973350ca65ab24..b329cae4ed4821ebfbe6678b121bcba4e83feabe 100644
--- a/crates/agent_ui/src/acp/thread_view/active_thread.rs
+++ b/crates/agent_ui/src/acp/thread_view/active_thread.rs
@@ -1,7 +1,7 @@
use gpui::{Corner, List};
use language_model::LanguageModelEffortLevel;
use settings::update_settings_file;
-use ui::SplitButton;
+use ui::{ButtonLike, SplitButton, SplitButtonStyle};
use super::*;
@@ -224,6 +224,7 @@ pub struct AcpThreadView {
pub _subscriptions: Vec,
pub message_editor: Entity,
pub add_context_menu_handle: PopoverMenuHandle,
+ pub thinking_effort_menu_handle: PopoverMenuHandle,
pub project: WeakEntity,
pub recent_history_entries: Vec,
pub hovered_recent_history_item: Option,
@@ -390,6 +391,7 @@ impl AcpThreadView {
in_flight_prompt: None,
message_editor,
add_context_menu_handle: PopoverMenuHandle::default(),
+ thinking_effort_menu_handle: PopoverMenuHandle::default(),
project,
recent_history_entries,
hovered_recent_history_item: None,
@@ -2747,18 +2749,21 @@ impl AcpThreadView {
return Some(thinking_toggle.into_any_element());
}
+ if !model.supported_effort_levels().is_empty() && !thinking {
+ return Some(thinking_toggle.into_any_element());
+ }
+
+ let left_btn = thinking_toggle;
+ let right_btn = self.render_effort_selector(
+ model.supported_effort_levels(),
+ thread.thinking_effort().cloned(),
+ cx,
+ );
+
Some(
- SplitButton::new(
- thinking_toggle,
- self.render_effort_selector(
- model.supported_effort_levels(),
- thread.thinking_effort().cloned(),
- cx,
- )
+ SplitButton::new(left_btn, right_btn.into_any_element())
+ .style(SplitButtonStyle::Transparent)
.into_any_element(),
- )
- .style(ui::SplitButtonStyle::Outlined)
- .into_any_element(),
)
}
@@ -2782,28 +2787,67 @@ impl AcpThreadView {
.cloned()
});
+ let label = selected
+ .clone()
+ .or(default_effort_level)
+ .map_or("Select Effort".into(), |effort| effort.name);
+
+ let (label_color, icon) = if self.thinking_effort_menu_handle.is_deployed() {
+ (Color::Accent, IconName::ChevronUp)
+ } else {
+ (Color::Muted, IconName::ChevronDown)
+ };
+
+ let focus_handle = self.message_editor.focus_handle(cx);
+ let show_cycle_row = supported_effort_levels.len() > 1;
+
+ let tooltip = Tooltip::element({
+ move |_, cx| {
+ let mut content = v_flex().gap_1().child(
+ h_flex()
+ .gap_2()
+ .justify_between()
+ .child(Label::new("Change Thinking Effort"))
+ .child(KeyBinding::for_action_in(
+ &ToggleThinkingEffortMenu,
+ &focus_handle,
+ cx,
+ )),
+ );
+
+ if show_cycle_row {
+ content = content.child(
+ h_flex()
+ .pt_1()
+ .gap_2()
+ .justify_between()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(Label::new("Cycle Thinking Effort"))
+ .child(KeyBinding::for_action_in(
+ &CycleThinkingEffort,
+ &focus_handle,
+ cx,
+ )),
+ );
+ }
+
+ content.into_any_element()
+ }
+ });
+
PopoverMenu::new("effort-selector")
- .trigger(
- ui::ButtonLike::new_rounded_right("effort-selector-trigger")
- .layer(ui::ElevationIndex::ModalSurface)
- .size(ui::ButtonSize::None)
- .child(
- Label::new(
- selected
- .clone()
- .or(default_effort_level)
- .map_or("Select Effort".into(), |effort| effort.name),
- )
- .size(LabelSize::Small),
- )
- .child(
- div()
- .px_1()
- .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
- ),
+ .trigger_with_tooltip(
+ ButtonLike::new_rounded_right("effort-selector-trigger")
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .child(Label::new(label).size(LabelSize::Small).color(label_color))
+ .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)),
+ tooltip,
)
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
+ menu = menu.header("Change Thinking Effort");
+
for effort_level in supported_effort_levels.clone() {
let is_selected = selected
.as_ref()
@@ -2846,7 +2890,12 @@ impl AcpThreadView {
menu
}))
})
- .anchor(Corner::BottomRight)
+ .with_handle(self.thinking_effort_menu_handle.clone())
+ .offset(gpui::Point {
+ x: px(0.0),
+ y: px(-2.0),
+ })
+ .anchor(Corner::BottomLeft)
}
fn render_send_button(&self, cx: &mut Context) -> AnyElement {
@@ -2945,7 +2994,7 @@ impl AcpThreadView {
}
},
)
- .anchor(gpui::Corner::BottomLeft)
+ .anchor(Corner::BottomLeft)
.with_handle(self.add_context_menu_handle.clone())
.offset(gpui::Point {
x: px(0.0),
@@ -6895,6 +6944,68 @@ impl AcpThreadView {
menu_handle.toggle(window, cx);
});
}
+
+ fn cycle_thinking_effort(&mut self, cx: &mut Context) {
+ if !cx.has_flag::() {
+ return;
+ }
+
+ let Some(thread) = self.as_native_thread(cx) else {
+ return;
+ };
+
+ let (effort_levels, current_effort) = {
+ let thread_ref = thread.read(cx);
+ let Some(model) = thread_ref.model() else {
+ return;
+ };
+ if !model.supports_thinking() || !thread_ref.thinking_enabled() {
+ return;
+ }
+ let effort_levels = model.supported_effort_levels();
+ if effort_levels.is_empty() {
+ return;
+ }
+ let current_effort = thread_ref.thinking_effort().cloned();
+ (effort_levels, current_effort)
+ };
+
+ let current_index = current_effort.and_then(|current| {
+ effort_levels
+ .iter()
+ .position(|level| level.value == current)
+ });
+ let next_index = match current_index {
+ Some(index) => (index + 1) % effort_levels.len(),
+ None => 0,
+ };
+ let next_effort = effort_levels[next_index].value.to_string();
+
+ thread.update(cx, |thread, cx| {
+ thread.set_thinking_effort(Some(next_effort.clone()), cx);
+
+ let fs = thread.project().read(cx).fs().clone();
+ update_settings_file(fs, cx, move |settings, _| {
+ if let Some(agent) = settings.agent.as_mut()
+ && let Some(default_model) = agent.default_model.as_mut()
+ {
+ default_model.effort = Some(next_effort);
+ }
+ });
+ });
+ }
+
+ fn toggle_thinking_effort_menu(
+ &mut self,
+ _action: &ToggleThinkingEffortMenu,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let menu_handle = self.thinking_effort_menu_handle.clone();
+ window.defer(cx, move |window, cx| {
+ menu_handle.toggle(window, cx);
+ });
+ }
}
impl Render for AcpThreadView {
@@ -6936,6 +7047,10 @@ impl Render for AcpThreadView {
});
}
}))
+ .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
+ this.cycle_thinking_effort(cx);
+ }))
+ .on_action(cx.listener(Self::toggle_thinking_effort_menu))
.on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
this.send_queued_message_at_index(0, true, window, cx);
}))
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 3bfd58ff4bb2defb8e3d7c11a52fed4575f7b747..d2beb4b7199bf1536985bed49958b0643703bce7 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -143,6 +143,10 @@ actions!(
OpenPermissionDropdown,
/// Toggles thinking mode for models that support extended thinking.
ToggleThinkingMode,
+ /// Cycles through available thinking effort levels for the current model.
+ CycleThinkingEffort,
+ /// Toggles the thinking effort selector menu open or closed.
+ ToggleThinkingEffortMenu,
]
);
diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs
index 2742ae834b186e876aefb007e387afce5197329f..6cf4b6fb3d3017449ef07c9cbe7fb68176eb7ba5 100644
--- a/crates/agent_ui/src/profile_selector.rs
+++ b/crates/agent_ui/src/profile_selector.rs
@@ -192,7 +192,7 @@ impl Render for ProfileSelector {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
- .child(container().child(Label::new("Toggle Profile Menu")).child(
+ .child(container().child(Label::new("Change Profile")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.child(
diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs
index 48f06ff3789e69b6d19cde2322932f4bd6e89f97..e6821e949009fe596c95652d2207be07e6eea2ae 100644
--- a/crates/ui/src/components/button/split_button.rs
+++ b/crates/ui/src/components/button/split_button.rs
@@ -1,10 +1,10 @@
use gpui::{
AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla,
- point, prelude::FluentBuilder, px,
+ point, prelude::FluentBuilder, px, relative,
};
use theme::ActiveTheme;
-use crate::{ElevationIndex, IconButton, h_flex};
+use crate::{ElevationIndex, prelude::*};
use super::ButtonLike;
@@ -68,18 +68,21 @@ impl RenderOnce for SplitButton {
);
h_flex()
- .rounded_sm()
.when(is_filled_or_outlined, |this| {
- this.border_1()
+ this.rounded_sm()
+ .border_1()
.border_color(cx.theme().colors().border.opacity(0.8))
})
+ .when(self.style == SplitButtonStyle::Transparent, |this| {
+ this.gap_px()
+ })
.child(div().flex_grow().child(match self.left {
SplitButtonKind::ButtonLike(button) => button.into_any_element(),
SplitButtonKind::IconButton(icon) => icon.into_any_element(),
}))
.child(
div()
- .h_full()
+ .h(relative(0.8))
.w_px()
.bg(cx.theme().colors().border.opacity(0.5)),
)