1use agent_client_protocol::ToolKind;
2use agent_settings::AgentSettings;
3use anyhow::{Context as _, Result, anyhow};
4use futures::FutureExt as _;
5use gpui::{App, Entity, SharedString, Task};
6use project::Project;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use settings::Settings;
10use std::sync::Arc;
11use util::markdown::MarkdownInlineCode;
12
13use crate::{
14 AgentTool, ToolCallEventStream, ToolPermissionDecision, decide_permission_from_settings,
15};
16
17/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
18///
19/// This tool creates a directory and all necessary parent directories. It should be used whenever you need to create new directories within the project.
20#[derive(Debug, Serialize, Deserialize, JsonSchema)]
21pub struct CreateDirectoryToolInput {
22 /// The path of the new directory.
23 ///
24 /// <example>
25 /// If the project has the following structure:
26 ///
27 /// - directory1/
28 /// - directory2/
29 ///
30 /// You can create a new directory by providing a path of "directory1/new_directory"
31 /// </example>
32 pub path: String,
33}
34
35pub struct CreateDirectoryTool {
36 project: Entity<Project>,
37}
38
39impl CreateDirectoryTool {
40 pub fn new(project: Entity<Project>) -> Self {
41 Self { project }
42 }
43}
44
45impl AgentTool for CreateDirectoryTool {
46 type Input = CreateDirectoryToolInput;
47 type Output = String;
48
49 fn name() -> &'static str {
50 "create_directory"
51 }
52
53 fn kind() -> ToolKind {
54 ToolKind::Read
55 }
56
57 fn initial_title(
58 &self,
59 input: Result<Self::Input, serde_json::Value>,
60 _cx: &mut App,
61 ) -> SharedString {
62 if let Ok(input) = input {
63 format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
64 } else {
65 "Create directory".into()
66 }
67 }
68
69 fn run(
70 self: Arc<Self>,
71 input: Self::Input,
72 event_stream: ToolCallEventStream,
73 cx: &mut App,
74 ) -> Task<Result<Self::Output>> {
75 let settings = AgentSettings::get_global(cx);
76 let decision = decide_permission_from_settings(Self::name(), &input.path, settings);
77
78 let authorize = match decision {
79 ToolPermissionDecision::Allow => None,
80 ToolPermissionDecision::Deny(reason) => {
81 return Task::ready(Err(anyhow!("{}", reason)));
82 }
83 ToolPermissionDecision::Confirm => {
84 let context = crate::ToolPermissionContext {
85 tool_name: "create_directory".to_string(),
86 input_value: input.path.clone(),
87 };
88 Some(event_stream.authorize(
89 format!("Create directory {}", MarkdownInlineCode(&input.path)),
90 context,
91 cx,
92 ))
93 }
94 };
95
96 let project_path = match self.project.read(cx).find_project_path(&input.path, cx) {
97 Some(project_path) => project_path,
98 None => {
99 return Task::ready(Err(anyhow!("Path to create was outside the project")));
100 }
101 };
102 let destination_path: Arc<str> = input.path.as_str().into();
103
104 let create_entry = self.project.update(cx, |project, cx| {
105 project.create_entry(project_path.clone(), true, cx)
106 });
107
108 cx.spawn(async move |_cx| {
109 if let Some(authorize) = authorize {
110 authorize.await?;
111 }
112
113 futures::select! {
114 result = create_entry.fuse() => {
115 result.with_context(|| format!("Creating directory {destination_path}"))?;
116 }
117 _ = event_stream.cancelled_by_user().fuse() => {
118 anyhow::bail!("Create directory cancelled by user");
119 }
120 }
121
122 Ok(format!("Created directory {destination_path}"))
123 })
124 }
125}