agent_ui: Disable pickers while thread is generating (#50519)

Bennet Bo Fenner and Danilo Leal created

It does not make sense to enable them during the running turn and it can
lead to more confusing states if subagents are used.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/agent_ui/src/connection_view/thread_view.rs | 48 +++++++++++++++-
crates/agent_ui/src/model_selector_popover.rs      | 48 +++++++++++++---
crates/agent_ui/src/profile_selector.rs            | 37 ++++++++++--
3 files changed, 114 insertions(+), 19 deletions(-)

Detailed changes

crates/agent_ui/src/connection_view/thread_view.rs 🔗

@@ -2674,6 +2674,14 @@ impl ThreadView {
             return div().into_any_element();
         }
 
+        let is_generating = self.thread.read(cx).status() != ThreadStatus::Idle;
+        if let Some(model_selector) = &self.model_selector {
+            model_selector.update(cx, |selector, _| selector.set_disabled(is_generating));
+        }
+        if let Some(profile_selector) = &self.profile_selector {
+            profile_selector.update(cx, |selector, _| selector.set_disabled(is_generating));
+        }
+
         let focus_handle = self.message_editor.focus_handle(cx);
         let editor_bg_color = cx.theme().colors().editor_background;
         let editor_expanded = self.editor_expanded;
@@ -3223,6 +3231,7 @@ impl ThreadView {
             return None;
         }
 
+        let is_generating = self.thread.read(cx).status() != ThreadStatus::Idle;
         let thinking = thread.thinking_enabled();
 
         let (tooltip_label, icon, color) = if thinking {
@@ -3244,8 +3253,13 @@ impl ThreadView {
         let thinking_toggle = IconButton::new("thinking-mode", icon)
             .icon_size(IconSize::Small)
             .icon_color(color)
-            .tooltip(move |_, cx| {
-                Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
+            .disabled(is_generating)
+            .tooltip(move |window, cx| {
+                if is_generating {
+                    Tooltip::text("Disabled until generation is done")(window, cx)
+                } else {
+                    Tooltip::for_action_in(tooltip_label, &ToggleThinkingMode, &focus_handle, cx)
+                }
             })
             .on_click(cx.listener(move |this, _, _window, cx| {
                 if let Some(thread) = this.as_native_thread(cx) {
@@ -3277,6 +3291,7 @@ impl ThreadView {
         let right_btn = self.render_effort_selector(
             model.supported_effort_levels(),
             thread.thinking_effort().cloned(),
+            is_generating,
             cx,
         );
 
@@ -3291,6 +3306,7 @@ impl ThreadView {
         &self,
         supported_effort_levels: Vec<LanguageModelEffortLevel>,
         selected_effort: Option<String>,
+        disabled: bool,
         cx: &Context<Self>,
     ) -> impl IntoElement {
         let weak_self = cx.weak_entity();
@@ -3359,6 +3375,7 @@ impl ThreadView {
         PopoverMenu::new("effort-selector")
             .trigger_with_tooltip(
                 ButtonLike::new_rounded_right("effort-selector-trigger")
+                    .disabled(disabled)
                     .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)),
@@ -7722,6 +7739,9 @@ impl Render for ThreadView {
                 this.toggle_fast_mode(cx);
             }))
             .on_action(cx.listener(|this, _: &ToggleThinkingMode, _window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 if let Some(thread) = this.as_native_thread(cx) {
                     thread.update(cx, |thread, cx| {
                         thread.set_thinking_enabled(!thread.thinking_enabled(), cx);
@@ -7729,9 +7749,19 @@ impl Render for ThreadView {
                 }
             }))
             .on_action(cx.listener(|this, _: &CycleThinkingEffort, _window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 this.cycle_thinking_effort(cx);
             }))
-            .on_action(cx.listener(Self::toggle_thinking_effort_menu))
+            .on_action(
+                cx.listener(|this, action: &ToggleThinkingEffortMenu, window, cx| {
+                    if this.thread.read(cx).status() != ThreadStatus::Idle {
+                        return;
+                    }
+                    this.toggle_thinking_effort_menu(action, window, cx);
+                }),
+            )
             .on_action(cx.listener(|this, _: &SendNextQueuedMessage, window, cx| {
                 this.send_queued_message_at_index(0, true, window, cx);
             }))
@@ -7749,6 +7779,9 @@ impl Render for ThreadView {
                 cx.notify();
             }))
             .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 if let Some(config_options_view) = this.config_options_view.clone() {
                     let handled = config_options_view.update(cx, |view, cx| {
                         view.toggle_category_picker(
@@ -7769,6 +7802,9 @@ impl Render for ThreadView {
                 }
             }))
             .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 if let Some(config_options_view) = this.config_options_view.clone() {
                     let handled = config_options_view.update(cx, |view, cx| {
                         view.cycle_category_option(
@@ -7793,6 +7829,9 @@ impl Render for ThreadView {
                 }
             }))
             .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 if let Some(config_options_view) = this.config_options_view.clone() {
                     let handled = config_options_view.update(cx, |view, cx| {
                         view.toggle_category_picker(
@@ -7812,6 +7851,9 @@ impl Render for ThreadView {
                 }
             }))
             .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+                if this.thread.read(cx).status() != ThreadStatus::Idle {
+                    return;
+                }
                 if let Some(config_options_view) = this.config_options_view.clone() {
                     let handled = config_options_view.update(cx, |view, cx| {
                         view.cycle_category_option(

crates/agent_ui/src/model_selector_popover.rs 🔗

@@ -3,7 +3,7 @@ use std::sync::Arc;
 
 use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
 use fs::Fs;
-use gpui::{Entity, FocusHandle};
+use gpui::{AnyView, Entity, FocusHandle};
 use picker::popover_menu::PickerPopoverMenu;
 use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
 
@@ -13,6 +13,7 @@ use crate::{ModelSelector, model_selector::acp_model_selector};
 pub struct ModelSelectorPopover {
     selector: Entity<ModelSelector>,
     menu_handle: PopoverMenuHandle<ModelSelector>,
+    disabled: bool,
 }
 
 impl ModelSelectorPopover {
@@ -30,10 +31,18 @@ impl ModelSelectorPopover {
                 acp_model_selector(selector, agent_server, fs, focus_handle.clone(), window, cx)
             }),
             menu_handle,
+            disabled: false,
         }
     }
 
+    pub fn set_disabled(&mut self, disabled: bool) {
+        self.disabled = disabled;
+    }
+
     pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
+        if self.disabled {
+            return;
+        }
         self.menu_handle.toggle(window, cx);
     }
 
@@ -42,6 +51,9 @@ impl ModelSelectorPopover {
     }
 
     pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
+        if self.disabled {
+            return;
+        }
         self.selector.update(cx, |selector, cx| {
             selector.delegate.cycle_favorite_models(window, cx);
         });
@@ -61,23 +73,31 @@ impl Render for ModelSelectorPopover {
 
         let (color, icon) = if self.menu_handle.is_deployed() {
             (Color::Accent, IconName::ChevronUp)
+        } else if self.disabled {
+            (Color::Disabled, IconName::ChevronDown)
         } else {
             (Color::Muted, IconName::ChevronDown)
         };
 
         let show_cycle_row = selector.delegate.favorites_count() > 1;
+        let disabled = self.disabled;
 
-        let tooltip = Tooltip::element({
-            move |_, _cx| {
-                ModelSelectorTooltip::new()
-                    .show_cycle_row(show_cycle_row)
-                    .into_any_element()
-            }
-        });
+        let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = if disabled {
+            Box::new(Tooltip::text("Disabled until generation is done"))
+        } else {
+            Box::new(Tooltip::element({
+                move |_, _cx| {
+                    ModelSelectorTooltip::new()
+                        .show_cycle_row(show_cycle_row)
+                        .into_any_element()
+                }
+            }))
+        };
 
         PickerPopoverMenu::new(
             self.selector.clone(),
             ButtonLike::new("active-model")
+                .disabled(self.disabled)
                 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                 .when_some(model_icon, |this, icon| {
                     this.child(
@@ -95,7 +115,17 @@ impl Render for ModelSelectorPopover {
                         .size(LabelSize::Small)
                         .ml_0p5(),
                 )
-                .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
+                .child(
+                    Icon::new(icon)
+                        .map(|this| {
+                            if self.disabled {
+                                this.color(Color::Disabled)
+                            } else {
+                                this.color(Color::Muted)
+                            }
+                        })
+                        .size(IconSize::XSmall),
+                ),
             tooltip,
             gpui::Corner::BottomRight,
             cx,

crates/agent_ui/src/profile_selector.rs 🔗

@@ -5,8 +5,8 @@ use agent_settings::{
 use fs::Fs;
 use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
 use gpui::{
-    Action, AnyElement, App, BackgroundExecutor, Context, DismissEvent, Entity, FocusHandle,
-    Focusable, ForegroundExecutor, SharedString, Subscription, Task, Window,
+    Action, AnyElement, AnyView, App, BackgroundExecutor, Context, DismissEvent, Entity,
+    FocusHandle, Focusable, ForegroundExecutor, SharedString, Subscription, Task, Window,
 };
 use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
 use settings::{Settings as _, SettingsStore, update_settings_file};
@@ -34,6 +34,7 @@ pub trait ProfileProvider {
 pub struct ProfileSelector {
     profiles: AvailableProfiles,
     pending_refresh: bool,
+    disabled: bool,
     fs: Arc<dyn Fs>,
     provider: Arc<dyn ProfileProvider>,
     picker: Option<Entity<Picker<ProfilePickerDelegate>>>,
@@ -57,6 +58,7 @@ impl ProfileSelector {
         Self {
             profiles: AgentProfile::available_profiles(cx),
             pending_refresh: false,
+            disabled: false,
             fs,
             provider,
             picker: None,
@@ -70,7 +72,19 @@ impl ProfileSelector {
         self.picker_handle.clone()
     }
 
+    pub fn set_disabled(&mut self, disabled: bool) {
+        self.disabled = disabled;
+    }
+
+    pub fn is_disabled(&self) -> bool {
+        self.disabled
+    }
+
     pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {
+        if self.disabled {
+            return;
+        }
+
         if !self.provider.profiles_supported(cx) {
             return;
         }
@@ -175,6 +189,7 @@ impl Render for ProfileSelector {
         };
 
         let trigger_button = Button::new("profile-selector", selected_profile)
+            .disabled(self.disabled)
             .label_size(LabelSize::Small)
             .color(Color::Muted)
             .icon(icon)
@@ -183,10 +198,12 @@ impl Render for ProfileSelector {
             .icon_color(Color::Muted)
             .selected_style(ButtonStyle::Tinted(TintColor::Accent));
 
-        PickerPopoverMenu::new(
-            picker,
-            trigger_button,
-            Tooltip::element({
+        let disabled = self.disabled;
+
+        let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = if disabled {
+            Box::new(Tooltip::text("Disabled until generation is done"))
+        } else {
+            Box::new(Tooltip::element({
                 move |_window, cx| {
                     let container = || h_flex().gap_1().justify_between();
                     v_flex()
@@ -206,7 +223,13 @@ impl Render for ProfileSelector {
                         )
                         .into_any()
                 }
-            }),
+            }))
+        };
+
+        PickerPopoverMenu::new(
+            picker,
+            trigger_button,
+            tooltip,
             gpui::Corner::BottomRight,
             cx,
         )