use super::deserialize_maybe_stringified;pub(crate) use super::edit_session::PartialEdit;pub use super::edit_session::{Edit, EditSessionOutput as EditFileToolOutput};use super::edit_session::{ EditSession, EditSessionContext, EditSessionMode, EditSessionResult, initial_title_from_partial_path, run_session,};use crate::{AgentTool, Thread, ToolCallEventStream, ToolInput, ToolInputPayload};use action_log::ActionLog;use agent_client_protocol::schema::v1 as acp;use anyhow::Result;use futures::FutureExt as _;use gpui::{App, AsyncApp, Entity, Task, WeakEntity};use language::LanguageRegistry;use project::Project;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::path::PathBuf;use std::sync::Arc;use ui::SharedString; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for applying edits to an existing file.////// Before using this tool, use the `read_file` tool to understand the file's contents and context./// To create a new file or overwrite an existing one with completely new contents, use the `write_file` tool instead.////// The only supported path outside the project is `~/.agents/skills` or a descendant, for global agent skills.////// `read_file` prefixes each line of its output with a line number right-aligned in a/// 6-character field followed by a single tab, then the line's actual content. When you/// derive `old_text` or `new_text` from that output, strip this prefix and keep only what/// comes after the tab, preserving the original indentation (tabs and spaces) exactly./// Never include any part of the line number prefix in `old_text` or `new_text`.#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct EditFileToolInput { /// The full path of the file to edit in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories, unless it's a global agent skill under `~/.agents/skills`. /// /// 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> /// /// <example> /// To edit a global agent skill file, you may provide a path under `~/.agents/skills`, such as `~/.agents/skills/my-skill/SKILL.md`. /// </example> pub path: PathBuf, /// List of edit operations to apply sequentially. /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde(deserialize_with = "deserialize_maybe_stringified")] pub edits: Vec<Edit>,} #[derive(Clone, Default, Debug, Deserialize)]struct EditFileToolPartialInput { #[serde(default)] path: Option<String>, #[serde(default, deserialize_with = "deserialize_maybe_stringified")] edits: Option<Vec<PartialEdit>>,} pub struct EditFileTool { session_context: Arc<EditSessionContext>,} impl EditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { session_context: Arc::new(EditSessionContext::new( project, thread, action_log, language_registry, )), } } #[cfg(test)] fn authorize( &self, path: &PathBuf, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { self.session_context .authorize(Self::NAME, path, event_stream, cx) } async fn process_streaming_edits( &self, input: &mut ToolInput<EditFileToolInput>, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> EditSessionResult { let mut session: Option<EditSession> = None; let mut last_path: Option<String> = None; loop { futures::select! { payload = input.next().fuse() => { match payload { Ok(payload) => match payload { ToolInputPayload::Partial(partial) => { if let Ok(parsed) = serde_json::from_value::<EditFileToolPartialInput>(partial) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_path.as_ref(); last_path = parsed.path.clone(); if session.is_none() && path_complete && let Some(path) = parsed.path.as_ref() { match EditSession::new( PathBuf::from(path), EditSessionMode::Edit, Self::NAME, self.session_context.clone(), event_stream, cx, ) .await { Ok(created_session) => session = Some(created_session), Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } } if let Some(current_session) = &mut session && let Err(error) = current_session.process_edit(parsed.edits.as_deref(), event_stream, cx) { log::error!("Failed to process edit: {}", error); return EditSessionResult::Failed { error, session }; } } } ToolInputPayload::Full(full_input) => { let mut session = if let Some(session) = session { session } else { match EditSession::new( full_input.path.clone(), EditSessionMode::Edit, Self::NAME, self.session_context.clone(), event_stream, cx, ) .await { Ok(created_session) => created_session, Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } }; return match session.finalize_edit(full_input.edits, event_stream, cx).await { Ok(()) => EditSessionResult::Completed(session), Err(error) => { log::error!("Failed to finalize edit: {}", error); EditSessionResult::Failed { error, session: Some(session), } } }; } ToolInputPayload::InvalidJson { error_message } => { log::error!("Received invalid JSON: {error_message}"); return EditSessionResult::Failed { error: error_message, session, }; } }, Err(error) => { return EditSessionResult::Failed { error: error.to_string(), session, }; } } } _ = event_stream.cancelled_by_user().fuse() => { return EditSessionResult::Failed { error: "Edit cancelled by user".to_string(), session, }; } } } }} impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; const NAME: &'static str = "edit_file"; fn supports_input_streaming() -> bool { true } 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.session_context .initial_title_from_path(&input.path, DEFAULT_UI_TEXT, cx) } Err(raw_input) => initial_title_from_partial_path::<EditFileToolPartialInput>( &self.session_context, raw_input, |partial| partial.path.clone(), DEFAULT_UI_TEXT, cx, ), } } fn run( self: Arc<Self>, mut input: ToolInput<Self::Input>, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output, Self::Output>> { cx.spawn(async move |cx: &mut AsyncApp| { run_session( self.process_streaming_edits(&mut input, &event_stream, cx) .await, &event_stream, cx, ) .await }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { self.session_context.replay_output(output, event_stream, cx) }} #[cfg(test)]mod tests { use super::*; use crate::{ContextServerRegistry, Templates, ToolInputSender}; use fs::Fs as _; use gpui::{AppContext as _, TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use project::ProjectPath; use prompt_store::ProjectContext; use serde_json::json; use settings::Settings; use settings::SettingsStore; use util::path; use util::rel_path::{RelPath, rel_path}; #[gpui::test] async fn test_streaming_edit_granular_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test(cx, json!({"file.txt": "line 1\nline 2\nline 3\n"})).await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![Edit { old_text: "line 2".into(), new_text: "modified line 2".into(), }], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n"); } #[gpui::test] async fn test_streaming_edit_first_line_missing_indent(cx: &mut TestAppContext) { // Reproduces https://github.com/zed-industries/zed/issues/60302: the // first line of the multi-line `old_text` omits its leading // indentation while subsequent lines include theirs, so the indent // delta computed from the first line must not be applied to the // following lines. `old_text` also omits the `self.extra` line, so // the query lines don't correspond one-to-one to the matched buffer // rows and the indent pairing must follow the fuzzy match's // alignment instead of assuming equal line counts. let content = concat!( "class Outer:\n", " def method(self):\n", " self.kept = \"unchanged\"\n", " self.target_a = \"before\"\n", " self.extra = \"row\"\n", " self.target_b = \"before\"\n", " self.target_c = \"before\"\n", " self.target_d = \"before\"\n", " self.kept_2 = \"unchanged\"\n", ); let (edit_tool, _project, _action_log, _fs, _thread) = setup_test(cx, json!({"file.py": content})).await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.py".into(), edits: vec![Edit { old_text: concat!( "self.target_a = \"before\"\n", " self.target_b = \"before\"\n", " self.target_c = \"before\"\n", " self.target_d = \"before\"", ) .into(), new_text: concat!( "self.target_a = \"after\"\n", " self.target_b = \"after\"\n", " self.target_c = \"after\"\n", " self.target_d = \"after\"", ) .into(), }], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; // The matched range includes the `self.extra` row, so it is replaced // along with the rest of the match. assert_eq!( new_text, concat!( "class Outer:\n", " def method(self):\n", " self.kept = \"unchanged\"\n", " self.target_a = \"after\"\n", " self.target_b = \"after\"\n", " self.target_c = \"after\"\n", " self.target_d = \"after\"\n", " self.kept_2 = \"unchanged\"\n", ) ); } #[gpui::test] async fn test_streaming_edit_multiple_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 5".into(), new_text: "modified line 5".into(), }, Edit { old_text: "line 1".into(), new_text: "modified line 1".into(), }, ], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!( new_text, "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n" ); } #[gpui::test] async fn test_streaming_edit_adjacent_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 2".into(), new_text: "modified line 2".into(), }, Edit { old_text: "line 3".into(), new_text: "modified line 3".into(), }, ], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!( new_text, "line 1\nmodified line 2\nmodified line 3\nline 4\nline 5\n" ); } #[gpui::test] async fn test_streaming_edit_ascending_order_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 1".into(), new_text: "modified line 1".into(), }, Edit { old_text: "line 5".into(), new_text: "modified line 5".into(), }, ], }), ToolCallEventStream::test().0, cx, ) })Lines 1–500 of 3031. Load more
use super::deserialize_maybe_stringified; pub(crate) use super::edit_session::PartialEdit; pub use super::edit_session::{Edit, EditSessionOutput as EditFileToolOutput}; use super::edit_session::{ EditSession, EditSessionContext, EditSessionMode, EditSessionResult, initial_title_from_partial_path, run_session, }; use crate::{AgentTool, Thread, ToolCallEventStream, ToolInput, ToolInputPayload}; use action_log::ActionLog; use agent_client_protocol::schema::v1 as acp; use anyhow::Result; use futures::FutureExt as _; use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; use language::LanguageRegistry; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use ui::SharedString; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for applying edits to an existing file. /// /// Before using this tool, use the `read_file` tool to understand the file's contents and context. /// To create a new file or overwrite an existing one with completely new contents, use the `write_file` tool instead. /// /// The only supported path outside the project is `~/.agents/skills` or a descendant, for global agent skills. /// /// `read_file` prefixes each line of its output with a line number right-aligned in a /// 6-character field followed by a single tab, then the line's actual content. When you /// derive `old_text` or `new_text` from that output, strip this prefix and keep only what /// comes after the tab, preserving the original indentation (tabs and spaces) exactly. /// Never include any part of the line number prefix in `old_text` or `new_text`. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { /// The full path of the file to edit in the project. /// /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories, unless it's a global agent skill under `~/.agents/skills`. /// /// 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> /// /// <example> /// To edit a global agent skill file, you may provide a path under `~/.agents/skills`, such as `~/.agents/skills/my-skill/SKILL.md`. /// </example> pub path: PathBuf, /// List of edit operations to apply sequentially. /// Each edit finds `old_text` in the file and replaces it with `new_text`. #[serde(deserialize_with = "deserialize_maybe_stringified")] pub edits: Vec<Edit>, } #[derive(Clone, Default, Debug, Deserialize)] struct EditFileToolPartialInput { #[serde(default)] path: Option<String>, #[serde(default, deserialize_with = "deserialize_maybe_stringified")] edits: Option<Vec<PartialEdit>>, } pub struct EditFileTool { session_context: Arc<EditSessionContext>, } impl EditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { session_context: Arc::new(EditSessionContext::new( project, thread, action_log, language_registry, )), } } #[cfg(test)] fn authorize( &self, path: &PathBuf, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { self.session_context .authorize(Self::NAME, path, event_stream, cx) } async fn process_streaming_edits( &self, input: &mut ToolInput<EditFileToolInput>, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> EditSessionResult { let mut session: Option<EditSession> = None; let mut last_path: Option<String> = None; loop { futures::select! { payload = input.next().fuse() => { match payload { Ok(payload) => match payload { ToolInputPayload::Partial(partial) => { if let Ok(parsed) = serde_json::from_value::<EditFileToolPartialInput>(partial) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_path.as_ref(); last_path = parsed.path.clone(); if session.is_none() && path_complete && let Some(path) = parsed.path.as_ref() { match EditSession::new( PathBuf::from(path), EditSessionMode::Edit, Self::NAME, self.session_context.clone(), event_stream, cx, ) .await { Ok(created_session) => session = Some(created_session), Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } } if let Some(current_session) = &mut session && let Err(error) = current_session.process_edit(parsed.edits.as_deref(), event_stream, cx) { log::error!("Failed to process edit: {}", error); return EditSessionResult::Failed { error, session }; } } } ToolInputPayload::Full(full_input) => { let mut session = if let Some(session) = session { session } else { match EditSession::new( full_input.path.clone(), EditSessionMode::Edit, Self::NAME, self.session_context.clone(), event_stream, cx, ) .await { Ok(created_session) => created_session, Err(error) => { log::error!("Failed to create edit session: {}", error); return EditSessionResult::Failed { error, session: None, }; } } }; return match session.finalize_edit(full_input.edits, event_stream, cx).await { Ok(()) => EditSessionResult::Completed(session), Err(error) => { log::error!("Failed to finalize edit: {}", error); EditSessionResult::Failed { error, session: Some(session), } } }; } ToolInputPayload::InvalidJson { error_message } => { log::error!("Received invalid JSON: {error_message}"); return EditSessionResult::Failed { error: error_message, session, }; } }, Err(error) => { return EditSessionResult::Failed { error: error.to_string(), session, }; } } } _ = event_stream.cancelled_by_user().fuse() => { return EditSessionResult::Failed { error: "Edit cancelled by user".to_string(), session, }; } } } } } impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; const NAME: &'static str = "edit_file"; fn supports_input_streaming() -> bool { true } 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.session_context .initial_title_from_path(&input.path, DEFAULT_UI_TEXT, cx) } Err(raw_input) => initial_title_from_partial_path::<EditFileToolPartialInput>( &self.session_context, raw_input, |partial| partial.path.clone(), DEFAULT_UI_TEXT, cx, ), } } fn run( self: Arc<Self>, mut input: ToolInput<Self::Input>, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output, Self::Output>> { cx.spawn(async move |cx: &mut AsyncApp| { run_session( self.process_streaming_edits(&mut input, &event_stream, cx) .await, &event_stream, cx, ) .await }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { self.session_context.replay_output(output, event_stream, cx) } } #[cfg(test)] mod tests { use super::*; use crate::{ContextServerRegistry, Templates, ToolInputSender}; use fs::Fs as _; use gpui::{AppContext as _, TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use project::ProjectPath; use prompt_store::ProjectContext; use serde_json::json; use settings::Settings; use settings::SettingsStore; use util::path; use util::rel_path::{RelPath, rel_path}; #[gpui::test] async fn test_streaming_edit_granular_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test(cx, json!({"file.txt": "line 1\nline 2\nline 3\n"})).await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![Edit { old_text: "line 2".into(), new_text: "modified line 2".into(), }], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n"); } #[gpui::test] async fn test_streaming_edit_first_line_missing_indent(cx: &mut TestAppContext) { // Reproduces https://github.com/zed-industries/zed/issues/60302: the // first line of the multi-line `old_text` omits its leading // indentation while subsequent lines include theirs, so the indent // delta computed from the first line must not be applied to the // following lines. `old_text` also omits the `self.extra` line, so // the query lines don't correspond one-to-one to the matched buffer // rows and the indent pairing must follow the fuzzy match's // alignment instead of assuming equal line counts. let content = concat!( "class Outer:\n", " def method(self):\n", " self.kept = \"unchanged\"\n", " self.target_a = \"before\"\n", " self.extra = \"row\"\n", " self.target_b = \"before\"\n", " self.target_c = \"before\"\n", " self.target_d = \"before\"\n", " self.kept_2 = \"unchanged\"\n", ); let (edit_tool, _project, _action_log, _fs, _thread) = setup_test(cx, json!({"file.py": content})).await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.py".into(), edits: vec![Edit { old_text: concat!( "self.target_a = \"before\"\n", " self.target_b = \"before\"\n", " self.target_c = \"before\"\n", " self.target_d = \"before\"", ) .into(), new_text: concat!( "self.target_a = \"after\"\n", " self.target_b = \"after\"\n", " self.target_c = \"after\"\n", " self.target_d = \"after\"", ) .into(), }], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; // The matched range includes the `self.extra` row, so it is replaced // along with the rest of the match. assert_eq!( new_text, concat!( "class Outer:\n", " def method(self):\n", " self.kept = \"unchanged\"\n", " self.target_a = \"after\"\n", " self.target_b = \"after\"\n", " self.target_c = \"after\"\n", " self.target_d = \"after\"\n", " self.kept_2 = \"unchanged\"\n", ) ); } #[gpui::test] async fn test_streaming_edit_multiple_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 5".into(), new_text: "modified line 5".into(), }, Edit { old_text: "line 1".into(), new_text: "modified line 1".into(), }, ], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!( new_text, "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n" ); } #[gpui::test] async fn test_streaming_edit_adjacent_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 2".into(), new_text: "modified line 2".into(), }, Edit { old_text: "line 3".into(), new_text: "modified line 3".into(), }, ], }), ToolCallEventStream::test().0, cx, ) }) .await; let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else { panic!("expected success"); }; assert_eq!( new_text, "line 1\nmodified line 2\nmodified line 3\nline 4\nline 5\n" ); } #[gpui::test] async fn test_streaming_edit_ascending_order_edits(cx: &mut TestAppContext) { let (edit_tool, _project, _action_log, _fs, _thread) = setup_test( cx, json!({"file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"}), ) .await; let result = cx .update(|cx| { edit_tool.clone().run( ToolInput::resolved(EditFileToolInput { path: "root/file.txt".into(), edits: vec![ Edit { old_text: "line 1".into(), new_text: "modified line 1".into(), }, Edit { old_text: "line 5".into(), new_text: "modified line 5".into(), }, ], }), ToolCallEventStream::test().0, cx, ) })