From 19b547565d76207370541773cc2b40d6ae7948e9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:51:57 -0300 Subject: [PATCH] agent: Refine the web search tool call UI (#29190) This PR refines a bit the web search tool UI by introducing a component (`ToolCallCardHeader`) that aims to standardize the heading element of tool calls in the thread. In terms of next steps, I plan to evolve this component further soon (e.g., building a full-blown "tool call card" component), and even move it to a place where I can re-use it in the active_thread as well without making the `assistant_tools` a dependency of it. Release Notes: - N/A --- crates/assistant_tools/src/assistant_tools.rs | 1 + crates/assistant_tools/src/ui.rs | 3 + .../src/ui/tool_call_card_header.rs | 102 ++++++++++++++++++ crates/assistant_tools/src/web_search_tool.rs | 77 ++++--------- 4 files changed, 128 insertions(+), 55 deletions(-) create mode 100644 crates/assistant_tools/src/ui.rs create mode 100644 crates/assistant_tools/src/ui/tool_call_card_header.rs diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 92e388407c6893344a036a9ab83e0ce2e80a6334..408d5655b07ac0dfee149424d40497b81d5bc8aa 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -22,6 +22,7 @@ mod schema; mod symbol_info_tool; mod terminal_tool; mod thinking_tool; +mod ui; mod web_search_tool; use std::sync::Arc; diff --git a/crates/assistant_tools/src/ui.rs b/crates/assistant_tools/src/ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..a8ff923ef5b7c4a206cebe89dae3373947fc79e2 --- /dev/null +++ b/crates/assistant_tools/src/ui.rs @@ -0,0 +1,3 @@ +mod tool_call_card_header; + +pub use tool_call_card_header::*; diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs new file mode 100644 index 0000000000000000000000000000000000000000..e6219a151fa600de213801b691f7ffd7ad3a993f --- /dev/null +++ b/crates/assistant_tools/src/ui/tool_call_card_header.rs @@ -0,0 +1,102 @@ +use gpui::{Animation, AnimationExt, App, IntoElement, pulsating_between}; +use std::time::Duration; +use ui::{Tooltip, prelude::*}; + +/// A reusable header component for tool call cards. +#[derive(IntoElement)] +pub struct ToolCallCardHeader { + icon: IconName, + primary_text: SharedString, + secondary_text: Option, + is_loading: bool, + error: Option, +} + +impl ToolCallCardHeader { + pub fn new(icon: IconName, primary_text: impl Into) -> Self { + Self { + icon, + primary_text: primary_text.into(), + secondary_text: None, + is_loading: false, + error: None, + } + } + + pub fn with_secondary_text(mut self, text: impl Into) -> Self { + self.secondary_text = Some(text.into()); + self + } + + pub fn loading(mut self) -> Self { + self.is_loading = true; + self + } + + pub fn with_error(mut self, error: impl Into) -> Self { + self.error = Some(error.into()); + self + } +} + +impl RenderOnce for ToolCallCardHeader { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let font_size = rems(0.8125); + let secondary_text = self.secondary_text; + + h_flex() + .id("tool-label-container") + .gap_1p5() + .max_w_full() + .overflow_x_scroll() + .opacity(0.8) + .child( + h_flex().h(window.line_height()).justify_center().child( + Icon::new(self.icon) + .size(IconSize::XSmall) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .h(window.line_height()) + .gap_1p5() + .text_size(font_size) + .map(|this| { + if let Some(error) = &self.error { + this.child(format!("{} failed", self.primary_text)).child( + IconButton::new("error_info", IconName::Warning) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Warning) + .tooltip(Tooltip::text(error.clone())), + ) + } else { + this.child(self.primary_text.clone()) + } + }) + .when_some(secondary_text, |this, secondary_text| { + this.child( + div() + .size(px(3.)) + .rounded_full() + .bg(cx.theme().colors().text), + ) + .child(div().text_size(font_size).child(secondary_text.clone())) + }) + .with_animation( + "loading-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + move |this, delta| { + if self.is_loading { + this.opacity(delta) + } else { + this + } + }, + ), + ) + } +} diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 39ac7bd427dda4976e35476a0c148bdcadcbdd22..b64b9a78c776376e8af0f425c749b1e23cff8b42 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -1,13 +1,11 @@ use std::{sync::Arc, time::Duration}; use crate::schema::json_schema_for; +use crate::ui::ToolCallCardHeader; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; -use futures::{FutureExt, TryFutureExt}; -use gpui::{ - Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window, - pulsating_between, -}; +use futures::{Future, FutureExt, TryFutureExt}; +use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use project::Project; use schemars::JsonSchema; @@ -47,7 +45,7 @@ impl Tool for WebSearchTool { } fn ui_text(&self, _input: &serde_json::Value) -> String { - "Web Search".to_string() + "Searching the Web".to_string() } fn run( @@ -115,61 +113,30 @@ impl ToolCard for WebSearchToolCard { _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let header = h_flex() - .id("tool-label-container") - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .child( - Icon::new(IconName::Globe) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(match self.response.as_ref() { - Some(Ok(response)) => { - let text: SharedString = if response.citations.len() == 1 { - "1 result".into() - } else { - format!("{} results", response.citations.len()).into() - }; - h_flex() - .gap_1p5() - .child(Label::new("Searched the Web").size(LabelSize::Small)) - .child( - div() - .size(px(3.)) - .rounded_full() - .bg(cx.theme().colors().text), - ) - .child(Label::new(text).size(LabelSize::Small)) - .into_any_element() - } - Some(Err(error)) => div() - .id("web-search-error") - .child(Label::new("Web Search failed").size(LabelSize::Small)) - .tooltip(Tooltip::text(error.to_string())) - .into_any_element(), - - None => Label::new("Searching the Web…") - .size(LabelSize::Small) - .with_animation( - "web-search-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.6, 1.)), - |label, delta| label.alpha(delta), - ) - .into_any_element(), - }) - .into_any(); + let header = match self.response.as_ref() { + Some(Ok(response)) => { + let text: SharedString = if response.citations.len() == 1 { + "1 result".into() + } else { + format!("{} results", response.citations.len()).into() + }; + ToolCallCardHeader::new(IconName::Globe, "Searched the Web") + .with_secondary_text(text) + } + Some(Err(error)) => { + ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string()) + } + None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(), + }; let content = self.response.as_ref().and_then(|response| match response { Ok(response) => { Some( v_flex() + .overflow_hidden() .ml_1p5() - .pl_1p5() + .pl(px(5.)) .border_l_1() .border_color(cx.theme().colors().border_variant) .gap_1() @@ -209,7 +176,7 @@ impl ToolCard for WebSearchToolCard { Err(_) => None, }); - v_flex().my_2().gap_1().child(header).children(content) + v_flex().mb_3().gap_1().child(header).children(content) } }