use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, ToolPermissionDecision, decide_permission_for_path, edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},};use acp_thread::Diff;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::{Context as _, Result, anyhow};use cloud_llm_client::CompletionIntent;use collections::HashSet;use futures::{FutureExt as _, StreamExt as _};use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use indoc::formatdoc;use language::language_settings::{self, FormatOnSave};use language::{LanguageRegistry, ToPoint};use language_model::LanguageModelToolResultContent;use paths;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{Project, ProjectPath};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::Settings;use std::ffi::OsStr;use std::path::{Path, PathBuf};use std::sync::Arc;use ui::SharedString;use util::ResultExt;use util::rel_path::RelPath; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.////// Before using this tool:////// 1. Use the `read_file` tool to understand the file's contents and context////// 2. Verify the directory path is correct (only applicable when creating new files):/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct EditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: EditFileMode,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]struct EditFileToolPartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "lowercase")]#[schemars(inline)]pub enum EditFileMode { Edit, Create, Overwrite,} #[derive(Debug, Serialize, Deserialize)]pub struct EditFileToolOutput { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, #[serde(alias = "raw_output")] edit_agent_output: EditAgentOutput,} impl From<EditFileToolOutput> for LanguageModelToolResultContent { fn from(output: EditFileToolOutput) -> Self { if output.diff.is_empty() { "No edits were made.".into() } else { format!( "Edited {}:\n\n```diff\n{}\n```", output.input_path.display(), output.diff ) .into() } }} pub struct EditFileTool { thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, project: Entity<Project>, templates: Arc<Templates>,} impl EditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, templates: Arc<Templates>, ) -> Self { Self { project, thread, language_registry, templates, } } fn authorize( &self, input: &EditFileToolInput, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { authorize_file_edit( Self::NAME, &input.path, &input.display_description, &self.thread, event_stream, cx, ) }} pub enum SensitiveSettingsKind { Local, Global,} /// Canonicalize a path, stripping the Windows extended-length path prefix (`\\?\`)/// that `std::fs::canonicalize` adds on Windows. This ensures that canonicalized/// paths can be compared with non-canonicalized paths via `starts_with`.fn safe_canonicalize(path: &Path) -> std::io::Result<PathBuf> { let canonical = std::fs::canonicalize(path)?; #[cfg(target_os = "windows")] { let s = canonical.to_string_lossy(); if let Some(stripped) = s.strip_prefix("\\\\?\\") { return Ok(PathBuf::from(stripped)); } } Ok(canonical)} /// Returns the kind of sensitive settings location this path targets, if any:/// either inside a `.zed/` local-settings directory or inside the global config dir.pub fn sensitive_settings_kind(path: &Path) -> Option<SensitiveSettingsKind> { let local_settings_folder = paths::local_settings_folder_name(); if path.components().any(|component| { component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder) }) { return Some(SensitiveSettingsKind::Local); } // Walk up the path hierarchy until we find an ancestor that exists and can // be canonicalized, then reconstruct the path from there. This handles // cases where multiple levels of subdirectories don't exist yet (e.g. // ~/.config/zed/new_subdir/evil.json). let canonical_path = { let mut current: Option<&Path> = Some(path); let mut suffix_components = Vec::new(); loop { match current { Some(ancestor) => match safe_canonicalize(ancestor) { Ok(canonical) => { let mut result = canonical; for component in suffix_components.into_iter().rev() { result.push(component); } break Some(result); } Err(_) => { if let Some(file_name) = ancestor.file_name() { suffix_components.push(file_name.to_os_string()); } current = ancestor.parent(); } }, None => break None, } } }; if let Some(canonical_path) = canonical_path { let config_dir = safe_canonicalize(paths::config_dir()) .unwrap_or_else(|_| paths::config_dir().to_path_buf()); if canonical_path.starts_with(&config_dir) { return Some(SensitiveSettingsKind::Global); } } None} pub fn is_sensitive_settings_path(path: &Path) -> bool { sensitive_settings_kind(path).is_some()} pub fn authorize_file_edit( tool_name: &str, path: &Path, display_description: &str, thread: &WeakEntity<Thread>, event_stream: &ToolCallEventStream, cx: &mut App,) -> Task<Result<()>> { let path_str = path.to_string_lossy(); let settings = agent_settings::AgentSettings::get_global(cx); let decision = decide_permission_for_path(tool_name, &path_str, settings); if let ToolPermissionDecision::Deny(reason) = decision { return Task::ready(Err(anyhow!("{}", reason))); } let explicitly_allowed = matches!(decision, ToolPermissionDecision::Allow); if explicitly_allowed && !is_sensitive_settings_path(path) { return Task::ready(Ok(())); } match sensitive_settings_kind(path) { Some(SensitiveSettingsKind::Local) => { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; return event_stream.authorize( format!("{} (local settings)", display_description), context, cx, ); } Some(SensitiveSettingsKind::Global) => { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; return event_stream.authorize( format!("{} (settings)", display_description), context, cx, ); } None => {} } let Ok(project_path) = thread.read_with(cx, |thread, cx| { thread.project().read(cx).find_project_path(path, cx) }) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; if project_path.is_some() { Task::ready(Ok(())) } else { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; event_stream.authorize(display_description, context, cx) }} impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; const NAME: &'static str = "edit_file"; fn kind() -> acp::ToolKind { acp::ToolKind::Edit } fn initial_title( &self, input: Result<Self::Input, serde_json::Value>, cx: &mut App, ) -> SharedString { match input { Ok(input) => self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path.to_string_lossy().into_owned()) .into(), Err(raw_input) => { if let Some(input) = serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok() { let path = input.path.trim(); if !path.is_empty() { return self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path) .into(); } let description = input.display_description.trim(); if !description.is_empty() { return description.to_string().into(); } } DEFAULT_UI_TEXT.into() } } } fn run( self: Arc<Self>, input: Self::Input, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output>> { let Ok(project) = self .thread .read_with(cx, |thread, _cx| thread.project().clone()) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))), }; let abs_path = project.read(cx).absolute_path(&project_path, cx); if let Some(abs_path) = abs_path.clone() { event_stream.update_fields( ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]), ); } let authorize = self.authorize(&input, &event_stream, cx); cx.spawn(async move |cx: &mut AsyncApp| { authorize.await?; let (request, model, action_log) = self.thread.update(cx, |thread, cx| { let request = thread.build_completion_request(CompletionIntent::ToolResults, cx); (request, thread.model().cloned(), thread.action_log().clone()) })?; let request = request?; let model = model.context("No language model configured")?; let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, project.clone(), action_log.clone(), self.templates.clone(), edit_format, ); let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) }) .await?; // Check if the file has been modified since the agent last read it if let Some(abs_path) = abs_path.as_ref() { let (last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.update(cx, |thread, cx| { let last_read = thread.file_read_times.get(abs_path).copied(); let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime()); let dirty = buffer.read(cx).is_dirty(); let has_save = thread.has_tool(SaveFileTool::NAME); let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME); (last_read, current, dirty, has_save, has_restore) })?; // Check for unsaved changes first - these indicate modifications we don't know about if is_dirty { let message = match (has_save_tool, has_restore_tool) { (true, true) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \ If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit." } (true, false) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \ If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed." } (false, true) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \ If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit." } (false, false) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \ then ask them to save or revert the file manually and inform you when it's ok to proceed." } }; anyhow::bail!("{}", message); } // Check if the file was modified on disk since we last read it if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { // MTime can be unreliable for comparisons, so our newtype intentionally // doesn't support comparing them. If the mtime at all different // (which could be because of a modification or because e.g. system clock changed), // we pessimistically assume it was modified. if current != last_read { anyhow::bail!( "The file {} has been modified since you last read it. \ Please read the file again to get the current state before editing it.", input.path.display() ); } } } let diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); event_stream.update_diff(diff.clone()); let _finalize_diff = util::defer({ let diff = diff.downgrade(); let mut cx = cx.clone(); move || { diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); } }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let old_text = cx .background_spawn({ let old_snapshot = old_snapshot.clone(); async move { Arc::new(old_snapshot.text()) } }) .await; let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { edit_agent.edit( buffer.clone(), input.display_description.clone(), &request, cx, ) } else { edit_agent.overwrite( buffer.clone(), input.display_description.clone(), &request, cx, ) }; let mut hallucinated_old_text = false; let mut ambiguous_ranges = Vec::new(); let mut emitted_location = false; loop {Lines 1–500 of 2432. Load more
use super::restore_file_from_disk_tool::RestoreFileFromDiskTool; use super::save_file_tool::SaveFileTool; use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, ToolPermissionDecision, decide_permission_for_path, edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, }; use acp_thread::Diff; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; use cloud_llm_client::CompletionIntent; use collections::HashSet; use futures::{FutureExt as _, StreamExt as _}; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use indoc::formatdoc; use language::language_settings::{self, FormatOnSave}; use language::{LanguageRegistry, ToPoint}; use language_model::LanguageModelToolResultContent; use paths; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{Project, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::Arc; use ui::SharedString; use util::ResultExt; use util::rel_path::RelPath; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. /// /// Before using this tool: /// /// 1. Use the `read_file` tool to understand the file's contents and context /// /// 2. Verify the directory path is correct (only applicable when creating new files): /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: EditFileMode, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] struct EditFileToolPartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] #[schemars(inline)] pub enum EditFileMode { Edit, Create, Overwrite, } #[derive(Debug, Serialize, Deserialize)] pub struct EditFileToolOutput { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, #[serde(alias = "raw_output")] edit_agent_output: EditAgentOutput, } impl From<EditFileToolOutput> for LanguageModelToolResultContent { fn from(output: EditFileToolOutput) -> Self { if output.diff.is_empty() { "No edits were made.".into() } else { format!( "Edited {}:\n\n```diff\n{}\n```", output.input_path.display(), output.diff ) .into() } } } pub struct EditFileTool { thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, project: Entity<Project>, templates: Arc<Templates>, } impl EditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, templates: Arc<Templates>, ) -> Self { Self { project, thread, language_registry, templates, } } fn authorize( &self, input: &EditFileToolInput, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { authorize_file_edit( Self::NAME, &input.path, &input.display_description, &self.thread, event_stream, cx, ) } } pub enum SensitiveSettingsKind { Local, Global, } /// Canonicalize a path, stripping the Windows extended-length path prefix (`\\?\`) /// that `std::fs::canonicalize` adds on Windows. This ensures that canonicalized /// paths can be compared with non-canonicalized paths via `starts_with`. fn safe_canonicalize(path: &Path) -> std::io::Result<PathBuf> { let canonical = std::fs::canonicalize(path)?; #[cfg(target_os = "windows")] { let s = canonical.to_string_lossy(); if let Some(stripped) = s.strip_prefix("\\\\?\\") { return Ok(PathBuf::from(stripped)); } } Ok(canonical) } /// Returns the kind of sensitive settings location this path targets, if any: /// either inside a `.zed/` local-settings directory or inside the global config dir. pub fn sensitive_settings_kind(path: &Path) -> Option<SensitiveSettingsKind> { let local_settings_folder = paths::local_settings_folder_name(); if path.components().any(|component| { component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder) }) { return Some(SensitiveSettingsKind::Local); } // Walk up the path hierarchy until we find an ancestor that exists and can // be canonicalized, then reconstruct the path from there. This handles // cases where multiple levels of subdirectories don't exist yet (e.g. // ~/.config/zed/new_subdir/evil.json). let canonical_path = { let mut current: Option<&Path> = Some(path); let mut suffix_components = Vec::new(); loop { match current { Some(ancestor) => match safe_canonicalize(ancestor) { Ok(canonical) => { let mut result = canonical; for component in suffix_components.into_iter().rev() { result.push(component); } break Some(result); } Err(_) => { if let Some(file_name) = ancestor.file_name() { suffix_components.push(file_name.to_os_string()); } current = ancestor.parent(); } }, None => break None, } } }; if let Some(canonical_path) = canonical_path { let config_dir = safe_canonicalize(paths::config_dir()) .unwrap_or_else(|_| paths::config_dir().to_path_buf()); if canonical_path.starts_with(&config_dir) { return Some(SensitiveSettingsKind::Global); } } None } pub fn is_sensitive_settings_path(path: &Path) -> bool { sensitive_settings_kind(path).is_some() } pub fn authorize_file_edit( tool_name: &str, path: &Path, display_description: &str, thread: &WeakEntity<Thread>, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { let path_str = path.to_string_lossy(); let settings = agent_settings::AgentSettings::get_global(cx); let decision = decide_permission_for_path(tool_name, &path_str, settings); if let ToolPermissionDecision::Deny(reason) = decision { return Task::ready(Err(anyhow!("{}", reason))); } let explicitly_allowed = matches!(decision, ToolPermissionDecision::Allow); if explicitly_allowed && !is_sensitive_settings_path(path) { return Task::ready(Ok(())); } match sensitive_settings_kind(path) { Some(SensitiveSettingsKind::Local) => { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; return event_stream.authorize( format!("{} (local settings)", display_description), context, cx, ); } Some(SensitiveSettingsKind::Global) => { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; return event_stream.authorize( format!("{} (settings)", display_description), context, cx, ); } None => {} } let Ok(project_path) = thread.read_with(cx, |thread, cx| { thread.project().read(cx).find_project_path(path, cx) }) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; if project_path.is_some() { Task::ready(Ok(())) } else { let context = crate::ToolPermissionContext { tool_name: tool_name.to_string(), input_values: vec![path_str.to_string()], }; event_stream.authorize(display_description, context, cx) } } impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; const NAME: &'static str = "edit_file"; fn kind() -> acp::ToolKind { acp::ToolKind::Edit } fn initial_title( &self, input: Result<Self::Input, serde_json::Value>, cx: &mut App, ) -> SharedString { match input { Ok(input) => self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path.to_string_lossy().into_owned()) .into(), Err(raw_input) => { if let Some(input) = serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok() { let path = input.path.trim(); if !path.is_empty() { return self .project .read(cx) .find_project_path(&input.path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or(input.path) .into(); } let description = input.display_description.trim(); if !description.is_empty() { return description.to_string().into(); } } DEFAULT_UI_TEXT.into() } } } fn run( self: Arc<Self>, input: Self::Input, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output>> { let Ok(project) = self .thread .read_with(cx, |thread, _cx| thread.project().clone()) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))), }; let abs_path = project.read(cx).absolute_path(&project_path, cx); if let Some(abs_path) = abs_path.clone() { event_stream.update_fields( ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]), ); } let authorize = self.authorize(&input, &event_stream, cx); cx.spawn(async move |cx: &mut AsyncApp| { authorize.await?; let (request, model, action_log) = self.thread.update(cx, |thread, cx| { let request = thread.build_completion_request(CompletionIntent::ToolResults, cx); (request, thread.model().cloned(), thread.action_log().clone()) })?; let request = request?; let model = model.context("No language model configured")?; let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, project.clone(), action_log.clone(), self.templates.clone(), edit_format, ); let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) }) .await?; // Check if the file has been modified since the agent last read it if let Some(abs_path) = abs_path.as_ref() { let (last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.update(cx, |thread, cx| { let last_read = thread.file_read_times.get(abs_path).copied(); let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime()); let dirty = buffer.read(cx).is_dirty(); let has_save = thread.has_tool(SaveFileTool::NAME); let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME); (last_read, current, dirty, has_save, has_restore) })?; // Check for unsaved changes first - these indicate modifications we don't know about if is_dirty { let message = match (has_save_tool, has_restore_tool) { (true, true) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \ If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit." } (true, false) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \ If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed." } (false, true) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \ If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \ If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit." } (false, false) => { "This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \ then ask them to save or revert the file manually and inform you when it's ok to proceed." } }; anyhow::bail!("{}", message); } // Check if the file was modified on disk since we last read it if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { // MTime can be unreliable for comparisons, so our newtype intentionally // doesn't support comparing them. If the mtime at all different // (which could be because of a modification or because e.g. system clock changed), // we pessimistically assume it was modified. if current != last_read { anyhow::bail!( "The file {} has been modified since you last read it. \ Please read the file again to get the current state before editing it.", input.path.display() ); } } } let diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); event_stream.update_diff(diff.clone()); let _finalize_diff = util::defer({ let diff = diff.downgrade(); let mut cx = cx.clone(); move || { diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); } }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let old_text = cx .background_spawn({ let old_snapshot = old_snapshot.clone(); async move { Arc::new(old_snapshot.text()) } }) .await; let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { edit_agent.edit( buffer.clone(), input.display_description.clone(), &request, cx, ) } else { edit_agent.overwrite( buffer.clone(), input.display_description.clone(), &request, cx, ) }; let mut hallucinated_old_text = false; let mut ambiguous_ranges = Vec::new(); let mut emitted_location = false; loop {