streaming_edit_file_tool.rs

   1use super::edit_file_tool::EditFileTool;
   2use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;
   3use super::save_file_tool::SaveFileTool;
   4use super::tool_edit_parser::{ToolEditEvent, ToolEditParser};
   5use crate::{
   6    AgentTool, Thread, ToolCallEventStream, ToolInput,
   7    edit_agent::{
   8        reindent::{Reindenter, compute_indent_delta},
   9        streaming_fuzzy_matcher::StreamingFuzzyMatcher,
  10    },
  11};
  12use acp_thread::Diff;
  13use action_log::ActionLog;
  14use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
  15use anyhow::{Context as _, Result};
  16use collections::HashSet;
  17use futures::FutureExt as _;
  18use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
  19use language::language_settings::{self, FormatOnSave};
  20use language::{Buffer, LanguageRegistry};
  21use language_model::LanguageModelToolResultContent;
  22use project::lsp_store::{FormatTrigger, LspFormatTarget};
  23use project::{AgentLocation, Project, ProjectPath};
  24use schemars::JsonSchema;
  25use serde::{Deserialize, Serialize};
  26use std::ops::Range;
  27use std::path::PathBuf;
  28use std::sync::Arc;
  29use streaming_diff::{CharOperation, StreamingDiff};
  30use text::ToOffset;
  31use ui::SharedString;
  32use util::rel_path::RelPath;
  33use util::{Deferred, ResultExt};
  34
  35const DEFAULT_UI_TEXT: &str = "Editing file";
  36
  37/// 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.
  38///
  39/// Before using this tool:
  40///
  41/// 1. Use the `read_file` tool to understand the file's contents and context
  42///
  43/// 2. Verify the directory path is correct (only applicable when creating new files):
  44///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location
  45#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  46pub struct StreamingEditFileToolInput {
  47    /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI.
  48    ///
  49    /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
  50    ///
  51    /// NEVER mention the file path in this description.
  52    ///
  53    /// <example>Fix API endpoint URLs</example>
  54    /// <example>Update copyright year in `page_footer`</example>
  55    ///
  56    /// Make sure to include this field before all the others in the input object so that we can display it immediately.
  57    pub display_description: String,
  58
  59    /// The full path of the file to create or modify in the project.
  60    ///
  61    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
  62    ///
  63    /// The following examples assume we have two root directories in the project:
  64    /// - /a/b/backend
  65    /// - /c/d/frontend
  66    ///
  67    /// <example>
  68    /// `backend/src/main.rs`
  69    ///
  70    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
  71    /// </example>
  72    ///
  73    /// <example>
  74    /// `frontend/db.js`
  75    /// </example>
  76    pub path: String,
  77
  78    /// The mode of operation on the file. Possible values:
  79    /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. Requires 'content' field.
  80    /// - 'edit': Make granular edits to an existing file. Requires 'edits' field.
  81    ///
  82    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
  83    pub mode: StreamingEditFileMode,
  84
  85    /// The complete content for the new file (required for 'write' mode).
  86    /// This field should contain the entire file content.
  87    #[serde(default, skip_serializing_if = "Option::is_none")]
  88    pub content: Option<String>,
  89
  90    /// List of edit operations to apply sequentially (required for 'edit' mode).
  91    /// Each edit finds `old_text` in the file and replaces it with `new_text`.
  92    #[serde(default, skip_serializing_if = "Option::is_none")]
  93    pub edits: Option<Vec<Edit>>,
  94}
  95
  96#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  97#[serde(rename_all = "snake_case")]
  98pub enum StreamingEditFileMode {
  99    /// Overwrite the file with new content (replacing any existing content).
 100    /// If the file does not exist, it will be created.
 101    Write,
 102    /// Make granular edits to an existing file
 103    Edit,
 104}
 105
 106/// A single edit operation that replaces old text with new text
 107#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 108pub struct Edit {
 109    /// The exact text to find in the file. This will be matched using fuzzy matching
 110    /// to handle minor differences in whitespace or formatting.
 111    pub old_text: String,
 112    /// The text to replace it with
 113    pub new_text: String,
 114}
 115
 116#[derive(Default, Debug, Deserialize)]
 117struct StreamingEditFileToolPartialInput {
 118    #[serde(default)]
 119    display_description: Option<String>,
 120    #[serde(default)]
 121    path: Option<String>,
 122    #[serde(default)]
 123    mode: Option<StreamingEditFileMode>,
 124    #[serde(default)]
 125    content: Option<String>,
 126    #[serde(default)]
 127    edits: Option<Vec<PartialEdit>>,
 128}
 129
 130#[derive(Default, Debug, Deserialize)]
 131pub struct PartialEdit {
 132    #[serde(default)]
 133    pub old_text: Option<String>,
 134    #[serde(default)]
 135    pub new_text: Option<String>,
 136}
 137
 138#[derive(Debug, Serialize, Deserialize)]
 139#[serde(untagged)]
 140pub enum StreamingEditFileToolOutput {
 141    Success {
 142        #[serde(alias = "original_path")]
 143        input_path: PathBuf,
 144        new_text: String,
 145        old_text: Arc<String>,
 146        #[serde(default)]
 147        diff: String,
 148    },
 149    Error {
 150        error: String,
 151    },
 152}
 153
 154impl StreamingEditFileToolOutput {
 155    pub fn error(error: impl Into<String>) -> Self {
 156        Self::Error {
 157            error: error.into(),
 158        }
 159    }
 160}
 161
 162impl std::fmt::Display for StreamingEditFileToolOutput {
 163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 164        match self {
 165            StreamingEditFileToolOutput::Success {
 166                diff, input_path, ..
 167            } => {
 168                if diff.is_empty() {
 169                    write!(f, "No edits were made.")
 170                } else {
 171                    write!(
 172                        f,
 173                        "Edited {}:\n\n```diff\n{diff}\n```",
 174                        input_path.display()
 175                    )
 176                }
 177            }
 178            StreamingEditFileToolOutput::Error { error } => write!(f, "{error}"),
 179        }
 180    }
 181}
 182
 183impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent {
 184    fn from(output: StreamingEditFileToolOutput) -> Self {
 185        output.to_string().into()
 186    }
 187}
 188
 189pub struct StreamingEditFileTool {
 190    thread: WeakEntity<Thread>,
 191    language_registry: Arc<LanguageRegistry>,
 192    project: Entity<Project>,
 193}
 194
 195impl StreamingEditFileTool {
 196    pub fn new(
 197        project: Entity<Project>,
 198        thread: WeakEntity<Thread>,
 199        language_registry: Arc<LanguageRegistry>,
 200    ) -> Self {
 201        Self {
 202            project,
 203            thread,
 204            language_registry,
 205        }
 206    }
 207
 208    fn authorize(
 209        &self,
 210        path: &PathBuf,
 211        description: &str,
 212        event_stream: &ToolCallEventStream,
 213        cx: &mut App,
 214    ) -> Task<Result<()>> {
 215        super::tool_permissions::authorize_file_edit(
 216            EditFileTool::NAME,
 217            path,
 218            description,
 219            &self.thread,
 220            event_stream,
 221            cx,
 222        )
 223    }
 224
 225    fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) {
 226        let should_update_agent_location = self
 227            .thread
 228            .read_with(cx, |thread, _cx| !thread.is_subagent())
 229            .unwrap_or_default();
 230        if should_update_agent_location {
 231            self.project.update(cx, |project, cx| {
 232                project.set_agent_location(Some(AgentLocation { buffer, position }), cx);
 233            });
 234        }
 235    }
 236}
 237
 238impl AgentTool for StreamingEditFileTool {
 239    type Input = StreamingEditFileToolInput;
 240    type Output = StreamingEditFileToolOutput;
 241
 242    const NAME: &'static str = "edit_file";
 243
 244    fn supports_input_streaming() -> bool {
 245        true
 246    }
 247
 248    fn kind() -> acp::ToolKind {
 249        acp::ToolKind::Edit
 250    }
 251
 252    fn initial_title(
 253        &self,
 254        input: Result<Self::Input, serde_json::Value>,
 255        cx: &mut App,
 256    ) -> SharedString {
 257        match input {
 258            Ok(input) => self
 259                .project
 260                .read(cx)
 261                .find_project_path(&input.path, cx)
 262                .and_then(|project_path| {
 263                    self.project
 264                        .read(cx)
 265                        .short_full_path_for_project_path(&project_path, cx)
 266                })
 267                .unwrap_or(input.path)
 268                .into(),
 269            Err(raw_input) => {
 270                if let Some(input) =
 271                    serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input).ok()
 272                {
 273                    let path = input.path.unwrap_or_default();
 274                    let path = path.trim();
 275                    if !path.is_empty() {
 276                        return self
 277                            .project
 278                            .read(cx)
 279                            .find_project_path(&path, cx)
 280                            .and_then(|project_path| {
 281                                self.project
 282                                    .read(cx)
 283                                    .short_full_path_for_project_path(&project_path, cx)
 284                            })
 285                            .unwrap_or_else(|| path.to_string())
 286                            .into();
 287                    }
 288
 289                    let description = input.display_description.unwrap_or_default();
 290                    let description = description.trim();
 291                    if !description.is_empty() {
 292                        return description.to_string().into();
 293                    }
 294                }
 295
 296                DEFAULT_UI_TEXT.into()
 297            }
 298        }
 299    }
 300
 301    fn run(
 302        self: Arc<Self>,
 303        mut input: ToolInput<Self::Input>,
 304        event_stream: ToolCallEventStream,
 305        cx: &mut App,
 306    ) -> Task<Result<Self::Output, Self::Output>> {
 307        cx.spawn(async move |cx: &mut AsyncApp| {
 308            let mut state: Option<EditSession> = None;
 309            loop {
 310                futures::select! {
 311                    partial = input.recv_partial().fuse() => {
 312                        let Some(partial_value) = partial else { break };
 313                        if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial_value) {
 314                            if state.is_none() && let Some(path_str) = &parsed.path
 315                                && let Some(display_description) = &parsed.display_description
 316                                && let Some(mode) = parsed.mode.clone() {
 317                                    state = Some(
 318                                        EditSession::new(
 319                                            path_str,
 320                                            display_description,
 321                                            mode,
 322                                            &self,
 323                                            &event_stream,
 324                                            cx,
 325                                        )
 326                                        .await?,
 327                                    );
 328                            }
 329
 330                            if let Some(state) = &mut state {
 331                                state.process(parsed, &self, &event_stream, cx)?;
 332                            }
 333                        }
 334                    }
 335                    _ = event_stream.cancelled_by_user().fuse() => {
 336                        return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 337                    }
 338                }
 339            }
 340            let full_input =
 341                input
 342                    .recv()
 343                    .await
 344                    .map_err(|e| StreamingEditFileToolOutput::error(format!("Failed to receive tool input: {e}")))?;
 345
 346            let mut state = if let Some(state) = state {
 347                state
 348            } else {
 349                EditSession::new(
 350                    &full_input.path,
 351                    &full_input.display_description,
 352                    full_input.mode.clone(),
 353                    &self,
 354                    &event_stream,
 355                    cx,
 356                )
 357                .await?
 358            };
 359            state.finalize(full_input, &self, &event_stream, cx).await
 360        })
 361    }
 362
 363    fn replay(
 364        &self,
 365        _input: Self::Input,
 366        output: Self::Output,
 367        event_stream: ToolCallEventStream,
 368        cx: &mut App,
 369    ) -> Result<()> {
 370        match output {
 371            StreamingEditFileToolOutput::Success {
 372                input_path,
 373                old_text,
 374                new_text,
 375                ..
 376            } => {
 377                event_stream.update_diff(cx.new(|cx| {
 378                    Diff::finalized(
 379                        input_path.to_string_lossy().into_owned(),
 380                        Some(old_text.to_string()),
 381                        new_text,
 382                        self.language_registry.clone(),
 383                        cx,
 384                    )
 385                }));
 386                Ok(())
 387            }
 388            StreamingEditFileToolOutput::Error { .. } => Ok(()),
 389        }
 390    }
 391}
 392
 393pub struct EditSession {
 394    abs_path: PathBuf,
 395    buffer: Entity<Buffer>,
 396    old_text: Arc<String>,
 397    diff: Entity<Diff>,
 398    mode: StreamingEditFileMode,
 399    parser: ToolEditParser,
 400    pipeline: EditPipeline,
 401    _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>,
 402}
 403
 404struct EditPipeline {
 405    edits: Vec<EditPipelineEntry>,
 406    content_written: bool,
 407}
 408
 409enum EditPipelineEntry {
 410    ResolvingOldText {
 411        matcher: StreamingFuzzyMatcher,
 412    },
 413    StreamingNewText {
 414        streaming_diff: StreamingDiff,
 415        edit_cursor: usize,
 416        reindenter: Reindenter,
 417        original_snapshot: text::BufferSnapshot,
 418    },
 419    Done,
 420}
 421
 422impl EditPipeline {
 423    fn new() -> Self {
 424        Self {
 425            edits: Vec::new(),
 426            content_written: false,
 427        }
 428    }
 429
 430    fn ensure_resolving_old_text(
 431        &mut self,
 432        edit_index: usize,
 433        buffer: &Entity<Buffer>,
 434        cx: &mut AsyncApp,
 435    ) {
 436        while self.edits.len() <= edit_index {
 437            let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());
 438            self.edits.push(EditPipelineEntry::ResolvingOldText {
 439                matcher: StreamingFuzzyMatcher::new(snapshot),
 440            });
 441        }
 442    }
 443}
 444
 445/// Compute the `LineIndent` of the first line in a set of query lines.
 446fn query_first_line_indent(query_lines: &[String]) -> text::LineIndent {
 447    let first_line = query_lines.first().map(|s| s.as_str()).unwrap_or("");
 448    text::LineIndent::from_iter(first_line.chars())
 449}
 450
 451impl EditSession {
 452    async fn new(
 453        path_str: &str,
 454        display_description: &str,
 455        mode: StreamingEditFileMode,
 456        tool: &StreamingEditFileTool,
 457        event_stream: &ToolCallEventStream,
 458        cx: &mut AsyncApp,
 459    ) -> Result<Self, StreamingEditFileToolOutput> {
 460        let path = PathBuf::from(path_str);
 461        let project_path = cx
 462            .update(|cx| resolve_path(mode.clone(), &path, &tool.project, cx))
 463            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 464
 465        let Some(abs_path) = cx.update(|cx| tool.project.read(cx).absolute_path(&project_path, cx))
 466        else {
 467            return Err(StreamingEditFileToolOutput::error(format!(
 468                "Worktree at '{path_str}' does not exist"
 469            )));
 470        };
 471
 472        event_stream.update_fields(
 473            ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path.clone())]),
 474        );
 475
 476        cx.update(|cx| tool.authorize(&path, &display_description, event_stream, cx))
 477            .await
 478            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 479
 480        let buffer = tool
 481            .project
 482            .update(cx, |project, cx| project.open_buffer(project_path, cx))
 483            .await
 484            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 485
 486        ensure_buffer_saved(&buffer, &abs_path, tool, cx)?;
 487
 488        let diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
 489        event_stream.update_diff(diff.clone());
 490        let finalize_diff_guard = util::defer(Box::new({
 491            let diff = diff.downgrade();
 492            let mut cx = cx.clone();
 493            move || {
 494                diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
 495            }
 496        }) as Box<dyn FnOnce()>);
 497
 498        tool.thread
 499            .update(cx, |thread, cx| {
 500                thread
 501                    .action_log()
 502                    .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))
 503            })
 504            .ok();
 505
 506        let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 507        let old_text = cx
 508            .background_spawn({
 509                let old_snapshot = old_snapshot.clone();
 510                async move { Arc::new(old_snapshot.text()) }
 511            })
 512            .await;
 513
 514        Ok(Self {
 515            abs_path,
 516            buffer,
 517            old_text,
 518            diff,
 519            mode,
 520            parser: ToolEditParser::default(),
 521            pipeline: EditPipeline::new(),
 522            _finalize_diff_guard: finalize_diff_guard,
 523        })
 524    }
 525
 526    async fn finalize(
 527        &mut self,
 528        input: StreamingEditFileToolInput,
 529        tool: &StreamingEditFileTool,
 530        event_stream: &ToolCallEventStream,
 531        cx: &mut AsyncApp,
 532    ) -> Result<StreamingEditFileToolOutput, StreamingEditFileToolOutput> {
 533        let Self {
 534            buffer,
 535            old_text,
 536            diff,
 537            abs_path,
 538            parser,
 539            pipeline,
 540            ..
 541        } = self;
 542
 543        let action_log = tool
 544            .thread
 545            .read_with(cx, |thread, _cx| thread.action_log().clone())
 546            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 547
 548        match input.mode {
 549            StreamingEditFileMode::Write => {
 550                action_log.update(cx, |log, cx| {
 551                    log.buffer_created(buffer.clone(), cx);
 552                });
 553                let content = input.content.ok_or_else(|| {
 554                    StreamingEditFileToolOutput::error("'content' field is required for write mode")
 555                })?;
 556
 557                let events = parser.finalize_content(&content);
 558                Self::process_events(
 559                    &events,
 560                    buffer,
 561                    diff,
 562                    pipeline,
 563                    abs_path,
 564                    tool,
 565                    event_stream,
 566                    cx,
 567                )?;
 568            }
 569            StreamingEditFileMode::Edit => {
 570                let edits = input.edits.ok_or_else(|| {
 571                    StreamingEditFileToolOutput::error("'edits' field is required for edit mode")
 572                })?;
 573
 574                let final_edits = edits
 575                    .into_iter()
 576                    .map(|e| Edit {
 577                        old_text: e.old_text,
 578                        new_text: e.new_text,
 579                    })
 580                    .collect::<Vec<_>>();
 581                let events = parser.finalize_edits(&final_edits);
 582                Self::process_events(
 583                    &events,
 584                    buffer,
 585                    diff,
 586                    pipeline,
 587                    abs_path,
 588                    tool,
 589                    event_stream,
 590                    cx,
 591                )?;
 592            }
 593        }
 594
 595        let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| {
 596            let settings = language_settings::language_settings(
 597                buffer.language().map(|l| l.name()),
 598                buffer.file(),
 599                cx,
 600            );
 601            settings.format_on_save != FormatOnSave::Off
 602        });
 603
 604        if format_on_save_enabled {
 605            action_log.update(cx, |log, cx| {
 606                log.buffer_edited(buffer.clone(), cx);
 607            });
 608
 609            let format_task = tool.project.update(cx, |project, cx| {
 610                project.format(
 611                    HashSet::from_iter([buffer.clone()]),
 612                    LspFormatTarget::Buffers,
 613                    false,
 614                    FormatTrigger::Save,
 615                    cx,
 616                )
 617            });
 618            futures::select! {
 619                result = format_task.fuse() => { result.log_err(); },
 620                _ = event_stream.cancelled_by_user().fuse() => {
 621                    return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 622                }
 623            };
 624        }
 625
 626        let save_task = tool
 627            .project
 628            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 629        futures::select! {
 630            result = save_task.fuse() => { result.map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; },
 631            _ = event_stream.cancelled_by_user().fuse() => {
 632                return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));
 633            }
 634        };
 635
 636        action_log.update(cx, |log, cx| {
 637            log.buffer_edited(buffer.clone(), cx);
 638        });
 639
 640        if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
 641            buffer.file().and_then(|file| file.disk_state().mtime())
 642        }) {
 643            tool.thread
 644                .update(cx, |thread, _| {
 645                    thread
 646                        .file_read_times
 647                        .insert(abs_path.to_path_buf(), new_mtime);
 648                })
 649                .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;
 650        }
 651
 652        let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 653        let (new_text, unified_diff) = cx
 654            .background_spawn({
 655                let new_snapshot = new_snapshot.clone();
 656                let old_text = old_text.clone();
 657                async move {
 658                    let new_text = new_snapshot.text();
 659                    let diff = language::unified_diff(&old_text, &new_text);
 660                    (new_text, diff)
 661                }
 662            })
 663            .await;
 664
 665        let output = StreamingEditFileToolOutput::Success {
 666            input_path: PathBuf::from(input.path),
 667            new_text,
 668            old_text: old_text.clone(),
 669            diff: unified_diff,
 670        };
 671        Ok(output)
 672    }
 673
 674    fn process(
 675        &mut self,
 676        partial: StreamingEditFileToolPartialInput,
 677        tool: &StreamingEditFileTool,
 678        event_stream: &ToolCallEventStream,
 679        cx: &mut AsyncApp,
 680    ) -> Result<(), StreamingEditFileToolOutput> {
 681        match &self.mode {
 682            StreamingEditFileMode::Write => {
 683                if let Some(content) = &partial.content {
 684                    let events = self.parser.push_content(content);
 685                    Self::process_events(
 686                        &events,
 687                        &self.buffer,
 688                        &self.diff,
 689                        &mut self.pipeline,
 690                        &self.abs_path,
 691                        tool,
 692                        event_stream,
 693                        cx,
 694                    )?;
 695                }
 696            }
 697            StreamingEditFileMode::Edit => {
 698                if let Some(edits) = partial.edits {
 699                    let events = self.parser.push_edits(&edits);
 700                    Self::process_events(
 701                        &events,
 702                        &self.buffer,
 703                        &self.diff,
 704                        &mut self.pipeline,
 705                        &self.abs_path,
 706                        tool,
 707                        event_stream,
 708                        cx,
 709                    )?;
 710                }
 711            }
 712        }
 713        Ok(())
 714    }
 715
 716    fn process_events(
 717        events: &[ToolEditEvent],
 718        buffer: &Entity<Buffer>,
 719        diff: &Entity<Diff>,
 720        pipeline: &mut EditPipeline,
 721        abs_path: &PathBuf,
 722        tool: &StreamingEditFileTool,
 723        event_stream: &ToolCallEventStream,
 724        cx: &mut AsyncApp,
 725    ) -> Result<(), StreamingEditFileToolOutput> {
 726        let action_log = tool
 727            .thread
 728            .read_with(cx, |thread, _cx| thread.action_log().clone())
 729            .ok();
 730
 731        for event in events {
 732            match event {
 733                ToolEditEvent::ContentChunk { chunk } => {
 734                    let (buffer_id, insert_at) = buffer.read_with(cx, |buffer, _cx| {
 735                        let insert_at = if !pipeline.content_written && buffer.len() > 0 {
 736                            0..buffer.len()
 737                        } else {
 738                            let len = buffer.len();
 739                            len..len
 740                        };
 741                        (buffer.remote_id(), insert_at)
 742                    });
 743                    agent_edit_buffer(
 744                        buffer,
 745                        [(insert_at, chunk.as_str())],
 746                        action_log.as_ref(),
 747                        cx,
 748                    );
 749                    cx.update(|cx| {
 750                        tool.set_agent_location(
 751                            buffer.downgrade(),
 752                            text::Anchor::max_for_buffer(buffer_id),
 753                            cx,
 754                        );
 755                    });
 756                    pipeline.content_written = true;
 757                }
 758
 759                ToolEditEvent::OldTextChunk {
 760                    edit_index,
 761                    chunk,
 762                    done: false,
 763                } => {
 764                    pipeline.ensure_resolving_old_text(*edit_index, buffer, cx);
 765
 766                    if let EditPipelineEntry::ResolvingOldText { matcher } =
 767                        &mut pipeline.edits[*edit_index]
 768                    {
 769                        if !chunk.is_empty() {
 770                            if let Some(match_range) = matcher.push(chunk, None) {
 771                                let anchor_range = buffer.read_with(cx, |buffer, _cx| {
 772                                    buffer.anchor_range_between(match_range.clone())
 773                                });
 774                                diff.update(cx, |diff, cx| diff.reveal_range(anchor_range, cx));
 775
 776                                cx.update(|cx| {
 777                                    let position = buffer.read(cx).anchor_before(match_range.end);
 778                                    tool.set_agent_location(buffer.downgrade(), position, cx);
 779                                });
 780                            }
 781                        }
 782                    }
 783                }
 784
 785                ToolEditEvent::OldTextChunk {
 786                    edit_index,
 787                    chunk,
 788                    done: true,
 789                } => {
 790                    pipeline.ensure_resolving_old_text(*edit_index, buffer, cx);
 791
 792                    let EditPipelineEntry::ResolvingOldText { matcher } =
 793                        &mut pipeline.edits[*edit_index]
 794                    else {
 795                        continue;
 796                    };
 797
 798                    if !chunk.is_empty() {
 799                        matcher.push(chunk, None);
 800                    }
 801                    let matches = matcher.finish();
 802
 803                    if matches.is_empty() {
 804                        return Err(StreamingEditFileToolOutput::error(format!(
 805                            "Could not find matching text for edit at index {}. \
 806                                 The old_text did not match any content in the file. \
 807                                 Please read the file again to get the current content.",
 808                            edit_index,
 809                        )));
 810                    }
 811                    if matches.len() > 1 {
 812                        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 813                        let lines = matches
 814                            .iter()
 815                            .map(|r| (snapshot.offset_to_point(r.start).row + 1).to_string())
 816                            .collect::<Vec<_>>()
 817                            .join(", ");
 818                        return Err(StreamingEditFileToolOutput::error(format!(
 819                            "Edit {} matched multiple locations in the file at lines: {}. \
 820                                 Please provide more context in old_text to uniquely \
 821                                 identify the location.",
 822                            edit_index, lines
 823                        )));
 824                    }
 825
 826                    let range = matches.into_iter().next().expect("checked len above");
 827
 828                    let anchor_range = buffer
 829                        .read_with(cx, |buffer, _cx| buffer.anchor_range_between(range.clone()));
 830                    diff.update(cx, |diff, cx| diff.reveal_range(anchor_range, cx));
 831
 832                    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 833
 834                    let line = snapshot.offset_to_point(range.start).row;
 835                    event_stream.update_fields(
 836                        ToolCallUpdateFields::new()
 837                            .locations(vec![ToolCallLocation::new(abs_path).line(Some(line))]),
 838                    );
 839
 840                    let EditPipelineEntry::ResolvingOldText { matcher } =
 841                        &pipeline.edits[*edit_index]
 842                    else {
 843                        continue;
 844                    };
 845                    let buffer_indent =
 846                        snapshot.line_indent_for_row(snapshot.offset_to_point(range.start).row);
 847                    let query_indent = query_first_line_indent(matcher.query_lines());
 848                    let indent_delta = compute_indent_delta(buffer_indent, query_indent);
 849
 850                    let old_text_in_buffer =
 851                        snapshot.text_for_range(range.clone()).collect::<String>();
 852
 853                    let text_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());
 854                    pipeline.edits[*edit_index] = EditPipelineEntry::StreamingNewText {
 855                        streaming_diff: StreamingDiff::new(old_text_in_buffer),
 856                        edit_cursor: range.start,
 857                        reindenter: Reindenter::new(indent_delta),
 858                        original_snapshot: text_snapshot,
 859                    };
 860
 861                    cx.update(|cx| {
 862                        let position = buffer.read(cx).anchor_before(range.end);
 863                        tool.set_agent_location(buffer.downgrade(), position, cx);
 864                    });
 865                }
 866
 867                ToolEditEvent::NewTextChunk {
 868                    edit_index,
 869                    chunk,
 870                    done: false,
 871                } => {
 872                    if *edit_index >= pipeline.edits.len() {
 873                        continue;
 874                    }
 875                    let EditPipelineEntry::StreamingNewText {
 876                        streaming_diff,
 877                        edit_cursor,
 878                        reindenter,
 879                        original_snapshot,
 880                        ..
 881                    } = &mut pipeline.edits[*edit_index]
 882                    else {
 883                        continue;
 884                    };
 885
 886                    let reindented = reindenter.push(chunk);
 887                    if reindented.is_empty() {
 888                        continue;
 889                    }
 890
 891                    let char_ops = streaming_diff.push_new(&reindented);
 892                    Self::apply_char_operations(
 893                        &char_ops,
 894                        buffer,
 895                        original_snapshot,
 896                        edit_cursor,
 897                        action_log.as_ref(),
 898                        cx,
 899                    );
 900
 901                    let position = original_snapshot.anchor_before(*edit_cursor);
 902                    cx.update(|cx| {
 903                        tool.set_agent_location(buffer.downgrade(), position, cx);
 904                    });
 905                }
 906
 907                ToolEditEvent::NewTextChunk {
 908                    edit_index,
 909                    chunk,
 910                    done: true,
 911                } => {
 912                    if *edit_index >= pipeline.edits.len() {
 913                        continue;
 914                    }
 915
 916                    let EditPipelineEntry::StreamingNewText {
 917                        mut streaming_diff,
 918                        mut edit_cursor,
 919                        mut reindenter,
 920                        original_snapshot,
 921                    } = std::mem::replace(
 922                        &mut pipeline.edits[*edit_index],
 923                        EditPipelineEntry::Done,
 924                    )
 925                    else {
 926                        continue;
 927                    };
 928
 929                    // Flush any remaining reindent buffer + final chunk.
 930                    let mut final_text = reindenter.push(chunk);
 931                    final_text.push_str(&reindenter.finish());
 932
 933                    if !final_text.is_empty() {
 934                        let char_ops = streaming_diff.push_new(&final_text);
 935                        Self::apply_char_operations(
 936                            &char_ops,
 937                            buffer,
 938                            &original_snapshot,
 939                            &mut edit_cursor,
 940                            action_log.as_ref(),
 941                            cx,
 942                        );
 943                    }
 944
 945                    let remaining_ops = streaming_diff.finish();
 946                    Self::apply_char_operations(
 947                        &remaining_ops,
 948                        buffer,
 949                        &original_snapshot,
 950                        &mut edit_cursor,
 951                        action_log.as_ref(),
 952                        cx,
 953                    );
 954
 955                    let position = original_snapshot.anchor_before(edit_cursor);
 956                    cx.update(|cx| {
 957                        tool.set_agent_location(buffer.downgrade(), position, cx);
 958                    });
 959                }
 960            }
 961        }
 962        Ok(())
 963    }
 964
 965    fn apply_char_operations(
 966        ops: &[CharOperation],
 967        buffer: &Entity<Buffer>,
 968        snapshot: &text::BufferSnapshot,
 969        edit_cursor: &mut usize,
 970        action_log: Option<&Entity<ActionLog>>,
 971        cx: &mut AsyncApp,
 972    ) {
 973        for op in ops {
 974            match op {
 975                CharOperation::Insert { text } => {
 976                    let anchor = snapshot.anchor_after(*edit_cursor);
 977                    agent_edit_buffer(&buffer, [(anchor..anchor, text.as_str())], action_log, cx);
 978                }
 979                CharOperation::Delete { bytes } => {
 980                    let delete_end = *edit_cursor + bytes;
 981                    let anchor_range = snapshot.anchor_range_around(*edit_cursor..delete_end);
 982                    agent_edit_buffer(&buffer, [(anchor_range, "")], action_log, cx);
 983                    *edit_cursor = delete_end;
 984                }
 985                CharOperation::Keep { bytes } => {
 986                    *edit_cursor += bytes;
 987                }
 988            }
 989        }
 990    }
 991}
 992
 993/// Edits a buffer and reports the edit to the action log in the same effect
 994/// cycle. This ensures the action log's subscription handler sees the version
 995/// already updated by `buffer_edited`, so it does not misattribute the agent's
 996/// edit as a user edit.
 997fn agent_edit_buffer<I, S, T>(
 998    buffer: &Entity<Buffer>,
 999    edits: I,
1000    action_log: Option<&Entity<ActionLog>>,
1001    cx: &mut AsyncApp,
1002) where
1003    I: IntoIterator<Item = (Range<S>, T)>,
1004    S: ToOffset,
1005    T: Into<Arc<str>>,
1006{
1007    cx.update(|cx| {
1008        buffer.update(cx, |buffer, cx| {
1009            buffer.edit(edits, None, cx);
1010        });
1011        if let Some(action_log) = action_log {
1012            action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
1013        }
1014    });
1015}
1016
1017fn ensure_buffer_saved(
1018    buffer: &Entity<Buffer>,
1019    abs_path: &PathBuf,
1020    tool: &StreamingEditFileTool,
1021    cx: &mut AsyncApp,
1022) -> Result<(), StreamingEditFileToolOutput> {
1023    let check_result = tool.thread.update(cx, |thread, cx| {
1024        let last_read = thread.file_read_times.get(abs_path).copied();
1025        let current = buffer
1026            .read(cx)
1027            .file()
1028            .and_then(|file| file.disk_state().mtime());
1029        let dirty = buffer.read(cx).is_dirty();
1030        let has_save = thread.has_tool(SaveFileTool::NAME);
1031        let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME);
1032        (last_read, current, dirty, has_save, has_restore)
1033    });
1034
1035    let Ok((last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool)) =
1036        check_result
1037    else {
1038        return Ok(());
1039    };
1040
1041    if is_dirty {
1042        let message = match (has_save_tool, has_restore_tool) {
1043            (true, true) => {
1044                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1045                         If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
1046                         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."
1047            }
1048            (true, false) => {
1049                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1050                         If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \
1051                         If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed."
1052            }
1053            (false, true) => {
1054                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \
1055                         If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \
1056                         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."
1057            }
1058            (false, false) => {
1059                "This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \
1060                         then ask them to save or revert the file manually and inform you when it's ok to proceed."
1061            }
1062        };
1063        return Err(StreamingEditFileToolOutput::error(message));
1064    }
1065
1066    if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
1067        if current != last_read {
1068            return Err(StreamingEditFileToolOutput::error(
1069                "The file has been modified since you last read it. \
1070                             Please read the file again to get the current state before editing it.",
1071            ));
1072        }
1073    }
1074
1075    Ok(())
1076}
1077
1078fn resolve_path(
1079    mode: StreamingEditFileMode,
1080    path: &PathBuf,
1081    project: &Entity<Project>,
1082    cx: &mut App,
1083) -> Result<ProjectPath> {
1084    let project = project.read(cx);
1085
1086    match mode {
1087        StreamingEditFileMode::Edit => {
1088            let path = project
1089                .find_project_path(&path, cx)
1090                .context("Can't edit file: path not found")?;
1091
1092            let entry = project
1093                .entry_for_path(&path, cx)
1094                .context("Can't edit file: path not found")?;
1095
1096            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
1097            Ok(path)
1098        }
1099        StreamingEditFileMode::Write => {
1100            if let Some(path) = project.find_project_path(&path, cx)
1101                && let Some(entry) = project.entry_for_path(&path, cx)
1102            {
1103                anyhow::ensure!(entry.is_file(), "Can't write to file: path is a directory");
1104                return Ok(path);
1105            }
1106
1107            let parent_path = path.parent().context("Can't create file: incorrect path")?;
1108
1109            let parent_project_path = project.find_project_path(&parent_path, cx);
1110
1111            let parent_entry = parent_project_path
1112                .as_ref()
1113                .and_then(|path| project.entry_for_path(path, cx))
1114                .context("Can't create file: parent directory doesn't exist")?;
1115
1116            anyhow::ensure!(
1117                parent_entry.is_dir(),
1118                "Can't create file: parent is not a directory"
1119            );
1120
1121            let file_name = path
1122                .file_name()
1123                .and_then(|file_name| file_name.to_str())
1124                .and_then(|file_name| RelPath::unix(file_name).ok())
1125                .context("Can't create file: invalid filename")?;
1126
1127            let new_file_path = parent_project_path.map(|parent| ProjectPath {
1128                path: parent.path.join(file_name),
1129                ..parent
1130            });
1131
1132            new_file_path.context("Can't create file")
1133        }
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140    use crate::{ContextServerRegistry, Templates, ToolInputSender};
1141    use fs::Fs as _;
1142    use futures::StreamExt as _;
1143    use gpui::{TestAppContext, UpdateGlobal};
1144    use language_model::fake_provider::FakeLanguageModel;
1145    use prompt_store::ProjectContext;
1146    use serde_json::json;
1147    use settings::Settings;
1148    use settings::SettingsStore;
1149    use util::path;
1150    use util::rel_path::rel_path;
1151
1152    #[gpui::test]
1153    async fn test_streaming_edit_create_file(cx: &mut TestAppContext) {
1154        init_test(cx);
1155
1156        let fs = project::FakeFs::new(cx.executor());
1157        fs.insert_tree("/root", json!({"dir": {}})).await;
1158        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1159        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1160        let context_server_registry =
1161            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1162        let model = Arc::new(FakeLanguageModel::default());
1163        let thread = cx.new(|cx| {
1164            crate::Thread::new(
1165                project.clone(),
1166                cx.new(|_cx| ProjectContext::default()),
1167                context_server_registry,
1168                Templates::new(),
1169                Some(model),
1170                cx,
1171            )
1172        });
1173
1174        let result = cx
1175            .update(|cx| {
1176                let input = StreamingEditFileToolInput {
1177                    display_description: "Create new file".into(),
1178                    path: "root/dir/new_file.txt".into(),
1179                    mode: StreamingEditFileMode::Write,
1180                    content: Some("Hello, World!".into()),
1181                    edits: None,
1182                };
1183                Arc::new(StreamingEditFileTool::new(
1184                    project.clone(),
1185                    thread.downgrade(),
1186                    language_registry,
1187                ))
1188                .run(
1189                    ToolInput::resolved(input),
1190                    ToolCallEventStream::test().0,
1191                    cx,
1192                )
1193            })
1194            .await;
1195
1196        let StreamingEditFileToolOutput::Success { new_text, diff, .. } = result.unwrap() else {
1197            panic!("expected success");
1198        };
1199        assert_eq!(new_text, "Hello, World!");
1200        assert!(!diff.is_empty());
1201    }
1202
1203    #[gpui::test]
1204    async fn test_streaming_edit_overwrite_file(cx: &mut TestAppContext) {
1205        init_test(cx);
1206
1207        let fs = project::FakeFs::new(cx.executor());
1208        fs.insert_tree("/root", json!({"file.txt": "old content"}))
1209            .await;
1210        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1211        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1212        let context_server_registry =
1213            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1214        let model = Arc::new(FakeLanguageModel::default());
1215        let thread = cx.new(|cx| {
1216            crate::Thread::new(
1217                project.clone(),
1218                cx.new(|_cx| ProjectContext::default()),
1219                context_server_registry,
1220                Templates::new(),
1221                Some(model),
1222                cx,
1223            )
1224        });
1225
1226        let result = cx
1227            .update(|cx| {
1228                let input = StreamingEditFileToolInput {
1229                    display_description: "Overwrite file".into(),
1230                    path: "root/file.txt".into(),
1231                    mode: StreamingEditFileMode::Write,
1232                    content: Some("new content".into()),
1233                    edits: None,
1234                };
1235                Arc::new(StreamingEditFileTool::new(
1236                    project.clone(),
1237                    thread.downgrade(),
1238                    language_registry,
1239                ))
1240                .run(
1241                    ToolInput::resolved(input),
1242                    ToolCallEventStream::test().0,
1243                    cx,
1244                )
1245            })
1246            .await;
1247
1248        let StreamingEditFileToolOutput::Success {
1249            new_text, old_text, ..
1250        } = result.unwrap()
1251        else {
1252            panic!("expected success");
1253        };
1254        assert_eq!(new_text, "new content");
1255        assert_eq!(*old_text, "old content");
1256    }
1257
1258    #[gpui::test]
1259    async fn test_streaming_edit_granular_edits(cx: &mut TestAppContext) {
1260        init_test(cx);
1261
1262        let fs = project::FakeFs::new(cx.executor());
1263        fs.insert_tree(
1264            "/root",
1265            json!({
1266                "file.txt": "line 1\nline 2\nline 3\n"
1267            }),
1268        )
1269        .await;
1270        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1271        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1272        let context_server_registry =
1273            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1274        let model = Arc::new(FakeLanguageModel::default());
1275        let thread = cx.new(|cx| {
1276            crate::Thread::new(
1277                project.clone(),
1278                cx.new(|_cx| ProjectContext::default()),
1279                context_server_registry,
1280                Templates::new(),
1281                Some(model),
1282                cx,
1283            )
1284        });
1285
1286        let result = cx
1287            .update(|cx| {
1288                let input = StreamingEditFileToolInput {
1289                    display_description: "Edit lines".into(),
1290                    path: "root/file.txt".into(),
1291                    mode: StreamingEditFileMode::Edit,
1292                    content: None,
1293                    edits: Some(vec![Edit {
1294                        old_text: "line 2".into(),
1295                        new_text: "modified line 2".into(),
1296                    }]),
1297                };
1298                Arc::new(StreamingEditFileTool::new(
1299                    project.clone(),
1300                    thread.downgrade(),
1301                    language_registry,
1302                ))
1303                .run(
1304                    ToolInput::resolved(input),
1305                    ToolCallEventStream::test().0,
1306                    cx,
1307                )
1308            })
1309            .await;
1310
1311        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1312            panic!("expected success");
1313        };
1314        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
1315    }
1316
1317    #[gpui::test]
1318    async fn test_streaming_edit_multiple_edits(cx: &mut TestAppContext) {
1319        init_test(cx);
1320
1321        let fs = project::FakeFs::new(cx.executor());
1322        fs.insert_tree(
1323            "/root",
1324            json!({
1325                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1326            }),
1327        )
1328        .await;
1329        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1330        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1331        let context_server_registry =
1332            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1333        let model = Arc::new(FakeLanguageModel::default());
1334        let thread = cx.new(|cx| {
1335            crate::Thread::new(
1336                project.clone(),
1337                cx.new(|_cx| ProjectContext::default()),
1338                context_server_registry,
1339                Templates::new(),
1340                Some(model),
1341                cx,
1342            )
1343        });
1344
1345        let result = cx
1346            .update(|cx| {
1347                let input = StreamingEditFileToolInput {
1348                    display_description: "Edit multiple lines".into(),
1349                    path: "root/file.txt".into(),
1350                    mode: StreamingEditFileMode::Edit,
1351                    content: None,
1352                    edits: Some(vec![
1353                        Edit {
1354                            old_text: "line 5".into(),
1355                            new_text: "modified line 5".into(),
1356                        },
1357                        Edit {
1358                            old_text: "line 1".into(),
1359                            new_text: "modified line 1".into(),
1360                        },
1361                    ]),
1362                };
1363                Arc::new(StreamingEditFileTool::new(
1364                    project.clone(),
1365                    thread.downgrade(),
1366                    language_registry,
1367                ))
1368                .run(
1369                    ToolInput::resolved(input),
1370                    ToolCallEventStream::test().0,
1371                    cx,
1372                )
1373            })
1374            .await;
1375
1376        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1377            panic!("expected success");
1378        };
1379        assert_eq!(
1380            new_text,
1381            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1382        );
1383    }
1384
1385    #[gpui::test]
1386    async fn test_streaming_edit_adjacent_edits(cx: &mut TestAppContext) {
1387        init_test(cx);
1388
1389        let fs = project::FakeFs::new(cx.executor());
1390        fs.insert_tree(
1391            "/root",
1392            json!({
1393                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1394            }),
1395        )
1396        .await;
1397        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1398        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1399        let context_server_registry =
1400            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1401        let model = Arc::new(FakeLanguageModel::default());
1402        let thread = cx.new(|cx| {
1403            crate::Thread::new(
1404                project.clone(),
1405                cx.new(|_cx| ProjectContext::default()),
1406                context_server_registry,
1407                Templates::new(),
1408                Some(model),
1409                cx,
1410            )
1411        });
1412
1413        let result = cx
1414            .update(|cx| {
1415                let input = StreamingEditFileToolInput {
1416                    display_description: "Edit adjacent lines".into(),
1417                    path: "root/file.txt".into(),
1418                    mode: StreamingEditFileMode::Edit,
1419                    content: None,
1420                    edits: Some(vec![
1421                        Edit {
1422                            old_text: "line 2".into(),
1423                            new_text: "modified line 2".into(),
1424                        },
1425                        Edit {
1426                            old_text: "line 3".into(),
1427                            new_text: "modified line 3".into(),
1428                        },
1429                    ]),
1430                };
1431                Arc::new(StreamingEditFileTool::new(
1432                    project.clone(),
1433                    thread.downgrade(),
1434                    language_registry,
1435                ))
1436                .run(
1437                    ToolInput::resolved(input),
1438                    ToolCallEventStream::test().0,
1439                    cx,
1440                )
1441            })
1442            .await;
1443
1444        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1445            panic!("expected success");
1446        };
1447        assert_eq!(
1448            new_text,
1449            "line 1\nmodified line 2\nmodified line 3\nline 4\nline 5\n"
1450        );
1451    }
1452
1453    #[gpui::test]
1454    async fn test_streaming_edit_ascending_order_edits(cx: &mut TestAppContext) {
1455        init_test(cx);
1456
1457        let fs = project::FakeFs::new(cx.executor());
1458        fs.insert_tree(
1459            "/root",
1460            json!({
1461                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1462            }),
1463        )
1464        .await;
1465        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1466        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1467        let context_server_registry =
1468            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1469        let model = Arc::new(FakeLanguageModel::default());
1470        let thread = cx.new(|cx| {
1471            crate::Thread::new(
1472                project.clone(),
1473                cx.new(|_cx| ProjectContext::default()),
1474                context_server_registry,
1475                Templates::new(),
1476                Some(model),
1477                cx,
1478            )
1479        });
1480
1481        let result = cx
1482            .update(|cx| {
1483                let input = StreamingEditFileToolInput {
1484                    display_description: "Edit multiple lines in ascending order".into(),
1485                    path: "root/file.txt".into(),
1486                    mode: StreamingEditFileMode::Edit,
1487                    content: None,
1488                    edits: Some(vec![
1489                        Edit {
1490                            old_text: "line 1".into(),
1491                            new_text: "modified line 1".into(),
1492                        },
1493                        Edit {
1494                            old_text: "line 5".into(),
1495                            new_text: "modified line 5".into(),
1496                        },
1497                    ]),
1498                };
1499                Arc::new(StreamingEditFileTool::new(
1500                    project.clone(),
1501                    thread.downgrade(),
1502                    language_registry,
1503                ))
1504                .run(
1505                    ToolInput::resolved(input),
1506                    ToolCallEventStream::test().0,
1507                    cx,
1508                )
1509            })
1510            .await;
1511
1512        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1513            panic!("expected success");
1514        };
1515        assert_eq!(
1516            new_text,
1517            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1518        );
1519    }
1520
1521    #[gpui::test]
1522    async fn test_streaming_edit_nonexistent_file(cx: &mut TestAppContext) {
1523        init_test(cx);
1524
1525        let fs = project::FakeFs::new(cx.executor());
1526        fs.insert_tree("/root", json!({})).await;
1527        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1528        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1529        let context_server_registry =
1530            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1531        let model = Arc::new(FakeLanguageModel::default());
1532        let thread = cx.new(|cx| {
1533            crate::Thread::new(
1534                project.clone(),
1535                cx.new(|_cx| ProjectContext::default()),
1536                context_server_registry,
1537                Templates::new(),
1538                Some(model),
1539                cx,
1540            )
1541        });
1542
1543        let result = cx
1544            .update(|cx| {
1545                let input = StreamingEditFileToolInput {
1546                    display_description: "Some edit".into(),
1547                    path: "root/nonexistent_file.txt".into(),
1548                    mode: StreamingEditFileMode::Edit,
1549                    content: None,
1550                    edits: Some(vec![Edit {
1551                        old_text: "foo".into(),
1552                        new_text: "bar".into(),
1553                    }]),
1554                };
1555                Arc::new(StreamingEditFileTool::new(
1556                    project,
1557                    thread.downgrade(),
1558                    language_registry,
1559                ))
1560                .run(
1561                    ToolInput::resolved(input),
1562                    ToolCallEventStream::test().0,
1563                    cx,
1564                )
1565            })
1566            .await;
1567
1568        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1569            panic!("expected error");
1570        };
1571        assert_eq!(error, "Can't edit file: path not found");
1572    }
1573
1574    #[gpui::test]
1575    async fn test_streaming_edit_failed_match(cx: &mut TestAppContext) {
1576        init_test(cx);
1577
1578        let fs = project::FakeFs::new(cx.executor());
1579        fs.insert_tree("/root", json!({"file.txt": "hello world"}))
1580            .await;
1581        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1582        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1583        let context_server_registry =
1584            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1585        let model = Arc::new(FakeLanguageModel::default());
1586        let thread = cx.new(|cx| {
1587            crate::Thread::new(
1588                project.clone(),
1589                cx.new(|_cx| ProjectContext::default()),
1590                context_server_registry,
1591                Templates::new(),
1592                Some(model),
1593                cx,
1594            )
1595        });
1596
1597        let result = cx
1598            .update(|cx| {
1599                let input = StreamingEditFileToolInput {
1600                    display_description: "Edit file".into(),
1601                    path: "root/file.txt".into(),
1602                    mode: StreamingEditFileMode::Edit,
1603                    content: None,
1604                    edits: Some(vec![Edit {
1605                        old_text: "nonexistent text that is not in the file".into(),
1606                        new_text: "replacement".into(),
1607                    }]),
1608                };
1609                Arc::new(StreamingEditFileTool::new(
1610                    project,
1611                    thread.downgrade(),
1612                    language_registry,
1613                ))
1614                .run(
1615                    ToolInput::resolved(input),
1616                    ToolCallEventStream::test().0,
1617                    cx,
1618                )
1619            })
1620            .await;
1621
1622        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1623            panic!("expected error");
1624        };
1625        assert!(
1626            error.contains("Could not find matching text"),
1627            "Expected error containing 'Could not find matching text' but got: {error}"
1628        );
1629    }
1630
1631    #[gpui::test]
1632    async fn test_streaming_early_buffer_open(cx: &mut TestAppContext) {
1633        init_test(cx);
1634
1635        let fs = project::FakeFs::new(cx.executor());
1636        fs.insert_tree(
1637            "/root",
1638            json!({
1639                "file.txt": "line 1\nline 2\nline 3\n"
1640            }),
1641        )
1642        .await;
1643        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1644        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1645        let context_server_registry =
1646            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1647        let model = Arc::new(FakeLanguageModel::default());
1648        let thread = cx.new(|cx| {
1649            crate::Thread::new(
1650                project.clone(),
1651                cx.new(|_cx| ProjectContext::default()),
1652                context_server_registry,
1653                Templates::new(),
1654                Some(model),
1655                cx,
1656            )
1657        });
1658
1659        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1660        let (event_stream, _receiver) = ToolCallEventStream::test();
1661
1662        let tool = Arc::new(StreamingEditFileTool::new(
1663            project.clone(),
1664            thread.downgrade(),
1665            language_registry,
1666        ));
1667
1668        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1669
1670        // Send partials simulating LLM streaming: description first, then path, then mode
1671        sender.send_partial(json!({"display_description": "Edit lines"}));
1672        cx.run_until_parked();
1673
1674        sender.send_partial(json!({
1675            "display_description": "Edit lines",
1676            "path": "root/file.txt"
1677        }));
1678        cx.run_until_parked();
1679
1680        // Path is NOT yet complete because mode hasn't appeared — no buffer open yet
1681        sender.send_partial(json!({
1682            "display_description": "Edit lines",
1683            "path": "root/file.txt",
1684            "mode": "edit"
1685        }));
1686        cx.run_until_parked();
1687
1688        // Now send the final complete input
1689        sender.send_final(json!({
1690            "display_description": "Edit lines",
1691            "path": "root/file.txt",
1692            "mode": "edit",
1693            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
1694        }));
1695
1696        let result = task.await;
1697        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1698            panic!("expected success");
1699        };
1700        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
1701    }
1702
1703    #[gpui::test]
1704    async fn test_streaming_path_completeness_heuristic(cx: &mut TestAppContext) {
1705        init_test(cx);
1706
1707        let fs = project::FakeFs::new(cx.executor());
1708        fs.insert_tree(
1709            "/root",
1710            json!({
1711                "file.txt": "hello world"
1712            }),
1713        )
1714        .await;
1715        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1716        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1717        let context_server_registry =
1718            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1719        let model = Arc::new(FakeLanguageModel::default());
1720        let thread = cx.new(|cx| {
1721            crate::Thread::new(
1722                project.clone(),
1723                cx.new(|_cx| ProjectContext::default()),
1724                context_server_registry,
1725                Templates::new(),
1726                Some(model),
1727                cx,
1728            )
1729        });
1730
1731        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1732        let (event_stream, _receiver) = ToolCallEventStream::test();
1733
1734        let tool = Arc::new(StreamingEditFileTool::new(
1735            project.clone(),
1736            thread.downgrade(),
1737            language_registry,
1738        ));
1739
1740        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1741
1742        // Send partial with path but NO mode — path should NOT be treated as complete
1743        sender.send_partial(json!({
1744            "display_description": "Overwrite file",
1745            "path": "root/file"
1746        }));
1747        cx.run_until_parked();
1748
1749        // Now the path grows and mode appears
1750        sender.send_partial(json!({
1751            "display_description": "Overwrite file",
1752            "path": "root/file.txt",
1753            "mode": "write"
1754        }));
1755        cx.run_until_parked();
1756
1757        // Send final
1758        sender.send_final(json!({
1759            "display_description": "Overwrite file",
1760            "path": "root/file.txt",
1761            "mode": "write",
1762            "content": "new content"
1763        }));
1764
1765        let result = task.await;
1766        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1767            panic!("expected success");
1768        };
1769        assert_eq!(new_text, "new content");
1770    }
1771
1772    #[gpui::test]
1773    async fn test_streaming_cancellation_during_partials(cx: &mut TestAppContext) {
1774        init_test(cx);
1775
1776        let fs = project::FakeFs::new(cx.executor());
1777        fs.insert_tree(
1778            "/root",
1779            json!({
1780                "file.txt": "hello world"
1781            }),
1782        )
1783        .await;
1784        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1785        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1786        let context_server_registry =
1787            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1788        let model = Arc::new(FakeLanguageModel::default());
1789        let thread = cx.new(|cx| {
1790            crate::Thread::new(
1791                project.clone(),
1792                cx.new(|_cx| ProjectContext::default()),
1793                context_server_registry,
1794                Templates::new(),
1795                Some(model),
1796                cx,
1797            )
1798        });
1799
1800        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1801        let (event_stream, _receiver, mut cancellation_tx) =
1802            ToolCallEventStream::test_with_cancellation();
1803
1804        let tool = Arc::new(StreamingEditFileTool::new(
1805            project.clone(),
1806            thread.downgrade(),
1807            language_registry,
1808        ));
1809
1810        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1811
1812        // Send a partial
1813        sender.send_partial(json!({"display_description": "Edit"}));
1814        cx.run_until_parked();
1815
1816        // Cancel during streaming
1817        ToolCallEventStream::signal_cancellation_with_sender(&mut cancellation_tx);
1818        cx.run_until_parked();
1819
1820        // The sender is still alive so the partial loop should detect cancellation
1821        // We need to drop the sender to also unblock recv() if the loop didn't catch it
1822        drop(sender);
1823
1824        let result = task.await;
1825        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
1826            panic!("expected error");
1827        };
1828        assert!(
1829            error.contains("cancelled"),
1830            "Expected cancellation error but got: {error}"
1831        );
1832    }
1833
1834    #[gpui::test]
1835    async fn test_streaming_edit_with_multiple_partials(cx: &mut TestAppContext) {
1836        init_test(cx);
1837
1838        let fs = project::FakeFs::new(cx.executor());
1839        fs.insert_tree(
1840            "/root",
1841            json!({
1842                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
1843            }),
1844        )
1845        .await;
1846        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1847        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1848        let context_server_registry =
1849            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1850        let model = Arc::new(FakeLanguageModel::default());
1851        let thread = cx.new(|cx| {
1852            crate::Thread::new(
1853                project.clone(),
1854                cx.new(|_cx| ProjectContext::default()),
1855                context_server_registry,
1856                Templates::new(),
1857                Some(model),
1858                cx,
1859            )
1860        });
1861
1862        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1863        let (event_stream, _receiver) = ToolCallEventStream::test();
1864
1865        let tool = Arc::new(StreamingEditFileTool::new(
1866            project.clone(),
1867            thread.downgrade(),
1868            language_registry,
1869        ));
1870
1871        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1872
1873        // Simulate fine-grained streaming of the JSON
1874        sender.send_partial(json!({"display_description": "Edit multiple"}));
1875        cx.run_until_parked();
1876
1877        sender.send_partial(json!({
1878            "display_description": "Edit multiple lines",
1879            "path": "root/file.txt"
1880        }));
1881        cx.run_until_parked();
1882
1883        sender.send_partial(json!({
1884            "display_description": "Edit multiple lines",
1885            "path": "root/file.txt",
1886            "mode": "edit"
1887        }));
1888        cx.run_until_parked();
1889
1890        sender.send_partial(json!({
1891            "display_description": "Edit multiple lines",
1892            "path": "root/file.txt",
1893            "mode": "edit",
1894            "edits": [{"old_text": "line 1"}]
1895        }));
1896        cx.run_until_parked();
1897
1898        sender.send_partial(json!({
1899            "display_description": "Edit multiple lines",
1900            "path": "root/file.txt",
1901            "mode": "edit",
1902            "edits": [
1903                {"old_text": "line 1", "new_text": "modified line 1"},
1904                {"old_text": "line 5"}
1905            ]
1906        }));
1907        cx.run_until_parked();
1908
1909        // Send final complete input
1910        sender.send_final(json!({
1911            "display_description": "Edit multiple lines",
1912            "path": "root/file.txt",
1913            "mode": "edit",
1914            "edits": [
1915                {"old_text": "line 1", "new_text": "modified line 1"},
1916                {"old_text": "line 5", "new_text": "modified line 5"}
1917            ]
1918        }));
1919
1920        let result = task.await;
1921        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1922            panic!("expected success");
1923        };
1924        assert_eq!(
1925            new_text,
1926            "modified line 1\nline 2\nline 3\nline 4\nmodified line 5\n"
1927        );
1928    }
1929
1930    #[gpui::test]
1931    async fn test_streaming_create_file_with_partials(cx: &mut TestAppContext) {
1932        init_test(cx);
1933
1934        let fs = project::FakeFs::new(cx.executor());
1935        fs.insert_tree("/root", json!({"dir": {}})).await;
1936        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1937        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1938        let context_server_registry =
1939            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1940        let model = Arc::new(FakeLanguageModel::default());
1941        let thread = cx.new(|cx| {
1942            crate::Thread::new(
1943                project.clone(),
1944                cx.new(|_cx| ProjectContext::default()),
1945                context_server_registry,
1946                Templates::new(),
1947                Some(model),
1948                cx,
1949            )
1950        });
1951
1952        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
1953        let (event_stream, _receiver) = ToolCallEventStream::test();
1954
1955        let tool = Arc::new(StreamingEditFileTool::new(
1956            project.clone(),
1957            thread.downgrade(),
1958            language_registry,
1959        ));
1960
1961        let task = cx.update(|cx| tool.run(input, event_stream, cx));
1962
1963        // Stream partials for create mode
1964        sender.send_partial(json!({"display_description": "Create new file"}));
1965        cx.run_until_parked();
1966
1967        sender.send_partial(json!({
1968            "display_description": "Create new file",
1969            "path": "root/dir/new_file.txt",
1970            "mode": "write"
1971        }));
1972        cx.run_until_parked();
1973
1974        sender.send_partial(json!({
1975            "display_description": "Create new file",
1976            "path": "root/dir/new_file.txt",
1977            "mode": "write",
1978            "content": "Hello, "
1979        }));
1980        cx.run_until_parked();
1981
1982        // Final with full content
1983        sender.send_final(json!({
1984            "display_description": "Create new file",
1985            "path": "root/dir/new_file.txt",
1986            "mode": "write",
1987            "content": "Hello, World!"
1988        }));
1989
1990        let result = task.await;
1991        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
1992            panic!("expected success");
1993        };
1994        assert_eq!(new_text, "Hello, World!");
1995    }
1996
1997    #[gpui::test]
1998    async fn test_streaming_no_partials_direct_final(cx: &mut TestAppContext) {
1999        init_test(cx);
2000
2001        let fs = project::FakeFs::new(cx.executor());
2002        fs.insert_tree(
2003            "/root",
2004            json!({
2005                "file.txt": "line 1\nline 2\nline 3\n"
2006            }),
2007        )
2008        .await;
2009        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2010        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2011        let context_server_registry =
2012            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2013        let model = Arc::new(FakeLanguageModel::default());
2014        let thread = cx.new(|cx| {
2015            crate::Thread::new(
2016                project.clone(),
2017                cx.new(|_cx| ProjectContext::default()),
2018                context_server_registry,
2019                Templates::new(),
2020                Some(model),
2021                cx,
2022            )
2023        });
2024
2025        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2026        let (event_stream, _receiver) = ToolCallEventStream::test();
2027
2028        let tool = Arc::new(StreamingEditFileTool::new(
2029            project.clone(),
2030            thread.downgrade(),
2031            language_registry,
2032        ));
2033
2034        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2035
2036        // Send final immediately with no partials (simulates non-streaming path)
2037        sender.send_final(json!({
2038            "display_description": "Edit lines",
2039            "path": "root/file.txt",
2040            "mode": "edit",
2041            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2042        }));
2043
2044        let result = task.await;
2045        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2046            panic!("expected success");
2047        };
2048        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
2049    }
2050
2051    #[gpui::test]
2052    async fn test_streaming_incremental_edit_application(cx: &mut TestAppContext) {
2053        init_test(cx);
2054
2055        let fs = project::FakeFs::new(cx.executor());
2056        fs.insert_tree(
2057            "/root",
2058            json!({
2059                "file.txt": "line 1\nline 2\nline 3\nline 4\nline 5\n"
2060            }),
2061        )
2062        .await;
2063        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2064        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2065        let context_server_registry =
2066            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2067        let model = Arc::new(FakeLanguageModel::default());
2068        let thread = cx.new(|cx| {
2069            crate::Thread::new(
2070                project.clone(),
2071                cx.new(|_cx| ProjectContext::default()),
2072                context_server_registry,
2073                Templates::new(),
2074                Some(model),
2075                cx,
2076            )
2077        });
2078
2079        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2080        let (event_stream, _receiver) = ToolCallEventStream::test();
2081
2082        let tool = Arc::new(StreamingEditFileTool::new(
2083            project.clone(),
2084            thread.downgrade(),
2085            language_registry,
2086        ));
2087
2088        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2089
2090        // Stream description, path, mode
2091        sender.send_partial(json!({"display_description": "Edit multiple lines"}));
2092        cx.run_until_parked();
2093
2094        sender.send_partial(json!({
2095            "display_description": "Edit multiple lines",
2096            "path": "root/file.txt",
2097            "mode": "edit"
2098        }));
2099        cx.run_until_parked();
2100
2101        // First edit starts streaming (old_text only, still in progress)
2102        sender.send_partial(json!({
2103            "display_description": "Edit multiple lines",
2104            "path": "root/file.txt",
2105            "mode": "edit",
2106            "edits": [{"old_text": "line 1"}]
2107        }));
2108        cx.run_until_parked();
2109
2110        // Buffer should not have changed yet — the first edit is still in progress
2111        // (no second edit has appeared to prove the first is complete)
2112        let buffer_text = project.update(cx, |project, cx| {
2113            let project_path = project.find_project_path(&PathBuf::from("root/file.txt"), cx);
2114            project_path.and_then(|pp| {
2115                project
2116                    .get_open_buffer(&pp, cx)
2117                    .map(|buffer| buffer.read(cx).text())
2118            })
2119        });
2120        // Buffer is open (from streaming) but edit 1 is still in-progress
2121        assert_eq!(
2122            buffer_text.as_deref(),
2123            Some("line 1\nline 2\nline 3\nline 4\nline 5\n"),
2124            "Buffer should not be modified while first edit is still in progress"
2125        );
2126
2127        // Second edit appears — this proves the first edit is complete, so it
2128        // should be applied immediately during streaming
2129        sender.send_partial(json!({
2130            "display_description": "Edit multiple lines",
2131            "path": "root/file.txt",
2132            "mode": "edit",
2133            "edits": [
2134                {"old_text": "line 1", "new_text": "MODIFIED 1"},
2135                {"old_text": "line 5"}
2136            ]
2137        }));
2138        cx.run_until_parked();
2139
2140        // First edit should now be applied to the buffer
2141        let buffer_text = project.update(cx, |project, cx| {
2142            let project_path = project.find_project_path(&PathBuf::from("root/file.txt"), cx);
2143            project_path.and_then(|pp| {
2144                project
2145                    .get_open_buffer(&pp, cx)
2146                    .map(|buffer| buffer.read(cx).text())
2147            })
2148        });
2149        assert_eq!(
2150            buffer_text.as_deref(),
2151            Some("MODIFIED 1\nline 2\nline 3\nline 4\nline 5\n"),
2152            "First edit should be applied during streaming when second edit appears"
2153        );
2154
2155        // Send final complete input
2156        sender.send_final(json!({
2157            "display_description": "Edit multiple lines",
2158            "path": "root/file.txt",
2159            "mode": "edit",
2160            "edits": [
2161                {"old_text": "line 1", "new_text": "MODIFIED 1"},
2162                {"old_text": "line 5", "new_text": "MODIFIED 5"}
2163            ]
2164        }));
2165
2166        let result = task.await;
2167        let StreamingEditFileToolOutput::Success {
2168            new_text, old_text, ..
2169        } = result.unwrap()
2170        else {
2171            panic!("expected success");
2172        };
2173        assert_eq!(new_text, "MODIFIED 1\nline 2\nline 3\nline 4\nMODIFIED 5\n");
2174        assert_eq!(
2175            *old_text, "line 1\nline 2\nline 3\nline 4\nline 5\n",
2176            "old_text should reflect the original file content before any edits"
2177        );
2178    }
2179
2180    #[gpui::test]
2181    async fn test_streaming_incremental_three_edits(cx: &mut TestAppContext) {
2182        init_test(cx);
2183
2184        let fs = project::FakeFs::new(cx.executor());
2185        fs.insert_tree(
2186            "/root",
2187            json!({
2188                "file.txt": "aaa\nbbb\nccc\nddd\neee\n"
2189            }),
2190        )
2191        .await;
2192        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2193        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2194        let context_server_registry =
2195            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2196        let model = Arc::new(FakeLanguageModel::default());
2197        let thread = cx.new(|cx| {
2198            crate::Thread::new(
2199                project.clone(),
2200                cx.new(|_cx| ProjectContext::default()),
2201                context_server_registry,
2202                Templates::new(),
2203                Some(model),
2204                cx,
2205            )
2206        });
2207
2208        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2209        let (event_stream, _receiver) = ToolCallEventStream::test();
2210
2211        let tool = Arc::new(StreamingEditFileTool::new(
2212            project.clone(),
2213            thread.downgrade(),
2214            language_registry,
2215        ));
2216
2217        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2218
2219        // Setup: description + path + mode
2220        sender.send_partial(json!({
2221            "display_description": "Edit three lines",
2222            "path": "root/file.txt",
2223            "mode": "edit"
2224        }));
2225        cx.run_until_parked();
2226
2227        // Edit 1 in progress
2228        sender.send_partial(json!({
2229            "display_description": "Edit three lines",
2230            "path": "root/file.txt",
2231            "mode": "edit",
2232            "edits": [{"old_text": "aaa", "new_text": "AAA"}]
2233        }));
2234        cx.run_until_parked();
2235
2236        // Edit 2 appears — edit 1 is now complete and should be applied
2237        sender.send_partial(json!({
2238            "display_description": "Edit three lines",
2239            "path": "root/file.txt",
2240            "mode": "edit",
2241            "edits": [
2242                {"old_text": "aaa", "new_text": "AAA"},
2243                {"old_text": "ccc", "new_text": "CCC"}
2244            ]
2245        }));
2246        cx.run_until_parked();
2247
2248        // Verify edit 1 fully applied. Edit 2's new_text is being
2249        // streamed: "CCC" is inserted but the old "ccc" isn't deleted
2250        // yet (StreamingDiff::finish runs when edit 3 marks edit 2 done).
2251        let buffer_text = project.update(cx, |project, cx| {
2252            let pp = project
2253                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2254                .unwrap();
2255            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2256        });
2257        assert_eq!(buffer_text.as_deref(), Some("AAA\nbbb\nCCCccc\nddd\neee\n"));
2258
2259        // Edit 3 appears — edit 2 is now complete and should be applied
2260        sender.send_partial(json!({
2261            "display_description": "Edit three lines",
2262            "path": "root/file.txt",
2263            "mode": "edit",
2264            "edits": [
2265                {"old_text": "aaa", "new_text": "AAA"},
2266                {"old_text": "ccc", "new_text": "CCC"},
2267                {"old_text": "eee", "new_text": "EEE"}
2268            ]
2269        }));
2270        cx.run_until_parked();
2271
2272        // Verify edits 1 and 2 fully applied. Edit 3's new_text is being
2273        // streamed: "EEE" is inserted but old "eee" isn't deleted yet.
2274        let buffer_text = project.update(cx, |project, cx| {
2275            let pp = project
2276                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2277                .unwrap();
2278            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2279        });
2280        assert_eq!(buffer_text.as_deref(), Some("AAA\nbbb\nCCC\nddd\nEEEeee\n"));
2281
2282        // Send final
2283        sender.send_final(json!({
2284            "display_description": "Edit three lines",
2285            "path": "root/file.txt",
2286            "mode": "edit",
2287            "edits": [
2288                {"old_text": "aaa", "new_text": "AAA"},
2289                {"old_text": "ccc", "new_text": "CCC"},
2290                {"old_text": "eee", "new_text": "EEE"}
2291            ]
2292        }));
2293
2294        let result = task.await;
2295        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2296            panic!("expected success");
2297        };
2298        assert_eq!(new_text, "AAA\nbbb\nCCC\nddd\nEEE\n");
2299    }
2300
2301    #[gpui::test]
2302    async fn test_streaming_edit_failure_mid_stream(cx: &mut TestAppContext) {
2303        init_test(cx);
2304
2305        let fs = project::FakeFs::new(cx.executor());
2306        fs.insert_tree(
2307            "/root",
2308            json!({
2309                "file.txt": "line 1\nline 2\nline 3\n"
2310            }),
2311        )
2312        .await;
2313        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2314        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2315        let context_server_registry =
2316            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2317        let model = Arc::new(FakeLanguageModel::default());
2318        let thread = cx.new(|cx| {
2319            crate::Thread::new(
2320                project.clone(),
2321                cx.new(|_cx| ProjectContext::default()),
2322                context_server_registry,
2323                Templates::new(),
2324                Some(model),
2325                cx,
2326            )
2327        });
2328
2329        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2330        let (event_stream, _receiver) = ToolCallEventStream::test();
2331
2332        let tool = Arc::new(StreamingEditFileTool::new(
2333            project.clone(),
2334            thread.downgrade(),
2335            language_registry,
2336        ));
2337
2338        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2339
2340        // Setup
2341        sender.send_partial(json!({
2342            "display_description": "Edit lines",
2343            "path": "root/file.txt",
2344            "mode": "edit"
2345        }));
2346        cx.run_until_parked();
2347
2348        // Edit 1 (valid) in progress — not yet complete (no second edit)
2349        sender.send_partial(json!({
2350            "display_description": "Edit lines",
2351            "path": "root/file.txt",
2352            "mode": "edit",
2353            "edits": [
2354                {"old_text": "line 1", "new_text": "MODIFIED"}
2355            ]
2356        }));
2357        cx.run_until_parked();
2358
2359        // Edit 2 appears (will fail to match) — this makes edit 1 complete.
2360        // Edit 1 should be applied. Edit 2 is still in-progress (last edit).
2361        sender.send_partial(json!({
2362            "display_description": "Edit lines",
2363            "path": "root/file.txt",
2364            "mode": "edit",
2365            "edits": [
2366                {"old_text": "line 1", "new_text": "MODIFIED"},
2367                {"old_text": "nonexistent text that does not appear anywhere in the file at all", "new_text": "whatever"}
2368            ]
2369        }));
2370        cx.run_until_parked();
2371
2372        // Verify edit 1 was applied
2373        let buffer_text = project.update(cx, |project, cx| {
2374            let pp = project
2375                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2376                .unwrap();
2377            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2378        });
2379        assert_eq!(
2380            buffer_text.as_deref(),
2381            Some("MODIFIED\nline 2\nline 3\n"),
2382            "First edit should be applied even though second edit will fail"
2383        );
2384
2385        // Edit 3 appears — this makes edit 2 "complete", triggering its
2386        // resolution which should fail (old_text doesn't exist in the file).
2387        sender.send_partial(json!({
2388            "display_description": "Edit lines",
2389            "path": "root/file.txt",
2390            "mode": "edit",
2391            "edits": [
2392                {"old_text": "line 1", "new_text": "MODIFIED"},
2393                {"old_text": "nonexistent text that does not appear anywhere in the file at all", "new_text": "whatever"},
2394                {"old_text": "line 3", "new_text": "MODIFIED 3"}
2395            ]
2396        }));
2397        cx.run_until_parked();
2398
2399        // The error from edit 2 should have propagated out of the partial loop.
2400        // Drop sender to unblock recv() if the loop didn't catch it.
2401        drop(sender);
2402
2403        let result = task.await;
2404        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
2405            panic!("expected error");
2406        };
2407        assert!(
2408            error.contains("Could not find matching text for edit at index 1"),
2409            "Expected error about edit 1 failing, got: {error}"
2410        );
2411    }
2412
2413    #[gpui::test]
2414    async fn test_streaming_single_edit_no_incremental(cx: &mut TestAppContext) {
2415        init_test(cx);
2416
2417        let fs = project::FakeFs::new(cx.executor());
2418        fs.insert_tree(
2419            "/root",
2420            json!({
2421                "file.txt": "hello world\n"
2422            }),
2423        )
2424        .await;
2425        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2426        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2427        let context_server_registry =
2428            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2429        let model = Arc::new(FakeLanguageModel::default());
2430        let thread = cx.new(|cx| {
2431            crate::Thread::new(
2432                project.clone(),
2433                cx.new(|_cx| ProjectContext::default()),
2434                context_server_registry,
2435                Templates::new(),
2436                Some(model),
2437                cx,
2438            )
2439        });
2440
2441        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2442        let (event_stream, _receiver) = ToolCallEventStream::test();
2443
2444        let tool = Arc::new(StreamingEditFileTool::new(
2445            project.clone(),
2446            thread.downgrade(),
2447            language_registry,
2448        ));
2449
2450        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2451
2452        // Setup + single edit that stays in-progress (no second edit to prove completion)
2453        sender.send_partial(json!({
2454            "display_description": "Single edit",
2455            "path": "root/file.txt",
2456            "mode": "edit",
2457            "edits": [{"old_text": "hello world", "new_text": "goodbye world"}]
2458        }));
2459        cx.run_until_parked();
2460
2461        // The edit's old_text and new_text both arrived in one partial, so
2462        // the old_text is resolved and new_text is being streamed via
2463        // StreamingDiff. The buffer reflects the in-progress diff (new text
2464        // inserted, old text not yet fully removed until finalization).
2465        let buffer_text = project.update(cx, |project, cx| {
2466            let pp = project
2467                .find_project_path(&PathBuf::from("root/file.txt"), cx)
2468                .unwrap();
2469            project.get_open_buffer(&pp, cx).map(|b| b.read(cx).text())
2470        });
2471        assert_eq!(
2472            buffer_text.as_deref(),
2473            Some("goodbye worldhello world\n"),
2474            "In-progress streaming diff: new text inserted, old text not yet removed"
2475        );
2476
2477        // Send final — the edit is applied during finalization
2478        sender.send_final(json!({
2479            "display_description": "Single edit",
2480            "path": "root/file.txt",
2481            "mode": "edit",
2482            "edits": [{"old_text": "hello world", "new_text": "goodbye world"}]
2483        }));
2484
2485        let result = task.await;
2486        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2487            panic!("expected success");
2488        };
2489        assert_eq!(new_text, "goodbye world\n");
2490    }
2491
2492    #[gpui::test]
2493    async fn test_streaming_input_partials_then_final(cx: &mut TestAppContext) {
2494        init_test(cx);
2495
2496        let fs = project::FakeFs::new(cx.executor());
2497        fs.insert_tree(
2498            "/root",
2499            json!({
2500                "file.txt": "line 1\nline 2\nline 3\n"
2501            }),
2502        )
2503        .await;
2504        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2505        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2506        let context_server_registry =
2507            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2508        let model = Arc::new(FakeLanguageModel::default());
2509        let thread = cx.new(|cx| {
2510            crate::Thread::new(
2511                project.clone(),
2512                cx.new(|_cx| ProjectContext::default()),
2513                context_server_registry,
2514                Templates::new(),
2515                Some(model),
2516                cx,
2517            )
2518        });
2519
2520        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2521            ToolInput::test();
2522
2523        let (event_stream, _event_rx) = ToolCallEventStream::test();
2524        let task = cx.update(|cx| {
2525            Arc::new(StreamingEditFileTool::new(
2526                project.clone(),
2527                thread.downgrade(),
2528                language_registry,
2529            ))
2530            .run(input, event_stream, cx)
2531        });
2532
2533        // Send progressively more complete partial snapshots, as the LLM would
2534        sender.send_partial(json!({
2535            "display_description": "Edit lines"
2536        }));
2537        cx.run_until_parked();
2538
2539        sender.send_partial(json!({
2540            "display_description": "Edit lines",
2541            "path": "root/file.txt",
2542            "mode": "edit"
2543        }));
2544        cx.run_until_parked();
2545
2546        sender.send_partial(json!({
2547            "display_description": "Edit lines",
2548            "path": "root/file.txt",
2549            "mode": "edit",
2550            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2551        }));
2552        cx.run_until_parked();
2553
2554        // Send the final complete input
2555        sender.send_final(json!({
2556            "display_description": "Edit lines",
2557            "path": "root/file.txt",
2558            "mode": "edit",
2559            "edits": [{"old_text": "line 2", "new_text": "modified line 2"}]
2560        }));
2561
2562        let result = task.await;
2563        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2564            panic!("expected success");
2565        };
2566        assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
2567    }
2568
2569    #[gpui::test]
2570    async fn test_streaming_input_sender_dropped_before_final(cx: &mut TestAppContext) {
2571        init_test(cx);
2572
2573        let fs = project::FakeFs::new(cx.executor());
2574        fs.insert_tree(
2575            "/root",
2576            json!({
2577                "file.txt": "hello world\n"
2578            }),
2579        )
2580        .await;
2581        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2582        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2583        let context_server_registry =
2584            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2585        let model = Arc::new(FakeLanguageModel::default());
2586        let thread = cx.new(|cx| {
2587            crate::Thread::new(
2588                project.clone(),
2589                cx.new(|_cx| ProjectContext::default()),
2590                context_server_registry,
2591                Templates::new(),
2592                Some(model),
2593                cx,
2594            )
2595        });
2596
2597        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2598            ToolInput::test();
2599
2600        let (event_stream, _event_rx) = ToolCallEventStream::test();
2601        let task = cx.update(|cx| {
2602            Arc::new(StreamingEditFileTool::new(
2603                project.clone(),
2604                thread.downgrade(),
2605                language_registry,
2606            ))
2607            .run(input, event_stream, cx)
2608        });
2609
2610        // Send a partial then drop the sender without sending final
2611        sender.send_partial(json!({
2612            "display_description": "Edit file"
2613        }));
2614        cx.run_until_parked();
2615
2616        drop(sender);
2617
2618        let result = task.await;
2619        assert!(
2620            result.is_err(),
2621            "Tool should error when sender is dropped without sending final input"
2622        );
2623    }
2624
2625    #[gpui::test]
2626    async fn test_streaming_input_recv_drains_partials(cx: &mut TestAppContext) {
2627        init_test(cx);
2628
2629        let fs = project::FakeFs::new(cx.executor());
2630        fs.insert_tree("/root", json!({"dir": {}})).await;
2631        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2632        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2633        let context_server_registry =
2634            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2635        let model = Arc::new(FakeLanguageModel::default());
2636        let thread = cx.new(|cx| {
2637            crate::Thread::new(
2638                project.clone(),
2639                cx.new(|_cx| ProjectContext::default()),
2640                context_server_registry,
2641                Templates::new(),
2642                Some(model),
2643                cx,
2644            )
2645        });
2646
2647        // Create a channel and send multiple partials before a final, then use
2648        // ToolInput::resolved-style immediate delivery to confirm recv() works
2649        // when partials are already buffered.
2650        let (sender, input): (ToolInputSender, ToolInput<StreamingEditFileToolInput>) =
2651            ToolInput::test();
2652
2653        let (event_stream, _event_rx) = ToolCallEventStream::test();
2654        let task = cx.update(|cx| {
2655            Arc::new(StreamingEditFileTool::new(
2656                project.clone(),
2657                thread.downgrade(),
2658                language_registry,
2659            ))
2660            .run(input, event_stream, cx)
2661        });
2662
2663        // Buffer several partials before sending the final
2664        sender.send_partial(json!({"display_description": "Create"}));
2665        sender.send_partial(json!({"display_description": "Create", "path": "root/dir/new.txt"}));
2666        sender.send_partial(json!({
2667            "display_description": "Create",
2668            "path": "root/dir/new.txt",
2669            "mode": "write"
2670        }));
2671        sender.send_final(json!({
2672            "display_description": "Create",
2673            "path": "root/dir/new.txt",
2674            "mode": "write",
2675            "content": "streamed content"
2676        }));
2677
2678        let result = task.await;
2679        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
2680            panic!("expected success");
2681        };
2682        assert_eq!(new_text, "streamed content");
2683    }
2684
2685    #[gpui::test]
2686    async fn test_streaming_resolve_path_for_creating_file(cx: &mut TestAppContext) {
2687        let mode = StreamingEditFileMode::Write;
2688
2689        let result = test_resolve_path(&mode, "root/new.txt", cx);
2690        assert_resolved_path_eq(result.await, rel_path("new.txt"));
2691
2692        let result = test_resolve_path(&mode, "new.txt", cx);
2693        assert_resolved_path_eq(result.await, rel_path("new.txt"));
2694
2695        let result = test_resolve_path(&mode, "dir/new.txt", cx);
2696        assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
2697
2698        let result = test_resolve_path(&mode, "root/dir/subdir/existing.txt", cx);
2699        assert_resolved_path_eq(result.await, rel_path("dir/subdir/existing.txt"));
2700
2701        let result = test_resolve_path(&mode, "root/dir/subdir", cx);
2702        assert_eq!(
2703            result.await.unwrap_err().to_string(),
2704            "Can't write to file: path is a directory"
2705        );
2706
2707        let result = test_resolve_path(&mode, "root/dir/nonexistent_dir/new.txt", cx);
2708        assert_eq!(
2709            result.await.unwrap_err().to_string(),
2710            "Can't create file: parent directory doesn't exist"
2711        );
2712    }
2713
2714    #[gpui::test]
2715    async fn test_streaming_resolve_path_for_editing_file(cx: &mut TestAppContext) {
2716        let mode = StreamingEditFileMode::Edit;
2717
2718        let path_with_root = "root/dir/subdir/existing.txt";
2719        let path_without_root = "dir/subdir/existing.txt";
2720        let result = test_resolve_path(&mode, path_with_root, cx);
2721        assert_resolved_path_eq(result.await, rel_path(path_without_root));
2722
2723        let result = test_resolve_path(&mode, path_without_root, cx);
2724        assert_resolved_path_eq(result.await, rel_path(path_without_root));
2725
2726        let result = test_resolve_path(&mode, "root/nonexistent.txt", cx);
2727        assert_eq!(
2728            result.await.unwrap_err().to_string(),
2729            "Can't edit file: path not found"
2730        );
2731
2732        let result = test_resolve_path(&mode, "root/dir", cx);
2733        assert_eq!(
2734            result.await.unwrap_err().to_string(),
2735            "Can't edit file: path is a directory"
2736        );
2737    }
2738
2739    async fn test_resolve_path(
2740        mode: &StreamingEditFileMode,
2741        path: &str,
2742        cx: &mut TestAppContext,
2743    ) -> anyhow::Result<ProjectPath> {
2744        init_test(cx);
2745
2746        let fs = project::FakeFs::new(cx.executor());
2747        fs.insert_tree(
2748            "/root",
2749            json!({
2750                "dir": {
2751                    "subdir": {
2752                        "existing.txt": "hello"
2753                    }
2754                }
2755            }),
2756        )
2757        .await;
2758        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2759
2760        cx.update(|cx| resolve_path(mode.clone(), &PathBuf::from(path), &project, cx))
2761    }
2762
2763    #[track_caller]
2764    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
2765        let actual = path.expect("Should return valid path").path;
2766        assert_eq!(actual.as_ref(), expected);
2767    }
2768
2769    #[gpui::test]
2770    async fn test_streaming_format_on_save(cx: &mut TestAppContext) {
2771        init_test(cx);
2772
2773        let fs = project::FakeFs::new(cx.executor());
2774        fs.insert_tree("/root", json!({"src": {}})).await;
2775
2776        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2777
2778        let rust_language = Arc::new(language::Language::new(
2779            language::LanguageConfig {
2780                name: "Rust".into(),
2781                matcher: language::LanguageMatcher {
2782                    path_suffixes: vec!["rs".to_string()],
2783                    ..Default::default()
2784                },
2785                ..Default::default()
2786            },
2787            None,
2788        ));
2789
2790        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2791        language_registry.add(rust_language);
2792
2793        let mut fake_language_servers = language_registry.register_fake_lsp(
2794            "Rust",
2795            language::FakeLspAdapter {
2796                capabilities: lsp::ServerCapabilities {
2797                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
2798                    ..Default::default()
2799                },
2800                ..Default::default()
2801            },
2802        );
2803
2804        fs.save(
2805            path!("/root/src/main.rs").as_ref(),
2806            &"initial content".into(),
2807            language::LineEnding::Unix,
2808        )
2809        .await
2810        .unwrap();
2811
2812        // Open the buffer to trigger LSP initialization
2813        let buffer = project
2814            .update(cx, |project, cx| {
2815                project.open_local_buffer(path!("/root/src/main.rs"), cx)
2816            })
2817            .await
2818            .unwrap();
2819
2820        // Register the buffer with language servers
2821        let _handle = project.update(cx, |project, cx| {
2822            project.register_buffer_with_language_servers(&buffer, cx)
2823        });
2824
2825        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
2826        const FORMATTED_CONTENT: &str =
2827            "This file was formatted by the fake formatter in the test.\n";
2828
2829        // Get the fake language server and set up formatting handler
2830        let fake_language_server = fake_language_servers.next().await.unwrap();
2831        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
2832            |_, _| async move {
2833                Ok(Some(vec![lsp::TextEdit {
2834                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
2835                    new_text: FORMATTED_CONTENT.to_string(),
2836                }]))
2837            }
2838        });
2839
2840        let context_server_registry =
2841            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2842        let model = Arc::new(FakeLanguageModel::default());
2843        let thread = cx.new(|cx| {
2844            crate::Thread::new(
2845                project.clone(),
2846                cx.new(|_cx| ProjectContext::default()),
2847                context_server_registry,
2848                Templates::new(),
2849                Some(model.clone()),
2850                cx,
2851            )
2852        });
2853
2854        // Test with format_on_save enabled
2855        cx.update(|cx| {
2856            SettingsStore::update_global(cx, |store, cx| {
2857                store.update_user_settings(cx, |settings| {
2858                    settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
2859                    settings.project.all_languages.defaults.formatter =
2860                        Some(language::language_settings::FormatterList::default());
2861                });
2862            });
2863        });
2864
2865        // Use streaming pattern so executor can pump the LSP request/response
2866        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2867        let (event_stream, _receiver) = ToolCallEventStream::test();
2868
2869        let tool = Arc::new(StreamingEditFileTool::new(
2870            project.clone(),
2871            thread.downgrade(),
2872            language_registry.clone(),
2873        ));
2874
2875        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2876
2877        sender.send_partial(json!({
2878            "display_description": "Create main function",
2879            "path": "root/src/main.rs",
2880            "mode": "write"
2881        }));
2882        cx.run_until_parked();
2883
2884        sender.send_final(json!({
2885            "display_description": "Create main function",
2886            "path": "root/src/main.rs",
2887            "mode": "write",
2888            "content": UNFORMATTED_CONTENT
2889        }));
2890
2891        let result = task.await;
2892        assert!(result.is_ok());
2893
2894        cx.executor().run_until_parked();
2895
2896        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
2897        assert_eq!(
2898            new_content.replace("\r\n", "\n"),
2899            FORMATTED_CONTENT,
2900            "Code should be formatted when format_on_save is enabled"
2901        );
2902
2903        let stale_buffer_count = thread
2904            .read_with(cx, |thread, _cx| thread.action_log.clone())
2905            .read_with(cx, |log, cx| log.stale_buffers(cx).count());
2906
2907        assert_eq!(
2908            stale_buffer_count, 0,
2909            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers.",
2910            stale_buffer_count
2911        );
2912
2913        // Test with format_on_save disabled
2914        cx.update(|cx| {
2915            SettingsStore::update_global(cx, |store, cx| {
2916                store.update_user_settings(cx, |settings| {
2917                    settings.project.all_languages.defaults.format_on_save =
2918                        Some(FormatOnSave::Off);
2919                });
2920            });
2921        });
2922
2923        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
2924        let (event_stream, _receiver) = ToolCallEventStream::test();
2925
2926        let tool = Arc::new(StreamingEditFileTool::new(
2927            project.clone(),
2928            thread.downgrade(),
2929            language_registry,
2930        ));
2931
2932        let task = cx.update(|cx| tool.run(input, event_stream, cx));
2933
2934        sender.send_partial(json!({
2935            "display_description": "Update main function",
2936            "path": "root/src/main.rs",
2937            "mode": "write"
2938        }));
2939        cx.run_until_parked();
2940
2941        sender.send_final(json!({
2942            "display_description": "Update main function",
2943            "path": "root/src/main.rs",
2944            "mode": "write",
2945            "content": UNFORMATTED_CONTENT
2946        }));
2947
2948        let result = task.await;
2949        assert!(result.is_ok());
2950
2951        cx.executor().run_until_parked();
2952
2953        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
2954        assert_eq!(
2955            new_content.replace("\r\n", "\n"),
2956            UNFORMATTED_CONTENT,
2957            "Code should not be formatted when format_on_save is disabled"
2958        );
2959    }
2960
2961    #[gpui::test]
2962    async fn test_streaming_remove_trailing_whitespace(cx: &mut TestAppContext) {
2963        init_test(cx);
2964
2965        let fs = project::FakeFs::new(cx.executor());
2966        fs.insert_tree("/root", json!({"src": {}})).await;
2967
2968        fs.save(
2969            path!("/root/src/main.rs").as_ref(),
2970            &"initial content".into(),
2971            language::LineEnding::Unix,
2972        )
2973        .await
2974        .unwrap();
2975
2976        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
2977        let context_server_registry =
2978            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
2979        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
2980        let model = Arc::new(FakeLanguageModel::default());
2981        let thread = cx.new(|cx| {
2982            crate::Thread::new(
2983                project.clone(),
2984                cx.new(|_cx| ProjectContext::default()),
2985                context_server_registry,
2986                Templates::new(),
2987                Some(model.clone()),
2988                cx,
2989            )
2990        });
2991
2992        // Test with remove_trailing_whitespace_on_save enabled
2993        cx.update(|cx| {
2994            SettingsStore::update_global(cx, |store, cx| {
2995                store.update_user_settings(cx, |settings| {
2996                    settings
2997                        .project
2998                        .all_languages
2999                        .defaults
3000                        .remove_trailing_whitespace_on_save = Some(true);
3001                });
3002            });
3003        });
3004
3005        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
3006            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
3007
3008        let result = cx
3009            .update(|cx| {
3010                let input = StreamingEditFileToolInput {
3011                    display_description: "Create main function".into(),
3012                    path: "root/src/main.rs".into(),
3013                    mode: StreamingEditFileMode::Write,
3014                    content: Some(CONTENT_WITH_TRAILING_WHITESPACE.into()),
3015                    edits: None,
3016                };
3017                Arc::new(StreamingEditFileTool::new(
3018                    project.clone(),
3019                    thread.downgrade(),
3020                    language_registry.clone(),
3021                ))
3022                .run(
3023                    ToolInput::resolved(input),
3024                    ToolCallEventStream::test().0,
3025                    cx,
3026                )
3027            })
3028            .await;
3029        assert!(result.is_ok());
3030
3031        cx.executor().run_until_parked();
3032
3033        assert_eq!(
3034            fs.load(path!("/root/src/main.rs").as_ref())
3035                .await
3036                .unwrap()
3037                .replace("\r\n", "\n"),
3038            "fn main() {\n    println!(\"Hello!\");\n}\n",
3039            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
3040        );
3041
3042        // Test with remove_trailing_whitespace_on_save disabled
3043        cx.update(|cx| {
3044            SettingsStore::update_global(cx, |store, cx| {
3045                store.update_user_settings(cx, |settings| {
3046                    settings
3047                        .project
3048                        .all_languages
3049                        .defaults
3050                        .remove_trailing_whitespace_on_save = Some(false);
3051                });
3052            });
3053        });
3054
3055        let result = cx
3056            .update(|cx| {
3057                let input = StreamingEditFileToolInput {
3058                    display_description: "Update main function".into(),
3059                    path: "root/src/main.rs".into(),
3060                    mode: StreamingEditFileMode::Write,
3061                    content: Some(CONTENT_WITH_TRAILING_WHITESPACE.into()),
3062                    edits: None,
3063                };
3064                Arc::new(StreamingEditFileTool::new(
3065                    project.clone(),
3066                    thread.downgrade(),
3067                    language_registry,
3068                ))
3069                .run(
3070                    ToolInput::resolved(input),
3071                    ToolCallEventStream::test().0,
3072                    cx,
3073                )
3074            })
3075            .await;
3076        assert!(result.is_ok());
3077
3078        cx.executor().run_until_parked();
3079
3080        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
3081        assert_eq!(
3082            final_content.replace("\r\n", "\n"),
3083            CONTENT_WITH_TRAILING_WHITESPACE,
3084            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
3085        );
3086    }
3087
3088    #[gpui::test]
3089    async fn test_streaming_authorize(cx: &mut TestAppContext) {
3090        init_test(cx);
3091        let fs = project::FakeFs::new(cx.executor());
3092        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3093        let context_server_registry =
3094            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3095        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3096        let model = Arc::new(FakeLanguageModel::default());
3097        let thread = cx.new(|cx| {
3098            crate::Thread::new(
3099                project.clone(),
3100                cx.new(|_cx| ProjectContext::default()),
3101                context_server_registry,
3102                Templates::new(),
3103                Some(model.clone()),
3104                cx,
3105            )
3106        });
3107        let tool = Arc::new(StreamingEditFileTool::new(
3108            project.clone(),
3109            thread.downgrade(),
3110            language_registry,
3111        ));
3112        fs.insert_tree("/root", json!({})).await;
3113
3114        // Test 1: Path with .zed component should require confirmation
3115        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3116        let _auth = cx.update(|cx| {
3117            tool.authorize(
3118                &PathBuf::from(".zed/settings.json"),
3119                "test 1",
3120                &stream_tx,
3121                cx,
3122            )
3123        });
3124
3125        let event = stream_rx.expect_authorization().await;
3126        assert_eq!(
3127            event.tool_call.fields.title,
3128            Some("test 1 (local settings)".into())
3129        );
3130
3131        // Test 2: Path outside project should require confirmation
3132        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3133        let _auth =
3134            cx.update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 2", &stream_tx, cx));
3135
3136        let event = stream_rx.expect_authorization().await;
3137        assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
3138
3139        // Test 3: Relative path without .zed should not require confirmation
3140        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3141        cx.update(|cx| {
3142            tool.authorize(&PathBuf::from("root/src/main.rs"), "test 3", &stream_tx, cx)
3143        })
3144        .await
3145        .unwrap();
3146        assert!(stream_rx.try_next().is_err());
3147
3148        // Test 4: Path with .zed in the middle should require confirmation
3149        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3150        let _auth = cx.update(|cx| {
3151            tool.authorize(
3152                &PathBuf::from("root/.zed/tasks.json"),
3153                "test 4",
3154                &stream_tx,
3155                cx,
3156            )
3157        });
3158        let event = stream_rx.expect_authorization().await;
3159        assert_eq!(
3160            event.tool_call.fields.title,
3161            Some("test 4 (local settings)".into())
3162        );
3163
3164        // Test 5: When global default is allow, sensitive and outside-project
3165        // paths still require confirmation
3166        cx.update(|cx| {
3167            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3168            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
3169            agent_settings::AgentSettings::override_global(settings, cx);
3170        });
3171
3172        // 5.1: .zed/settings.json is a sensitive path — still prompts
3173        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3174        let _auth = cx.update(|cx| {
3175            tool.authorize(
3176                &PathBuf::from(".zed/settings.json"),
3177                "test 5.1",
3178                &stream_tx,
3179                cx,
3180            )
3181        });
3182        let event = stream_rx.expect_authorization().await;
3183        assert_eq!(
3184            event.tool_call.fields.title,
3185            Some("test 5.1 (local settings)".into())
3186        );
3187
3188        // 5.2: /etc/hosts is outside the project, but Allow auto-approves
3189        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3190        cx.update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 5.2", &stream_tx, cx))
3191            .await
3192            .unwrap();
3193        assert!(stream_rx.try_next().is_err());
3194
3195        // 5.3: Normal in-project path with allow — no confirmation needed
3196        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3197        cx.update(|cx| {
3198            tool.authorize(
3199                &PathBuf::from("root/src/main.rs"),
3200                "test 5.3",
3201                &stream_tx,
3202                cx,
3203            )
3204        })
3205        .await
3206        .unwrap();
3207        assert!(stream_rx.try_next().is_err());
3208
3209        // 5.4: With Confirm default, non-project paths still prompt
3210        cx.update(|cx| {
3211            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3212            settings.tool_permissions.default = settings::ToolPermissionMode::Confirm;
3213            agent_settings::AgentSettings::override_global(settings, cx);
3214        });
3215
3216        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3217        let _auth = cx
3218            .update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 5.4", &stream_tx, cx));
3219
3220        let event = stream_rx.expect_authorization().await;
3221        assert_eq!(event.tool_call.fields.title, Some("test 5.4".into()));
3222    }
3223
3224    #[gpui::test]
3225    async fn test_streaming_authorize_create_under_symlink_with_allow(cx: &mut TestAppContext) {
3226        init_test(cx);
3227
3228        let fs = project::FakeFs::new(cx.executor());
3229        fs.insert_tree("/root", json!({})).await;
3230        fs.insert_tree("/outside", json!({})).await;
3231        fs.insert_symlink("/root/link", PathBuf::from("/outside"))
3232            .await;
3233
3234        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3235        let context_server_registry =
3236            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3237        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3238        let model = Arc::new(FakeLanguageModel::default());
3239        let thread = cx.new(|cx| {
3240            crate::Thread::new(
3241                project.clone(),
3242                cx.new(|_cx| ProjectContext::default()),
3243                context_server_registry,
3244                Templates::new(),
3245                Some(model),
3246                cx,
3247            )
3248        });
3249        let tool = Arc::new(StreamingEditFileTool::new(
3250            project,
3251            thread.downgrade(),
3252            language_registry,
3253        ));
3254
3255        cx.update(|cx| {
3256            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3257            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
3258            agent_settings::AgentSettings::override_global(settings, cx);
3259        });
3260
3261        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3262        let authorize_task = cx.update(|cx| {
3263            tool.authorize(
3264                &PathBuf::from("link/new.txt"),
3265                "create through symlink",
3266                &stream_tx,
3267                cx,
3268            )
3269        });
3270
3271        let event = stream_rx.expect_authorization().await;
3272        assert!(
3273            event
3274                .tool_call
3275                .fields
3276                .title
3277                .as_deref()
3278                .is_some_and(|title| title.contains("points outside the project")),
3279            "Expected symlink escape authorization for create under external symlink"
3280        );
3281
3282        event
3283            .response
3284            .send(acp::PermissionOptionId::new("allow"))
3285            .unwrap();
3286        authorize_task.await.unwrap();
3287    }
3288
3289    #[gpui::test]
3290    async fn test_streaming_edit_file_symlink_escape_requests_authorization(
3291        cx: &mut TestAppContext,
3292    ) {
3293        init_test(cx);
3294
3295        let fs = project::FakeFs::new(cx.executor());
3296        fs.insert_tree(
3297            path!("/root"),
3298            json!({
3299                "src": { "main.rs": "fn main() {}" }
3300            }),
3301        )
3302        .await;
3303        fs.insert_tree(
3304            path!("/outside"),
3305            json!({
3306                "config.txt": "old content"
3307            }),
3308        )
3309        .await;
3310        fs.create_symlink(
3311            path!("/root/link_to_external").as_ref(),
3312            PathBuf::from("/outside"),
3313        )
3314        .await
3315        .unwrap();
3316
3317        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3318        cx.executor().run_until_parked();
3319
3320        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3321        let context_server_registry =
3322            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3323        let model = Arc::new(FakeLanguageModel::default());
3324        let thread = cx.new(|cx| {
3325            crate::Thread::new(
3326                project.clone(),
3327                cx.new(|_cx| ProjectContext::default()),
3328                context_server_registry,
3329                Templates::new(),
3330                Some(model),
3331                cx,
3332            )
3333        });
3334        let tool = Arc::new(StreamingEditFileTool::new(
3335            project.clone(),
3336            thread.downgrade(),
3337            language_registry,
3338        ));
3339
3340        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3341        let _authorize_task = cx.update(|cx| {
3342            tool.authorize(
3343                &PathBuf::from("link_to_external/config.txt"),
3344                "edit through symlink",
3345                &stream_tx,
3346                cx,
3347            )
3348        });
3349
3350        let auth = stream_rx.expect_authorization().await;
3351        let title = auth.tool_call.fields.title.as_deref().unwrap_or("");
3352        assert!(
3353            title.contains("points outside the project"),
3354            "title should mention symlink escape, got: {title}"
3355        );
3356    }
3357
3358    #[gpui::test]
3359    async fn test_streaming_edit_file_symlink_escape_denied(cx: &mut TestAppContext) {
3360        init_test(cx);
3361
3362        let fs = project::FakeFs::new(cx.executor());
3363        fs.insert_tree(
3364            path!("/root"),
3365            json!({
3366                "src": { "main.rs": "fn main() {}" }
3367            }),
3368        )
3369        .await;
3370        fs.insert_tree(
3371            path!("/outside"),
3372            json!({
3373                "config.txt": "old content"
3374            }),
3375        )
3376        .await;
3377        fs.create_symlink(
3378            path!("/root/link_to_external").as_ref(),
3379            PathBuf::from("/outside"),
3380        )
3381        .await
3382        .unwrap();
3383
3384        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3385        cx.executor().run_until_parked();
3386
3387        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3388        let context_server_registry =
3389            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3390        let model = Arc::new(FakeLanguageModel::default());
3391        let thread = cx.new(|cx| {
3392            crate::Thread::new(
3393                project.clone(),
3394                cx.new(|_cx| ProjectContext::default()),
3395                context_server_registry,
3396                Templates::new(),
3397                Some(model),
3398                cx,
3399            )
3400        });
3401        let tool = Arc::new(StreamingEditFileTool::new(
3402            project.clone(),
3403            thread.downgrade(),
3404            language_registry,
3405        ));
3406
3407        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3408        let authorize_task = cx.update(|cx| {
3409            tool.authorize(
3410                &PathBuf::from("link_to_external/config.txt"),
3411                "edit through symlink",
3412                &stream_tx,
3413                cx,
3414            )
3415        });
3416
3417        let auth = stream_rx.expect_authorization().await;
3418        drop(auth); // deny by dropping
3419
3420        let result = authorize_task.await;
3421        assert!(result.is_err(), "should fail when denied");
3422    }
3423
3424    #[gpui::test]
3425    async fn test_streaming_edit_file_symlink_escape_honors_deny_policy(cx: &mut TestAppContext) {
3426        init_test(cx);
3427        cx.update(|cx| {
3428            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
3429            settings.tool_permissions.tools.insert(
3430                "edit_file".into(),
3431                agent_settings::ToolRules {
3432                    default: Some(settings::ToolPermissionMode::Deny),
3433                    ..Default::default()
3434                },
3435            );
3436            agent_settings::AgentSettings::override_global(settings, cx);
3437        });
3438
3439        let fs = project::FakeFs::new(cx.executor());
3440        fs.insert_tree(
3441            path!("/root"),
3442            json!({
3443                "src": { "main.rs": "fn main() {}" }
3444            }),
3445        )
3446        .await;
3447        fs.insert_tree(
3448            path!("/outside"),
3449            json!({
3450                "config.txt": "old content"
3451            }),
3452        )
3453        .await;
3454        fs.create_symlink(
3455            path!("/root/link_to_external").as_ref(),
3456            PathBuf::from("/outside"),
3457        )
3458        .await
3459        .unwrap();
3460
3461        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3462        cx.executor().run_until_parked();
3463
3464        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3465        let context_server_registry =
3466            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3467        let model = Arc::new(FakeLanguageModel::default());
3468        let thread = cx.new(|cx| {
3469            crate::Thread::new(
3470                project.clone(),
3471                cx.new(|_cx| ProjectContext::default()),
3472                context_server_registry,
3473                Templates::new(),
3474                Some(model),
3475                cx,
3476            )
3477        });
3478        let tool = Arc::new(StreamingEditFileTool::new(
3479            project.clone(),
3480            thread.downgrade(),
3481            language_registry,
3482        ));
3483
3484        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3485        let result = cx
3486            .update(|cx| {
3487                tool.authorize(
3488                    &PathBuf::from("link_to_external/config.txt"),
3489                    "edit through symlink",
3490                    &stream_tx,
3491                    cx,
3492                )
3493            })
3494            .await;
3495
3496        assert!(result.is_err(), "Tool should fail when policy denies");
3497        assert!(
3498            !matches!(
3499                stream_rx.try_next(),
3500                Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
3501            ),
3502            "Deny policy should not emit symlink authorization prompt",
3503        );
3504    }
3505
3506    #[gpui::test]
3507    async fn test_streaming_authorize_global_config(cx: &mut TestAppContext) {
3508        init_test(cx);
3509        let fs = project::FakeFs::new(cx.executor());
3510        fs.insert_tree("/project", json!({})).await;
3511        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3512        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3513        let context_server_registry =
3514            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3515        let model = Arc::new(FakeLanguageModel::default());
3516        let thread = cx.new(|cx| {
3517            crate::Thread::new(
3518                project.clone(),
3519                cx.new(|_cx| ProjectContext::default()),
3520                context_server_registry,
3521                Templates::new(),
3522                Some(model.clone()),
3523                cx,
3524            )
3525        });
3526        let tool = Arc::new(StreamingEditFileTool::new(
3527            project.clone(),
3528            thread.downgrade(),
3529            language_registry,
3530        ));
3531
3532        let test_cases = vec![
3533            (
3534                "/etc/hosts",
3535                true,
3536                "System file should require confirmation",
3537            ),
3538            (
3539                "/usr/local/bin/script",
3540                true,
3541                "System bin file should require confirmation",
3542            ),
3543            (
3544                "project/normal_file.rs",
3545                false,
3546                "Normal project file should not require confirmation",
3547            ),
3548        ];
3549
3550        for (path, should_confirm, description) in test_cases {
3551            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3552            let auth =
3553                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3554
3555            if should_confirm {
3556                stream_rx.expect_authorization().await;
3557            } else {
3558                auth.await.unwrap();
3559                assert!(
3560                    stream_rx.try_next().is_err(),
3561                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3562                    description,
3563                    path
3564                );
3565            }
3566        }
3567    }
3568
3569    #[gpui::test]
3570    async fn test_streaming_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
3571        init_test(cx);
3572        let fs = project::FakeFs::new(cx.executor());
3573
3574        fs.insert_tree(
3575            "/workspace/frontend",
3576            json!({
3577                "src": {
3578                    "main.js": "console.log('frontend');"
3579                }
3580            }),
3581        )
3582        .await;
3583        fs.insert_tree(
3584            "/workspace/backend",
3585            json!({
3586                "src": {
3587                    "main.rs": "fn main() {}"
3588                }
3589            }),
3590        )
3591        .await;
3592        fs.insert_tree(
3593            "/workspace/shared",
3594            json!({
3595                ".zed": {
3596                    "settings.json": "{}"
3597                }
3598            }),
3599        )
3600        .await;
3601
3602        let project = Project::test(
3603            fs.clone(),
3604            [
3605                path!("/workspace/frontend").as_ref(),
3606                path!("/workspace/backend").as_ref(),
3607                path!("/workspace/shared").as_ref(),
3608            ],
3609            cx,
3610        )
3611        .await;
3612        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3613        let context_server_registry =
3614            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3615        let model = Arc::new(FakeLanguageModel::default());
3616        let thread = cx.new(|cx| {
3617            crate::Thread::new(
3618                project.clone(),
3619                cx.new(|_cx| ProjectContext::default()),
3620                context_server_registry.clone(),
3621                Templates::new(),
3622                Some(model.clone()),
3623                cx,
3624            )
3625        });
3626        let tool = Arc::new(StreamingEditFileTool::new(
3627            project.clone(),
3628            thread.downgrade(),
3629            language_registry,
3630        ));
3631
3632        let test_cases = vec![
3633            ("frontend/src/main.js", false, "File in first worktree"),
3634            ("backend/src/main.rs", false, "File in second worktree"),
3635            (
3636                "shared/.zed/settings.json",
3637                true,
3638                ".zed file in third worktree",
3639            ),
3640            ("/etc/hosts", true, "Absolute path outside all worktrees"),
3641            (
3642                "../outside/file.txt",
3643                true,
3644                "Relative path outside worktrees",
3645            ),
3646        ];
3647
3648        for (path, should_confirm, description) in test_cases {
3649            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3650            let auth =
3651                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3652
3653            if should_confirm {
3654                stream_rx.expect_authorization().await;
3655            } else {
3656                auth.await.unwrap();
3657                assert!(
3658                    stream_rx.try_next().is_err(),
3659                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3660                    description,
3661                    path
3662                );
3663            }
3664        }
3665    }
3666
3667    #[gpui::test]
3668    async fn test_streaming_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
3669        init_test(cx);
3670        let fs = project::FakeFs::new(cx.executor());
3671        fs.insert_tree(
3672            "/project",
3673            json!({
3674                ".zed": {
3675                    "settings.json": "{}"
3676                },
3677                "src": {
3678                    ".zed": {
3679                        "local.json": "{}"
3680                    }
3681                }
3682            }),
3683        )
3684        .await;
3685        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3686        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3687        let context_server_registry =
3688            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3689        let model = Arc::new(FakeLanguageModel::default());
3690        let thread = cx.new(|cx| {
3691            crate::Thread::new(
3692                project.clone(),
3693                cx.new(|_cx| ProjectContext::default()),
3694                context_server_registry.clone(),
3695                Templates::new(),
3696                Some(model.clone()),
3697                cx,
3698            )
3699        });
3700        let tool = Arc::new(StreamingEditFileTool::new(
3701            project.clone(),
3702            thread.downgrade(),
3703            language_registry,
3704        ));
3705
3706        let test_cases = vec![
3707            ("", false, "Empty path is treated as project root"),
3708            ("/", true, "Root directory should be outside project"),
3709            (
3710                "project/../other",
3711                true,
3712                "Path with .. that goes outside of root directory",
3713            ),
3714            (
3715                "project/./src/file.rs",
3716                false,
3717                "Path with . should work normally",
3718            ),
3719            #[cfg(target_os = "windows")]
3720            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
3721            #[cfg(target_os = "windows")]
3722            ("project\\src\\main.rs", false, "Windows-style project path"),
3723        ];
3724
3725        for (path, should_confirm, description) in test_cases {
3726            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3727            let auth =
3728                cx.update(|cx| tool.authorize(&PathBuf::from(path), "Edit file", &stream_tx, cx));
3729
3730            cx.run_until_parked();
3731
3732            if should_confirm {
3733                stream_rx.expect_authorization().await;
3734            } else {
3735                assert!(
3736                    stream_rx.try_next().is_err(),
3737                    "Failed for case: {} - path: {} - expected no confirmation but got one",
3738                    description,
3739                    path
3740                );
3741                auth.await.unwrap();
3742            }
3743        }
3744    }
3745
3746    #[gpui::test]
3747    async fn test_streaming_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
3748        init_test(cx);
3749        let fs = project::FakeFs::new(cx.executor());
3750        fs.insert_tree(
3751            "/project",
3752            json!({
3753                "existing.txt": "content",
3754                ".zed": {
3755                    "settings.json": "{}"
3756                }
3757            }),
3758        )
3759        .await;
3760        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3761        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3762        let context_server_registry =
3763            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3764        let model = Arc::new(FakeLanguageModel::default());
3765        let thread = cx.new(|cx| {
3766            crate::Thread::new(
3767                project.clone(),
3768                cx.new(|_cx| ProjectContext::default()),
3769                context_server_registry.clone(),
3770                Templates::new(),
3771                Some(model.clone()),
3772                cx,
3773            )
3774        });
3775        let tool = Arc::new(StreamingEditFileTool::new(
3776            project.clone(),
3777            thread.downgrade(),
3778            language_registry,
3779        ));
3780
3781        let modes = vec![StreamingEditFileMode::Edit, StreamingEditFileMode::Write];
3782
3783        for _mode in modes {
3784            // Test .zed path with different modes
3785            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3786            let _auth = cx.update(|cx| {
3787                tool.authorize(
3788                    &PathBuf::from("project/.zed/settings.json"),
3789                    "Edit settings",
3790                    &stream_tx,
3791                    cx,
3792                )
3793            });
3794
3795            stream_rx.expect_authorization().await;
3796
3797            // Test outside path with different modes
3798            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3799            let _auth = cx.update(|cx| {
3800                tool.authorize(
3801                    &PathBuf::from("/outside/file.txt"),
3802                    "Edit file",
3803                    &stream_tx,
3804                    cx,
3805                )
3806            });
3807
3808            stream_rx.expect_authorization().await;
3809
3810            // Test normal path with different modes
3811            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3812            cx.update(|cx| {
3813                tool.authorize(
3814                    &PathBuf::from("project/normal.txt"),
3815                    "Edit file",
3816                    &stream_tx,
3817                    cx,
3818                )
3819            })
3820            .await
3821            .unwrap();
3822            assert!(stream_rx.try_next().is_err());
3823        }
3824    }
3825
3826    #[gpui::test]
3827    async fn test_streaming_initial_title_with_partial_input(cx: &mut TestAppContext) {
3828        init_test(cx);
3829        let fs = project::FakeFs::new(cx.executor());
3830        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
3831        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
3832        let context_server_registry =
3833            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3834        let model = Arc::new(FakeLanguageModel::default());
3835        let thread = cx.new(|cx| {
3836            crate::Thread::new(
3837                project.clone(),
3838                cx.new(|_cx| ProjectContext::default()),
3839                context_server_registry,
3840                Templates::new(),
3841                Some(model.clone()),
3842                cx,
3843            )
3844        });
3845        let tool = Arc::new(StreamingEditFileTool::new(
3846            project,
3847            thread.downgrade(),
3848            language_registry,
3849        ));
3850
3851        cx.update(|cx| {
3852            assert_eq!(
3853                tool.initial_title(
3854                    Err(json!({
3855                        "path": "src/main.rs",
3856                        "display_description": "",
3857                    })),
3858                    cx
3859                ),
3860                "src/main.rs"
3861            );
3862            assert_eq!(
3863                tool.initial_title(
3864                    Err(json!({
3865                        "path": "",
3866                        "display_description": "Fix error handling",
3867                    })),
3868                    cx
3869                ),
3870                "Fix error handling"
3871            );
3872            assert_eq!(
3873                tool.initial_title(
3874                    Err(json!({
3875                        "path": "src/main.rs",
3876                        "display_description": "Fix error handling",
3877                    })),
3878                    cx
3879                ),
3880                "src/main.rs"
3881            );
3882            assert_eq!(
3883                tool.initial_title(
3884                    Err(json!({
3885                        "path": "",
3886                        "display_description": "",
3887                    })),
3888                    cx
3889                ),
3890                DEFAULT_UI_TEXT
3891            );
3892            assert_eq!(
3893                tool.initial_title(Err(serde_json::Value::Null), cx),
3894                DEFAULT_UI_TEXT
3895            );
3896        });
3897    }
3898
3899    #[gpui::test]
3900    async fn test_streaming_diff_finalization(cx: &mut TestAppContext) {
3901        init_test(cx);
3902        let fs = project::FakeFs::new(cx.executor());
3903        fs.insert_tree("/", json!({"main.rs": ""})).await;
3904
3905        let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
3906        let languages = project.read_with(cx, |project, _cx| project.languages().clone());
3907        let context_server_registry =
3908            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3909        let model = Arc::new(FakeLanguageModel::default());
3910        let thread = cx.new(|cx| {
3911            crate::Thread::new(
3912                project.clone(),
3913                cx.new(|_cx| ProjectContext::default()),
3914                context_server_registry.clone(),
3915                Templates::new(),
3916                Some(model.clone()),
3917                cx,
3918            )
3919        });
3920
3921        // Ensure the diff is finalized after the edit completes.
3922        {
3923            let tool = Arc::new(StreamingEditFileTool::new(
3924                project.clone(),
3925                thread.downgrade(),
3926                languages.clone(),
3927            ));
3928            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3929            let edit = cx.update(|cx| {
3930                tool.run(
3931                    ToolInput::resolved(StreamingEditFileToolInput {
3932                        display_description: "Edit file".into(),
3933                        path: path!("/main.rs").into(),
3934                        mode: StreamingEditFileMode::Write,
3935                        content: Some("new content".into()),
3936                        edits: None,
3937                    }),
3938                    stream_tx,
3939                    cx,
3940                )
3941            });
3942            stream_rx.expect_update_fields().await;
3943            let diff = stream_rx.expect_diff().await;
3944            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
3945            cx.run_until_parked();
3946            edit.await.unwrap();
3947            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
3948        }
3949
3950        // Ensure the diff is finalized if the tool call gets dropped.
3951        {
3952            let tool = Arc::new(StreamingEditFileTool::new(
3953                project.clone(),
3954                thread.downgrade(),
3955                languages.clone(),
3956            ));
3957            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
3958            let edit = cx.update(|cx| {
3959                tool.run(
3960                    ToolInput::resolved(StreamingEditFileToolInput {
3961                        display_description: "Edit file".into(),
3962                        path: path!("/main.rs").into(),
3963                        mode: StreamingEditFileMode::Write,
3964                        content: Some("dropped content".into()),
3965                        edits: None,
3966                    }),
3967                    stream_tx,
3968                    cx,
3969                )
3970            });
3971            stream_rx.expect_update_fields().await;
3972            let diff = stream_rx.expect_diff().await;
3973            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
3974            drop(edit);
3975            cx.run_until_parked();
3976            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
3977        }
3978    }
3979
3980    #[gpui::test]
3981    async fn test_streaming_consecutive_edits_work(cx: &mut TestAppContext) {
3982        init_test(cx);
3983
3984        let fs = project::FakeFs::new(cx.executor());
3985        fs.insert_tree(
3986            "/root",
3987            json!({
3988                "test.txt": "original content"
3989            }),
3990        )
3991        .await;
3992        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
3993        let context_server_registry =
3994            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
3995        let model = Arc::new(FakeLanguageModel::default());
3996        let thread = cx.new(|cx| {
3997            crate::Thread::new(
3998                project.clone(),
3999                cx.new(|_cx| ProjectContext::default()),
4000                context_server_registry,
4001                Templates::new(),
4002                Some(model.clone()),
4003                cx,
4004            )
4005        });
4006        let languages = project.read_with(cx, |project, _| project.languages().clone());
4007        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4008
4009        let read_tool = Arc::new(crate::ReadFileTool::new(
4010            thread.downgrade(),
4011            project.clone(),
4012            action_log,
4013        ));
4014        let edit_tool = Arc::new(StreamingEditFileTool::new(
4015            project.clone(),
4016            thread.downgrade(),
4017            languages,
4018        ));
4019
4020        // Read the file first
4021        cx.update(|cx| {
4022            read_tool.clone().run(
4023                ToolInput::resolved(crate::ReadFileToolInput {
4024                    path: "root/test.txt".to_string(),
4025                    start_line: None,
4026                    end_line: None,
4027                }),
4028                ToolCallEventStream::test().0,
4029                cx,
4030            )
4031        })
4032        .await
4033        .unwrap();
4034
4035        // First edit should work
4036        let edit_result = cx
4037            .update(|cx| {
4038                edit_tool.clone().run(
4039                    ToolInput::resolved(StreamingEditFileToolInput {
4040                        display_description: "First edit".into(),
4041                        path: "root/test.txt".into(),
4042                        mode: StreamingEditFileMode::Edit,
4043                        content: None,
4044                        edits: Some(vec![Edit {
4045                            old_text: "original content".into(),
4046                            new_text: "modified content".into(),
4047                        }]),
4048                    }),
4049                    ToolCallEventStream::test().0,
4050                    cx,
4051                )
4052            })
4053            .await;
4054        assert!(
4055            edit_result.is_ok(),
4056            "First edit should succeed, got error: {:?}",
4057            edit_result.as_ref().err()
4058        );
4059
4060        // Second edit should also work because the edit updated the recorded read time
4061        let edit_result = cx
4062            .update(|cx| {
4063                edit_tool.clone().run(
4064                    ToolInput::resolved(StreamingEditFileToolInput {
4065                        display_description: "Second edit".into(),
4066                        path: "root/test.txt".into(),
4067                        mode: StreamingEditFileMode::Edit,
4068                        content: None,
4069                        edits: Some(vec![Edit {
4070                            old_text: "modified content".into(),
4071                            new_text: "further modified content".into(),
4072                        }]),
4073                    }),
4074                    ToolCallEventStream::test().0,
4075                    cx,
4076                )
4077            })
4078            .await;
4079        assert!(
4080            edit_result.is_ok(),
4081            "Second consecutive edit should succeed, got error: {:?}",
4082            edit_result.as_ref().err()
4083        );
4084    }
4085
4086    #[gpui::test]
4087    async fn test_streaming_external_modification_detected(cx: &mut TestAppContext) {
4088        init_test(cx);
4089
4090        let fs = project::FakeFs::new(cx.executor());
4091        fs.insert_tree(
4092            "/root",
4093            json!({
4094                "test.txt": "original content"
4095            }),
4096        )
4097        .await;
4098        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4099        let context_server_registry =
4100            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4101        let model = Arc::new(FakeLanguageModel::default());
4102        let thread = cx.new(|cx| {
4103            crate::Thread::new(
4104                project.clone(),
4105                cx.new(|_cx| ProjectContext::default()),
4106                context_server_registry,
4107                Templates::new(),
4108                Some(model.clone()),
4109                cx,
4110            )
4111        });
4112        let languages = project.read_with(cx, |project, _| project.languages().clone());
4113        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4114
4115        let read_tool = Arc::new(crate::ReadFileTool::new(
4116            thread.downgrade(),
4117            project.clone(),
4118            action_log,
4119        ));
4120        let edit_tool = Arc::new(StreamingEditFileTool::new(
4121            project.clone(),
4122            thread.downgrade(),
4123            languages,
4124        ));
4125
4126        // Read the file first
4127        cx.update(|cx| {
4128            read_tool.clone().run(
4129                ToolInput::resolved(crate::ReadFileToolInput {
4130                    path: "root/test.txt".to_string(),
4131                    start_line: None,
4132                    end_line: None,
4133                }),
4134                ToolCallEventStream::test().0,
4135                cx,
4136            )
4137        })
4138        .await
4139        .unwrap();
4140
4141        // Simulate external modification
4142        cx.background_executor
4143            .advance_clock(std::time::Duration::from_secs(2));
4144        fs.save(
4145            path!("/root/test.txt").as_ref(),
4146            &"externally modified content".into(),
4147            language::LineEnding::Unix,
4148        )
4149        .await
4150        .unwrap();
4151
4152        // Reload the buffer to pick up the new mtime
4153        let project_path = project
4154            .read_with(cx, |project, cx| {
4155                project.find_project_path("root/test.txt", cx)
4156            })
4157            .expect("Should find project path");
4158        let buffer = project
4159            .update(cx, |project, cx| project.open_buffer(project_path, cx))
4160            .await
4161            .unwrap();
4162        buffer
4163            .update(cx, |buffer, cx| buffer.reload(cx))
4164            .await
4165            .unwrap();
4166
4167        cx.executor().run_until_parked();
4168
4169        // Try to edit - should fail because file was modified externally
4170        let result = cx
4171            .update(|cx| {
4172                edit_tool.clone().run(
4173                    ToolInput::resolved(StreamingEditFileToolInput {
4174                        display_description: "Edit after external change".into(),
4175                        path: "root/test.txt".into(),
4176                        mode: StreamingEditFileMode::Edit,
4177                        content: None,
4178                        edits: Some(vec![Edit {
4179                            old_text: "externally modified content".into(),
4180                            new_text: "new content".into(),
4181                        }]),
4182                    }),
4183                    ToolCallEventStream::test().0,
4184                    cx,
4185                )
4186            })
4187            .await;
4188
4189        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
4190            panic!("expected error");
4191        };
4192        assert!(
4193            error.contains("has been modified since you last read it"),
4194            "Error should mention file modification, got: {}",
4195            error
4196        );
4197    }
4198
4199    #[gpui::test]
4200    async fn test_streaming_dirty_buffer_detected(cx: &mut TestAppContext) {
4201        init_test(cx);
4202
4203        let fs = project::FakeFs::new(cx.executor());
4204        fs.insert_tree(
4205            "/root",
4206            json!({
4207                "test.txt": "original content"
4208            }),
4209        )
4210        .await;
4211        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4212        let context_server_registry =
4213            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4214        let model = Arc::new(FakeLanguageModel::default());
4215        let thread = cx.new(|cx| {
4216            crate::Thread::new(
4217                project.clone(),
4218                cx.new(|_cx| ProjectContext::default()),
4219                context_server_registry,
4220                Templates::new(),
4221                Some(model.clone()),
4222                cx,
4223            )
4224        });
4225        let languages = project.read_with(cx, |project, _| project.languages().clone());
4226        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4227
4228        let read_tool = Arc::new(crate::ReadFileTool::new(
4229            thread.downgrade(),
4230            project.clone(),
4231            action_log,
4232        ));
4233        let edit_tool = Arc::new(StreamingEditFileTool::new(
4234            project.clone(),
4235            thread.downgrade(),
4236            languages,
4237        ));
4238
4239        // Read the file first
4240        cx.update(|cx| {
4241            read_tool.clone().run(
4242                ToolInput::resolved(crate::ReadFileToolInput {
4243                    path: "root/test.txt".to_string(),
4244                    start_line: None,
4245                    end_line: None,
4246                }),
4247                ToolCallEventStream::test().0,
4248                cx,
4249            )
4250        })
4251        .await
4252        .unwrap();
4253
4254        // Open the buffer and make it dirty
4255        let project_path = project
4256            .read_with(cx, |project, cx| {
4257                project.find_project_path("root/test.txt", cx)
4258            })
4259            .expect("Should find project path");
4260        let buffer = project
4261            .update(cx, |project, cx| project.open_buffer(project_path, cx))
4262            .await
4263            .unwrap();
4264
4265        buffer.update(cx, |buffer, cx| {
4266            let end_point = buffer.max_point();
4267            buffer.edit([(end_point..end_point, " added text")], None, cx);
4268        });
4269
4270        let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
4271        assert!(is_dirty, "Buffer should be dirty after in-memory edit");
4272
4273        // Try to edit - should fail because buffer has unsaved changes
4274        let result = cx
4275            .update(|cx| {
4276                edit_tool.clone().run(
4277                    ToolInput::resolved(StreamingEditFileToolInput {
4278                        display_description: "Edit with dirty buffer".into(),
4279                        path: "root/test.txt".into(),
4280                        mode: StreamingEditFileMode::Edit,
4281                        content: None,
4282                        edits: Some(vec![Edit {
4283                            old_text: "original content".into(),
4284                            new_text: "new content".into(),
4285                        }]),
4286                    }),
4287                    ToolCallEventStream::test().0,
4288                    cx,
4289                )
4290            })
4291            .await;
4292
4293        let StreamingEditFileToolOutput::Error { error } = result.unwrap_err() else {
4294            panic!("expected error");
4295        };
4296        assert!(
4297            error.contains("This file has unsaved changes."),
4298            "Error should mention unsaved changes, got: {}",
4299            error
4300        );
4301        assert!(
4302            error.contains("keep or discard"),
4303            "Error should ask whether to keep or discard changes, got: {}",
4304            error
4305        );
4306        assert!(
4307            error.contains("save or revert the file manually"),
4308            "Error should ask user to manually save or revert when tools aren't available, got: {}",
4309            error
4310        );
4311    }
4312
4313    #[gpui::test]
4314    async fn test_streaming_overlapping_edits_resolved_sequentially(cx: &mut TestAppContext) {
4315        init_test(cx);
4316
4317        let fs = project::FakeFs::new(cx.executor());
4318        // Edit 1's replacement introduces text that contains edit 2's
4319        // old_text as a substring. Because edits resolve sequentially
4320        // against the current buffer, edit 2 finds a unique match in
4321        // the modified buffer and succeeds.
4322        fs.insert_tree(
4323            "/root",
4324            json!({
4325                "file.txt": "aaa\nbbb\nccc\nddd\neee\n"
4326            }),
4327        )
4328        .await;
4329        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4330        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4331        let context_server_registry =
4332            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4333        let model = Arc::new(FakeLanguageModel::default());
4334        let thread = cx.new(|cx| {
4335            crate::Thread::new(
4336                project.clone(),
4337                cx.new(|_cx| ProjectContext::default()),
4338                context_server_registry,
4339                Templates::new(),
4340                Some(model),
4341                cx,
4342            )
4343        });
4344
4345        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4346        let (event_stream, _receiver) = ToolCallEventStream::test();
4347
4348        let tool = Arc::new(StreamingEditFileTool::new(
4349            project.clone(),
4350            thread.downgrade(),
4351            language_registry,
4352        ));
4353
4354        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4355
4356        // Setup: resolve the buffer
4357        sender.send_partial(json!({
4358            "display_description": "Overlapping edits",
4359            "path": "root/file.txt",
4360            "mode": "edit"
4361        }));
4362        cx.run_until_parked();
4363
4364        // Edit 1 replaces "bbb\nccc" with "XXX\nccc\nddd", so the
4365        // buffer becomes "aaa\nXXX\nccc\nddd\nddd\neee\n".
4366        // Edit 2's old_text "ccc\nddd" matches the first occurrence
4367        // in the modified buffer and replaces it with "ZZZ".
4368        // Edit 3 exists only to mark edit 2 as "complete" during streaming.
4369        sender.send_partial(json!({
4370            "display_description": "Overlapping edits",
4371            "path": "root/file.txt",
4372            "mode": "edit",
4373            "edits": [
4374                {"old_text": "bbb\nccc", "new_text": "XXX\nccc\nddd"},
4375                {"old_text": "ccc\nddd", "new_text": "ZZZ"},
4376                {"old_text": "eee", "new_text": "DUMMY"}
4377            ]
4378        }));
4379        cx.run_until_parked();
4380
4381        // Send the final input with all three edits.
4382        sender.send_final(json!({
4383            "display_description": "Overlapping edits",
4384            "path": "root/file.txt",
4385            "mode": "edit",
4386            "edits": [
4387                {"old_text": "bbb\nccc", "new_text": "XXX\nccc\nddd"},
4388                {"old_text": "ccc\nddd", "new_text": "ZZZ"},
4389                {"old_text": "eee", "new_text": "DUMMY"}
4390            ]
4391        }));
4392
4393        let result = task.await;
4394        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4395            panic!("expected success");
4396        };
4397        assert_eq!(new_text, "aaa\nXXX\nZZZ\nddd\nDUMMY\n");
4398    }
4399
4400    #[gpui::test]
4401    async fn test_streaming_create_content_streamed(cx: &mut TestAppContext) {
4402        init_test(cx);
4403
4404        let fs = project::FakeFs::new(cx.executor());
4405        fs.insert_tree("/root", json!({"dir": {}})).await;
4406        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4407        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4408        let context_server_registry =
4409            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4410        let model = Arc::new(FakeLanguageModel::default());
4411        let thread = cx.new(|cx| {
4412            crate::Thread::new(
4413                project.clone(),
4414                cx.new(|_cx| ProjectContext::default()),
4415                context_server_registry,
4416                Templates::new(),
4417                Some(model),
4418                cx,
4419            )
4420        });
4421
4422        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4423        let (event_stream, _receiver) = ToolCallEventStream::test();
4424
4425        let tool = Arc::new(StreamingEditFileTool::new(
4426            project.clone(),
4427            thread.downgrade(),
4428            language_registry,
4429        ));
4430
4431        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4432
4433        // Transition to BufferResolved
4434        sender.send_partial(json!({
4435            "display_description": "Create new file",
4436            "path": "root/dir/new_file.txt",
4437            "mode": "write"
4438        }));
4439        cx.run_until_parked();
4440
4441        // Stream content incrementally
4442        sender.send_partial(json!({
4443            "display_description": "Create new file",
4444            "path": "root/dir/new_file.txt",
4445            "mode": "write",
4446            "content": "line 1\n"
4447        }));
4448        cx.run_until_parked();
4449
4450        // Verify buffer has partial content
4451        let buffer = project.update(cx, |project, cx| {
4452            let path = project
4453                .find_project_path("root/dir/new_file.txt", cx)
4454                .unwrap();
4455            project.get_open_buffer(&path, cx).unwrap()
4456        });
4457        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\n");
4458
4459        // Stream more content
4460        sender.send_partial(json!({
4461            "display_description": "Create new file",
4462            "path": "root/dir/new_file.txt",
4463            "mode": "write",
4464            "content": "line 1\nline 2\n"
4465        }));
4466        cx.run_until_parked();
4467        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\nline 2\n");
4468
4469        // Stream final chunk
4470        sender.send_partial(json!({
4471            "display_description": "Create new file",
4472            "path": "root/dir/new_file.txt",
4473            "mode": "write",
4474            "content": "line 1\nline 2\nline 3\n"
4475        }));
4476        cx.run_until_parked();
4477        assert_eq!(
4478            buffer.read_with(cx, |b, _| b.text()),
4479            "line 1\nline 2\nline 3\n"
4480        );
4481
4482        // Send final input
4483        sender.send_final(json!({
4484            "display_description": "Create new file",
4485            "path": "root/dir/new_file.txt",
4486            "mode": "write",
4487            "content": "line 1\nline 2\nline 3\n"
4488        }));
4489
4490        let result = task.await;
4491        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4492            panic!("expected success");
4493        };
4494        assert_eq!(new_text, "line 1\nline 2\nline 3\n");
4495    }
4496
4497    #[gpui::test]
4498    async fn test_streaming_overwrite_diff_revealed_during_streaming(cx: &mut TestAppContext) {
4499        init_test(cx);
4500
4501        let fs = project::FakeFs::new(cx.executor());
4502        fs.insert_tree(
4503            "/root",
4504            json!({
4505                "file.txt": "old line 1\nold line 2\nold line 3\n"
4506            }),
4507        )
4508        .await;
4509        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4510        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4511        let context_server_registry =
4512            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4513        let model = Arc::new(FakeLanguageModel::default());
4514        let thread = cx.new(|cx| {
4515            crate::Thread::new(
4516                project.clone(),
4517                cx.new(|_cx| ProjectContext::default()),
4518                context_server_registry,
4519                Templates::new(),
4520                Some(model),
4521                cx,
4522            )
4523        });
4524
4525        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4526        let (event_stream, mut receiver) = ToolCallEventStream::test();
4527
4528        let tool = Arc::new(StreamingEditFileTool::new(
4529            project.clone(),
4530            thread.downgrade(),
4531            language_registry,
4532        ));
4533
4534        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4535
4536        // Transition to BufferResolved
4537        sender.send_partial(json!({
4538            "display_description": "Overwrite file",
4539            "path": "root/file.txt",
4540            "mode": "write"
4541        }));
4542        cx.run_until_parked();
4543
4544        // Get the diff entity from the event stream
4545        receiver.expect_update_fields().await;
4546        let diff = receiver.expect_diff().await;
4547
4548        // Diff starts pending with no revealed ranges
4549        diff.read_with(cx, |diff, cx| {
4550            assert!(matches!(diff, Diff::Pending(_)));
4551            assert!(!diff.has_revealed_range(cx));
4552        });
4553
4554        // Stream first content chunk
4555        sender.send_partial(json!({
4556            "display_description": "Overwrite file",
4557            "path": "root/file.txt",
4558            "mode": "write",
4559            "content": "new line 1\n"
4560        }));
4561        cx.run_until_parked();
4562
4563        // Diff should now have revealed ranges showing the new content
4564        diff.read_with(cx, |diff, cx| {
4565            assert!(diff.has_revealed_range(cx));
4566        });
4567
4568        // Send final input
4569        sender.send_final(json!({
4570            "display_description": "Overwrite file",
4571            "path": "root/file.txt",
4572            "mode": "write",
4573            "content": "new line 1\nnew line 2\n"
4574        }));
4575
4576        let result = task.await;
4577        let StreamingEditFileToolOutput::Success {
4578            new_text, old_text, ..
4579        } = result.unwrap()
4580        else {
4581            panic!("expected success");
4582        };
4583        assert_eq!(new_text, "new line 1\nnew line 2\n");
4584        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
4585
4586        // Diff is finalized after completion
4587        diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
4588    }
4589
4590    #[gpui::test]
4591    async fn test_streaming_overwrite_content_streamed(cx: &mut TestAppContext) {
4592        init_test(cx);
4593
4594        let fs = project::FakeFs::new(cx.executor());
4595        fs.insert_tree(
4596            "/root",
4597            json!({
4598                "file.txt": "old line 1\nold line 2\nold line 3\n"
4599            }),
4600        )
4601        .await;
4602        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4603        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4604        let context_server_registry =
4605            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4606        let model = Arc::new(FakeLanguageModel::default());
4607        let thread = cx.new(|cx| {
4608            crate::Thread::new(
4609                project.clone(),
4610                cx.new(|_cx| ProjectContext::default()),
4611                context_server_registry,
4612                Templates::new(),
4613                Some(model),
4614                cx,
4615            )
4616        });
4617
4618        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4619        let (event_stream, _receiver) = ToolCallEventStream::test();
4620
4621        let tool = Arc::new(StreamingEditFileTool::new(
4622            project.clone(),
4623            thread.downgrade(),
4624            language_registry,
4625        ));
4626
4627        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4628
4629        // Transition to BufferResolved
4630        sender.send_partial(json!({
4631            "display_description": "Overwrite file",
4632            "path": "root/file.txt",
4633            "mode": "write"
4634        }));
4635        cx.run_until_parked();
4636
4637        // Verify buffer still has old content (no content partial yet)
4638        let buffer = project.update(cx, |project, cx| {
4639            let path = project.find_project_path("root/file.txt", cx).unwrap();
4640            project.get_open_buffer(&path, cx).unwrap()
4641        });
4642        assert_eq!(
4643            buffer.read_with(cx, |b, _| b.text()),
4644            "old line 1\nold line 2\nold line 3\n"
4645        );
4646
4647        // First content partial replaces old content
4648        sender.send_partial(json!({
4649            "display_description": "Overwrite file",
4650            "path": "root/file.txt",
4651            "mode": "write",
4652            "content": "new line 1\n"
4653        }));
4654        cx.run_until_parked();
4655        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "new line 1\n");
4656
4657        // Subsequent content partials append
4658        sender.send_partial(json!({
4659            "display_description": "Overwrite file",
4660            "path": "root/file.txt",
4661            "mode": "write",
4662            "content": "new line 1\nnew line 2\n"
4663        }));
4664        cx.run_until_parked();
4665        assert_eq!(
4666            buffer.read_with(cx, |b, _| b.text()),
4667            "new line 1\nnew line 2\n"
4668        );
4669
4670        // Send final input with complete content
4671        sender.send_final(json!({
4672            "display_description": "Overwrite file",
4673            "path": "root/file.txt",
4674            "mode": "write",
4675            "content": "new line 1\nnew line 2\nnew line 3\n"
4676        }));
4677
4678        let result = task.await;
4679        let StreamingEditFileToolOutput::Success {
4680            new_text, old_text, ..
4681        } = result.unwrap()
4682        else {
4683            panic!("expected success");
4684        };
4685        assert_eq!(new_text, "new line 1\nnew line 2\nnew line 3\n");
4686        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
4687    }
4688
4689    #[gpui::test]
4690    async fn test_streaming_edit_json_fixer_escape_corruption(cx: &mut TestAppContext) {
4691        init_test(cx);
4692
4693        let fs = project::FakeFs::new(cx.executor());
4694        fs.insert_tree(
4695            "/root",
4696            json!({
4697                "file.txt": "hello\nworld\nfoo\n"
4698            }),
4699        )
4700        .await;
4701        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4702        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4703        let context_server_registry =
4704            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4705        let model = Arc::new(FakeLanguageModel::default());
4706        let thread = cx.new(|cx| {
4707            crate::Thread::new(
4708                project.clone(),
4709                cx.new(|_cx| ProjectContext::default()),
4710                context_server_registry,
4711                Templates::new(),
4712                Some(model),
4713                cx,
4714            )
4715        });
4716
4717        let (sender, input) = ToolInput::<StreamingEditFileToolInput>::test();
4718        let (event_stream, _receiver) = ToolCallEventStream::test();
4719
4720        let tool = Arc::new(StreamingEditFileTool::new(
4721            project.clone(),
4722            thread.downgrade(),
4723            language_registry,
4724        ));
4725
4726        let task = cx.update(|cx| tool.run(input, event_stream, cx));
4727
4728        sender.send_partial(json!({
4729            "display_description": "Edit",
4730            "path": "root/file.txt",
4731            "mode": "edit"
4732        }));
4733        cx.run_until_parked();
4734
4735        // Simulate JSON fixer producing a literal backslash when the LLM
4736        // stream cuts in the middle of a \n escape sequence.
4737        // The old_text "hello\nworld" would be streamed as:
4738        //   partial 1: old_text = "hello\\" (fixer closes incomplete \n as \\)
4739        //   partial 2: old_text = "hello\nworld" (fixer corrected the escape)
4740        sender.send_partial(json!({
4741            "display_description": "Edit",
4742            "path": "root/file.txt",
4743            "mode": "edit",
4744            "edits": [{"old_text": "hello\\"}]
4745        }));
4746        cx.run_until_parked();
4747
4748        // Now the fixer corrects it to the real newline.
4749        sender.send_partial(json!({
4750            "display_description": "Edit",
4751            "path": "root/file.txt",
4752            "mode": "edit",
4753            "edits": [{"old_text": "hello\nworld"}]
4754        }));
4755        cx.run_until_parked();
4756
4757        // Send final.
4758        sender.send_final(json!({
4759            "display_description": "Edit",
4760            "path": "root/file.txt",
4761            "mode": "edit",
4762            "edits": [{"old_text": "hello\nworld", "new_text": "HELLO\nWORLD"}]
4763        }));
4764
4765        let result = task.await;
4766        let StreamingEditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
4767            panic!("expected success");
4768        };
4769        assert_eq!(new_text, "HELLO\nWORLD\nfoo\n");
4770    }
4771
4772    // Verifies that after streaming_edit_file_tool edits a file, the action log
4773    // reports changed buffers so that the Accept All / Reject All review UI appears.
4774    #[gpui::test]
4775    async fn test_streaming_edit_file_tool_registers_changed_buffers(cx: &mut TestAppContext) {
4776        init_test(cx);
4777        cx.update(|cx| {
4778            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
4779            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
4780            agent_settings::AgentSettings::override_global(settings, cx);
4781        });
4782
4783        let fs = project::FakeFs::new(cx.executor());
4784        fs.insert_tree(
4785            path!("/root"),
4786            json!({
4787                "file.txt": "line 1\nline 2\nline 3\n"
4788            }),
4789        )
4790        .await;
4791
4792        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4793        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4794        let context_server_registry =
4795            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4796        let thread = cx.new(|cx| {
4797            crate::Thread::new(
4798                project.clone(),
4799                cx.new(|_cx| ProjectContext::default()),
4800                context_server_registry,
4801                Templates::new(),
4802                None,
4803                cx,
4804            )
4805        });
4806        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4807
4808        let tool = Arc::new(StreamingEditFileTool::new(
4809            project.clone(),
4810            thread.downgrade(),
4811            language_registry,
4812        ));
4813        let (event_stream, _rx) = ToolCallEventStream::test();
4814
4815        let task = cx.update(|cx| {
4816            tool.run(
4817                ToolInput::resolved(StreamingEditFileToolInput {
4818                    display_description: "Edit lines".to_string(),
4819                    path: "root/file.txt".into(),
4820                    mode: StreamingEditFileMode::Edit,
4821                    content: None,
4822                    edits: Some(vec![Edit {
4823                        old_text: "line 2".into(),
4824                        new_text: "modified line 2".into(),
4825                    }]),
4826                }),
4827                event_stream,
4828                cx,
4829            )
4830        });
4831
4832        let result = task.await;
4833        assert!(result.is_ok(), "edit should succeed: {:?}", result.err());
4834
4835        cx.run_until_parked();
4836
4837        let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
4838        assert!(
4839            !changed.is_empty(),
4840            "action_log.changed_buffers() should be non-empty after streaming edit, \
4841             but no changed buffers were found \u{2014} Accept All / Reject All will not appear"
4842        );
4843    }
4844
4845    // Same test but for Write mode (overwrite entire file).
4846    #[gpui::test]
4847    async fn test_streaming_edit_file_tool_write_mode_registers_changed_buffers(
4848        cx: &mut TestAppContext,
4849    ) {
4850        init_test(cx);
4851        cx.update(|cx| {
4852            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
4853            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
4854            agent_settings::AgentSettings::override_global(settings, cx);
4855        });
4856
4857        let fs = project::FakeFs::new(cx.executor());
4858        fs.insert_tree(
4859            path!("/root"),
4860            json!({
4861                "file.txt": "original content"
4862            }),
4863        )
4864        .await;
4865
4866        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
4867        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
4868        let context_server_registry =
4869            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
4870        let thread = cx.new(|cx| {
4871            crate::Thread::new(
4872                project.clone(),
4873                cx.new(|_cx| ProjectContext::default()),
4874                context_server_registry,
4875                Templates::new(),
4876                None,
4877                cx,
4878            )
4879        });
4880        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4881
4882        let tool = Arc::new(StreamingEditFileTool::new(
4883            project.clone(),
4884            thread.downgrade(),
4885            language_registry,
4886        ));
4887        let (event_stream, _rx) = ToolCallEventStream::test();
4888
4889        let task = cx.update(|cx| {
4890            tool.run(
4891                ToolInput::resolved(StreamingEditFileToolInput {
4892                    display_description: "Overwrite file".to_string(),
4893                    path: "root/file.txt".into(),
4894                    mode: StreamingEditFileMode::Write,
4895                    content: Some("completely new content".into()),
4896                    edits: None,
4897                }),
4898                event_stream,
4899                cx,
4900            )
4901        });
4902
4903        let result = task.await;
4904        assert!(result.is_ok(), "write should succeed: {:?}", result.err());
4905
4906        cx.run_until_parked();
4907
4908        let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
4909        assert!(
4910            !changed.is_empty(),
4911            "action_log.changed_buffers() should be non-empty after streaming write, \
4912             but no changed buffers were found \u{2014} Accept All / Reject All will not appear"
4913        );
4914    }
4915
4916    fn init_test(cx: &mut TestAppContext) {
4917        cx.update(|cx| {
4918            let settings_store = SettingsStore::test(cx);
4919            cx.set_global(settings_store);
4920            SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
4921                store.update_user_settings(cx, |settings| {
4922                    settings
4923                        .project
4924                        .all_languages
4925                        .defaults
4926                        .ensure_final_newline_on_save = Some(false);
4927                });
4928            });
4929        });
4930    }
4931}