From 698cdc4d1aea213e6946bb8479f758ffb7981cbe Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:31:32 -0300 Subject: [PATCH] agent: Display "generating" label in the active thread (#28297) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- crates/agent/src/active_thread.rs | 238 ++++++++++++++++-------- crates/ui/src/components/label/label.rs | 5 + 2 files changed, 162 insertions(+), 81 deletions(-) diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 58069c5f69da3906fe9fd014706d9844f232b456..f489a07f8def3affb13904749570408841ef6a58 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -1199,16 +1199,62 @@ impl ActiveThread { let context_store = self.context_store.clone(); let workspace = self.workspace.clone(); - let thread = self.thread.read(cx); + // Get all the data we need from thread before we start using it in closures let checkpoint = thread.checkpoint_for_message(message_id); let context = thread.context_for_message(message_id).collect::>(); + let tool_uses = thread.tool_uses_for_message(message_id, cx); let has_tool_uses = !tool_uses.is_empty(); + let is_generating = thread.is_generating(); + + let is_first_message = ix == 0; + let is_last_message = ix == self.messages.len() - 1; + let show_feedback = is_last_message && message.role != Role::User; + + let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation); + + let generating_label = (is_generating && is_last_message).then(|| { + Label::new("Generating") + .color(Color::Muted) + .size(LabelSize::Small) + .with_animation( + "generating-label", + Animation::new(Duration::from_secs(1)).repeat(), + |mut label, delta| { + let text = match delta { + d if d < 0.25 => "Generating", + d if d < 0.5 => "Generating.", + d if d < 0.75 => "Generating..", + _ => "Generating...", + }; + label.set_text(text); + label + }, + ) + .with_animation( + "pulsating-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| label.map_element(|label| label.alpha(delta)), + ) + }); // Don't render user messages that are just there for returning tool results. if message.role == Role::User && thread.message_has_tool_results(message_id) { + if let Some(generating_label) = generating_label { + return h_flex() + .w_full() + .h_10() + .py_1p5() + .pl_4() + .pb_3() + .child(generating_label) + .into_any_element(); + } + return Empty.into_any(); } @@ -1220,9 +1266,6 @@ impl ActiveThread { .filter(|(id, _)| *id == message_id) .map(|(_, state)| state.editor.clone()); - let first_message = ix == 0; - let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User; - let colors = cx.theme().colors(); let active_color = colors.element_active; let editor_bg_color = colors.editor_background; @@ -1391,7 +1434,7 @@ impl ActiveThread { Role::User => v_flex() .id(("message-container", ix)) .map(|this| { - if first_message { + if is_first_message { this.pt_2() } else { this.pt_4() @@ -1509,15 +1552,11 @@ impl ActiveThread { .border_l_1() .border_color(cx.theme().colors().border_variant) .children(message_content) - .gap_2p5() - .pb_2p5() - .when(!tool_uses.is_empty(), |parent| { - parent.child( - div().children( - tool_uses - .into_iter() - .map(|tool_use| self.render_tool_use(tool_use, window, cx)), - ), + .when(has_tool_uses, |parent| { + parent.children( + tool_uses + .into_iter() + .map(|tool_use| self.render_tool_use(tool_use, window, cx)), ) }), Role::System => div().id(("message-container", ix)).py_1().px_2().child( @@ -1530,9 +1569,6 @@ impl ActiveThread { v_flex() .w_full() - .when(first_message, |parent| { - parent.child(self.render_rules_item(cx)) - }) .when_some(checkpoint, |parent, checkpoint| { let mut is_pending = false; let mut error = None; @@ -1602,65 +1638,56 @@ impl ActiveThread { .child(ui::Divider::horizontal()), ) }) + .when(is_first_message, |parent| { + parent.child(self.render_rules_item(cx)) + }) .child(styled_message) - .when( - show_feedback && !self.thread.read(cx).is_generating(), - |parent| { - parent.child(feedback_items).when_some( - self.feedback_message_editor.clone(), - |parent, feedback_editor| { - let focus_handle = feedback_editor.focus_handle(cx); - parent.child( - v_flex() - .key_context("AgentFeedbackMessageEditor") - .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| { - this.feedback_message_editor = None; - cx.notify(); - })) - .on_action(cx.listener(|this, _: &menu::Confirm, _, cx| { - this.submit_feedback_message(cx); - cx.notify(); - })) - .on_action(cx.listener(Self::confirm_editing_message)) - .mx_4() - .mb_3() - .p_2() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child(feedback_editor) - .child( - h_flex() - .gap_1() - .justify_end() - .child( - Button::new("dismiss-feedback-message", "Cancel") - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &menu::Cancel, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, _, cx| { - this.feedback_message_editor = None; - cx.notify(); - })), - ) - .child( - Button::new( - "submit-feedback-message", - "Share Feedback", - ) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when(!needs_confirmation && generating_label.is_some(), |this| { + this.child( + h_flex() + .h_8() + .mt_2() + .mb_4() + .ml_4() + .py_1p5() + .child(generating_label.unwrap()), + ) + }) + .when(show_feedback && !is_generating, |parent| { + parent.child(feedback_items).when_some( + self.feedback_message_editor.clone(), + |parent, feedback_editor| { + let focus_handle = feedback_editor.focus_handle(cx); + parent.child( + v_flex() + .key_context("AgentFeedbackMessageEditor") + .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| { + this.feedback_message_editor = None; + cx.notify(); + })) + .on_action(cx.listener(|this, _: &menu::Confirm, _, cx| { + this.submit_feedback_message(cx); + cx.notify(); + })) + .on_action(cx.listener(Self::confirm_editing_message)) + .my_3() + .mx_4() + .p_2() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .child(feedback_editor) + .child( + h_flex() + .gap_1() + .justify_end() + .child( + Button::new("dismiss-feedback-message", "Cancel") .label_size(LabelSize::Small) .key_binding( KeyBinding::for_action_in( - &menu::Confirm, + &menu::Cancel, &focus_handle, window, cx, @@ -1668,16 +1695,38 @@ impl ActiveThread { .map(|kb| kb.size(rems_from_px(10.))), ) .on_click(cx.listener(|this, _, _, cx| { - this.submit_feedback_message(cx); + this.feedback_message_editor = None; cx.notify(); })), + ) + .child( + Button::new( + "submit-feedback-message", + "Share Feedback", + ) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click( + cx.listener(|this, _, _, cx| { + this.submit_feedback_message(cx); + cx.notify(); + }), ), - ), - ) - }, - ) - }, - ) + ), + ), + ) + }, + ) + }) .into_any() } @@ -2160,6 +2209,7 @@ impl ActiveThread { if !tool_use.needs_confirmation { element.child( v_flex() + .my_1p5() .child( h_flex() .group("disclosure-header") @@ -2231,6 +2281,7 @@ impl ActiveThread { ) } else { v_flex() + .my_3() .rounded_lg() .border_1() .border_color(self.tool_card_border_color(cx)) @@ -2333,7 +2384,32 @@ impl ActiveThread { .border_t_1() .border_color(self.tool_card_border_color(cx)) .rounded_b_lg() - .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small)) + .child( + Label::new("Waiting for Confirmation…") + .color(Color::Muted) + .size(LabelSize::Small) + .with_animation( + "generating-label", + Animation::new(Duration::from_secs(1)).repeat(), + |mut label, delta| { + let text = match delta { + d if d < 0.25 => "Waiting for Confirmation", + d if d < 0.5 => "Waiting for Confirmation.", + d if d < 0.75 => "Waiting for Confirmation..", + _ => "Waiting for Confirmation...", + }; + label.set_text(text); + label + }, + ) + .with_animation( + "pulsating-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| label.map_element(|label| label.alpha(delta)), + ), + ) .child( h_flex() .gap_0p5() @@ -2448,7 +2524,7 @@ impl ActiveThread { }; div() - .pt_1() + .pt_2() .px_2p5() .child( h_flex() diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 388984d3c96970b17111f8461d404aa8159f745f..4fb280233ae161e0e288ebaca4bc7dbb185cfb9b 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -51,6 +51,11 @@ impl Label { label: label.into(), } } + + /// Sets the text of the [`Label`]. + pub fn set_text(&mut self, text: impl Into) { + self.label = text.into(); + } } // nate: If we are going to do this, we might as well just