atomcode_core/tool/
file_deps.rs1use std::collections::HashSet;
2use std::path::Path;
3
4use anyhow::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7use serde_json::json;
8
9use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
10
11pub struct FileDependenciesTool;
12
13#[derive(Deserialize)]
14struct FileDepsArgs {
15 file: String,
16}
17
18fn shorten_path(path: &Path) -> String {
19 let components: Vec<_> = path.components().collect();
20 if components.len() <= 3 {
21 return path.display().to_string();
22 }
23 let last3: Vec<_> = components[components.len() - 3..]
24 .iter()
25 .map(|c| c.as_os_str())
26 .collect();
27 format!(
28 ".../{}",
29 last3
30 .iter()
31 .map(|s| s.to_string_lossy())
32 .collect::<Vec<_>>()
33 .join("/")
34 )
35}
36
37#[async_trait]
38impl Tool for FileDependenciesTool {
39 fn definition(&self) -> ToolDef {
40 ToolDef {
41 name: "file_dependencies",
42 description:
43 "Show file-level dependencies: which files this file USES (imports/calls into) \
44 and which files USE this file (depend on it).\n\
45 Accepts relative or absolute file paths.\n\
46 Example: {\"file\": \"src/agent/mod.rs\"}"
47 .to_string(),
48 parameters: json!({
49 "type": "object",
50 "properties": {
51 "file": { "type": "string", "description": "File path (relative to working dir or absolute)" }
52 },
53 "required": ["file"]
54 }),
55 }
56 }
57
58 fn approval(&self, _args: &str) -> ApprovalRequirement {
59 ApprovalRequirement::AutoApprove
60 }
61
62 fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
63 let parsed = match serde_json::from_str::<FileDepsArgs>(args) {
64 Ok(parsed) => parsed,
65 Err(_) => return self.approval(args),
66 };
67 let working_dir = match ctx.working_dir.try_read() {
68 Ok(wd) => wd.clone(),
69 Err(_) => return self.approval(args),
70 };
71 match super::approval_for_path(
72 &parsed.file,
73 &working_dir,
74 super::ExternalPathAction::Enumerate,
75 ) {
76 Ok(approval) => approval,
77 Err(_) => self.approval(args),
78 }
79 }
80
81 async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
82 let parsed: FileDepsArgs = serde_json::from_str(args)?;
83 let wd = ctx.working_dir.read().await.clone();
84 let file_path = match super::inspect_path_access(&parsed.file, &wd) {
85 Ok(access) => access.path,
86 Err(err) => {
87 return Ok(ToolResult {
88 call_id: String::new(),
89 output: err.to_string(),
90 success: false,
91 });
92 }
93 };
94
95 let graph = ctx.graph.read().await;
96
97 if !graph.is_ready() {
98 return Ok(ToolResult {
99 call_id: String::new(),
100 output: "Code graph is not yet indexed. The graph will be available after the \
101 background indexer completes. Try again shortly."
102 .to_string(),
103 success: false,
104 });
105 }
106
107 let symbols = match graph.symbols_in_file(&file_path) {
108 Some(ids) => ids.clone(),
109 None => {
110 return Ok(ToolResult {
111 call_id: String::new(),
112 output: format!(
113 "File '{}' not found in code graph. Check the path or wait for indexing.",
114 parsed.file
115 ),
116 success: false,
117 });
118 }
119 };
120
121 let mut uses_files = HashSet::new();
123 for &sym_id in &symbols {
124 if let Some(edges) = graph.callees(sym_id) {
125 for edge in edges {
126 if let Some(node) = graph.node(edge.to) {
127 if node.file != file_path {
128 uses_files.insert(node.file.clone());
129 }
130 }
131 }
132 }
133 }
134
135 let mut used_by_files = HashSet::new();
137 for &sym_id in &symbols {
138 if let Some(edges) = graph.callers(sym_id) {
139 for edge in edges {
140 if let Some(node) = graph.node(edge.to) {
141 if node.file != file_path {
142 used_by_files.insert(node.file.clone());
143 }
144 }
145 }
146 }
147 }
148
149 let mut out = format!("File dependencies for {}:\n\n", shorten_path(&file_path));
150
151 out.push_str(&format!("USES ({} files):\n", uses_files.len()));
152 if uses_files.is_empty() {
153 out.push_str(" (none)\n");
154 } else {
155 let mut sorted: Vec<_> = uses_files.iter().collect();
156 sorted.sort();
157 for f in sorted {
158 out.push_str(&format!(" {}\n", shorten_path(f)));
159 }
160 }
161
162 out.push_str(&format!("\nUSED BY ({} files):\n", used_by_files.len()));
163 if used_by_files.is_empty() {
164 out.push_str(" (none)\n");
165 } else {
166 let mut sorted: Vec<_> = used_by_files.iter().collect();
167 sorted.sort();
168 for f in sorted {
169 out.push_str(&format!(" {}\n", shorten_path(f)));
170 }
171 }
172
173 Ok(ToolResult {
174 call_id: String::new(),
175 output: out,
176 success: true,
177 })
178 }
179}