Detailed changes
@@ -322,6 +322,8 @@
"alt-l": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"ctrl-alt-k": "agent::ToggleThinkingMode",
+ "ctrl-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
@@ -366,6 +366,8 @@
"alt-tab": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"cmd-alt-k": "agent::ToggleThinkingMode",
+ "cmd-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
@@ -324,6 +324,8 @@
"alt-l": "agent::CycleFavoriteModels",
"ctrl-;": "agent::OpenAddContextMenu",
"ctrl-alt-k": "agent::ToggleThinkingMode",
+ "ctrl-alt-'": "agent::ToggleThinkingEffortMenu",
+ "ctrl-'": "agent::CycleThinkingEffort",
},
},
{
@@ -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,
@@ -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);
@@ -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<Subscription>,
pub message_editor: Entity<MessageEditor>,
pub add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
+ pub thinking_effort_menu_handle: PopoverMenuHandle<ContextMenu>,
pub project: WeakEntity<Project>,
pub recent_history_entries: Vec<AgentSessionInfo>,
pub hovered_recent_history_item: Option<usize>,
@@ -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<Self>) -> 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<Self>) {
+ if !cx.has_flag::<CloudThinkingEffortFeatureFlag>() {
+ 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<Self>,
+ ) {
+ 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);
}))
@@ -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,
]
);
@@ -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(
@@ -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)),
)