assistant2: Improve clarity of loading state (#26178)

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
This commit is contained in:
Danilo Leal 2025-03-05 21:39:29 -03:00 committed by GitHub
parent e505d6bf5b
commit 43339c6869
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 192 additions and 153 deletions

View file

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

View file

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