From 2df06cd2e4dbe1c8f348db01565913c8f75f6fe5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:13:49 -0300 Subject: [PATCH] agent: Improve thinking design display (#28186) Release Notes: - N/A --- assets/icons/light_bulb.svg | 3 + crates/agent/src/active_thread.rs | 375 ++++++++++-------- .../src/context_editor.rs | 4 +- crates/assistant_tools/src/thinking_tool.rs | 2 +- crates/icons/src/icons.rs | 1 + 5 files changed, 219 insertions(+), 166 deletions(-) create mode 100644 assets/icons/light_bulb.svg diff --git a/assets/icons/light_bulb.svg b/assets/icons/light_bulb.svg new file mode 100644 index 0000000000..61a8f04211 --- /dev/null +++ b/assets/icons/light_bulb.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 3bd9d33786..2b54fe962f 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -1756,7 +1756,7 @@ impl ActiveThread { None }; - div() + v_flex() .text_ui(cx) .gap_2() .children( @@ -1841,177 +1841,225 @@ impl ActiveThread { .copied() .unwrap_or_default(); - let editor_bg = cx.theme().colors().editor_background; + let editor_bg = cx.theme().colors().panel_background; - div().pt_0p5().pb_2().child( - v_flex() - .rounded_lg() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .child( - h_flex() - .group("disclosure-header") - .justify_between() - .py_1() - .px_2() - .bg(self.tool_card_header_bg(cx)) - .map(|this| { - if pending || is_open { - this.rounded_t_md() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) - } else { - this.rounded_md() - } - }) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Brain) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child({ - if pending { - Label::new("Thinking…") + div().map(|this| { + if pending { + this.v_flex() + .mt_neg_2() + .mb_1p5() + .child( + h_flex() + .group("disclosure-header") + .justify_between() + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::LightBulb) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child({ + Label::new("Thinking") + .color(Color::Muted) .size(LabelSize::Small) - .buffer_font(cx) + .with_animation( + "generating-label", + Animation::new(Duration::from_secs(1)).repeat(), + |mut label, delta| { + let text = match delta { + d if d < 0.25 => "Thinking", + d if d < 0.5 => "Thinking.", + d if d < 0.75 => "Thinking..", + _ => "Thinking...", + }; + label.set_text(text); + label + }, + ) .with_animation( "pulsating-label", Animation::new(Duration::from_secs(2)) .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.alpha(delta), + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| { + label.map_element(|label| label.alpha(delta)) + }, ) - .into_any_element() - } else { - Label::new("Thought Process") - .size(LabelSize::Small) - .buffer_font(cx) - .into_any_element() - } - }), - ) - .child( - h_flex() - .gap_1() - .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("thinking-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - move |this, _event, _window, _cx| { - let is_open = this - .expanded_thinking_segments - .entry((message_id, ix)) - .or_insert(false); - - *is_open = !*is_open; - } - })), - ), - ) - .child({ - let (icon_name, color, animated) = if pending { - (IconName::ArrowCircle, Color::Accent, true) - } else { - (IconName::Check, Color::Success, false) - }; - - let icon = - Icon::new(icon_name).color(color).size(IconSize::Small); - - if animated { - icon.with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ) - .into_any_element() - } else { - icon.into_any_element() - } - }), - ), - ) - .when(pending && !is_open, |this| { - let gradient_overlay = div() - .rounded_b_lg() - .h_20() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - this.child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", ix)) - .p_2() - .h_20() - .track_scroll(scroll_handle) - .text_ui_sm(cx) - .child( - MarkdownElement::new( - markdown.clone(), - default_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link( - text, - workspace.clone(), - window, - cx, - ); - } - }), - ) - .overflow_hidden(), + }), ) - .child(gradient_overlay), - ) - }) - .when(is_open, |this| { - this.child( - div() - .id(("thinking-content", ix)) - .h_full() - .p_2() - .rounded_b_lg() - .bg(editor_bg) - .text_ui_sm(cx) .child( - MarkdownElement::new( - markdown.clone(), - default_markdown_style(window, cx), - ) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - open_markdown_link(text, workspace.clone(), window, cx); - } - }), + h_flex() + .gap_1() + .child( + div().visible_on_hover("disclosure-header").child( + Disclosure::new("thinking-disclosure", is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener({ + move |this, _event, _window, _cx| { + let is_open = this + .expanded_thinking_segments + .entry((message_id, ix)) + .or_insert(false); + + *is_open = !*is_open; + } + })), + ), + ) + .child({ + Icon::new(IconName::ArrowCircle) + .color(Color::Accent) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate( + percentage(delta), + )) + }, + ) + }), ), ) - }), - ) + .when(!is_open, |this| { + let gradient_overlay = div() + .rounded_b_lg() + .h_full() + .absolute() + .w_full() + .bottom_0() + .left_0() + .bg(linear_gradient( + 180., + linear_color_stop(editor_bg, 1.), + linear_color_stop(editor_bg.opacity(0.2), 0.), + )); + + this.child( + div() + .relative() + .bg(editor_bg) + .rounded_b_lg() + .mt_2() + .pl_4() + .child( + div() + .id(("thinking-content", ix)) + .max_h_20() + .track_scroll(scroll_handle) + .text_ui_sm(cx) + .overflow_hidden() + .child( + MarkdownElement::new( + markdown.clone(), + default_markdown_style(window, cx), + ) + .on_url_click({ + let workspace = self.workspace.clone(); + move |text, window, cx| { + open_markdown_link( + text, + workspace.clone(), + window, + cx, + ); + } + }), + ), + ) + .child(gradient_overlay), + ) + }) + .when(is_open, |this| { + this.child( + div() + .id(("thinking-content", ix)) + .h_full() + .bg(editor_bg) + .text_ui_sm(cx) + .child( + MarkdownElement::new( + markdown.clone(), + default_markdown_style(window, cx), + ) + .on_url_click({ + let workspace = self.workspace.clone(); + move |text, window, cx| { + open_markdown_link(text, workspace.clone(), window, cx); + } + }), + ), + ) + }) + } else { + this.v_flex() + .mt_neg_2() + .child( + h_flex() + .group("disclosure-header") + .pr_1() + .justify_between() + .opacity(0.8) + .hover(|style| style.opacity(1.)) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::LightBulb) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new("Thought Process").size(LabelSize::Small)), + ) + .child( + div().visible_on_hover("disclosure-header").child( + Disclosure::new("thinking-disclosure", is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener({ + move |this, _event, _window, _cx| { + let is_open = this + .expanded_thinking_segments + .entry((message_id, ix)) + .or_insert(false); + + *is_open = !*is_open; + } + })), + ), + ), + ) + .child( + div() + .id(("thinking-content", ix)) + .relative() + .mt_1p5() + .ml_1p5() + .pl_2p5() + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .text_ui_sm(cx) + .when(is_open, |this| { + this.child( + MarkdownElement::new( + markdown.clone(), + default_markdown_style(window, cx), + ) + .on_url_click({ + let workspace = self.workspace.clone(); + move |text, window, cx| { + open_markdown_link(text, workspace.clone(), window, cx); + } + }), + ) + }), + ) + } + }) } fn render_tool_use( @@ -2033,6 +2081,7 @@ impl ActiveThread { .upgrade() .map(|workspace| workspace.read(cx).app_state().fs.clone()); let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation); + let edit_tools = tool_use.needs_confirmation; let status_icons = div().child(match &tool_use.status { ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => { @@ -2209,10 +2258,10 @@ impl ActiveThread { }; div().map(|element| { - if !tool_use.needs_confirmation { + if !edit_tools { element.child( v_flex() - .my_1p5() + .my_2() .child( h_flex() .group("disclosure-header") diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index dcae1b36b8..5248e97559 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -2793,7 +2793,7 @@ fn render_thought_process_fold_icon_button( let button = match status { ThoughtProcessStatus::Pending => button .child( - Icon::new(IconName::Brain) + Icon::new(IconName::LightBulb) .size(IconSize::Small) .color(Color::Muted), ) @@ -2808,7 +2808,7 @@ fn render_thought_process_fold_icon_button( ), ThoughtProcessStatus::Completed => button .style(ButtonStyle::Filled) - .child(Icon::new(IconName::Brain).size(IconSize::Small)) + .child(Icon::new(IconName::LightBulb).size(IconSize::Small)) .child(Label::new("Thought Process").single_line()), }; diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index 4a5800a6ad..07f40daaeb 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -33,7 +33,7 @@ impl Tool for ThinkingTool { } fn icon(&self) -> IconName { - IconName::Brain + IconName::LightBulb } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 62e92f2d22..7e419fa59a 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -141,6 +141,7 @@ pub enum IconName { InlayHint, Keyboard, Library, + LightBulb, LineHeight, Link, ListTree,