agent: Fix layout shift due to the "Generating" label (#30422)

Danilo Leal created

Closes https://github.com/zed-industries/zed/issues/30238

Release Notes:

- agent: Fixed layout shift happening in the toolbar (both in the
singleton and multibuffers) due to the "Generating" label that appeared
while the agent is still generating a response.

Change summary

assets/icons/load_circle.svg   |  1 +
crates/agent/src/agent_diff.rs | 31 +++++++++++++++++++++----------
crates/icons/src/icons.rs      |  1 +
3 files changed, 23 insertions(+), 10 deletions(-)

Detailed changes

assets/icons/load_circle.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>

crates/agent/src/agent_diff.rs πŸ”—

@@ -1,6 +1,4 @@
-use crate::{
-    Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel,
-};
+use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
 use anyhow::Result;
 use assistant_settings::AssistantSettings;
 use buffer_diff::DiffHunkStatus;
@@ -11,8 +9,9 @@ use editor::{
     scroll::Autoscroll,
 };
 use gpui::{
-    Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
-    Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
+    Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
+    EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
+    WeakEntity, Window, percentage, prelude::*,
 };
 
 use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -25,6 +24,7 @@ use std::{
     collections::hash_map::Entry,
     ops::Range,
     sync::Arc,
+    time::Duration,
 };
 use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
 use util::ResultExt;
@@ -978,9 +978,20 @@ impl ToolbarItemView for AgentDiffToolbar {
 
 impl Render for AgentDiffToolbar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let generating_label = div()
-            .w(rems_from_px(110.)) // Arbitrary size so the label doesn't dance around
-            .child(AnimatedLabel::new("Generating"))
+        let spinner_icon = div()
+            .px_0p5()
+            .id("generating")
+            .tooltip(Tooltip::text("Generating Changes…"))
+            .child(
+                Icon::new(IconName::LoadCircle)
+                    .size(IconSize::Small)
+                    .color(Color::Accent)
+                    .with_animation(
+                        "load_circle",
+                        Animation::new(Duration::from_secs(3)).repeat(),
+                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+                    ),
+            )
             .into_any();
 
         let Some(active_item) = self.active_item.as_ref() else {
@@ -997,7 +1008,7 @@ impl Render for AgentDiffToolbar {
 
                 let content = match state {
                     EditorState::Idle => return Empty.into_any(),
-                    EditorState::Generating => vec![generating_label],
+                    EditorState::Generating => vec![spinner_icon],
                     EditorState::Reviewing => vec![
                         h_flex()
                             .child(
@@ -1115,7 +1126,7 @@ impl Render for AgentDiffToolbar {
 
                 let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
                 if is_generating {
-                    return div().px_2().child(generating_label).into_any();
+                    return div().px_2().child(spinner_icon).into_any();
                 }
 
                 let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();