skill_tool.rs

  1use super::tool_permissions::canonicalize_worktree_roots;
  2use crate::{AgentTool, ToolCallEventStream, ToolInput};
  3use agent_client_protocol as acp;
  4use futures::StreamExt;
  5use gpui::{App, Entity, SharedString, Task};
  6use project::Project;
  7use schemars::JsonSchema;
  8use serde::{Deserialize, Serialize};
  9use std::fmt::Write;
 10use std::sync::Arc;
 11
 12/// Reads a file or lists a directory within an agent skill.
 13///
 14/// Skills are located in:
 15/// - Global: `~/.config/zed/skills/<skill-name>/`
 16/// - Worktree-specific: `<worktree>/.agents/skills/<skill-name>/`
 17///
 18/// Each skill contains:
 19/// - `SKILL.md` - Main instructions
 20/// - `scripts/` - Executable scripts
 21/// - `references/` - Additional documentation
 22/// - `assets/` - Templates, data files, images
 23///
 24/// To use a skill, first read its SKILL.md file, then explore its resources as needed.
 25#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 26pub struct SkillToolInput {
 27    /// The path to read or list within a skills directory.
 28    /// Use `~` for the home directory prefix.
 29    ///
 30    /// Examples:
 31    /// - `~/.config/zed/skills/brainstorming/SKILL.md`
 32    /// - `~/.config/zed/skills/brainstorming/references/`
 33    pub path: String,
 34}
 35
 36pub struct SkillTool {
 37    project: Entity<Project>,
 38}
 39
 40impl SkillTool {
 41    pub fn new(project: Entity<Project>) -> Self {
 42        Self { project }
 43    }
 44}
 45
 46impl AgentTool for SkillTool {
 47    type Input = SkillToolInput;
 48    type Output = String;
 49
 50    const NAME: &'static str = "read_skill";
 51
 52    fn kind() -> acp::ToolKind {
 53        acp::ToolKind::Read
 54    }
 55
 56    fn initial_title(
 57        &self,
 58        input: Result<Self::Input, serde_json::Value>,
 59        _cx: &mut App,
 60    ) -> SharedString {
 61        if let Ok(input) = input {
 62            let path = std::path::Path::new(&input.path);
 63            let skill_name = path
 64                .components()
 65                .rev()
 66                .find_map(|component| {
 67                    let name = component.as_os_str().to_str()?;
 68                    if name == "skills"
 69                        || name == ".agents"
 70                        || name == ".config"
 71                        || name == "zed"
 72                        || name == "~"
 73                        || name == "SKILL.md"
 74                        || name == "references"
 75                        || name == "scripts"
 76                        || name == "assets"
 77                    {
 78                        None
 79                    } else {
 80                        Some(name.to_string())
 81                    }
 82                })
 83                .unwrap_or_default();
 84
 85            if skill_name.is_empty() {
 86                "Read skill".into()
 87            } else {
 88                format!("Read skill `{skill_name}`").into()
 89            }
 90        } else {
 91            "Read skill".into()
 92        }
 93    }
 94
 95    fn run(
 96        self: Arc<Self>,
 97        input: ToolInput<Self::Input>,
 98        _event_stream: ToolCallEventStream,
 99        cx: &mut App,
100    ) -> Task<Result<String, String>> {
101        let project = self.project.clone();
102        cx.spawn(async move |cx| {
103            let input = input
104                .recv()
105                .await
106                .map_err(|e| format!("Failed to receive tool input: {e}"))?;
107
108            let fs = project.read_with(cx, |project, _cx| project.fs().clone());
109            let canonical_roots = canonicalize_worktree_roots(&project, &fs, cx).await;
110
111            let canonical_path = crate::skills::is_skills_path(&input.path, &canonical_roots)
112                .ok_or_else(|| {
113                    format!(
114                        "Path {} is not within a skills directory. \
115                         Skills are located at ~/.config/zed/skills/<skill-name>/ \
116                         or <worktree>/.agents/skills/<skill-name>/",
117                        input.path
118                    )
119                })?;
120
121            if fs.is_file(&canonical_path).await {
122                fs.load(&canonical_path)
123                    .await
124                    .map_err(|e| format!("Failed to read {}: {e}", input.path))
125            } else if fs.is_dir(&canonical_path).await {
126                let mut entries = fs
127                    .read_dir(&canonical_path)
128                    .await
129                    .map_err(|e| format!("Failed to list {}: {e}", input.path))?;
130
131                let mut folders = Vec::new();
132                let mut files = Vec::new();
133
134                while let Some(entry) = entries.next().await {
135                    let path = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
136                    let name = path
137                        .file_name()
138                        .unwrap_or_default()
139                        .to_string_lossy()
140                        .into_owned();
141                    if fs.is_dir(&path).await {
142                        folders.push(name);
143                    } else {
144                        files.push(name);
145                    }
146                }
147
148                folders.sort();
149                files.sort();
150
151                let mut output = String::new();
152                if !folders.is_empty() {
153                    writeln!(output, "# Folders:\n{}", folders.join("\n"))
154                        .map_err(|e| e.to_string())?;
155                }
156                if !files.is_empty() {
157                    writeln!(output, "\n# Files:\n{}", files.join("\n"))
158                        .map_err(|e| e.to_string())?;
159                }
160                if output.is_empty() {
161                    output = format!("{} is empty.", input.path);
162                }
163
164                Ok(output)
165            } else {
166                Err(format!("Path not found: {}", input.path))
167            }
168        })
169    }
170}