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