agent: Improve thinking design display (#28186)

Danilo Leal created

Release Notes:

- N/A

Change summary

assets/icons/light_bulb.svg                           |   3 
crates/agent/src/active_thread.rs                     | 357 +++++++-----
crates/assistant_context_editor/src/context_editor.rs |   4 
crates/assistant_tools/src/thinking_tool.rs           |   2 
crates/icons/src/icons.rs                             |   1 
5 files changed, 210 insertions(+), 157 deletions(-)

Detailed changes

assets/icons/light_bulb.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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(
+                                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().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;
+                                    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({
-                                    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(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(
-                                div()
-                                    .id(("thinking-content", ix))
-                                    .p_2()
-                                    .h_20()
-                                    .track_scroll(scroll_handle)
-                                    .text_ui_sm(cx)
+                                h_flex()
+                                    .gap_1p5()
                                     .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,
-                                                );
-                                            }
-                                        }),
+                                        Icon::new(IconName::LightBulb)
+                                            .size(IconSize::XSmall)
+                                            .color(Color::Muted),
                                     )
-                                    .overflow_hidden(),
+                                    .child(Label::new("Thought Process").size(LabelSize::Small)),
                             )
-                            .child(gradient_overlay),
+                            .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;
+                                            }
+                                        })),
+                                ),
+                            ),
                     )
-                })
-                .when(is_open, |this| {
-                    this.child(
+                    .child(
                         div()
                             .id(("thinking-content", ix))
-                            .h_full()
-                            .p_2()
-                            .rounded_b_lg()
-                            .bg(editor_bg)
+                            .relative()
+                            .mt_1p5()
+                            .ml_1p5()
+                            .pl_2p5()
+                            .border_l_1()
+                            .border_color(cx.theme().colors().border_variant)
                             .text_ui_sm(cx)
-                            .child(
-                                MarkdownElement::new(
-                                    markdown.clone(),
-                                    default_markdown_style(window, 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);
+                                        }
+                                    }),
                                 )
-                                .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")

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

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 {