Skip to main content

atomcode_core/tool/
file_deps.rs

1use 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        // USES: files that this file's symbols call into
122        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        // USED BY: files that call into this file's symbols
136        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}