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 const NAME: &'static str = "create_directory";
50
51 fn kind() -> ToolKind {
52 ToolKind::Read
53 }
54
55 fn initial_title(
56 &self,
57 input: Result<Self::Input, serde_json::Value>,
58 _cx: &mut App,
59 ) -> SharedString {
60 if let Ok(input) = input {
61 format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
62 } else {
63 "Create directory".into()
64 }
65 }
66
67 fn run(
68 self: Arc<Self>,
69 input: Self::Input,
70 event_stream: ToolCallEventStream,
71 cx: &mut App,
72 ) -> Task<Result<Self::Output>> {
73 let settings = AgentSettings::get_global(cx);
74 let decision = decide_permission_from_settings(Self::NAME, &input.path, settings);
75
76 let authorize = match decision {
77 ToolPermissionDecision::Allow => None,
78 ToolPermissionDecision::Deny(reason) => {
79 return Task::ready(Err(anyhow!("{}", reason)));
80 }
81 ToolPermissionDecision::Confirm => {
82 let context = crate::ToolPermissionContext {
83 tool_name: Self::NAME.to_string(),
84 input_value: input.path.clone(),
85 };
86 Some(event_stream.authorize(
87 format!("Create directory {}", MarkdownInlineCode(&input.path)),
88 context,
89 cx,
90 ))
91 }
92 };
93
94 let project_path = match self.project.read(cx).find_project_path(&input.path, cx) {
95 Some(project_path) => project_path,
96 None => {
97 return Task::ready(Err(anyhow!("Path to create was outside the project")));
98 }
99 };
100 let destination_path: Arc<str> = input.path.as_str().into();
101
102 let create_entry = self.project.update(cx, |project, cx| {
103 project.create_entry(project_path.clone(), true, cx)
104 });
105
106 cx.spawn(async move |_cx| {
107 if let Some(authorize) = authorize {
108 authorize.await?;
109 }
110
111 futures::select! {
112 result = create_entry.fuse() => {
113 result.with_context(|| format!("Creating directory {destination_path}"))?;
114 }
115 _ = event_stream.cancelled_by_user().fuse() => {
116 anyhow::bail!("Create directory cancelled by user");
117 }
118 }
119
120 Ok(format!("Created directory {destination_path}"))
121 })
122 }
123}