use super::edit_file_tool::EditFileTool;use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use super::tool_edit_parser::{ToolEditEvent, ToolEditParser};use crate::{ AgentTool, Thread, ToolCallEventStream, ToolInput, edit_agent::{ reindent::{Reindenter, compute_indent_delta}, streaming_fuzzy_matcher::StreamingFuzzyMatcher, },};use acp_thread::Diff;use action_log::ActionLog;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::{Context as _, Result};use collections::HashSet;use futures::FutureExt as _;use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use language::language_settings::{self, FormatOnSave};use language::{Buffer, LanguageRegistry};use language_model::LanguageModelToolResultContent;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{AgentLocation, Project, ProjectPath};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use streaming_diff::{CharOperation, StreamingDiff};use text::ToOffset;use ui::SharedString;use util::rel_path::RelPath;use util::{Deferred, ResultExt}; 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: /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. 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 'write' mode). /// 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<Edit>>,} #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum StreamingEditFileMode { /// Overwrite the file with new content (replacing any existing content). /// If the file does not exist, it will be created. Write, /// 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 Edit { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. /// /// Always include complete lines. Do not start or end mid-line. /// Be minimal with replacements: /// - For unique lines, include only those lines /// - For non-unique lines, include enough context to identify them pub old_text: String, /// The text to replace it with pub new_text: String,} #[derive(Clone, Default, Debug, Deserialize)]struct StreamingEditFileToolPartialInput { #[serde(default)] display_description: Option<String>, #[serde(default)] path: Option<String>, #[serde(default)] mode: Option<StreamingEditFileMode>, #[serde(default)] content: Option<String>, #[serde(default)] edits: Option<Vec<PartialEdit>>,} #[derive(Clone, Default, Debug, Deserialize)]pub struct PartialEdit { #[serde(default)] pub old_text: Option<String>, #[serde(default)] pub new_text: Option<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 StreamingEditFileToolOutput { pub fn error(error: impl Into<String>) -> Self { Self::Error { error: error.into(), } }} 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 { project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>,} impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { project, thread, action_log, language_registry, } } fn authorize( &self, path: &PathBuf, description: &str, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, path, description, &self.thread, event_stream, cx, ) } fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) { let should_update_agent_location = self .thread .read_with(cx, |thread, _cx| !thread.is_subagent()) .unwrap_or_default(); if should_update_agent_location { self.project.update(cx, |project, cx| { project.set_agent_location(Some(AgentLocation { buffer, position }), cx); }); } }} impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_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 .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 Ok(input) = serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input) { let path = input.path.unwrap_or_default(); let path = path.trim(); if !path.is_empty() { return self .project .read(cx) .find_project_path(&path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or_else(|| path.to_string()) .into(); } let description = input.display_description.unwrap_or_default(); let description = description.trim(); if !description.is_empty() { return description.to_string().into(); } } DEFAULT_UI_TEXT.into() } } } 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| { let mut state: Option<EditSession> = None; let mut last_partial: Option<StreamingEditFileToolPartialInput> = None; loop { futures::select! { partial = input.recv_partial().fuse() => { let Some(partial_value) = partial else { break }; if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial_value) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_partial.as_ref().and_then(|p| p.path.as_ref()); last_partial = Some(parsed.clone()); if state.is_none() && path_complete && let StreamingEditFileToolPartialInput { path: Some(path), display_description: Some(display_description), mode: Some(mode), .. } = &parsed { match EditSession::new( &PathBuf::from(path), display_description, *mode, &self, &event_stream, cx, ) .await { Ok(session) => state = Some(session), Err(e) => { log::error!("Failed to create edit session: {}", e); return Err(e); } } } if let Some(state) = &mut state { if let Err(e) = state.process(parsed, &self, &event_stream, cx) { log::error!("Failed to process edit: {}", e); return Err(e); } } } } _ = event_stream.cancelled_by_user().fuse() => { return Err(StreamingEditFileToolOutput::error("Edit cancelled by user")); } } } let full_input = input .recv() .await .map_err(|e| { let err = StreamingEditFileToolOutput::error(format!("Failed to receive tool input: {e}")); log::error!("Failed to receive tool input: {e}"); err })?; let mut state = if let Some(state) = state { state } else { match EditSession::new( &full_input.path, &full_input.display_description, full_input.mode, &self, &event_stream, cx, ) .await { Ok(session) => session, Err(e) => { log::error!("Failed to create edit session: {}", e); return Err(e); } } }; match state.finalize(full_input, &self, &event_stream, cx).await { Ok(output) => Ok(output), Err(e) => { log::error!("Failed to finalize edit: {}", e); Err(e) } } }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { match output { StreamingEditFileToolOutput::Success { input_path, old_text, new_text, .. } => { event_stream.update_diff(cx.new(|cx| { Diff::finalized( input_path.to_string_lossy().into_owned(), Some(old_text.to_string()), new_text, self.language_registry.clone(), cx, ) })); Ok(()) } StreamingEditFileToolOutput::Error { .. } => Ok(()), } }} pub struct EditSession { abs_path: PathBuf, buffer: Entity<Buffer>, old_text: Arc<String>, diff: Entity<Diff>, mode: StreamingEditFileMode, parser: ToolEditParser, pipeline: EditPipeline, _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>,} struct EditPipeline { current_edit: Option<EditPipelineEntry>, content_written: bool,} enum EditPipelineEntry { ResolvingOldText { matcher: StreamingFuzzyMatcher, }, StreamingNewText { streaming_diff: StreamingDiff, edit_cursor: usize, reindenter: Reindenter, original_snapshot: text::BufferSnapshot, },} impl EditPipeline { fn new() -> Self { Self { current_edit: None, content_written: false, } } fn ensure_resolving_old_text(&mut self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) { if self.current_edit.is_none() { let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot()); self.current_edit = Some(EditPipelineEntry::ResolvingOldText { matcher: StreamingFuzzyMatcher::new(snapshot), }); } }} impl EditSession { async fn new( path: &PathBuf, display_description: &str, mode: StreamingEditFileMode, tool: &StreamingEditFileTool, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> Result<Self, StreamingEditFileToolOutput> { let project_path = cx .update(|cx| resolve_path(mode, &path, &tool.project, cx)) .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; let Some(abs_path) = cx.update(|cx| tool.project.read(cx).absolute_path(&project_path, cx)) else { return Err(StreamingEditFileToolOutput::error(format!( "Worktree at '{}' does not exist",Lines 1–500 of 3926. Load more
use super::edit_file_tool::EditFileTool; use super::restore_file_from_disk_tool::RestoreFileFromDiskTool; use super::save_file_tool::SaveFileTool; use super::tool_edit_parser::{ToolEditEvent, ToolEditParser}; use crate::{ AgentTool, Thread, ToolCallEventStream, ToolInput, edit_agent::{ reindent::{Reindenter, compute_indent_delta}, streaming_fuzzy_matcher::StreamingFuzzyMatcher, }, }; use acp_thread::Diff; use action_log::ActionLog; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::{Context as _, Result}; use collections::HashSet; use futures::FutureExt as _; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use language::language_settings::{self, FormatOnSave}; use language::{Buffer, LanguageRegistry}; use language_model::LanguageModelToolResultContent; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{AgentLocation, Project, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; use streaming_diff::{CharOperation, StreamingDiff}; use text::ToOffset; use ui::SharedString; use util::rel_path::RelPath; use util::{Deferred, ResultExt}; 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: /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. 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 'write' mode). /// 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<Edit>>, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum StreamingEditFileMode { /// Overwrite the file with new content (replacing any existing content). /// If the file does not exist, it will be created. Write, /// 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 Edit { /// The exact text to find in the file. This will be matched using fuzzy matching /// to handle minor differences in whitespace or formatting. /// /// Always include complete lines. Do not start or end mid-line. /// Be minimal with replacements: /// - For unique lines, include only those lines /// - For non-unique lines, include enough context to identify them pub old_text: String, /// The text to replace it with pub new_text: String, } #[derive(Clone, Default, Debug, Deserialize)] struct StreamingEditFileToolPartialInput { #[serde(default)] display_description: Option<String>, #[serde(default)] path: Option<String>, #[serde(default)] mode: Option<StreamingEditFileMode>, #[serde(default)] content: Option<String>, #[serde(default)] edits: Option<Vec<PartialEdit>>, } #[derive(Clone, Default, Debug, Deserialize)] pub struct PartialEdit { #[serde(default)] pub old_text: Option<String>, #[serde(default)] pub new_text: Option<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 StreamingEditFileToolOutput { pub fn error(error: impl Into<String>) -> Self { Self::Error { error: error.into(), } } } 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 { project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, } impl StreamingEditFileTool { pub fn new( project: Entity<Project>, thread: WeakEntity<Thread>, action_log: Entity<ActionLog>, language_registry: Arc<LanguageRegistry>, ) -> Self { Self { project, thread, action_log, language_registry, } } fn authorize( &self, path: &PathBuf, description: &str, event_stream: &ToolCallEventStream, cx: &mut App, ) -> Task<Result<()>> { super::tool_permissions::authorize_file_edit( EditFileTool::NAME, path, description, &self.thread, event_stream, cx, ) } fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) { let should_update_agent_location = self .thread .read_with(cx, |thread, _cx| !thread.is_subagent()) .unwrap_or_default(); if should_update_agent_location { self.project.update(cx, |project, cx| { project.set_agent_location(Some(AgentLocation { buffer, position }), cx); }); } } } impl AgentTool for StreamingEditFileTool { type Input = StreamingEditFileToolInput; type Output = StreamingEditFileToolOutput; const NAME: &'static str = "streaming_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 .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 Ok(input) = serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input) { let path = input.path.unwrap_or_default(); let path = path.trim(); if !path.is_empty() { return self .project .read(cx) .find_project_path(&path, cx) .and_then(|project_path| { self.project .read(cx) .short_full_path_for_project_path(&project_path, cx) }) .unwrap_or_else(|| path.to_string()) .into(); } let description = input.display_description.unwrap_or_default(); let description = description.trim(); if !description.is_empty() { return description.to_string().into(); } } DEFAULT_UI_TEXT.into() } } } 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| { let mut state: Option<EditSession> = None; let mut last_partial: Option<StreamingEditFileToolPartialInput> = None; loop { futures::select! { partial = input.recv_partial().fuse() => { let Some(partial_value) = partial else { break }; if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial_value) { let path_complete = parsed.path.is_some() && parsed.path.as_ref() == last_partial.as_ref().and_then(|p| p.path.as_ref()); last_partial = Some(parsed.clone()); if state.is_none() && path_complete && let StreamingEditFileToolPartialInput { path: Some(path), display_description: Some(display_description), mode: Some(mode), .. } = &parsed { match EditSession::new( &PathBuf::from(path), display_description, *mode, &self, &event_stream, cx, ) .await { Ok(session) => state = Some(session), Err(e) => { log::error!("Failed to create edit session: {}", e); return Err(e); } } } if let Some(state) = &mut state { if let Err(e) = state.process(parsed, &self, &event_stream, cx) { log::error!("Failed to process edit: {}", e); return Err(e); } } } } _ = event_stream.cancelled_by_user().fuse() => { return Err(StreamingEditFileToolOutput::error("Edit cancelled by user")); } } } let full_input = input .recv() .await .map_err(|e| { let err = StreamingEditFileToolOutput::error(format!("Failed to receive tool input: {e}")); log::error!("Failed to receive tool input: {e}"); err })?; let mut state = if let Some(state) = state { state } else { match EditSession::new( &full_input.path, &full_input.display_description, full_input.mode, &self, &event_stream, cx, ) .await { Ok(session) => session, Err(e) => { log::error!("Failed to create edit session: {}", e); return Err(e); } } }; match state.finalize(full_input, &self, &event_stream, cx).await { Ok(output) => Ok(output), Err(e) => { log::error!("Failed to finalize edit: {}", e); Err(e) } } }) } fn replay( &self, _input: Self::Input, output: Self::Output, event_stream: ToolCallEventStream, cx: &mut App, ) -> Result<()> { match output { StreamingEditFileToolOutput::Success { input_path, old_text, new_text, .. } => { event_stream.update_diff(cx.new(|cx| { Diff::finalized( input_path.to_string_lossy().into_owned(), Some(old_text.to_string()), new_text, self.language_registry.clone(), cx, ) })); Ok(()) } StreamingEditFileToolOutput::Error { .. } => Ok(()), } } } pub struct EditSession { abs_path: PathBuf, buffer: Entity<Buffer>, old_text: Arc<String>, diff: Entity<Diff>, mode: StreamingEditFileMode, parser: ToolEditParser, pipeline: EditPipeline, _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>, } struct EditPipeline { current_edit: Option<EditPipelineEntry>, content_written: bool, } enum EditPipelineEntry { ResolvingOldText { matcher: StreamingFuzzyMatcher, }, StreamingNewText { streaming_diff: StreamingDiff, edit_cursor: usize, reindenter: Reindenter, original_snapshot: text::BufferSnapshot, }, } impl EditPipeline { fn new() -> Self { Self { current_edit: None, content_written: false, } } fn ensure_resolving_old_text(&mut self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) { if self.current_edit.is_none() { let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot()); self.current_edit = Some(EditPipelineEntry::ResolvingOldText { matcher: StreamingFuzzyMatcher::new(snapshot), }); } } } impl EditSession { async fn new( path: &PathBuf, display_description: &str, mode: StreamingEditFileMode, tool: &StreamingEditFileTool, event_stream: &ToolCallEventStream, cx: &mut AsyncApp, ) -> Result<Self, StreamingEditFileToolOutput> { let project_path = cx .update(|cx| resolve_path(mode, &path, &tool.project, cx)) .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; let Some(abs_path) = cx.update(|cx| tool.project.read(cx).absolute_path(&project_path, cx)) else { return Err(StreamingEditFileToolOutput::error(format!( "Worktree at '{}' does not exist",