
This PR introduces the "Reject All" and "Accept All" buttons in the panel's edit bar, which appears as soon as the agent starts editing a file. I'm also adding here a new method to the thread called `has_pending_edit_tool_uses`, which is a more specific way of knowing, in comparison to the `is_generating` method, whether or not the reject/accept all actions can be triggered. Previously, without this new method, you'd be waiting for the whole generation to end (e.g., the agent would be generating markdown with things like change summary) to be able to click those buttons, when the edit was already there, ready for you. It always felt like waiting for the whole thing was unnecessary when you really wanted to just wait for the _edits_ to be done, as so to avoid any potential conflicting state. <img src="https://github.com/user-attachments/assets/0927f3a6-c9ee-46ae-8f7b-97157d39a7b5" width="500"/> --- Release Notes: - agent: Added ability to reject and accept all changes from the agent panel. --------- Co-authored-by: Agus Zubiaga <hi@aguz.me>
328 lines
12 KiB
Rust
328 lines
12 KiB
Rust
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, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
|
};
|
|
use futures::{Future, FutureExt, TryFutureExt};
|
|
use gpui::{
|
|
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
|
};
|
|
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
|
use project::Project;
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use ui::{IconName, Tooltip, prelude::*};
|
|
use web_search::WebSearchRegistry;
|
|
use workspace::Workspace;
|
|
use zed_llm_client::{WebSearchResponse, WebSearchResult};
|
|
|
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
|
pub struct WebSearchToolInput {
|
|
/// The search term or question to query on the web.
|
|
query: String,
|
|
}
|
|
|
|
pub struct WebSearchTool;
|
|
|
|
impl Tool for WebSearchTool {
|
|
fn name(&self) -> String {
|
|
"web_search".into()
|
|
}
|
|
|
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
|
false
|
|
}
|
|
|
|
fn may_perform_edits(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn description(&self) -> String {
|
|
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
|
}
|
|
|
|
fn icon(&self) -> IconName {
|
|
IconName::Globe
|
|
}
|
|
|
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
|
json_schema_for::<WebSearchToolInput>(format)
|
|
}
|
|
|
|
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
|
"Searching the Web".to_string()
|
|
}
|
|
|
|
fn run(
|
|
self: Arc<Self>,
|
|
input: serde_json::Value,
|
|
_request: Arc<LanguageModelRequest>,
|
|
_project: Entity<Project>,
|
|
_action_log: Entity<ActionLog>,
|
|
_model: Arc<dyn LanguageModel>,
|
|
_window: Option<AnyWindowHandle>,
|
|
cx: &mut App,
|
|
) -> ToolResult {
|
|
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
|
|
Ok(input) => input,
|
|
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
|
};
|
|
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
|
|
return Task::ready(Err(anyhow!("Web search is not available."))).into();
|
|
};
|
|
|
|
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
|
|
let output = cx.background_spawn({
|
|
let search_task = search_task.clone();
|
|
async move {
|
|
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
|
Ok(ToolResultOutput {
|
|
content: ToolResultContent::Text(
|
|
serde_json::to_string(&response)
|
|
.context("Failed to serialize search results")?,
|
|
),
|
|
output: Some(serde_json::to_value(response)?),
|
|
})
|
|
}
|
|
});
|
|
|
|
ToolResult {
|
|
output,
|
|
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
|
|
}
|
|
}
|
|
|
|
fn deserialize_card(
|
|
self: Arc<Self>,
|
|
output: serde_json::Value,
|
|
_project: Entity<Project>,
|
|
_window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<assistant_tool::AnyToolCard> {
|
|
let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
|
|
let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
|
|
Some(card.into())
|
|
}
|
|
}
|
|
|
|
#[derive(RegisterComponent)]
|
|
struct WebSearchToolCard {
|
|
response: Option<Result<WebSearchResponse>>,
|
|
_task: Task<()>,
|
|
}
|
|
|
|
impl WebSearchToolCard {
|
|
fn new(
|
|
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let _task = cx.spawn(async move |this, cx| {
|
|
let response = search_task.await.map_err(|err| anyhow!(err));
|
|
this.update(cx, |this, cx| {
|
|
this.response = Some(response);
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
});
|
|
|
|
Self {
|
|
response: None,
|
|
_task,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToolCard for WebSearchToolCard {
|
|
fn render(
|
|
&mut self,
|
|
_status: &ToolUseStatus,
|
|
_window: &mut Window,
|
|
_workspace: WeakEntity<Workspace>,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
let header = match self.response.as_ref() {
|
|
Some(Ok(response)) => {
|
|
let text: SharedString = if response.results.len() == 1 {
|
|
"1 result".into()
|
|
} else {
|
|
format!("{} results", response.results.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(px(5.))
|
|
.border_l_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.gap_1()
|
|
.children(response.results.iter().enumerate().map(|(index, result)| {
|
|
let title = result.title.clone();
|
|
let url = SharedString::from(result.url.clone());
|
|
|
|
Button::new(("result", index), title)
|
|
.label_size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.icon(IconName::ArrowUpRight)
|
|
.icon_size(IconSize::XSmall)
|
|
.icon_position(IconPosition::End)
|
|
.truncate(true)
|
|
.tooltip({
|
|
let url = url.clone();
|
|
move |window, cx| {
|
|
Tooltip::with_meta(
|
|
"Web Search Result",
|
|
None,
|
|
url.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
})
|
|
.on_click({
|
|
let url = url.clone();
|
|
move |_, _, cx| cx.open_url(&url)
|
|
})
|
|
}))
|
|
.into_any(),
|
|
),
|
|
Err(_) => None,
|
|
});
|
|
|
|
v_flex().mb_3().gap_1().child(header).children(content)
|
|
}
|
|
}
|
|
|
|
impl Component for WebSearchToolCard {
|
|
fn scope() -> ComponentScope {
|
|
ComponentScope::Agent
|
|
}
|
|
|
|
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,
|
|
WeakEntity::new_invalid(),
|
|
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,
|
|
WeakEntity::new_invalid(),
|
|
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,
|
|
WeakEntity::new_invalid(),
|
|
cx,
|
|
)
|
|
.into_any_element()
|
|
}))
|
|
.into_any_element(),
|
|
),
|
|
])])
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn example_search_response() -> WebSearchResponse {
|
|
WebSearchResponse {
|
|
results: vec![
|
|
WebSearchResult {
|
|
title: "Alo".to_string(),
|
|
url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
|
|
text: "Alo is a popular restaurant in Toronto.".to_string(),
|
|
},
|
|
WebSearchResult {
|
|
title: "Alo".to_string(),
|
|
url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
|
|
text: "Information about Alo restaurant in Toronto.".to_string(),
|
|
},
|
|
WebSearchResult {
|
|
title: "Edulis".to_string(),
|
|
url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(),
|
|
text: "Details about Edulis restaurant in Toronto.".to_string(),
|
|
},
|
|
WebSearchResult {
|
|
title: "Sushi Masaki Saito".to_string(),
|
|
url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada"
|
|
.to_string(),
|
|
text: "Information about Sushi Masaki Saito in Toronto.".to_string(),
|
|
},
|
|
WebSearchResult {
|
|
title: "Shoushin".to_string(),
|
|
url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(),
|
|
text: "Details about Shoushin restaurant in Toronto.".to_string(),
|
|
},
|
|
WebSearchResult {
|
|
title: "Restaurant 20 Victoria".to_string(),
|
|
url:
|
|
"https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada"
|
|
.to_string(),
|
|
text: "Information about Restaurant 20 Victoria in Toronto.".to_string(),
|
|
},
|
|
],
|
|
}
|
|
}
|