Add path search glob tool (#26567)

Richard Feldman created

<img width="638" alt="Screenshot 2025-03-12 at 1 33 31 PM"
src="https://github.com/user-attachments/assets/f29b9dae-59eb-4d7a-bc26-aa4721cb829a"
/>

Release Notes:

- N/A

Change summary

crates/assistant_tools/src/assistant_tools.rs              |  3 
crates/assistant_tools/src/path_search_tool.rs             | 88 ++++++++
crates/assistant_tools/src/path_search_tool/description.md |  1 
3 files changed, 92 insertions(+)

Detailed changes

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -3,6 +3,7 @@ mod delete_path_tool;
 mod edit_files_tool;
 mod list_directory_tool;
 mod now_tool;
+mod path_search_tool;
 mod read_file_tool;
 mod regex_search;
 
@@ -14,6 +15,7 @@ use crate::delete_path_tool::DeletePathTool;
 use crate::edit_files_tool::EditFilesTool;
 use crate::list_directory_tool::ListDirectoryTool;
 use crate::now_tool::NowTool;
+use crate::path_search_tool::PathSearchTool;
 use crate::read_file_tool::ReadFileTool;
 use crate::regex_search::RegexSearchTool;
 
@@ -25,6 +27,7 @@ pub fn init(cx: &mut App) {
     registry.register_tool(ReadFileTool);
     registry.register_tool(ListDirectoryTool);
     registry.register_tool(EditFilesTool);
+    registry.register_tool(PathSearchTool);
     registry.register_tool(RegexSearchTool);
     registry.register_tool(DeletePathTool);
     registry.register_tool(BashTool);

crates/assistant_tools/src/path_search_tool.rs 🔗

@@ -0,0 +1,88 @@
+use anyhow::{anyhow, Result};
+use assistant_tool::Tool;
+use gpui::{App, Entity, Task};
+use language_model::LanguageModelRequestMessage;
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{path::PathBuf, sync::Arc};
+use util::paths::PathMatcher;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct PathSearchToolInput {
+    /// The glob to search all project paths for.
+    ///
+    /// <example>
+    /// If the project has the following top-level 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,
+}
+
+pub struct PathSearchTool;
+
+impl Tool for PathSearchTool {
+    fn name(&self) -> String {
+        "path-search".into()
+    }
+
+    fn description(&self) -> String {
+        include_str!("./path_search_tool/description.md").into()
+    }
+
+    fn input_schema(&self) -> serde_json::Value {
+        let schema = schemars::schema_for!(PathSearchToolInput);
+        serde_json::to_value(&schema).unwrap()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: serde_json::Value,
+        _messages: &[LanguageModelRequestMessage],
+        project: Entity<Project>,
+        cx: &mut App,
+    ) -> Task<Result<String>> {
+        let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
+            Ok(input) => input.glob,
+            Err(err) => return Task::ready(Err(anyhow!(err))),
+        };
+        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
+            Ok(matcher) => matcher,
+            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
+        };
+
+        let mut matches = Vec::new();
+
+        for worktree_handle in project.read(cx).worktrees(cx) {
+            let worktree = worktree_handle.read(cx);
+            let root_name = worktree.root_name();
+
+            // Don't consider ignored entries.
+            for entry in worktree.entries(false, 0) {
+                if path_matcher.is_match(&entry.path) {
+                    matches.push(
+                        PathBuf::from(root_name)
+                            .join(&entry.path)
+                            .to_string_lossy()
+                            .to_string(),
+                    );
+                }
+            }
+        }
+
+        if matches.is_empty() {
+            Task::ready(Ok(format!(
+                "No paths in the project matched the glob {glob:?}"
+            )))
+        } else {
+            // Sort to group entries in the same directory together.
+            matches.sort();
+            Task::ready(Ok(matches.join("\n")))
+        }
+    }
+}