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 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 smol::stream::StreamExt as _;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<()>> { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { return Task::ready(Ok(())); } // If any path component matches the local settings folder, then this could affect // the editor in ways beyond the project source, so prompt. let local_settings_folder = paths::local_settings_folder_name(); let path = Path::new(&input.path); if path.components().any(|component| { component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder) }) { return event_stream.authorize( format!("{} (local settings)", input.display_description), cx, ); } // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. // TODO this is broken when remoting if let Ok(canonical_path) = std::fs::canonicalize(&input.path) && canonical_path.starts_with(paths::config_dir()) { return event_stream.authorize( format!("{} (global settings)", input.display_description), cx, ); } // Check if path is inside the global config directory // First check if it's already inside project - if not, try to canonicalize let Ok(project_path) = self.thread.read_with(cx, |thread, cx| { thread.project().read(cx).find_project_path(&input.path, cx) }) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; // If the path is inside the project, and it's not one of the above edge cases, // then no confirmation is necessary. Otherwise, confirmation is necessary. if project_path.is_some() { Task::ready(Ok(())) } else { event_stream.authorize(&input.display_description, cx) } }} impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; fn 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 { locations: Some(vec![acp::ToolCallLocation { path: abs_path, line: None, meta: None, }]), ..Default::default() }); } 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?; 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; while let Some(event) = events.next().await { match event { EditAgentOutputEvent::Edited(range) => { 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, meta: None }]), ..Default::default() }); } 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() // }); // } // } } } } // If format_on_save is enabled, format the buffer 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 }) .unwrap_or(false); let edit_agent_output = output.await?; 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); })?; 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, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { event_stream.update_diff(cx.new(|cx| { Diff::finalized( output.input_path.to_string_lossy().into_owned(), Some(output.old_text.to_string()), output.new_text, self.language_registry.clone(), cx, ) })); Ok(()) }} /// Validate that the file path is valid, meaning:////// - For `edit` and `overwrite`, the path must point to an existing file./// - For `create`, the file must not already exist, but it's parent dir must exist.fn resolve_path( input: &EditFileToolInput, project: Entity<Project>,Lines 1–500 of 1762. Load more
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 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 smol::stream::StreamExt as _; 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<()>> { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { return Task::ready(Ok(())); } // If any path component matches the local settings folder, then this could affect // the editor in ways beyond the project source, so prompt. let local_settings_folder = paths::local_settings_folder_name(); let path = Path::new(&input.path); if path.components().any(|component| { component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder) }) { return event_stream.authorize( format!("{} (local settings)", input.display_description), cx, ); } // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. // TODO this is broken when remoting if let Ok(canonical_path) = std::fs::canonicalize(&input.path) && canonical_path.starts_with(paths::config_dir()) { return event_stream.authorize( format!("{} (global settings)", input.display_description), cx, ); } // Check if path is inside the global config directory // First check if it's already inside project - if not, try to canonicalize let Ok(project_path) = self.thread.read_with(cx, |thread, cx| { thread.project().read(cx).find_project_path(&input.path, cx) }) else { return Task::ready(Err(anyhow!("thread was dropped"))); }; // If the path is inside the project, and it's not one of the above edge cases, // then no confirmation is necessary. Otherwise, confirmation is necessary. if project_path.is_some() { Task::ready(Ok(())) } else { event_stream.authorize(&input.display_description, cx) } } } impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; fn 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 { locations: Some(vec![acp::ToolCallLocation { path: abs_path, line: None, meta: None, }]), ..Default::default() }); } 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?; 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; while let Some(event) = events.next().await { match event { EditAgentOutputEvent::Edited(range) => { 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, meta: None }]), ..Default::default() }); } 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() // }); // } // } } } } // If format_on_save is enabled, format the buffer 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 }) .unwrap_or(false); let edit_agent_output = output.await?; 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); })?; 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, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { event_stream.update_diff(cx.new(|cx| { Diff::finalized( output.input_path.to_string_lossy().into_owned(), Some(output.old_text.to_string()), output.new_text, self.language_registry.clone(), cx, ) })); Ok(()) } } /// Validate that the file path is valid, meaning: /// /// - For `edit` and `overwrite`, the path must point to an existing file. /// - For `create`, the file must not already exist, but it's parent dir must exist. fn resolve_path( input: &EditFileToolInput, project: Entity<Project>,