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