create_file_tool.rs

  1use anyhow::{anyhow, Result};
  2use assistant_tool::{ActionLog, Tool};
  3use gpui::{App, Entity, Task};
  4use language_model::LanguageModelRequestMessage;
  5use project::Project;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use std::sync::Arc;
  9use ui::IconName;
 10
 11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 12pub struct CreateFileToolInput {
 13    /// The path where the file should be created.
 14    ///
 15    /// <example>
 16    /// If the project has the following structure:
 17    ///
 18    /// - directory1/
 19    /// - directory2/
 20    ///
 21    /// You can create a new file by providing a path of "directory1/new_file.txt"
 22    /// </example>
 23    pub path: String,
 24
 25    /// The text contents of the file to create.
 26    ///
 27    /// <example>
 28    /// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
 29    /// </example>
 30    pub contents: String,
 31}
 32
 33pub struct CreateFileTool;
 34
 35impl Tool for CreateFileTool {
 36    fn name(&self) -> String {
 37        "create-file".into()
 38    }
 39
 40    fn needs_confirmation(&self) -> bool {
 41        true
 42    }
 43
 44    fn description(&self) -> String {
 45        include_str!("./create_file_tool/description.md").into()
 46    }
 47
 48    fn icon(&self) -> IconName {
 49        IconName::File
 50    }
 51
 52    fn input_schema(&self) -> serde_json::Value {
 53        let schema = schemars::schema_for!(CreateFileToolInput);
 54        serde_json::to_value(&schema).unwrap()
 55    }
 56
 57    fn ui_text(&self, input: &serde_json::Value) -> String {
 58        match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
 59            Ok(input) => {
 60                let path = input.path.as_str();
 61                format!("Create file `{path}`")
 62            }
 63            Err(_) => "Create file".to_string(),
 64        }
 65    }
 66
 67    fn run(
 68        self: Arc<Self>,
 69        input: serde_json::Value,
 70        _messages: &[LanguageModelRequestMessage],
 71        project: Entity<Project>,
 72        _action_log: Entity<ActionLog>,
 73        cx: &mut App,
 74    ) -> Task<Result<String>> {
 75        let input = match serde_json::from_value::<CreateFileToolInput>(input) {
 76            Ok(input) => input,
 77            Err(err) => return Task::ready(Err(anyhow!(err))),
 78        };
 79        let project_path = match project.read(cx).find_project_path(&input.path, cx) {
 80            Some(project_path) => project_path,
 81            None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
 82        };
 83        let contents: Arc<str> = input.contents.as_str().into();
 84        let destination_path: Arc<str> = input.path.as_str().into();
 85
 86        cx.spawn(async move |cx| {
 87            project
 88                .update(cx, |project, cx| {
 89                    project.create_entry(project_path.clone(), false, cx)
 90                })?
 91                .await
 92                .map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
 93            let buffer = project
 94                .update(cx, |project, cx| {
 95                    project.open_buffer(project_path.clone(), cx)
 96                })?
 97                .await
 98                .map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
 99            buffer.update(cx, |buffer, cx| {
100                buffer.set_text(contents, cx);
101            })?;
102
103            project
104                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
105                .await
106                .map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
107
108            Ok(format!("Created file {destination_path}"))
109        })
110    }
111}