diff --git a/Cargo.lock b/Cargo.lock index 6a95865144..fcb7d5412f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,6 +658,7 @@ dependencies = [ "assistant_tool", "chrono", "gpui", + "project", "schemars", "serde", "serde_json", diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 9e102588f2..3bb81fbba8 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -16,6 +16,7 @@ anyhow.workspace = true assistant_tool.workspace = true chrono.workspace = true gpui.workspace = true +project.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index b0afd6441f..d1e9081c23 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -1,13 +1,19 @@ +mod list_worktrees_tool; mod now_tool; +mod read_file_tool; use assistant_tool::ToolRegistry; use gpui::App; +use crate::list_worktrees_tool::ListWorktreesTool; use crate::now_tool::NowTool; +use crate::read_file_tool::ReadFileTool; pub fn init(cx: &mut App) { assistant_tool::init(cx); let registry = ToolRegistry::global(cx); registry.register_tool(NowTool); + registry.register_tool(ListWorktreesTool); + registry.register_tool(ReadFileTool); } diff --git a/crates/assistant_tools/src/list_worktrees_tool.rs b/crates/assistant_tools/src/list_worktrees_tool.rs new file mode 100644 index 0000000000..c1d9a72211 --- /dev/null +++ b/crates/assistant_tools/src/list_worktrees_tool.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use assistant_tool::Tool; +use gpui::{App, Task, WeakEntity, Window}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use workspace::Workspace; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ListWorktreesToolInput {} + +pub struct ListWorktreesTool; + +impl Tool for ListWorktreesTool { + fn name(&self) -> String { + "list-worktrees".into() + } + + fn description(&self) -> String { + "Lists all worktrees in the current project. Use this tool when you need to find available worktrees and their IDs.".into() + } + + fn input_schema(&self) -> serde_json::Value { + serde_json::json!( + { + "type": "object", + "properties": {}, + "required": [] + } + ) + } + + fn run( + self: Arc, + _input: serde_json::Value, + workspace: WeakEntity, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(Err(anyhow!("workspace dropped"))); + }; + + let project = workspace.read(cx).project().clone(); + + cx.spawn(|cx| async move { + cx.update(|cx| { + #[derive(Debug, Serialize)] + struct WorktreeInfo { + id: usize, + root_name: String, + root_dir: Option, + } + + let worktrees = project.update(cx, |project, cx| { + project + .visible_worktrees(cx) + .map(|worktree| { + worktree.read_with(cx, |worktree, _cx| WorktreeInfo { + id: worktree.id().to_usize(), + root_dir: worktree + .root_dir() + .map(|root_dir| root_dir.to_string_lossy().to_string()), + root_name: worktree.root_name().to_string(), + }) + }) + .collect::>() + }); + + if worktrees.is_empty() { + return Ok("No worktrees found in the current project.".to_string()); + } + + let mut result = String::from("Worktrees in the current project:\n\n"); + for worktree in worktrees { + result.push_str(&serde_json::to_string(&worktree)?); + } + + Ok(result) + })? + }) + } +} diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs new file mode 100644 index 0000000000..e218a435b3 --- /dev/null +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -0,0 +1,69 @@ +use std::path::Path; +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use assistant_tool::Tool; +use gpui::{App, Task, WeakEntity, Window}; +use project::{ProjectPath, WorktreeId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use workspace::Workspace; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ReadFileToolInput { + /// The ID of the worktree in which the file resides. + pub worktree_id: usize, + /// The path to the file to read. + /// + /// This path is relative to the worktree root, it must not be an absolute path. + pub path: Arc, +} + +pub struct ReadFileTool; + +impl Tool for ReadFileTool { + fn name(&self) -> String { + "read-file".into() + } + + fn description(&self) -> String { + "Reads the content of a file specified by a worktree ID and path. Use this tool when you need to access the contents of a file in the project.".into() + } + + fn input_schema(&self) -> serde_json::Value { + let schema = schemars::schema_for!(ReadFileToolInput); + serde_json::to_value(&schema).unwrap() + } + + fn run( + self: Arc, + input: serde_json::Value, + workspace: WeakEntity, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(Err(anyhow!("workspace dropped"))); + }; + + let input = match serde_json::from_value::(input) { + Ok(input) => input, + Err(err) => return Task::ready(Err(anyhow!(err))), + }; + + let project = workspace.read(cx).project().clone(); + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(input.worktree_id), + path: input.path, + }; + cx.spawn(|cx| async move { + let buffer = cx + .update(|cx| { + project.update(cx, |project, cx| project.open_buffer(project_path, cx)) + })? + .await?; + + cx.update(|cx| buffer.read(cx).text()) + }) + } +}