Skip to main content

atomcode_core/tool/
cd.rs

1use std::path::PathBuf;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde::Deserialize;
6use serde_json::json;
7
8use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
9
10pub struct CdTool;
11
12#[derive(Deserialize)]
13struct CdArgs {
14    path: String,
15}
16
17#[async_trait]
18impl Tool for CdTool {
19    fn definition(&self) -> ToolDef {
20        ToolDef {
21            name: "change_dir",
22            description: "Change the working directory. All subsequent file operations and bash commands will execute in the new directory.".to_string(),
23            parameters: json!({
24                "type": "object",
25                "properties": {
26                    "path": {
27                        "type": "string",
28                        "description": "The directory path to change to. Can be absolute or relative to current working directory."
29                    }
30                },
31                "required": ["path"]
32            }),
33        }
34    }
35
36    fn approval(&self, _args: &str) -> ApprovalRequirement {
37        ApprovalRequirement::AutoApprove
38    }
39
40    fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
41        let parsed = match serde_json::from_str::<CdArgs>(args) {
42            Ok(parsed) => parsed,
43            Err(_) => return self.approval(args),
44        };
45        let working_dir = match ctx.working_dir.try_read() {
46            Ok(wd) => wd.clone(),
47            Err(_) => return self.approval(args),
48        };
49        match super::approval_for_path(
50            &parsed.path,
51            &working_dir,
52            super::ExternalPathAction::Enumerate,
53        ) {
54            Ok(approval) => approval,
55            Err(_) => self.approval(args),
56        }
57    }
58
59    async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
60        let parsed: CdArgs = serde_json::from_str(args)?;
61        let path = parsed.path.as_str();
62
63        // Resolve the path (expand ~ if needed, resolve relative to current working_dir)
64        let current_wd = ctx.working_dir.read().await.clone();
65        let target = if path == "~" {
66            super::real_home_dir().unwrap_or_else(|| PathBuf::from(path))
67        } else if let Some(rest) = path.strip_prefix("~/") {
68            super::real_home_dir()
69                .map(|h| h.join(rest))
70                .unwrap_or_else(|| PathBuf::from(path))
71        } else if path.starts_with('/') {
72            PathBuf::from(path)
73        } else {
74            current_wd.join(path)
75        };
76
77        // Validate the target is a directory
78        if target.is_dir() {
79            let resolved = std::fs::canonicalize(&target).unwrap_or(target);
80            // Update shared working directory
81            let mut wd = ctx.working_dir.write().await;
82            *wd = resolved.clone();
83            Ok(ToolResult {
84                call_id: String::new(),
85                output: format!("Changed working directory to {}", resolved.display()),
86                success: true,
87            })
88        } else if target.exists() {
89            Ok(ToolResult {
90                call_id: String::new(),
91                output: format!("Not a directory: {}", target.display()),
92                success: false,
93            })
94        } else {
95            Ok(ToolResult {
96                call_id: String::new(),
97                output: format!("Path does not exist: {}", target.display()),
98                success: false,
99            })
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use tempfile::TempDir;
108
109    #[tokio::test]
110    async fn tilde_prefixed_relative_dir_is_not_expanded_to_home() {
111        let workspace = TempDir::new().unwrap();
112        let target = workspace.path().join("~cache");
113        std::fs::create_dir(&target).unwrap();
114
115        let ctx = ToolContext::new(workspace.path().to_path_buf());
116        let tool = CdTool;
117
118        let result = tool.execute(r#"{"path":"~cache"}"#, &ctx).await.unwrap();
119        assert!(result.success, "unexpected output: {}", result.output);
120        assert_eq!(
121            *ctx.working_dir.read().await,
122            std::fs::canonicalize(target).unwrap()
123        );
124    }
125
126    #[tokio::test]
127    async fn slash_after_tilde_still_expands_to_home() {
128        let Some(home) = super::super::real_home_dir() else {
129            return;
130        };
131
132        let ctx = ToolContext::new(PathBuf::from("/tmp"));
133        let tool = CdTool;
134
135        let result = tool.execute(r#"{"path":"~/"}"#, &ctx).await.unwrap();
136        assert!(result.success, "unexpected output: {}", result.output);
137        assert_eq!(
138            *ctx.working_dir.read().await,
139            std::fs::canonicalize(home).unwrap()
140        );
141    }
142}