edit_file_tool.rs

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