use super::edit_file_tool::EditFileTool;use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, edit_agent::streaming_fuzzy_matcher::StreamingFuzzyMatcher,};use acp_thread::Diff;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::{Context as _, Result, anyhow};use collections::HashSet;use futures::FutureExt as _;use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use language::LanguageRegistry;use language::language_settings::{self, FormatOnSave};use language_model::LanguageModelToolResultContent;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{Project, ProjectPath};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use text::BufferSnapshot;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 StreamingEditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI. /// /// 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: /// - 'create': Create a new file if it doesn't exist. Requires 'content' field. /// - 'overwrite': Replace the entire contents of an existing file. Requires 'content' field. /// - 'edit': Make granular edits to an existing file. Requires 'edits' field. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: StreamingEditFileMode, /// The complete content for the new file (required for 'create' and 'overwrite' modes). /// This field should contain the entire file content. #[serde(default, skip_serializing_if = "Option::is_none")] pub content: Option<String>, /// List of edit operations to apply sequentially (required for 'edit' mode). /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde(default, skip_serializing_if = "Option::is_none")] pub edits: Option<Vec<EditOperation>>,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum StreamingEditFileMode { /// Create a new file if it doesn't exist Create, /// Replace the entire contents of an existing file Overwrite, /// Make granular edits to an existing file Edit,} /// A single edit operation that replaces old text with new text#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct EditOperation { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. pub old_text: String, /// The text to replace it with pub new_text: String,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]struct StreamingEditFileToolPartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String,} #[derive(Debug, Serialize, Deserialize)]#[serde(untagged)]pub enum StreamingEditFileToolOutput { Success { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, }, Error { error: String, },} impl std::fmt::Display for StreamingEditFileToolOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StreamingEditFileToolOutput::Success { diff, input_path, .. } => { if diff.is_empty() { write!(f, "No edits were made.") } else { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } } StreamingEditFileToolOutput::Error { error } => write!(f, "{error}"), } }} impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent { fn from(output: StreamingEditFileToolOutput) -> Self { output.to_string().into() }} pub struct StreamingEditFileTool { thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, project: Entity<Project>, #[allow(dead_code)] templates: Arc<Templates>,} impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, templates: Arc<Templates>, ) -> Self { Self { project, thread, language_registry, templates, } } pub fn with_thread(&self, new_thread: WeakEntity<Thread>) -> Self { Self { project: self.project.clone(), thread: new_thread, language_registry: self.language_registry.clone(), templates: self.templates.clone(), } } fn authorize( &self, input: &StreamingEditFileToolInput, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, &input.path, &input.display_description, &self.thread, event_stream, cx, ) }} impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_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::<StreamingEditFileToolPartialInput>(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, Self::Output>> { let Ok(project) = self .thread .read_with(cx, |thread, _cx| thread.project().clone()) else { return Task::ready(Err(StreamingEditFileToolOutput::Error { error: "thread was dropped".to_string(), })); }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => { return Task::ready(Err(StreamingEditFileToolOutput::Error { error: err.to_string(), })); } }; 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| { let result: anyhow::Result<StreamingEditFileToolOutput> = async { authorize.await?; let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) }) .await?; 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) })?; 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); } if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { 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 action_log = self.thread.read_with(cx, |thread, _cx| thread.action_log().clone())?; // Edit the buffer and report edits to the action log as part of the // same effect cycle, otherwise the edit will be reported as if the // user made it (due to the buffer subscription in action_log). match input.mode { StreamingEditFileMode::Create | StreamingEditFileMode::Overwrite => { action_log.update(cx, |log, cx| { log.buffer_created(buffer.clone(), cx); }); let content = input.content.ok_or_else(|| { anyhow!("'content' field is required for create and overwrite modes") })?; cx.update(|cx| { buffer.update(cx, |buffer, cx| { buffer.edit([(0..buffer.len(), content.as_str())], None, cx); }); action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); }); } StreamingEditFileMode::Edit => { action_log.update(cx, |log, cx| { log.buffer_read(buffer.clone(), cx); }); let edits = input.edits.ok_or_else(|| { anyhow!("'edits' field is required for edit mode") })?; // apply_edits now handles buffer_edited internally in the same effect cycle apply_edits(&buffer, &action_log, &edits, &diff, &event_stream, &abs_path, cx)?; } } 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, FormatTrigger::Save, cx, ) }); futures::select! { result = format_task.fuse() => { result.log_err(); }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; } let save_task = project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); futures::select! { result = save_task.fuse() => { result?; }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); 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 output = StreamingEditFileToolOutput::Success { input_path: input.path, new_text, old_text, diff: unified_diff, }; Ok(output) }.await; result .map_err(|e| StreamingEditFileToolOutput::Error { error: e.to_string() }) }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App,Lines 1–500 of 1295. Load more
use super::edit_file_tool::EditFileTool; use super::restore_file_from_disk_tool::RestoreFileFromDiskTool; use super::save_file_tool::SaveFileTool; use crate::{ AgentTool, Templates, Thread, ToolCallEventStream, edit_agent::streaming_fuzzy_matcher::StreamingFuzzyMatcher, }; use acp_thread::Diff; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; use collections::HashSet; use futures::FutureExt as _; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use language::LanguageRegistry; use language::language_settings::{self, FormatOnSave}; use language_model::LanguageModelToolResultContent; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{Project, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; use text::BufferSnapshot; 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 StreamingEditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI. /// /// 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: /// - 'create': Create a new file if it doesn't exist. Requires 'content' field. /// - 'overwrite': Replace the entire contents of an existing file. Requires 'content' field. /// - 'edit': Make granular edits to an existing file. Requires 'edits' field. /// /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: StreamingEditFileMode, /// The complete content for the new file (required for 'create' and 'overwrite' modes). /// This field should contain the entire file content. #[serde(default, skip_serializing_if = "Option::is_none")] pub content: Option<String>, /// List of edit operations to apply sequentially (required for 'edit' mode). /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde(default, skip_serializing_if = "Option::is_none")] pub edits: Option<Vec<EditOperation>>, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum StreamingEditFileMode { /// Create a new file if it doesn't exist Create, /// Replace the entire contents of an existing file Overwrite, /// Make granular edits to an existing file Edit, } /// A single edit operation that replaces old text with new text #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct EditOperation { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. pub old_text: String, /// The text to replace it with pub new_text: String, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] struct StreamingEditFileToolPartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum StreamingEditFileToolOutput { Success { #[serde(alias = "original_path")] input_path: PathBuf, new_text: String, old_text: Arc<String>, #[serde(default)] diff: String, }, Error { error: String, }, } impl std::fmt::Display for StreamingEditFileToolOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { StreamingEditFileToolOutput::Success { diff, input_path, .. } => { if diff.is_empty() { write!(f, "No edits were made.") } else { write!( f, "Edited {}:\n\n```diff\n{diff}\n```", input_path.display() ) } } StreamingEditFileToolOutput::Error { error } => write!(f, "{error}"), } } } impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent { fn from(output: StreamingEditFileToolOutput) -> Self { output.to_string().into() } } pub struct StreamingEditFileTool { thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, project: Entity<Project>, #[allow(dead_code)] templates: Arc<Templates>, } impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>, templates: Arc<Templates>, ) -> Self { Self { project, thread, language_registry, templates, } } pub fn with_thread(&self, new_thread: WeakEntity<Thread>) -> Self { Self { project: self.project.clone(), thread: new_thread, language_registry: self.language_registry.clone(), templates: self.templates.clone(), } } fn authorize( &self, input: &StreamingEditFileToolInput, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, &input.path, &input.display_description, &self.thread, event_stream, cx, ) } } impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_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::<StreamingEditFileToolPartialInput>(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, Self::Output>> { let Ok(project) = self .thread .read_with(cx, |thread, _cx| thread.project().clone()) else { return Task::ready(Err(StreamingEditFileToolOutput::Error { error: "thread was dropped".to_string(), })); }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => { return Task::ready(Err(StreamingEditFileToolOutput::Error { error: err.to_string(), })); } }; 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| { let result: anyhow::Result<StreamingEditFileToolOutput> = async { authorize.await?; let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) }) .await?; 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) })?; 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); } if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { 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 action_log = self.thread.read_with(cx, |thread, _cx| thread.action_log().clone())?; // Edit the buffer and report edits to the action log as part of the // same effect cycle, otherwise the edit will be reported as if the // user made it (due to the buffer subscription in action_log). match input.mode { StreamingEditFileMode::Create | StreamingEditFileMode::Overwrite => { action_log.update(cx, |log, cx| { log.buffer_created(buffer.clone(), cx); }); let content = input.content.ok_or_else(|| { anyhow!("'content' field is required for create and overwrite modes") })?; cx.update(|cx| { buffer.update(cx, |buffer, cx| { buffer.edit([(0..buffer.len(), content.as_str())], None, cx); }); action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); }); } StreamingEditFileMode::Edit => { action_log.update(cx, |log, cx| { log.buffer_read(buffer.clone(), cx); }); let edits = input.edits.ok_or_else(|| { anyhow!("'edits' field is required for edit mode") })?; // apply_edits now handles buffer_edited internally in the same effect cycle apply_edits(&buffer, &action_log, &edits, &diff, &event_stream, &abs_path, cx)?; } } 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, FormatTrigger::Save, cx, ) }); futures::select! { result = format_task.fuse() => { result.log_err(); }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; } let save_task = project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); futures::select! { result = save_task.fuse() => { result?; }, _ = event_stream.cancelled_by_user().fuse() => { anyhow::bail!("Edit cancelled by user"); } }; action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); }); 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 output = StreamingEditFileToolOutput::Success { input_path: input.path, new_text, old_text, diff: unified_diff, }; Ok(output) }.await; result .map_err(|e| StreamingEditFileToolOutput::Error { error: e.to_string() }) }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App,