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