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}