edit_file_tool.rs

   1use crate::{AgentTool, Thread, ToolCallEventStream};
   2use acp_thread::Diff;
   3use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
   4use anyhow::{Context as _, Result, anyhow};
   5use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
   6use cloud_llm_client::CompletionIntent;
   7use collections::HashSet;
   8use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
   9use indoc::formatdoc;
  10use language::language_settings::{self, FormatOnSave};
  11use language::{LanguageRegistry, ToPoint};
  12use language_model::LanguageModelToolResultContent;
  13use paths;
  14use project::lsp_store::{FormatTrigger, LspFormatTarget};
  15use project::{Project, ProjectPath};
  16use schemars::JsonSchema;
  17use serde::{Deserialize, Serialize};
  18use settings::Settings;
  19use smol::stream::StreamExt as _;
  20use std::path::{Path, PathBuf};
  21use std::sync::Arc;
  22use ui::SharedString;
  23use util::ResultExt;
  24
  25const DEFAULT_UI_TEXT: &str = "Editing file";
  26
  27/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
  28///
  29/// Before using this tool:
  30///
  31/// 1. Use the `read_file` tool to understand the file's contents and context
  32///
  33/// 2. Verify the directory path is correct (only applicable when creating new files):
  34///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location
  35#[derive(Debug, Serialize, Deserialize, JsonSchema)]
  36pub struct EditFileToolInput {
  37    /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
  38    ///
  39    /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
  40    ///
  41    /// NEVER mention the file path in this description.
  42    ///
  43    /// <example>Fix API endpoint URLs</example>
  44    /// <example>Update copyright year in `page_footer`</example>
  45    ///
  46    /// Make sure to include this field before all the others in the input object so that we can display it immediately.
  47    pub display_description: String,
  48
  49    /// The full path of the file to create or modify in the project.
  50    ///
  51    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
  52    ///
  53    /// The following examples assume we have two root directories in the project:
  54    /// - /a/b/backend
  55    /// - /c/d/frontend
  56    ///
  57    /// <example>
  58    /// `backend/src/main.rs`
  59    ///
  60    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
  61    /// </example>
  62    ///
  63    /// <example>
  64    /// `frontend/db.js`
  65    /// </example>
  66    pub path: PathBuf,
  67    /// The mode of operation on the file. Possible values:
  68    /// - 'edit': Make granular edits to an existing file.
  69    /// - 'create': Create a new file if it doesn't exist.
  70    /// - 'overwrite': Replace the entire contents of an existing file.
  71    ///
  72    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
  73    pub mode: EditFileMode,
  74}
  75
  76#[derive(Debug, Serialize, Deserialize, JsonSchema)]
  77struct EditFileToolPartialInput {
  78    #[serde(default)]
  79    path: String,
  80    #[serde(default)]
  81    display_description: String,
  82}
  83
  84#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
  85#[serde(rename_all = "lowercase")]
  86pub enum EditFileMode {
  87    Edit,
  88    Create,
  89    Overwrite,
  90}
  91
  92#[derive(Debug, Serialize, Deserialize)]
  93pub struct EditFileToolOutput {
  94    #[serde(alias = "original_path")]
  95    input_path: PathBuf,
  96    new_text: String,
  97    old_text: Arc<String>,
  98    #[serde(default)]
  99    diff: String,
 100    #[serde(alias = "raw_output")]
 101    edit_agent_output: EditAgentOutput,
 102}
 103
 104impl From<EditFileToolOutput> for LanguageModelToolResultContent {
 105    fn from(output: EditFileToolOutput) -> Self {
 106        if output.diff.is_empty() {
 107            "No edits were made.".into()
 108        } else {
 109            format!(
 110                "Edited {}:\n\n```diff\n{}\n```",
 111                output.input_path.display(),
 112                output.diff
 113            )
 114            .into()
 115        }
 116    }
 117}
 118
 119pub struct EditFileTool {
 120    thread: WeakEntity<Thread>,
 121    language_registry: Arc<LanguageRegistry>,
 122}
 123
 124impl EditFileTool {
 125    pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
 126        Self {
 127            thread,
 128            language_registry,
 129        }
 130    }
 131
 132    fn authorize(
 133        &self,
 134        input: &EditFileToolInput,
 135        event_stream: &ToolCallEventStream,
 136        cx: &mut App,
 137    ) -> Task<Result<()>> {
 138        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
 139            return Task::ready(Ok(()));
 140        }
 141
 142        // If any path component matches the local settings folder, then this could affect
 143        // the editor in ways beyond the project source, so prompt.
 144        let local_settings_folder = paths::local_settings_folder_relative_path();
 145        let path = Path::new(&input.path);
 146        if path
 147            .components()
 148            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
 149        {
 150            return event_stream.authorize(
 151                format!("{} (local settings)", input.display_description),
 152                cx,
 153            );
 154        }
 155
 156        // It's also possible that the global config dir is configured to be inside the project,
 157        // so check for that edge case too.
 158        if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
 159            && canonical_path.starts_with(paths::config_dir())
 160        {
 161            return event_stream.authorize(
 162                format!("{} (global settings)", input.display_description),
 163                cx,
 164            );
 165        }
 166
 167        // Check if path is inside the global config directory
 168        // First check if it's already inside project - if not, try to canonicalize
 169        let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
 170            thread.project().read(cx).find_project_path(&input.path, cx)
 171        }) else {
 172            return Task::ready(Err(anyhow!("thread was dropped")));
 173        };
 174
 175        // If the path is inside the project, and it's not one of the above edge cases,
 176        // then no confirmation is necessary. Otherwise, confirmation is necessary.
 177        if project_path.is_some() {
 178            Task::ready(Ok(()))
 179        } else {
 180            event_stream.authorize(&input.display_description, cx)
 181        }
 182    }
 183}
 184
 185impl AgentTool for EditFileTool {
 186    type Input = EditFileToolInput;
 187    type Output = EditFileToolOutput;
 188
 189    fn name() -> &'static str {
 190        "edit_file"
 191    }
 192
 193    fn kind() -> acp::ToolKind {
 194        acp::ToolKind::Edit
 195    }
 196
 197    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
 198        match input {
 199            Ok(input) => input.display_description.into(),
 200            Err(raw_input) => {
 201                if let Some(input) =
 202                    serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
 203                {
 204                    let description = input.display_description.trim();
 205                    if !description.is_empty() {
 206                        return description.to_string().into();
 207                    }
 208
 209                    let path = input.path.trim().to_string();
 210                    if !path.is_empty() {
 211                        return path.into();
 212                    }
 213                }
 214
 215                DEFAULT_UI_TEXT.into()
 216            }
 217        }
 218    }
 219
 220    fn run(
 221        self: Arc<Self>,
 222        input: Self::Input,
 223        event_stream: ToolCallEventStream,
 224        cx: &mut App,
 225    ) -> Task<Result<Self::Output>> {
 226        let Ok(project) = self
 227            .thread
 228            .read_with(cx, |thread, _cx| thread.project().clone())
 229        else {
 230            return Task::ready(Err(anyhow!("thread was dropped")));
 231        };
 232        let project_path = match resolve_path(&input, project.clone(), cx) {
 233            Ok(path) => path,
 234            Err(err) => return Task::ready(Err(anyhow!(err))),
 235        };
 236        let abs_path = project.read(cx).absolute_path(&project_path, cx);
 237        if let Some(abs_path) = abs_path.clone() {
 238            event_stream.update_fields(ToolCallUpdateFields {
 239                locations: Some(vec![acp::ToolCallLocation {
 240                    path: abs_path,
 241                    line: None,
 242                }]),
 243                ..Default::default()
 244            });
 245        }
 246
 247        let authorize = self.authorize(&input, &event_stream, cx);
 248        cx.spawn(async move |cx: &mut AsyncApp| {
 249            authorize.await?;
 250
 251            let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
 252                let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
 253                (request, thread.model().cloned(), thread.action_log().clone())
 254            })?;
 255            let request = request?;
 256            let model = model.context("No language model configured")?;
 257
 258            let edit_format = EditFormat::from_model(model.clone())?;
 259            let edit_agent = EditAgent::new(
 260                model,
 261                project.clone(),
 262                action_log.clone(),
 263                // TODO: move edit agent to this crate so we can use our templates
 264                assistant_tools::templates::Templates::new(),
 265                edit_format,
 266            );
 267
 268            let buffer = project
 269                .update(cx, |project, cx| {
 270                    project.open_buffer(project_path.clone(), cx)
 271                })?
 272                .await?;
 273
 274            let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
 275            event_stream.update_diff(diff.clone());
 276
 277            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 278            let old_text = cx
 279                .background_spawn({
 280                    let old_snapshot = old_snapshot.clone();
 281                    async move { Arc::new(old_snapshot.text()) }
 282                })
 283                .await;
 284
 285
 286            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
 287                edit_agent.edit(
 288                    buffer.clone(),
 289                    input.display_description.clone(),
 290                    &request,
 291                    cx,
 292                )
 293            } else {
 294                edit_agent.overwrite(
 295                    buffer.clone(),
 296                    input.display_description.clone(),
 297                    &request,
 298                    cx,
 299                )
 300            };
 301
 302            let mut hallucinated_old_text = false;
 303            let mut ambiguous_ranges = Vec::new();
 304            let mut emitted_location = false;
 305            while let Some(event) = events.next().await {
 306                match event {
 307                    EditAgentOutputEvent::Edited(range) => {
 308                        if !emitted_location {
 309                            let line = buffer.update(cx, |buffer, _cx| {
 310                                range.start.to_point(&buffer.snapshot()).row
 311                            }).ok();
 312                            if let Some(abs_path) = abs_path.clone() {
 313                                event_stream.update_fields(ToolCallUpdateFields {
 314                                    locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
 315                                    ..Default::default()
 316                                });
 317                            }
 318                            emitted_location = true;
 319                        }
 320                    },
 321                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
 322                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
 323                    EditAgentOutputEvent::ResolvingEditRange(range) => {
 324                        diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
 325                        // if !emitted_location {
 326                        //     let line = buffer.update(cx, |buffer, _cx| {
 327                        //         range.start.to_point(&buffer.snapshot()).row
 328                        //     }).ok();
 329                        //     if let Some(abs_path) = abs_path.clone() {
 330                        //         event_stream.update_fields(ToolCallUpdateFields {
 331                        //             locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
 332                        //             ..Default::default()
 333                        //         });
 334                        //     }
 335                        // }
 336                    }
 337                }
 338            }
 339
 340            // If format_on_save is enabled, format the buffer
 341            let format_on_save_enabled = buffer
 342                .read_with(cx, |buffer, cx| {
 343                    let settings = language_settings::language_settings(
 344                        buffer.language().map(|l| l.name()),
 345                        buffer.file(),
 346                        cx,
 347                    );
 348                    settings.format_on_save != FormatOnSave::Off
 349                })
 350                .unwrap_or(false);
 351
 352            let edit_agent_output = output.await?;
 353
 354            if format_on_save_enabled {
 355                action_log.update(cx, |log, cx| {
 356                    log.buffer_edited(buffer.clone(), cx);
 357                })?;
 358
 359                let format_task = project.update(cx, |project, cx| {
 360                    project.format(
 361                        HashSet::from_iter([buffer.clone()]),
 362                        LspFormatTarget::Buffers,
 363                        false, // Don't push to history since the tool did it.
 364                        FormatTrigger::Save,
 365                        cx,
 366                    )
 367                })?;
 368                format_task.await.log_err();
 369            }
 370
 371            project
 372                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
 373                .await?;
 374
 375            action_log.update(cx, |log, cx| {
 376                log.buffer_edited(buffer.clone(), cx);
 377            })?;
 378
 379            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 380            let (new_text, unified_diff) = cx
 381                .background_spawn({
 382                    let new_snapshot = new_snapshot.clone();
 383                    let old_text = old_text.clone();
 384                    async move {
 385                        let new_text = new_snapshot.text();
 386                        let diff = language::unified_diff(&old_text, &new_text);
 387                        (new_text, diff)
 388                    }
 389                })
 390                .await;
 391
 392            diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
 393
 394            let input_path = input.path.display();
 395            if unified_diff.is_empty() {
 396                anyhow::ensure!(
 397                    !hallucinated_old_text,
 398                    formatdoc! {"
 399                        Some edits were produced but none of them could be applied.
 400                        Read the relevant sections of {input_path} again so that
 401                        I can perform the requested edits.
 402                    "}
 403                );
 404                anyhow::ensure!(
 405                    ambiguous_ranges.is_empty(),
 406                    {
 407                        let line_numbers = ambiguous_ranges
 408                            .iter()
 409                            .map(|range| range.start.to_string())
 410                            .collect::<Vec<_>>()
 411                            .join(", ");
 412                        formatdoc! {"
 413                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
 414                            relevant sections of {input_path} again and extend <old_text> so
 415                            that I can perform the requested edits.
 416                        "}
 417                    }
 418                );
 419            }
 420
 421            Ok(EditFileToolOutput {
 422                input_path: input.path,
 423                new_text,
 424                old_text,
 425                diff: unified_diff,
 426                edit_agent_output,
 427            })
 428        })
 429    }
 430
 431    fn replay(
 432        &self,
 433        _input: Self::Input,
 434        output: Self::Output,
 435        event_stream: ToolCallEventStream,
 436        cx: &mut App,
 437    ) -> Result<()> {
 438        event_stream.update_diff(cx.new(|cx| {
 439            Diff::finalized(
 440                output.input_path,
 441                Some(output.old_text.to_string()),
 442                output.new_text,
 443                self.language_registry.clone(),
 444                cx,
 445            )
 446        }));
 447        Ok(())
 448    }
 449}
 450
 451/// Validate that the file path is valid, meaning:
 452///
 453/// - For `edit` and `overwrite`, the path must point to an existing file.
 454/// - For `create`, the file must not already exist, but it's parent dir must exist.
 455fn resolve_path(
 456    input: &EditFileToolInput,
 457    project: Entity<Project>,
 458    cx: &mut App,
 459) -> Result<ProjectPath> {
 460    let project = project.read(cx);
 461
 462    match input.mode {
 463        EditFileMode::Edit | EditFileMode::Overwrite => {
 464            let path = project
 465                .find_project_path(&input.path, cx)
 466                .context("Can't edit file: path not found")?;
 467
 468            let entry = project
 469                .entry_for_path(&path, cx)
 470                .context("Can't edit file: path not found")?;
 471
 472            anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
 473            Ok(path)
 474        }
 475
 476        EditFileMode::Create => {
 477            if let Some(path) = project.find_project_path(&input.path, cx) {
 478                anyhow::ensure!(
 479                    project.entry_for_path(&path, cx).is_none(),
 480                    "Can't create file: file already exists"
 481                );
 482            }
 483
 484            let parent_path = input
 485                .path
 486                .parent()
 487                .context("Can't create file: incorrect path")?;
 488
 489            let parent_project_path = project.find_project_path(&parent_path, cx);
 490
 491            let parent_entry = parent_project_path
 492                .as_ref()
 493                .and_then(|path| project.entry_for_path(path, cx))
 494                .context("Can't create file: parent directory doesn't exist")?;
 495
 496            anyhow::ensure!(
 497                parent_entry.is_dir(),
 498                "Can't create file: parent is not a directory"
 499            );
 500
 501            let file_name = input
 502                .path
 503                .file_name()
 504                .context("Can't create file: invalid filename")?;
 505
 506            let new_file_path = parent_project_path.map(|parent| ProjectPath {
 507                path: Arc::from(parent.path.join(file_name)),
 508                ..parent
 509            });
 510
 511            new_file_path.context("Can't create file")
 512        }
 513    }
 514}
 515
 516#[cfg(test)]
 517mod tests {
 518    use super::*;
 519    use crate::{ContextServerRegistry, Templates};
 520    use client::TelemetrySettings;
 521    use fs::Fs;
 522    use gpui::{TestAppContext, UpdateGlobal};
 523    use language_model::fake_provider::FakeLanguageModel;
 524    use prompt_store::ProjectContext;
 525    use serde_json::json;
 526    use settings::SettingsStore;
 527    use util::path;
 528
 529    #[gpui::test]
 530    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
 531        init_test(cx);
 532
 533        let fs = project::FakeFs::new(cx.executor());
 534        fs.insert_tree("/root", json!({})).await;
 535        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 536        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
 537        let context_server_registry =
 538            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 539        let model = Arc::new(FakeLanguageModel::default());
 540        let thread = cx.new(|cx| {
 541            Thread::new(
 542                project,
 543                cx.new(|_cx| ProjectContext::default()),
 544                context_server_registry,
 545                Templates::new(),
 546                Some(model),
 547                cx,
 548            )
 549        });
 550        let result = cx
 551            .update(|cx| {
 552                let input = EditFileToolInput {
 553                    display_description: "Some edit".into(),
 554                    path: "root/nonexistent_file.txt".into(),
 555                    mode: EditFileMode::Edit,
 556                };
 557                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 558                    input,
 559                    ToolCallEventStream::test().0,
 560                    cx,
 561                )
 562            })
 563            .await;
 564        assert_eq!(
 565            result.unwrap_err().to_string(),
 566            "Can't edit file: path not found"
 567        );
 568    }
 569
 570    #[gpui::test]
 571    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
 572        let mode = &EditFileMode::Create;
 573
 574        let result = test_resolve_path(mode, "root/new.txt", cx);
 575        assert_resolved_path_eq(result.await, "new.txt");
 576
 577        let result = test_resolve_path(mode, "new.txt", cx);
 578        assert_resolved_path_eq(result.await, "new.txt");
 579
 580        let result = test_resolve_path(mode, "dir/new.txt", cx);
 581        assert_resolved_path_eq(result.await, "dir/new.txt");
 582
 583        let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
 584        assert_eq!(
 585            result.await.unwrap_err().to_string(),
 586            "Can't create file: file already exists"
 587        );
 588
 589        let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
 590        assert_eq!(
 591            result.await.unwrap_err().to_string(),
 592            "Can't create file: parent directory doesn't exist"
 593        );
 594    }
 595
 596    #[gpui::test]
 597    async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
 598        let mode = &EditFileMode::Edit;
 599
 600        let path_with_root = "root/dir/subdir/existing.txt";
 601        let path_without_root = "dir/subdir/existing.txt";
 602        let result = test_resolve_path(mode, path_with_root, cx);
 603        assert_resolved_path_eq(result.await, path_without_root);
 604
 605        let result = test_resolve_path(mode, path_without_root, cx);
 606        assert_resolved_path_eq(result.await, path_without_root);
 607
 608        let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
 609        assert_eq!(
 610            result.await.unwrap_err().to_string(),
 611            "Can't edit file: path not found"
 612        );
 613
 614        let result = test_resolve_path(mode, "root/dir", cx);
 615        assert_eq!(
 616            result.await.unwrap_err().to_string(),
 617            "Can't edit file: path is a directory"
 618        );
 619    }
 620
 621    async fn test_resolve_path(
 622        mode: &EditFileMode,
 623        path: &str,
 624        cx: &mut TestAppContext,
 625    ) -> anyhow::Result<ProjectPath> {
 626        init_test(cx);
 627
 628        let fs = project::FakeFs::new(cx.executor());
 629        fs.insert_tree(
 630            "/root",
 631            json!({
 632                "dir": {
 633                    "subdir": {
 634                        "existing.txt": "hello"
 635                    }
 636                }
 637            }),
 638        )
 639        .await;
 640        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 641
 642        let input = EditFileToolInput {
 643            display_description: "Some edit".into(),
 644            path: path.into(),
 645            mode: mode.clone(),
 646        };
 647
 648        cx.update(|cx| resolve_path(&input, project, cx))
 649    }
 650
 651    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
 652        let actual = path
 653            .expect("Should return valid path")
 654            .path
 655            .to_str()
 656            .unwrap()
 657            .replace("\\", "/"); // Naive Windows paths normalization
 658        assert_eq!(actual, expected);
 659    }
 660
 661    #[gpui::test]
 662    async fn test_format_on_save(cx: &mut TestAppContext) {
 663        init_test(cx);
 664
 665        let fs = project::FakeFs::new(cx.executor());
 666        fs.insert_tree("/root", json!({"src": {}})).await;
 667
 668        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 669
 670        // Set up a Rust language with LSP formatting support
 671        let rust_language = Arc::new(language::Language::new(
 672            language::LanguageConfig {
 673                name: "Rust".into(),
 674                matcher: language::LanguageMatcher {
 675                    path_suffixes: vec!["rs".to_string()],
 676                    ..Default::default()
 677                },
 678                ..Default::default()
 679            },
 680            None,
 681        ));
 682
 683        // Register the language and fake LSP
 684        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 685        language_registry.add(rust_language);
 686
 687        let mut fake_language_servers = language_registry.register_fake_lsp(
 688            "Rust",
 689            language::FakeLspAdapter {
 690                capabilities: lsp::ServerCapabilities {
 691                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
 692                    ..Default::default()
 693                },
 694                ..Default::default()
 695            },
 696        );
 697
 698        // Create the file
 699        fs.save(
 700            path!("/root/src/main.rs").as_ref(),
 701            &"initial content".into(),
 702            language::LineEnding::Unix,
 703        )
 704        .await
 705        .unwrap();
 706
 707        // Open the buffer to trigger LSP initialization
 708        let buffer = project
 709            .update(cx, |project, cx| {
 710                project.open_local_buffer(path!("/root/src/main.rs"), cx)
 711            })
 712            .await
 713            .unwrap();
 714
 715        // Register the buffer with language servers
 716        let _handle = project.update(cx, |project, cx| {
 717            project.register_buffer_with_language_servers(&buffer, cx)
 718        });
 719
 720        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
 721        const FORMATTED_CONTENT: &str =
 722            "This file was formatted by the fake formatter in the test.\n";
 723
 724        // Get the fake language server and set up formatting handler
 725        let fake_language_server = fake_language_servers.next().await.unwrap();
 726        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
 727            |_, _| async move {
 728                Ok(Some(vec![lsp::TextEdit {
 729                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
 730                    new_text: FORMATTED_CONTENT.to_string(),
 731                }]))
 732            }
 733        });
 734
 735        let context_server_registry =
 736            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 737        let model = Arc::new(FakeLanguageModel::default());
 738        let thread = cx.new(|cx| {
 739            Thread::new(
 740                project,
 741                cx.new(|_cx| ProjectContext::default()),
 742                context_server_registry,
 743                Templates::new(),
 744                Some(model.clone()),
 745                cx,
 746            )
 747        });
 748
 749        // First, test with format_on_save enabled
 750        cx.update(|cx| {
 751            SettingsStore::update_global(cx, |store, cx| {
 752                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 753                    cx,
 754                    |settings| {
 755                        settings.defaults.format_on_save = Some(FormatOnSave::On);
 756                        settings.defaults.formatter =
 757                            Some(language::language_settings::SelectedFormatter::Auto);
 758                    },
 759                );
 760            });
 761        });
 762
 763        // Have the model stream unformatted content
 764        let edit_result = {
 765            let edit_task = cx.update(|cx| {
 766                let input = EditFileToolInput {
 767                    display_description: "Create main function".into(),
 768                    path: "root/src/main.rs".into(),
 769                    mode: EditFileMode::Overwrite,
 770                };
 771                Arc::new(EditFileTool::new(
 772                    thread.downgrade(),
 773                    language_registry.clone(),
 774                ))
 775                .run(input, ToolCallEventStream::test().0, cx)
 776            });
 777
 778            // Stream the unformatted content
 779            cx.executor().run_until_parked();
 780            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
 781            model.end_last_completion_stream();
 782
 783            edit_task.await
 784        };
 785        assert!(edit_result.is_ok());
 786
 787        // Wait for any async operations (e.g. formatting) to complete
 788        cx.executor().run_until_parked();
 789
 790        // Read the file to verify it was formatted automatically
 791        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 792        assert_eq!(
 793            // Ignore carriage returns on Windows
 794            new_content.replace("\r\n", "\n"),
 795            FORMATTED_CONTENT,
 796            "Code should be formatted when format_on_save is enabled"
 797        );
 798
 799        let stale_buffer_count = thread
 800            .read_with(cx, |thread, _cx| thread.action_log.clone())
 801            .read_with(cx, |log, cx| log.stale_buffers(cx).count());
 802
 803        assert_eq!(
 804            stale_buffer_count, 0,
 805            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
 806             This causes the agent to think the file was modified externally when it was just formatted.",
 807            stale_buffer_count
 808        );
 809
 810        // Next, test with format_on_save disabled
 811        cx.update(|cx| {
 812            SettingsStore::update_global(cx, |store, cx| {
 813                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 814                    cx,
 815                    |settings| {
 816                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
 817                    },
 818                );
 819            });
 820        });
 821
 822        // Stream unformatted edits again
 823        let edit_result = {
 824            let edit_task = cx.update(|cx| {
 825                let input = EditFileToolInput {
 826                    display_description: "Update main function".into(),
 827                    path: "root/src/main.rs".into(),
 828                    mode: EditFileMode::Overwrite,
 829                };
 830                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 831                    input,
 832                    ToolCallEventStream::test().0,
 833                    cx,
 834                )
 835            });
 836
 837            // Stream the unformatted content
 838            cx.executor().run_until_parked();
 839            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
 840            model.end_last_completion_stream();
 841
 842            edit_task.await
 843        };
 844        assert!(edit_result.is_ok());
 845
 846        // Wait for any async operations (e.g. formatting) to complete
 847        cx.executor().run_until_parked();
 848
 849        // Verify the file was not formatted
 850        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 851        assert_eq!(
 852            // Ignore carriage returns on Windows
 853            new_content.replace("\r\n", "\n"),
 854            UNFORMATTED_CONTENT,
 855            "Code should not be formatted when format_on_save is disabled"
 856        );
 857    }
 858
 859    #[gpui::test]
 860    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
 861        init_test(cx);
 862
 863        let fs = project::FakeFs::new(cx.executor());
 864        fs.insert_tree("/root", json!({"src": {}})).await;
 865
 866        // Create a simple file with trailing whitespace
 867        fs.save(
 868            path!("/root/src/main.rs").as_ref(),
 869            &"initial content".into(),
 870            language::LineEnding::Unix,
 871        )
 872        .await
 873        .unwrap();
 874
 875        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 876        let context_server_registry =
 877            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 878        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
 879        let model = Arc::new(FakeLanguageModel::default());
 880        let thread = cx.new(|cx| {
 881            Thread::new(
 882                project,
 883                cx.new(|_cx| ProjectContext::default()),
 884                context_server_registry,
 885                Templates::new(),
 886                Some(model.clone()),
 887                cx,
 888            )
 889        });
 890
 891        // First, test with remove_trailing_whitespace_on_save enabled
 892        cx.update(|cx| {
 893            SettingsStore::update_global(cx, |store, cx| {
 894                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 895                    cx,
 896                    |settings| {
 897                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
 898                    },
 899                );
 900            });
 901        });
 902
 903        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
 904            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
 905
 906        // Have the model stream content that contains trailing whitespace
 907        let edit_result = {
 908            let edit_task = cx.update(|cx| {
 909                let input = EditFileToolInput {
 910                    display_description: "Create main function".into(),
 911                    path: "root/src/main.rs".into(),
 912                    mode: EditFileMode::Overwrite,
 913                };
 914                Arc::new(EditFileTool::new(
 915                    thread.downgrade(),
 916                    language_registry.clone(),
 917                ))
 918                .run(input, ToolCallEventStream::test().0, cx)
 919            });
 920
 921            // Stream the content with trailing whitespace
 922            cx.executor().run_until_parked();
 923            model.send_last_completion_stream_text_chunk(
 924                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
 925            );
 926            model.end_last_completion_stream();
 927
 928            edit_task.await
 929        };
 930        assert!(edit_result.is_ok());
 931
 932        // Wait for any async operations (e.g. formatting) to complete
 933        cx.executor().run_until_parked();
 934
 935        // Read the file to verify trailing whitespace was removed automatically
 936        assert_eq!(
 937            // Ignore carriage returns on Windows
 938            fs.load(path!("/root/src/main.rs").as_ref())
 939                .await
 940                .unwrap()
 941                .replace("\r\n", "\n"),
 942            "fn main() {\n    println!(\"Hello!\");\n}\n",
 943            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
 944        );
 945
 946        // Next, test with remove_trailing_whitespace_on_save disabled
 947        cx.update(|cx| {
 948            SettingsStore::update_global(cx, |store, cx| {
 949                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
 950                    cx,
 951                    |settings| {
 952                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
 953                    },
 954                );
 955            });
 956        });
 957
 958        // Stream edits again with trailing whitespace
 959        let edit_result = {
 960            let edit_task = cx.update(|cx| {
 961                let input = EditFileToolInput {
 962                    display_description: "Update main function".into(),
 963                    path: "root/src/main.rs".into(),
 964                    mode: EditFileMode::Overwrite,
 965                };
 966                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
 967                    input,
 968                    ToolCallEventStream::test().0,
 969                    cx,
 970                )
 971            });
 972
 973            // Stream the content with trailing whitespace
 974            cx.executor().run_until_parked();
 975            model.send_last_completion_stream_text_chunk(
 976                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
 977            );
 978            model.end_last_completion_stream();
 979
 980            edit_task.await
 981        };
 982        assert!(edit_result.is_ok());
 983
 984        // Wait for any async operations (e.g. formatting) to complete
 985        cx.executor().run_until_parked();
 986
 987        // Verify the file still has trailing whitespace
 988        // Read the file again - it should still have trailing whitespace
 989        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
 990        assert_eq!(
 991            // Ignore carriage returns on Windows
 992            final_content.replace("\r\n", "\n"),
 993            CONTENT_WITH_TRAILING_WHITESPACE,
 994            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
 995        );
 996    }
 997
 998    #[gpui::test]
 999    async fn test_authorize(cx: &mut TestAppContext) {
1000        init_test(cx);
1001        let fs = project::FakeFs::new(cx.executor());
1002        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1003        let context_server_registry =
1004            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1005        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1006        let model = Arc::new(FakeLanguageModel::default());
1007        let thread = cx.new(|cx| {
1008            Thread::new(
1009                project,
1010                cx.new(|_cx| ProjectContext::default()),
1011                context_server_registry,
1012                Templates::new(),
1013                Some(model.clone()),
1014                cx,
1015            )
1016        });
1017        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1018        fs.insert_tree("/root", json!({})).await;
1019
1020        // Test 1: Path with .zed component should require confirmation
1021        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1022        let _auth = cx.update(|cx| {
1023            tool.authorize(
1024                &EditFileToolInput {
1025                    display_description: "test 1".into(),
1026                    path: ".zed/settings.json".into(),
1027                    mode: EditFileMode::Edit,
1028                },
1029                &stream_tx,
1030                cx,
1031            )
1032        });
1033
1034        let event = stream_rx.expect_authorization().await;
1035        assert_eq!(
1036            event.tool_call.fields.title,
1037            Some("test 1 (local settings)".into())
1038        );
1039
1040        // Test 2: Path outside project should require confirmation
1041        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1042        let _auth = cx.update(|cx| {
1043            tool.authorize(
1044                &EditFileToolInput {
1045                    display_description: "test 2".into(),
1046                    path: "/etc/hosts".into(),
1047                    mode: EditFileMode::Edit,
1048                },
1049                &stream_tx,
1050                cx,
1051            )
1052        });
1053
1054        let event = stream_rx.expect_authorization().await;
1055        assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
1056
1057        // Test 3: Relative path without .zed should not require confirmation
1058        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1059        cx.update(|cx| {
1060            tool.authorize(
1061                &EditFileToolInput {
1062                    display_description: "test 3".into(),
1063                    path: "root/src/main.rs".into(),
1064                    mode: EditFileMode::Edit,
1065                },
1066                &stream_tx,
1067                cx,
1068            )
1069        })
1070        .await
1071        .unwrap();
1072        assert!(stream_rx.try_next().is_err());
1073
1074        // Test 4: Path with .zed in the middle should require confirmation
1075        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1076        let _auth = cx.update(|cx| {
1077            tool.authorize(
1078                &EditFileToolInput {
1079                    display_description: "test 4".into(),
1080                    path: "root/.zed/tasks.json".into(),
1081                    mode: EditFileMode::Edit,
1082                },
1083                &stream_tx,
1084                cx,
1085            )
1086        });
1087        let event = stream_rx.expect_authorization().await;
1088        assert_eq!(
1089            event.tool_call.fields.title,
1090            Some("test 4 (local settings)".into())
1091        );
1092
1093        // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
1094        cx.update(|cx| {
1095            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1096            settings.always_allow_tool_actions = true;
1097            agent_settings::AgentSettings::override_global(settings, cx);
1098        });
1099
1100        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1101        cx.update(|cx| {
1102            tool.authorize(
1103                &EditFileToolInput {
1104                    display_description: "test 5.1".into(),
1105                    path: ".zed/settings.json".into(),
1106                    mode: EditFileMode::Edit,
1107                },
1108                &stream_tx,
1109                cx,
1110            )
1111        })
1112        .await
1113        .unwrap();
1114        assert!(stream_rx.try_next().is_err());
1115
1116        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1117        cx.update(|cx| {
1118            tool.authorize(
1119                &EditFileToolInput {
1120                    display_description: "test 5.2".into(),
1121                    path: "/etc/hosts".into(),
1122                    mode: EditFileMode::Edit,
1123                },
1124                &stream_tx,
1125                cx,
1126            )
1127        })
1128        .await
1129        .unwrap();
1130        assert!(stream_rx.try_next().is_err());
1131    }
1132
1133    #[gpui::test]
1134    async fn test_authorize_global_config(cx: &mut TestAppContext) {
1135        init_test(cx);
1136        let fs = project::FakeFs::new(cx.executor());
1137        fs.insert_tree("/project", json!({})).await;
1138        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1139        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1140        let context_server_registry =
1141            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1142        let model = Arc::new(FakeLanguageModel::default());
1143        let thread = cx.new(|cx| {
1144            Thread::new(
1145                project,
1146                cx.new(|_cx| ProjectContext::default()),
1147                context_server_registry,
1148                Templates::new(),
1149                Some(model.clone()),
1150                cx,
1151            )
1152        });
1153        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1154
1155        // Test global config paths - these should require confirmation if they exist and are outside the project
1156        let test_cases = vec![
1157            (
1158                "/etc/hosts",
1159                true,
1160                "System file should require confirmation",
1161            ),
1162            (
1163                "/usr/local/bin/script",
1164                true,
1165                "System bin file should require confirmation",
1166            ),
1167            (
1168                "project/normal_file.rs",
1169                false,
1170                "Normal project file should not require confirmation",
1171            ),
1172        ];
1173
1174        for (path, should_confirm, description) in test_cases {
1175            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1176            let auth = cx.update(|cx| {
1177                tool.authorize(
1178                    &EditFileToolInput {
1179                        display_description: "Edit file".into(),
1180                        path: path.into(),
1181                        mode: EditFileMode::Edit,
1182                    },
1183                    &stream_tx,
1184                    cx,
1185                )
1186            });
1187
1188            if should_confirm {
1189                stream_rx.expect_authorization().await;
1190            } else {
1191                auth.await.unwrap();
1192                assert!(
1193                    stream_rx.try_next().is_err(),
1194                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1195                    description,
1196                    path
1197                );
1198            }
1199        }
1200    }
1201
1202    #[gpui::test]
1203    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
1204        init_test(cx);
1205        let fs = project::FakeFs::new(cx.executor());
1206
1207        // Create multiple worktree directories
1208        fs.insert_tree(
1209            "/workspace/frontend",
1210            json!({
1211                "src": {
1212                    "main.js": "console.log('frontend');"
1213                }
1214            }),
1215        )
1216        .await;
1217        fs.insert_tree(
1218            "/workspace/backend",
1219            json!({
1220                "src": {
1221                    "main.rs": "fn main() {}"
1222                }
1223            }),
1224        )
1225        .await;
1226        fs.insert_tree(
1227            "/workspace/shared",
1228            json!({
1229                ".zed": {
1230                    "settings.json": "{}"
1231                }
1232            }),
1233        )
1234        .await;
1235
1236        // Create project with multiple worktrees
1237        let project = Project::test(
1238            fs.clone(),
1239            [
1240                path!("/workspace/frontend").as_ref(),
1241                path!("/workspace/backend").as_ref(),
1242                path!("/workspace/shared").as_ref(),
1243            ],
1244            cx,
1245        )
1246        .await;
1247        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1248        let context_server_registry =
1249            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1250        let model = Arc::new(FakeLanguageModel::default());
1251        let thread = cx.new(|cx| {
1252            Thread::new(
1253                project.clone(),
1254                cx.new(|_cx| ProjectContext::default()),
1255                context_server_registry.clone(),
1256                Templates::new(),
1257                Some(model.clone()),
1258                cx,
1259            )
1260        });
1261        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1262
1263        // Test files in different worktrees
1264        let test_cases = vec![
1265            ("frontend/src/main.js", false, "File in first worktree"),
1266            ("backend/src/main.rs", false, "File in second worktree"),
1267            (
1268                "shared/.zed/settings.json",
1269                true,
1270                ".zed file in third worktree",
1271            ),
1272            ("/etc/hosts", true, "Absolute path outside all worktrees"),
1273            (
1274                "../outside/file.txt",
1275                true,
1276                "Relative path outside worktrees",
1277            ),
1278        ];
1279
1280        for (path, should_confirm, description) in test_cases {
1281            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1282            let auth = cx.update(|cx| {
1283                tool.authorize(
1284                    &EditFileToolInput {
1285                        display_description: "Edit file".into(),
1286                        path: path.into(),
1287                        mode: EditFileMode::Edit,
1288                    },
1289                    &stream_tx,
1290                    cx,
1291                )
1292            });
1293
1294            if should_confirm {
1295                stream_rx.expect_authorization().await;
1296            } else {
1297                auth.await.unwrap();
1298                assert!(
1299                    stream_rx.try_next().is_err(),
1300                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1301                    description,
1302                    path
1303                );
1304            }
1305        }
1306    }
1307
1308    #[gpui::test]
1309    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
1310        init_test(cx);
1311        let fs = project::FakeFs::new(cx.executor());
1312        fs.insert_tree(
1313            "/project",
1314            json!({
1315                ".zed": {
1316                    "settings.json": "{}"
1317                },
1318                "src": {
1319                    ".zed": {
1320                        "local.json": "{}"
1321                    }
1322                }
1323            }),
1324        )
1325        .await;
1326        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1327        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1328        let context_server_registry =
1329            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1330        let model = Arc::new(FakeLanguageModel::default());
1331        let thread = cx.new(|cx| {
1332            Thread::new(
1333                project.clone(),
1334                cx.new(|_cx| ProjectContext::default()),
1335                context_server_registry.clone(),
1336                Templates::new(),
1337                Some(model.clone()),
1338                cx,
1339            )
1340        });
1341        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1342
1343        // Test edge cases
1344        let test_cases = vec![
1345            // Empty path - find_project_path returns Some for empty paths
1346            ("", false, "Empty path is treated as project root"),
1347            // Root directory
1348            ("/", true, "Root directory should be outside project"),
1349            // Parent directory references - find_project_path resolves these
1350            (
1351                "project/../other",
1352                false,
1353                "Path with .. is resolved by find_project_path",
1354            ),
1355            (
1356                "project/./src/file.rs",
1357                false,
1358                "Path with . should work normally",
1359            ),
1360            // Windows-style paths (if on Windows)
1361            #[cfg(target_os = "windows")]
1362            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
1363            #[cfg(target_os = "windows")]
1364            ("project\\src\\main.rs", false, "Windows-style project path"),
1365        ];
1366
1367        for (path, should_confirm, description) in test_cases {
1368            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1369            let auth = cx.update(|cx| {
1370                tool.authorize(
1371                    &EditFileToolInput {
1372                        display_description: "Edit file".into(),
1373                        path: path.into(),
1374                        mode: EditFileMode::Edit,
1375                    },
1376                    &stream_tx,
1377                    cx,
1378                )
1379            });
1380
1381            if should_confirm {
1382                stream_rx.expect_authorization().await;
1383            } else {
1384                auth.await.unwrap();
1385                assert!(
1386                    stream_rx.try_next().is_err(),
1387                    "Failed for case: {} - path: {} - expected no confirmation but got one",
1388                    description,
1389                    path
1390                );
1391            }
1392        }
1393    }
1394
1395    #[gpui::test]
1396    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
1397        init_test(cx);
1398        let fs = project::FakeFs::new(cx.executor());
1399        fs.insert_tree(
1400            "/project",
1401            json!({
1402                "existing.txt": "content",
1403                ".zed": {
1404                    "settings.json": "{}"
1405                }
1406            }),
1407        )
1408        .await;
1409        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1410        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1411        let context_server_registry =
1412            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1413        let model = Arc::new(FakeLanguageModel::default());
1414        let thread = cx.new(|cx| {
1415            Thread::new(
1416                project.clone(),
1417                cx.new(|_cx| ProjectContext::default()),
1418                context_server_registry.clone(),
1419                Templates::new(),
1420                Some(model.clone()),
1421                cx,
1422            )
1423        });
1424        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1425
1426        // Test different EditFileMode values
1427        let modes = vec![
1428            EditFileMode::Edit,
1429            EditFileMode::Create,
1430            EditFileMode::Overwrite,
1431        ];
1432
1433        for mode in modes {
1434            // Test .zed path with different modes
1435            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1436            let _auth = cx.update(|cx| {
1437                tool.authorize(
1438                    &EditFileToolInput {
1439                        display_description: "Edit settings".into(),
1440                        path: "project/.zed/settings.json".into(),
1441                        mode: mode.clone(),
1442                    },
1443                    &stream_tx,
1444                    cx,
1445                )
1446            });
1447
1448            stream_rx.expect_authorization().await;
1449
1450            // Test outside path with different modes
1451            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1452            let _auth = cx.update(|cx| {
1453                tool.authorize(
1454                    &EditFileToolInput {
1455                        display_description: "Edit file".into(),
1456                        path: "/outside/file.txt".into(),
1457                        mode: mode.clone(),
1458                    },
1459                    &stream_tx,
1460                    cx,
1461                )
1462            });
1463
1464            stream_rx.expect_authorization().await;
1465
1466            // Test normal path with different modes
1467            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1468            cx.update(|cx| {
1469                tool.authorize(
1470                    &EditFileToolInput {
1471                        display_description: "Edit file".into(),
1472                        path: "project/normal.txt".into(),
1473                        mode: mode.clone(),
1474                    },
1475                    &stream_tx,
1476                    cx,
1477                )
1478            })
1479            .await
1480            .unwrap();
1481            assert!(stream_rx.try_next().is_err());
1482        }
1483    }
1484
1485    #[gpui::test]
1486    async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
1487        init_test(cx);
1488        let fs = project::FakeFs::new(cx.executor());
1489        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1490        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1491        let context_server_registry =
1492            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1493        let model = Arc::new(FakeLanguageModel::default());
1494        let thread = cx.new(|cx| {
1495            Thread::new(
1496                project.clone(),
1497                cx.new(|_cx| ProjectContext::default()),
1498                context_server_registry,
1499                Templates::new(),
1500                Some(model.clone()),
1501                cx,
1502            )
1503        });
1504        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
1505
1506        assert_eq!(
1507            tool.initial_title(Err(json!({
1508                "path": "src/main.rs",
1509                "display_description": "",
1510                "old_string": "old code",
1511                "new_string": "new code"
1512            }))),
1513            "src/main.rs"
1514        );
1515        assert_eq!(
1516            tool.initial_title(Err(json!({
1517                "path": "",
1518                "display_description": "Fix error handling",
1519                "old_string": "old code",
1520                "new_string": "new code"
1521            }))),
1522            "Fix error handling"
1523        );
1524        assert_eq!(
1525            tool.initial_title(Err(json!({
1526                "path": "src/main.rs",
1527                "display_description": "Fix error handling",
1528                "old_string": "old code",
1529                "new_string": "new code"
1530            }))),
1531            "Fix error handling"
1532        );
1533        assert_eq!(
1534            tool.initial_title(Err(json!({
1535                "path": "",
1536                "display_description": "",
1537                "old_string": "old code",
1538                "new_string": "new code"
1539            }))),
1540            DEFAULT_UI_TEXT
1541        );
1542        assert_eq!(
1543            tool.initial_title(Err(serde_json::Value::Null)),
1544            DEFAULT_UI_TEXT
1545        );
1546    }
1547
1548    fn init_test(cx: &mut TestAppContext) {
1549        cx.update(|cx| {
1550            let settings_store = SettingsStore::test(cx);
1551            cx.set_global(settings_store);
1552            language::init(cx);
1553            TelemetrySettings::register(cx);
1554            agent_settings::AgentSettings::register(cx);
1555            Project::init_settings(cx);
1556        });
1557    }
1558}