use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use super::tool_permissions::authorize_file_edit;use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, 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 project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{Project, ProjectPath};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::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 `move_path` tool 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, ) }} 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 { let event = futures::select! { event = events.next().fuse() => match event { Some(event) => event, None => break, }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; match event { EditAgentOutputEvent::Edited(range) => { if !emitted_location { let line = Some(buffer.update(cx, |buffer, _cx| { range.start.to_point(&buffer.snapshot()).row })); if let Some(abs_path) = abs_path.clone() { event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path).line(line)])); } emitted_location = true; } }, EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx)); // if !emitted_location { // let line = buffer.update(cx, |buffer, _cx| { // range.start.to_point(&buffer.snapshot()).row // }).ok(); // if let Some(abs_path) = abs_path.clone() { // event_stream.update_fields(ToolCallUpdateFields { // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), // ..Default::default() // }); // } // } } } } let edit_agent_output = output.await?; let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| { let settings = language_settings::language_settings( buffer.language().map(|l| l.name()), buffer.file(), cx, ); settings.format_on_save != FormatOnSave::Off }); if format_on_save_enabled { action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); let format_task = project.update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, // Don't push to history since the tool did it. FormatTrigger::Save, cx, ) }); format_task.await.log_err(); } project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await?; action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); // Update the recorded read time after a successful edit so consecutive edits work if let Some(abs_path) = abs_path.as_ref() { if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { buffer.file().and_then(|file| file.disk_state().mtime()) }) { self.thread.update(cx, |thread, _| { thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime); })?; } } let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let (new_text, unified_diff) = cx .background_spawn({ let new_snapshot = new_snapshot.clone(); let old_text = old_text.clone(); async move { let new_text = new_snapshot.text(); let diff = language::unified_diff(&old_text, &new_text); (new_text, diff) } }) .await; let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( !hallucinated_old_text, formatdoc! {" Some edits were produced but none of them could be applied. Read the relevant sections of {input_path} again so that I can perform the requested edits. "} ); anyhow::ensure!( ambiguous_ranges.is_empty(), { let line_numbers = ambiguous_ranges .iter() .map(|range| range.start.to_string()) .collect::<Vec<_>>() .join(", "); formatdoc! {" <old_text> matches more than one position in the file (lines: {line_numbers}). Read the relevant sections of {input_path} again and extend <old_text> so that I can perform the requested edits. "} } ); } Ok(EditFileToolOutput { input_path: input.path, new_text, old_text, diff: unified_diff, edit_agent_output, }) }) } fn replay( &self, _input: Self::Input, output: Self::Output,Lines 1–500 of 2602. Load more
use super::restore_file_from_disk_tool::RestoreFileFromDiskTool; use super::save_file_tool::SaveFileTool; use super::tool_permissions::authorize_file_edit; use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, 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 project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{Project, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::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 `move_path` tool 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, ) } } 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 { let event = futures::select! { event = events.next().fuse() => match event { Some(event) => event, None => break, }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; match event { EditAgentOutputEvent::Edited(range) => { if !emitted_location { let line = Some(buffer.update(cx, |buffer, _cx| { range.start.to_point(&buffer.snapshot()).row })); if let Some(abs_path) = abs_path.clone() { event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path).line(line)])); } emitted_location = true; } }, EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx)); // if !emitted_location { // let line = buffer.update(cx, |buffer, _cx| { // range.start.to_point(&buffer.snapshot()).row // }).ok(); // if let Some(abs_path) = abs_path.clone() { // event_stream.update_fields(ToolCallUpdateFields { // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), // ..Default::default() // }); // } // } } } } let edit_agent_output = output.await?; let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| { let settings = language_settings::language_settings( buffer.language().map(|l| l.name()), buffer.file(), cx, ); settings.format_on_save != FormatOnSave::Off }); if format_on_save_enabled { action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); let format_task = project.update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, // Don't push to history since the tool did it. FormatTrigger::Save, cx, ) }); format_task.await.log_err(); } project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) .await?; action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); // Update the recorded read time after a successful edit so consecutive edits work if let Some(abs_path) = abs_path.as_ref() { if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { buffer.file().and_then(|file| file.disk_state().mtime()) }) { self.thread.update(cx, |thread, _| { thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime); })?; } } let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let (new_text, unified_diff) = cx .background_spawn({ let new_snapshot = new_snapshot.clone(); let old_text = old_text.clone(); async move { let new_text = new_snapshot.text(); let diff = language::unified_diff(&old_text, &new_text); (new_text, diff) } }) .await; let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( !hallucinated_old_text, formatdoc! {" Some edits were produced but none of them could be applied. Read the relevant sections of {input_path} again so that I can perform the requested edits. "} ); anyhow::ensure!( ambiguous_ranges.is_empty(), { let line_numbers = ambiguous_ranges .iter() .map(|range| range.start.to_string()) .collect::<Vec<_>>() .join(", "); formatdoc! {" <old_text> matches more than one position in the file (lines: {line_numbers}). Read the relevant sections of {input_path} again and extend <old_text> so that I can perform the requested edits. "} } ); } Ok(EditFileToolOutput { input_path: input.path, new_text, old_text, diff: unified_diff, edit_agent_output, }) }) } fn replay( &self, _input: Self::Input, output: Self::Output,