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;
Danilo Leal created
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
crates/assistant_tools/src/ui/tool_call_card_header.rs | 102 ++++++++++++
crates/assistant_tools/src/web_search_tool.rs | 77 ++------
4 files changed, 128 insertions(+), 55 deletions(-)
@@ -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;
@@ -0,0 +1,3 @@
+mod tool_call_card_header;
+
+pub use tool_call_card_header::*;
@@ -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<SharedString>,
+ is_loading: bool,
+ error: Option<String>,
+}
+
+impl ToolCallCardHeader {
+ pub fn new(icon: IconName, primary_text: impl Into<SharedString>) -> 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<SharedString>) -> 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<String>) -> 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
+ }
+ },
+ ),
+ )
+ }
+}
@@ -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<Self>,
) -> 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)
}
}