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