Checkpoint
This commit is contained in:
parent
a5c25e0366
commit
f0bf2e79d6
13 changed files with 369 additions and 138 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -139,8 +139,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ae3c22c23b64a5c3b7fc8a86fcc7c494e989bd2cd66fdce14a58cfc8078381"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
|
|
|
@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-protocol = "0.0.21"
|
||||
agent-client-protocol = { path = "../agent-client-protocol" }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
|
|
@ -167,6 +167,7 @@ pub struct ToolCall {
|
|||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
|
@ -195,6 +196,7 @@ impl ToolCall {
|
|||
locations: tool_call.locations,
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_output: tool_call.raw_output,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,6 +213,7 @@ impl ToolCall {
|
|||
content,
|
||||
locations,
|
||||
raw_input,
|
||||
raw_output,
|
||||
} = fields;
|
||||
|
||||
if let Some(kind) = kind {
|
||||
|
@ -241,6 +244,10 @@ impl ToolCall {
|
|||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
if let Some(raw_output) = raw_output {
|
||||
self.raw_output = Some(raw_output);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diffs(&self) -> impl Iterator<Item = &Diff> {
|
||||
|
@ -1547,6 +1554,7 @@ mod tests {
|
|||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
|
@ -1659,6 +1667,7 @@ mod tests {
|
|||
}],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::ToolCallAuthorization;
|
||||
use crate::{templates::Templates, AgentResponseEvent, Thread};
|
||||
use crate::{FindPathTool, ToolCallAuthorization};
|
||||
use acp_thread::ModelSelector;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
|
@ -412,7 +412,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
anyhow!("No default model configured. Please configure a default model in settings.")
|
||||
})?;
|
||||
|
||||
let thread = cx.new(|_| Thread::new(project, agent.project_context.clone(), action_log, agent.templates.clone(), default_model));
|
||||
let thread = cx.new(|_| {
|
||||
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log, agent.templates.clone(), default_model);
|
||||
thread.add_tool(FindPathTool::new(project.clone()));
|
||||
thread
|
||||
});
|
||||
|
||||
Ok(thread)
|
||||
},
|
||||
)??;
|
||||
|
|
|
@ -10,3 +10,4 @@ mod tests;
|
|||
pub use agent::*;
|
||||
pub use native_agent_server::NativeAgentServer;
|
||||
pub use thread::*;
|
||||
pub use tools::*;
|
||||
|
|
|
@ -19,11 +19,20 @@ impl AgentTool for EchoTool {
|
|||
"echo".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, input: Self::Input, _cx: &mut App) -> Task<Result<String>> {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
Task::ready(Ok(input.text))
|
||||
}
|
||||
}
|
||||
|
@ -44,11 +53,20 @@ impl AgentTool for DelayTool {
|
|||
"delay".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, input: Self::Input, cx: &mut App) -> Task<Result<String>>
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
@ -71,11 +89,20 @@ impl AgentTool for ToolRequiringPermission {
|
|||
"tool_requiring_permission".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, _input: Self::Input, cx: &mut App) -> Task<Result<String>>
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
@ -96,11 +123,20 @@ impl AgentTool for InfiniteTool {
|
|||
"infinite".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, _input: Self::Input, cx: &mut App) -> Task<Result<String>> {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
future::pending::<()>().await;
|
||||
unreachable!()
|
||||
|
@ -137,11 +173,20 @@ impl AgentTool for WordListTool {
|
|||
"word_list".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, _input: Self::Input, _cx: &mut App) -> Task<Result<String>> {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
Task::ready(Ok("ok".to_string()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ use futures::{
|
|||
channel::{mpsc, oneshot},
|
||||
stream::FuturesUnordered,
|
||||
};
|
||||
use gpui::{App, Context, Entity, ImageFormat, SharedString, Task};
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
|
||||
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
|
@ -276,7 +276,17 @@ impl Thread {
|
|||
while let Some(tool_result) = tool_uses.next().await {
|
||||
log::info!("Tool finished {:?}", tool_result);
|
||||
|
||||
event_stream.send_tool_call_result(&tool_result);
|
||||
event_stream.send_tool_call_update(
|
||||
&tool_result.tool_use_id,
|
||||
acp::ToolCallUpdateFields {
|
||||
status: Some(if tool_result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
thread
|
||||
.update(cx, |thread, _cx| {
|
||||
thread.pending_tool_uses.remove(&tool_result.tool_use_id);
|
||||
|
@ -502,20 +512,20 @@ impl Thread {
|
|||
event_stream: AgentResponseEventStream,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String>> {
|
||||
// TODO: should we push this down into the tool itself?
|
||||
let needs_authorization = tool.needs_authorization(tool_use.input.clone(), cx);
|
||||
cx.spawn(async move |_this, cx| {
|
||||
if needs_authorization? {
|
||||
event_stream.authorize_tool_call(&tool_use).await?;
|
||||
}
|
||||
|
||||
event_stream.send_tool_call_update(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
cx.update(|cx| tool.run(tool_use.input, cx))?.await
|
||||
let tool_event_stream = ToolCallEventStream::new(tool_use.id, event_stream);
|
||||
tool_event_stream.send_update(acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
});
|
||||
cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -654,6 +664,7 @@ where
|
|||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
||||
|
||||
fn name(&self) -> SharedString;
|
||||
|
||||
fn description(&self, _cx: &mut App) -> SharedString {
|
||||
let schema = schemars::schema_for!(Self::Input);
|
||||
SharedString::new(
|
||||
|
@ -664,6 +675,8 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind;
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self, _format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
schemars::schema_for!(Self::Input)
|
||||
|
@ -674,7 +687,12 @@ where
|
|||
fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool;
|
||||
|
||||
/// Runs the tool with the provided input.
|
||||
fn run(self: Arc<Self>, input: Self::Input, cx: &mut App) -> Task<Result<String>>;
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
|
||||
fn erase(self) -> Arc<dyn AnyAgentTool> {
|
||||
Arc::new(Erased(Arc::new(self)))
|
||||
|
@ -688,7 +706,12 @@ pub trait AnyAgentTool {
|
|||
fn description(&self, cx: &mut App) -> SharedString;
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||
fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result<bool>;
|
||||
fn run(self: Arc<Self>, input: serde_json::Value, cx: &mut App) -> Task<Result<String>>;
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
impl<T> AnyAgentTool for Erased<Arc<T>>
|
||||
|
@ -715,10 +738,15 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, input: serde_json::Value, cx: &mut App) -> Task<Result<String>> {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let parsed_input: Result<T::Input> = serde_json::from_value(input).map_err(Into::into);
|
||||
match parsed_input {
|
||||
Ok(input) => self.0.clone().run(input, cx),
|
||||
Ok(input) => self.0.clone().run(input, event_stream, cx),
|
||||
Err(error) => Task::ready(Err(anyhow!(error))),
|
||||
}
|
||||
}
|
||||
|
@ -758,6 +786,7 @@ impl AgentResponseEventStream {
|
|||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: Some(tool_use.input.clone()),
|
||||
raw_output: None,
|
||||
},
|
||||
options: vec![
|
||||
acp::PermissionOption {
|
||||
|
@ -798,6 +827,7 @@ impl AgentResponseEventStream {
|
|||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: Some(tool_use.input.clone()),
|
||||
raw_output: None,
|
||||
})))
|
||||
.ok();
|
||||
}
|
||||
|
@ -817,38 +847,6 @@ impl AgentResponseEventStream {
|
|||
.ok();
|
||||
}
|
||||
|
||||
fn send_tool_call_result(&self, tool_result: &LanguageModelToolResult) {
|
||||
let status = if tool_result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
};
|
||||
let content = match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text) => text.to_string().into(),
|
||||
LanguageModelToolResultContent::Image(LanguageModelImage { source, .. }) => {
|
||||
acp::ToolCallContent::Content {
|
||||
content: acp::ContentBlock::Image(acp::ImageContent {
|
||||
annotations: None,
|
||||
data: source.to_string(),
|
||||
mime_type: ImageFormat::Png.mime_type().to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
self.0
|
||||
.unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(tool_result.tool_use_id.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(status),
|
||||
content: Some(vec![content]),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn send_stop(&self, reason: StopReason) {
|
||||
match reason {
|
||||
StopReason::EndTurn => {
|
||||
|
@ -874,3 +872,22 @@ impl AgentResponseEventStream {
|
|||
self.0.unbounded_send(Err(error)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolCallEventStream {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
stream: AgentResponseEventStream,
|
||||
}
|
||||
|
||||
impl ToolCallEventStream {
|
||||
fn new(tool_use_id: LanguageModelToolUseId, stream: AgentResponseEventStream) -> Self {
|
||||
Self {
|
||||
tool_use_id,
|
||||
stream,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_update(&self, fields: acp::ToolCallUpdateFields) {
|
||||
self.stream.send_tool_call_update(&self.tool_use_id, fields);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
mod glob;
|
||||
mod find_path_tool;
|
||||
|
||||
pub use find_path_tool::*;
|
||||
|
|
231
crates/agent2/src/tools/find_path_tool.rs
Normal file
231
crates/agent2/src/tools/find_path_tool.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
use agent_client_protocol as acp;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{App, AppContext, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::{cmp, path::PathBuf, sync::Arc};
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
/// Fast file path pattern matching tool that works with any codebase size
|
||||
///
|
||||
/// - Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
/// - Returns matching file paths sorted alphabetically
|
||||
/// - Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
|
||||
/// - Use this tool when you need to find files by name patterns
|
||||
/// - Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindPathToolInput {
|
||||
/// The glob to match against every path in the project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can get back the first two paths by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FindPathToolOutput {
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: usize = 50;
|
||||
|
||||
pub struct FindPathTool {
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl FindPathTool {
|
||||
pub fn new(project: Entity<Project>) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentTool for FindPathTool {
|
||||
type Input = FindPathToolInput;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"find_path".into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> acp::ToolKind {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _: Self::Input, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let search_paths_task = search_paths(&input.glob, self.project.clone(), cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let matches = search_paths_task.await?;
|
||||
let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
|
||||
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
|
||||
|
||||
event_stream.send_update(acp::ToolCallUpdateFields {
|
||||
title: Some(if paginated_matches.len() == 0 {
|
||||
"No matches".into()
|
||||
} else if paginated_matches.len() == 1 {
|
||||
"1 match".into()
|
||||
} else {
|
||||
format!("{} matches", paginated_matches.len()).into()
|
||||
}),
|
||||
content: Some(
|
||||
paginated_matches
|
||||
.iter()
|
||||
.map(|path| acp::ToolCallContent::Content {
|
||||
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: format!("file://{}", path.display()),
|
||||
name: path.to_string_lossy().into(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
raw_output: Some(serde_json::json!({
|
||||
"paths": &matches,
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if matches.is_empty() {
|
||||
Ok("No matches found".into())
|
||||
} else {
|
||||
let mut message = format!("Found {} total matches.", matches.len());
|
||||
if matches.len() > RESULTS_PER_PAGE {
|
||||
write!(
|
||||
&mut message,
|
||||
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
|
||||
input.offset + 1,
|
||||
input.offset + paginated_matches.len()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for mat in matches.iter().skip(input.offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
let snapshots: Vec<_> = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_path_tool(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"apple": {
|
||||
"banana": {
|
||||
"carrot": "1",
|
||||
},
|
||||
"bandana": {
|
||||
"carbonara": "2",
|
||||
},
|
||||
"endive": "3"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use gpui::{App, AppContext, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::paths::PathMatcher;
|
||||
use worktree::Snapshot as WorktreeSnapshot;
|
||||
|
||||
use crate::{
|
||||
templates::{GlobTemplate, Template, Templates},
|
||||
thread::AgentTool,
|
||||
};
|
||||
|
||||
// Description is dynamic, see `fn description` below
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct GlobInput {
|
||||
/// A POSIX glob pattern
|
||||
glob: SharedString,
|
||||
}
|
||||
|
||||
struct GlobTool {
|
||||
project: Entity<Project>,
|
||||
templates: Arc<Templates>,
|
||||
}
|
||||
|
||||
impl AgentTool for GlobTool {
|
||||
type Input = GlobInput;
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"glob".into()
|
||||
}
|
||||
|
||||
fn description(&self, cx: &mut App) -> SharedString {
|
||||
let project_roots = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).root_name().into())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
GlobTemplate { project_roots }
|
||||
.render(&self.templates)
|
||||
.expect("template failed to render")
|
||||
.into()
|
||||
}
|
||||
|
||||
fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(self: Arc<Self>, input: Self::Input, cx: &mut App) -> Task<Result<String>> {
|
||||
let path_matcher = match PathMatcher::new([&input.glob]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => return Task::ready(Err(anyhow!(error))),
|
||||
};
|
||||
|
||||
let snapshots: Vec<WorktreeSnapshot> = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let paths = snapshots.iter().flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
});
|
||||
let output = paths
|
||||
.map(|path| format!("{}\n", path.display()))
|
||||
.collect::<String>();
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -280,6 +280,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams)
|
|||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -297,6 +297,7 @@ impl ClaudeTool {
|
|||
content: self.content(),
|
||||
locations: self.locations(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2962,6 +2962,7 @@ mod tests {
|
|||
content: vec!["hi".into()],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
};
|
||||
let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
|
||||
.with_permission_requests(HashMap::from_iter([(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue