Add example agent tool preview (#28984)

This PR adds an example of rendering previews for tools using the new
Agent ToolCard style.

![CleanShot 2025-04-17 at 13 03
12@2x](https://github.com/user-attachments/assets/d4c7d266-cc32-4038-9170-f3e070fce60e)

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
Nate Butler 2025-04-17 13:29:19 -04:00 committed by GitHub
parent 7a95c14625
commit acc4a5ccb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 121 additions and 3 deletions

2
Cargo.lock generated
View file

@ -703,6 +703,7 @@ dependencies = [
"assistant_tool", "assistant_tool",
"chrono", "chrono",
"collections", "collections",
"component",
"feature_flags", "feature_flags",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
@ -711,6 +712,7 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
"language_model", "language_model",
"linkme",
"open", "open",
"project", "project",
"rand 0.8.5", "rand 0.8.5",

View file

@ -16,6 +16,7 @@ anyhow.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
chrono.workspace = true chrono.workspace = true
collections.workspace = true collections.workspace = true
component.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
@ -24,6 +25,8 @@ http_client.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
linkme.workspace = true
open.workspace = true
project.workspace = true project.workspace = true
regex.workspace = true regex.workspace = true
schemars.workspace = true schemars.workspace = true
@ -31,10 +34,9 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
worktree.workspace = true
open = { workspace = true }
web_search.workspace = true web_search.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true zed_llm_client.workspace = true
[dev-dependencies] [dev-dependencies]

View file

@ -14,7 +14,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*}; use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry; use web_search::WebSearchRegistry;
use zed_llm_client::WebSearchResponse; use zed_llm_client::{WebSearchCitation, WebSearchResponse};
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput { pub struct WebSearchToolInput {
@ -22,6 +22,7 @@ pub struct WebSearchToolInput {
query: String, query: String,
} }
#[derive(RegisterComponent)]
pub struct WebSearchTool; pub struct WebSearchTool;
impl Tool for WebSearchTool { impl Tool for WebSearchTool {
@ -211,3 +212,111 @@ impl ToolCard for WebSearchToolCard {
v_flex().my_2().gap_1().child(header).children(content) v_flex().my_2().gap_1().child(header).children(content)
} }
} }
impl Component for WebSearchTool {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"ToolWebSearch"
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let in_progress_search = cx.new(|cx| WebSearchToolCard {
response: None,
_task: cx.spawn(async move |_this, cx| {
loop {
cx.background_executor()
.timer(Duration::from_secs(60))
.await
}
}),
});
let successful_search = cx.new(|_cx| WebSearchToolCard {
response: Some(Ok(example_search_response())),
_task: Task::ready(()),
});
let error_search = cx.new(|_cx| WebSearchToolCard {
response: Some(Err(anyhow!("Failed to resolve https://google.com"))),
_task: Task::ready(()),
});
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example(
"In Progress",
div()
.size_full()
.child(in_progress_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Pending, window, cx)
.into_any_element()
}))
.into_any_element(),
),
single_example(
"Successful",
div()
.size_full()
.child(successful_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
.into_any_element()
}))
.into_any_element(),
),
single_example(
"Error",
div()
.size_full()
.child(error_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Error("".into()), window, cx)
.into_any_element()
}))
.into_any_element(),
),
])])
.into_any_element(),
)
}
}
fn example_search_response() -> WebSearchResponse {
WebSearchResponse {
summary: r#"Toronto boasts a vibrant culinary scene with a diverse array of..."#
.to_string(),
citations: vec![
WebSearchCitation {
title: "Alo".to_string(),
url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
range: Some(147..213),
},
WebSearchCitation {
title: "Edulis".to_string(),
url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(),
range: Some(447..519),
},
WebSearchCitation {
title: "Sushi Masaki Saito".to_string(),
url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada"
.to_string(),
range: Some(776..872),
},
WebSearchCitation {
title: "Shoushin".to_string(),
url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(),
range: Some(1072..1148),
},
WebSearchCitation {
title: "Restaurant 20 Victoria".to_string(),
url:
"https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada"
.to_string(),
range: Some(1291..1395),
},
],
}
}

View file

@ -201,6 +201,7 @@ pub fn components() -> AllComponents {
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ComponentScope { pub enum ComponentScope {
Agent,
Collaboration, Collaboration,
DataDisplay, DataDisplay,
Editor, Editor,
@ -220,6 +221,7 @@ pub enum ComponentScope {
impl Display for ComponentScope { impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
ComponentScope::Agent => write!(f, "Agent"),
ComponentScope::Collaboration => write!(f, "Collaboration"), ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"), ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"), ComponentScope::Editor => write!(f, "Editor"),

View file

@ -19,6 +19,9 @@ extend-exclude = [
# Some crate names are flagged as typos. # Some crate names are flagged as typos.
"crates/indexed_docs/src/providers/rustdoc/popular_crates.txt", "crates/indexed_docs/src/providers/rustdoc/popular_crates.txt",
# Some mock data is flagged as typos.
"crates/assistant_tools/src/web_search_tool.rs",
# Stripe IDs are flagged as typos. # Stripe IDs are flagged as typos.
"crates/collab/src/db/tests/processed_stripe_event_tests.rs", "crates/collab/src/db/tests/processed_stripe_event_tests.rs",
# Not our typos. # Not our typos.