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
This commit is contained in:
parent
109f1d43fc
commit
19b547565d
4 changed files with 128 additions and 55 deletions
|
@ -22,6 +22,7 @@ mod schema;
|
||||||
mod symbol_info_tool;
|
mod symbol_info_tool;
|
||||||
mod terminal_tool;
|
mod terminal_tool;
|
||||||
mod thinking_tool;
|
mod thinking_tool;
|
||||||
|
mod ui;
|
||||||
mod web_search_tool;
|
mod web_search_tool;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
3
crates/assistant_tools/src/ui.rs
Normal file
3
crates/assistant_tools/src/ui.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod tool_call_card_header;
|
||||||
|
|
||||||
|
pub use tool_call_card_header::*;
|
102
crates/assistant_tools/src/ui/tool_call_card_header.rs
Normal file
102
crates/assistant_tools/src/ui/tool_call_card_header.rs
Normal file
|
@ -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 std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::schema::json_schema_for;
|
use crate::schema::json_schema_for;
|
||||||
|
use crate::ui::ToolCallCardHeader;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||||
use futures::{FutureExt, TryFutureExt};
|
use futures::{Future, FutureExt, TryFutureExt};
|
||||||
use gpui::{
|
use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
|
||||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
|
||||||
pulsating_between,
|
|
||||||
};
|
|
||||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -47,7 +45,7 @@ impl Tool for WebSearchTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||||
"Web Search".to_string()
|
"Searching the Web".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -115,61 +113,30 @@ impl ToolCard for WebSearchToolCard {
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let header = h_flex()
|
let header = match self.response.as_ref() {
|
||||||
.id("tool-label-container")
|
Some(Ok(response)) => {
|
||||||
.gap_1p5()
|
let text: SharedString = if response.citations.len() == 1 {
|
||||||
.max_w_full()
|
"1 result".into()
|
||||||
.overflow_x_scroll()
|
} else {
|
||||||
.child(
|
format!("{} results", response.citations.len()).into()
|
||||||
Icon::new(IconName::Globe)
|
};
|
||||||
.size(IconSize::XSmall)
|
ToolCallCardHeader::new(IconName::Globe, "Searched the Web")
|
||||||
.color(Color::Muted),
|
.with_secondary_text(text)
|
||||||
)
|
}
|
||||||
.child(match self.response.as_ref() {
|
Some(Err(error)) => {
|
||||||
Some(Ok(response)) => {
|
ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string())
|
||||||
let text: SharedString = if response.citations.len() == 1 {
|
}
|
||||||
"1 result".into()
|
None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(),
|
||||||
} 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 content =
|
let content =
|
||||||
self.response.as_ref().and_then(|response| match response {
|
self.response.as_ref().and_then(|response| match response {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Some(
|
Some(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.overflow_hidden()
|
||||||
.ml_1p5()
|
.ml_1p5()
|
||||||
.pl_1p5()
|
.pl(px(5.))
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
@ -209,7 +176,7 @@ impl ToolCard for WebSearchToolCard {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
v_flex().my_2().gap_1().child(header).children(content)
|
v_flex().mb_3().gap_1().child(header).children(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue