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