agent_ui: Make thread loading state clearer (#43765)

Danilo Leal created

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.

Change summary

crates/agent_ui/src/acp/thread_view.rs | 22 ++++++++----
crates/agent_ui/src/agent_panel.rs     | 49 ++++++++++++++++-----------
2 files changed, 43 insertions(+), 28 deletions(-)

Detailed changes

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>) {
         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(),
         }
     }
 }

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")