assistant2: Improve clarity of loading state (#26178)

Danilo Leal created

Follow up to https://github.com/zed-industries/zed/pull/23299.

Having the loading state on the button makes sense, but it's also too
subtle. If you're waiting on an LLM response that takes a while, like a
"thinking state", not having anything more clearly visible communicating
that the model is still in-progress can make you think something is
wrong.

<img
src="https://github.com/user-attachments/assets/da64516e-5540-4294-97a2-e4542ce704f3"
width="700px" />

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant_panel.rs |   7 
crates/assistant2/src/message_editor.rs  | 336 ++++++++++++++-----------
2 files changed, 191 insertions(+), 152 deletions(-)

Detailed changes

crates/assistant2/src/assistant_panel.rs 🔗

@@ -1023,12 +1023,7 @@ impl Render for AssistantPanel {
             .map(|parent| match self.active_view {
                 ActiveView::Thread => parent
                     .child(self.render_active_thread_or_empty_state(window, cx))
-                    .child(
-                        h_flex()
-                            .border_t_1()
-                            .border_color(cx.theme().colors().border)
-                            .child(self.message_editor.clone()),
-                    )
+                    .child(h_flex().child(self.message_editor.clone()))
                     .children(self.render_last_error(cx)),
                 ActiveView::History => parent.child(self.history.clone()),
                 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),

crates/assistant2/src/message_editor.rs 🔗

@@ -4,8 +4,8 @@ use editor::actions::MoveUp;
 use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
 use fs::Fs;
 use gpui::{
-    pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
-    TextStyle, WeakEntity,
+    Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
+    WeakEntity,
 };
 use language_model::LanguageModelRegistry;
 use language_model_selector::ToggleModelSelector;
@@ -16,7 +16,7 @@ use text::Bias;
 use theme::ThemeSettings;
 use ui::{
     prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
-    TintColor, Tooltip,
+    Tooltip,
 };
 use vim_mode_setting::VimModeSetting;
 use workspace::Workspace;
@@ -298,166 +298,210 @@ impl Render for MessageEditor {
         let linux = platform == PlatformStyle::Linux;
         let windows = platform == PlatformStyle::Windows;
         let button_width = if linux || windows || vim_mode_enabled {
-            px(92.)
+            px(82.)
         } else {
             px(64.)
         };
 
         v_flex()
-            .key_context("MessageEditor")
-            .on_action(cx.listener(Self::chat))
-            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
-                this.model_selector
-                    .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
-            }))
-            .on_action(cx.listener(Self::toggle_context_picker))
-            .on_action(cx.listener(Self::remove_all_context))
-            .on_action(cx.listener(Self::move_up))
-            .on_action(cx.listener(Self::toggle_chat_mode))
             .size_full()
-            .gap_2()
-            .p_2()
-            .bg(bg_color)
-            .child(self.context_strip.clone())
+            .when(is_streaming_completion, |parent| {
+                let focus_handle = self.editor.focus_handle(cx).clone();
+                parent.child(
+                    h_flex().py_3().w_full().justify_center().child(
+                        h_flex()
+                            .flex_none()
+                            .pl_2()
+                            .pr_1()
+                            .py_1()
+                            .bg(cx.theme().colors().editor_background)
+                            .border_1()
+                            .border_color(cx.theme().colors().border_variant)
+                            .rounded_lg()
+                            .shadow_md()
+                            .gap_1()
+                            .child(
+                                Icon::new(IconName::ArrowCircle)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Muted)
+                                    .with_animation(
+                                        "arrow-circle",
+                                        Animation::new(Duration::from_secs(2)).repeat(),
+                                        |icon, delta| {
+                                            icon.transform(gpui::Transformation::rotate(
+                                                gpui::percentage(delta),
+                                            ))
+                                        },
+                                    ),
+                            )
+                            .child(
+                                Label::new("Generating…")
+                                    .size(LabelSize::XSmall)
+                                    .color(Color::Muted),
+                            )
+                            .child(ui::Divider::vertical())
+                            .child(
+                                Button::new("cancel-generation", "Cancel")
+                                    .label_size(LabelSize::XSmall)
+                                    .key_binding(
+                                        KeyBinding::for_action_in(
+                                            &editor::actions::Cancel,
+                                            &focus_handle,
+                                            window,
+                                            cx,
+                                        )
+                                        .map(|kb| kb.size(rems_from_px(10.))),
+                                    )
+                                    .on_click(move |_event, window, cx| {
+                                        focus_handle.dispatch_action(
+                                            &editor::actions::Cancel,
+                                            window,
+                                            cx,
+                                        );
+                                    }),
+                            ),
+                    ),
+                )
+            })
             .child(
                 v_flex()
-                    .gap_5()
-                    .child({
-                        let settings = ThemeSettings::get_global(cx);
-                        let text_style = TextStyle {
-                            color: cx.theme().colors().text,
-                            font_family: settings.ui_font.family.clone(),
-                            font_features: settings.ui_font.features.clone(),
-                            font_size: font_size.into(),
-                            font_weight: settings.ui_font.weight,
-                            line_height: line_height.into(),
-                            ..Default::default()
-                        };
-
-                        EditorElement::new(
-                            &self.editor,
-                            EditorStyle {
-                                background: bg_color,
-                                local_player: cx.theme().players().local(),
-                                text: text_style,
-                                ..Default::default()
-                            },
-                        )
-                    })
+                    .key_context("MessageEditor")
+                    .on_action(cx.listener(Self::chat))
+                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+                        this.model_selector
+                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
+                    }))
+                    .on_action(cx.listener(Self::toggle_context_picker))
+                    .on_action(cx.listener(Self::remove_all_context))
+                    .on_action(cx.listener(Self::move_up))
+                    .on_action(cx.listener(Self::toggle_chat_mode))
+                    .gap_2()
+                    .p_2()
+                    .bg(bg_color)
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border)
+                    .child(self.context_strip.clone())
                     .child(
-                        PopoverMenu::new("inline-context-picker")
-                            .menu(move |window, cx| {
-                                inline_context_picker.update(cx, |this, cx| {
-                                    this.init(window, cx);
-                                });
+                        v_flex()
+                            .gap_5()
+                            .child({
+                                let settings = ThemeSettings::get_global(cx);
+                                let text_style = TextStyle {
+                                    color: cx.theme().colors().text,
+                                    font_family: settings.ui_font.family.clone(),
+                                    font_features: settings.ui_font.features.clone(),
+                                    font_size: font_size.into(),
+                                    font_weight: settings.ui_font.weight,
+                                    line_height: line_height.into(),
+                                    ..Default::default()
+                                };
 
-                                Some(inline_context_picker.clone())
-                            })
-                            .attach(gpui::Corner::TopLeft)
-                            .anchor(gpui::Corner::BottomLeft)
-                            .offset(gpui::Point {
-                                x: px(0.0),
-                                y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
+                                EditorElement::new(
+                                    &self.editor,
+                                    EditorStyle {
+                                        background: bg_color,
+                                        local_player: cx.theme().players().local(),
+                                        text: text_style,
+                                        ..Default::default()
+                                    },
+                                )
                             })
-                            .with_handle(self.inline_context_picker_menu_handle.clone()),
-                    )
-                    .child(
-                        h_flex()
-                            .justify_between()
                             .child(
-                                Switch::new("use-tools", self.use_tools.into())
-                                    .label("Tools")
-                                    .on_click(cx.listener(|this, selection, _window, _cx| {
-                                        this.use_tools = match selection {
-                                            ToggleState::Selected => true,
-                                            ToggleState::Unselected
-                                            | ToggleState::Indeterminate => false,
-                                        };
-                                    }))
-                                    .key_binding(KeyBinding::for_action_in(
-                                        &ChatMode,
-                                        &focus_handle,
-                                        window,
-                                        cx,
-                                    )),
+                                PopoverMenu::new("inline-context-picker")
+                                    .menu(move |window, cx| {
+                                        inline_context_picker.update(cx, |this, cx| {
+                                            this.init(window, cx);
+                                        });
+
+                                        Some(inline_context_picker.clone())
+                                    })
+                                    .attach(gpui::Corner::TopLeft)
+                                    .anchor(gpui::Corner::BottomLeft)
+                                    .offset(gpui::Point {
+                                        x: px(0.0),
+                                        y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
+                                            - px(4.0),
+                                    })
+                                    .with_handle(self.inline_context_picker_menu_handle.clone()),
                             )
-                            .child(h_flex().gap_1().child(self.model_selector.clone()).child(
-                                if is_streaming_completion {
-                                    ButtonLike::new("cancel-generation")
-                                        .width(button_width.into())
-                                        .style(ButtonStyle::Tinted(TintColor::Accent))
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
-                                                .child(
-                                                    Label::new("Cancel")
-                                                        .size(LabelSize::Small)
-                                                        .with_animation(
-                                                            "pulsating-label",
-                                                            Animation::new(Duration::from_secs(2))
-                                                                .repeat()
-                                                                .with_easing(pulsating_between(
-                                                                    0.4, 0.8,
-                                                                )),
-                                                            |label, delta| label.alpha(delta),
-                                                        ),
-                                                )
-                                                .children(
-                                                    KeyBinding::for_action_in(
-                                                        &editor::actions::Cancel,
-                                                        &focus_handle,
-                                                        window,
-                                                        cx,
-                                                    )
-                                                    .map(|binding| binding.into_any_element()),
-                                                ),
-                                        )
-                                        .on_click(move |_event, window, cx| {
-                                            focus_handle.dispatch_action(
-                                                &editor::actions::Cancel,
+                            .child(
+                                h_flex()
+                                    .justify_between()
+                                    .child(
+                                        Switch::new("use-tools", self.use_tools.into())
+                                            .label("Tools")
+                                            .on_click(cx.listener(
+                                                |this, selection, _window, _cx| {
+                                                    this.use_tools = match selection {
+                                                        ToggleState::Selected => true,
+                                                        ToggleState::Unselected
+                                                        | ToggleState::Indeterminate => false,
+                                                    };
+                                                },
+                                            ))
+                                            .key_binding(KeyBinding::for_action_in(
+                                                &ChatMode,
+                                                &focus_handle,
                                                 window,
                                                 cx,
-                                            );
-                                        })
-                                } else {
-                                    ButtonLike::new("submit-message")
-                                        .width(button_width.into())
-                                        .style(ButtonStyle::Filled)
-                                        .disabled(is_editor_empty || !is_model_selected)
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
+                                            )),
+                                    )
+                                    .child(
+                                        h_flex().gap_1().child(self.model_selector.clone()).child(
+                                            ButtonLike::new("submit-message")
+                                                .width(button_width.into())
+                                                .style(ButtonStyle::Filled)
+                                                .disabled(
+                                                    is_editor_empty
+                                                        || !is_model_selected
+                                                        || is_streaming_completion,
+                                                )
                                                 .child(
-                                                    Label::new("Submit")
-                                                        .size(LabelSize::Small)
-                                                        .color(submit_label_color),
+                                                    h_flex()
+                                                        .w_full()
+                                                        .justify_between()
+                                                        .child(
+                                                            Label::new("Submit")
+                                                                .size(LabelSize::Small)
+                                                                .color(submit_label_color),
+                                                        )
+                                                        .children(
+                                                            KeyBinding::for_action_in(
+                                                                &Chat,
+                                                                &focus_handle,
+                                                                window,
+                                                                cx,
+                                                            )
+                                                            .map(|binding| {
+                                                                binding
+                                                                    .when(vim_mode_enabled, |kb| {
+                                                                        kb.size(rems_from_px(12.))
+                                                                    })
+                                                                    .into_any_element()
+                                                            }),
+                                                        ),
                                                 )
-                                                .children(
-                                                    KeyBinding::for_action_in(
-                                                        &Chat,
-                                                        &focus_handle,
-                                                        window,
-                                                        cx,
-                                                    )
-                                                    .map(|binding| binding.into_any_element()),
-                                                ),
-                                        )
-                                        .on_click(move |_event, window, cx| {
-                                            focus_handle.dispatch_action(&Chat, window, cx);
-                                        })
-                                        .when(is_editor_empty, |button| {
-                                            button
-                                                .tooltip(Tooltip::text("Type a message to submit"))
-                                        })
-                                        .when(!is_model_selected, |button| {
-                                            button.tooltip(Tooltip::text(
-                                                "Select a model to continue",
-                                            ))
-                                        })
-                                },
-                            )),
+                                                .on_click(move |_event, window, cx| {
+                                                    focus_handle.dispatch_action(&Chat, window, cx);
+                                                })
+                                                .when(is_editor_empty, |button| {
+                                                    button.tooltip(Tooltip::text(
+                                                        "Type a message to submit",
+                                                    ))
+                                                })
+                                                .when(is_streaming_completion, |button| {
+                                                    button.tooltip(Tooltip::text(
+                                                        "Cancel to submit a new message",
+                                                    ))
+                                                })
+                                                .when(!is_model_selected, |button| {
+                                                    button.tooltip(Tooltip::text(
+                                                        "Select a model to continue",
+                                                    ))
+                                                }),
+                                        ),
+                                    ),
+                            ),
                     ),
             )
     }