From be8605bb1082070c74ed517a00fe256cfbfbc56d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:52:44 -0300 Subject: [PATCH] agent_ui: Make thread loading state clearer (#43765) Closes https://github.com/zed-industries/zed/issues/43721 This PR makes the loading state clearer by disabling the message editor while the agent is loading as well as pulsating the icon. Release Notes: - agent: Made the thread loading state clearer in the agent panel. --- crates/agent_ui/src/acp/thread_view.rs | 22 +++++++----- crates/agent_ui/src/agent_panel.rs | 49 +++++++++++++++----------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d77b6e51c12f485ddc553d43f0eb83cdf8196577..a9b4127ea97f62dde3cb2af299050bc0e06a06bc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -675,6 +675,8 @@ impl AcpThreadView { }) }); + this.message_editor.focus_handle(cx).focus(window); + cx.notify(); } Err(err) => { @@ -1009,6 +1011,10 @@ impl AcpThreadView { } } + pub fn is_loading(&self) -> bool { + matches!(self.thread_state, ThreadState::Loading { .. }) + } + fn resume_chat(&mut self, cx: &mut Context) { self.thread_error.take(); let Some(thread) = self.thread() else { @@ -4196,8 +4202,10 @@ impl AcpThreadView { .block_mouse_except_scroll(); let enable_editor = match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, - ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, + ThreadState::Ready { .. } => true, + ThreadState::Loading { .. } + | ThreadState::Unauthenticated { .. } + | ThreadState::LoadError(..) => false, }; v_flex() @@ -5858,12 +5866,10 @@ fn placeholder_text(agent_name: &str, has_commands: bool) -> String { impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => { - self.active_editor(cx).focus_handle(cx) - } - ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { - self.focus_handle.clone() - } + ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx), + ThreadState::Loading { .. } + | ThreadState::LoadError(_) + | ThreadState::Unauthenticated { .. } => self.focus_handle.clone(), } } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index aa152018b180047815cc461d80e48dba0996b3cd..9dd77774ff4e6f00bdfd26d024e9ee4b389b7f7e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,7 +1,4 @@ -use std::ops::Range; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; +use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration}; use acp_thread::AcpThread; use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}; @@ -46,9 +43,9 @@ use extension::ExtensionEvents; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, Entity, EventEmitter, - ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal, - WeakEntity, prelude::*, + Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, + Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, + Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ConfigurationError, LanguageModelRegistry}; @@ -58,10 +55,9 @@ use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, update_settings_file}; use theme::ThemeSettings; -use ui::utils::WithRemSize; use ui::{ Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, - ProgressBar, Tab, Tooltip, prelude::*, + ProgressBar, Tab, Tooltip, prelude::*, utils::WithRemSize, }; use util::ResultExt as _; use workspace::{ @@ -2159,28 +2155,41 @@ impl AgentPanel { let selected_agent_label = self.selected_agent.label(); + let is_thread_loading = self + .active_thread_view() + .map(|thread| thread.read(cx).is_loading()) + .unwrap_or(false); + let has_custom_icon = selected_agent_custom_icon.is_some(); + let selected_agent = div() .id("selected_agent_icon") .when_some(selected_agent_custom_icon, |this, icon_path| { - let label = selected_agent_label.clone(); this.px_1() .child(Icon::from_external_svg(icon_path).color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::with_meta(label.clone(), None, "Selected Agent", cx) - }) }) .when(!has_custom_icon, |this| { this.when_some(self.selected_agent.icon(), |this, icon| { - let label = selected_agent_label.clone(); - this.px_1() - .child(Icon::new(icon).color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::with_meta(label.clone(), None, "Selected Agent", cx) - }) + this.px_1().child(Icon::new(icon).color(Color::Muted)) }) }) - .into_any_element(); + .tooltip(move |_, cx| { + Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx) + }); + + let selected_agent = if is_thread_loading { + selected_agent + .with_animation( + "pulsating-icon", + Animation::new(Duration::from_secs(1)) + .repeat() + .with_easing(pulsating_between(0.2, 0.6)), + |icon, delta| icon.opacity(delta), + ) + .into_any_element() + } else { + selected_agent.into_any_element() + }; h_flex() .id("agent-panel-toolbar")