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