From 225deb6785ad49734cb1835a9c1de79a33da2ca2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 2 May 2025 05:48:40 -0700 Subject: [PATCH] agent: Add animation in the edit file tool card until a diff is assigned (#29773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR prevents this edit card from being shown expanded but empty, like this: Screenshot 2025-05-01 at 7 38 47 PM Now, we will show an animation until it has a diff computed. https://github.com/user-attachments/assets/52900cdf-ee3d-4c3b-88c7-c53377543bcf Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- crates/assistant_tools/src/edit_file_tool.rs | 213 ++++++++++++------- 1 file changed, 135 insertions(+), 78 deletions(-) diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index be276fcc81..ff080e3157 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -7,7 +7,8 @@ use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUse use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorMode, MultiBuffer, PathKey}; use gpui::{ - AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity, + Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, + Task, WeakEntity, pulsating_between, }; use language::{ Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer, @@ -20,6 +21,7 @@ use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, sync::Arc, + time::Duration, }; use ui::{Disclosure, Tooltip, Window, prelude::*}; use util::ResultExt; @@ -323,6 +325,10 @@ impl EditFileToolCard { } } + pub fn has_diff(&self) -> bool { + self.total_lines.is_some() + } + pub fn set_diff( &mut self, path: Arc, @@ -463,45 +469,44 @@ impl ToolCard for EditFileToolCard { .rounded_t_md() .when(!failed, |header| header.bg(codeblock_header_bg)) .child(path_label_button) - .map(|container| { - if failed { - container.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .child( - Disclosure::new( - ("edit-file-error-disclosure", self.editor_unique_id), - self.error_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener( - move |this, _event, _window, _cx| { - this.error_expanded = !this.error_expanded; - }, - )), - ), - ) - } else { - container.child( - Disclosure::new( - ("edit-file-disclosure", self.editor_unique_id), - self.preview_expanded, + .when(failed, |header| { + header.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener( - move |this, _event, _window, _cx| { - this.preview_expanded = !this.preview_expanded; - }, - )), + .child( + Disclosure::new( + ("edit-file-error-disclosure", self.editor_unique_id), + self.error_expanded, + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener( + move |this, _event, _window, _cx| { + this.error_expanded = !this.error_expanded; + }, + )), + ), + ) + }) + .when(!failed && self.has_diff(), |header| { + header.child( + Disclosure::new( + ("edit-file-disclosure", self.editor_unique_id), + self.preview_expanded, ) - } + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener( + move |this, _event, _window, _cx| { + this.preview_expanded = !this.preview_expanded; + }, + )), + ) }); let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| { @@ -538,6 +543,50 @@ impl ToolCard for EditFileToolCard { const DEFAULT_COLLAPSED_LINES: u32 = 10; let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES; + let waiting_for_diff = { + let styles = [ + ("w_4_5", (0.1, 0.85), 2000), + ("w_1_4", (0.2, 0.75), 2200), + ("w_2_4", (0.15, 0.64), 1900), + ("w_3_5", (0.25, 0.72), 2300), + ("w_2_5", (0.3, 0.56), 1800), + ]; + + let mut container = v_flex() + .p_3() + .gap_1p5() + .border_t_1() + .border_color(border_color) + .bg(cx.theme().colors().editor_background); + + for (width_method, pulse_range, duration_ms) in styles.iter() { + let (min_opacity, max_opacity) = *pulse_range; + let placeholder = match *width_method { + "w_4_5" => div().w_3_4(), + "w_1_4" => div().w_1_4(), + "w_2_4" => div().w_2_4(), + "w_3_5" => div().w_3_5(), + "w_2_5" => div().w_2_5(), + _ => div().w_1_2(), + } + .id("loading_div") + .h_2() + .rounded_full() + .bg(cx.theme().colors().element_active) + .with_animation( + "loading_pulsate", + Animation::new(Duration::from_millis(*duration_ms)) + .repeat() + .with_easing(pulsating_between(min_opacity, max_opacity)), + |label, delta| label.opacity(delta), + ); + + container = container.child(placeholder); + } + + container + }; + v_flex() .mb_2() .border_1() @@ -573,50 +622,58 @@ impl ToolCard for EditFileToolCard { ), ) }) - .when(!failed && self.preview_expanded, |card| { - card.child( - v_flex() - .relative() - .h_full() - .when(!self.full_height_expanded, |editor_container| { - editor_container - .max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height) - }) - .overflow_hidden() - .border_t_1() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .child(div().pl_1().child(editor)) - .when( - !self.full_height_expanded && is_collapsible, - |editor_container| editor_container.child(gradient_overlay), - ), - ) - .when(is_collapsible, |editor_container| { - editor_container.child( - h_flex() - .id(("expand-button", self.editor_unique_id)) - .flex_none() - .cursor_pointer() - .h_5() - .justify_center() - .rounded_b_md() + .when(!self.has_diff() && !failed, |card| { + card.child(waiting_for_diff) + }) + .when( + !failed && self.preview_expanded && self.has_diff(), + |card| { + card.child( + v_flex() + .relative() + .h_full() + .when(!self.full_height_expanded, |editor_container| { + editor_container + .max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height) + }) + .overflow_hidden() .border_t_1() .border_color(border_color) .bg(cx.theme().colors().editor_background) - .hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1))) - .child( - Icon::new(full_height_icon) - .size(IconSize::Small) - .color(Color::Muted), - ) - .tooltip(Tooltip::text(full_height_tooltip_label)) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.full_height_expanded = !this.full_height_expanded; - })), + .child(div().pl_1().child(editor)) + .when( + !self.full_height_expanded && is_collapsible, + |editor_container| editor_container.child(gradient_overlay), + ), ) - }) - }) + .when(is_collapsible, |editor_container| { + editor_container.child( + h_flex() + .id(("expand-button", self.editor_unique_id)) + .flex_none() + .cursor_pointer() + .h_5() + .justify_center() + .rounded_b_md() + .border_t_1() + .border_color(border_color) + .bg(cx.theme().colors().editor_background) + .hover(|style| { + style.bg(cx.theme().colors().element_hover.opacity(0.1)) + }) + .child( + Icon::new(full_height_icon) + .size(IconSize::Small) + .color(Color::Muted), + ) + .tooltip(Tooltip::text(full_height_tooltip_label)) + .on_click(cx.listener(move |this, _event, _window, _cx| { + this.full_height_expanded = !this.full_height_expanded; + })), + ) + }) + }, + ) } }