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