Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:36:16.079Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

write_file_tool.rs

1467 lines · 55.0 KB · rust
1use super::edit_session::{
2    EditSession, EditSessionContext, EditSessionMode, EditSessionOutput, EditSessionResult,
3    initial_title_from_partial_path, run_session,
4};
5use crate::{AgentTool, Thread, ToolCallEventStream, ToolInput, ToolInputPayload};
6use action_log::ActionLog;
7use agent_client_protocol::schema::v1 as acp;
8use futures::FutureExt as _;
9use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
10use language::LanguageRegistry;
11use project::Project;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::path::PathBuf;
15use std::sync::Arc;
16use ui::SharedString;
17
18const DEFAULT_UI_TEXT: &str = "Writing file";
19
20/// This is a tool for creating a new file or overwriting an existing file with completely new contents.
21///
22/// To make granular edits to an existing file, prefer the `edit_file` tool instead.
23///
24/// Before using this tool, verify the directory path is correct (only applicable when creating new files). Use the `list_directory` tool to verify the parent directory exists and is the correct location
25///
26/// The only supported path outside the project is `~/.agents/skills` or a descendant, for global agent skills.
27#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
28pub struct WriteFileToolInput {
29    /// The full path of the file to create or overwrite in the project.
30    ///
31    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories, unless it's a global agent skill under `~/.agents/skills`.
32    ///
33    /// The following examples assume we have two root directories in the project:
34    /// - /a/b/backend
35    /// - /c/d/frontend
36    ///
37    /// <example>
38    /// `backend/src/main.rs`
39    ///
40    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
41    /// </example>
42    ///
43    /// <example>
44    /// `frontend/db.js`
45    /// </example>
46    ///
47    /// <example>
48    /// To create or overwrite a global agent skill file, you may provide a path under `~/.agents/skills`, such as `~/.agents/skills/my-skill/SKILL.md`.
49    /// </example>
50    pub path: PathBuf,
51
52    /// The entire content for the file.
53    pub content: String,
54}
55
56#[derive(Clone, Default, Debug, Deserialize)]
57struct WriteFileToolPartialInput {
58    #[serde(default)]
59    path: Option<String>,
60    #[serde(default)]
61    content: Option<String>,
62}
63
64pub struct WriteFileTool {
65    session_context: Arc<EditSessionContext>,
66}
67
68impl WriteFileTool {
69    pub fn new(
70        project: Entity<Project>,
71        thread: WeakEntity<Thread>,
72        action_log: Entity<ActionLog>,
73        language_registry: Arc<LanguageRegistry>,
74    ) -> Self {
75        Self {
76            session_context: Arc::new(EditSessionContext::new(
77                project,
78                thread,
79                action_log,
80                language_registry,
81            )),
82        }
83    }
84
85    async fn process_streaming_writes(
86        &self,
87        input: &mut ToolInput<WriteFileToolInput>,
88        event_stream: &ToolCallEventStream,
89        cx: &mut AsyncApp,
90    ) -> EditSessionResult {
91        let mut session: Option<EditSession> = None;
92        let mut last_path: Option<String> = None;
93
94        loop {
95            futures::select! {
96                payload = input.next().fuse() => {
97                    match payload {
98                        Ok(payload) => match payload {
99                            ToolInputPayload::Partial(partial) => {
100                                if let Ok(parsed) = serde_json::from_value::<WriteFileToolPartialInput>(partial) {
101                                    let path_complete = parsed.path.is_some()
102                                        && parsed.path.as_ref() == last_path.as_ref();
103
104                                    last_path = parsed.path.clone();
105
106                                    if session.is_none()
107                                        && path_complete
108                                        && let Some(path) = parsed.path.as_ref()
109                                    {
110                                        match EditSession::new(
111                                            PathBuf::from(path),
112                                            EditSessionMode::Write,
113                                            Self::NAME,
114                                            self.session_context.clone(),
115                                            event_stream,
116                                            cx,
117                                        )
118                                        .await
119                                        {
120                                            Ok(created_session) => session = Some(created_session),
121                                            Err(error) => {
122                                                log::error!("Failed to create edit session: {}", error);
123                                                return EditSessionResult::Failed {
124                                                    error,
125                                                    session: None,
126                                                };
127                                            }
128                                        }
129                                    }
130
131                                    if let Some(current_session) = &mut session
132                                        && let Err(error) = current_session.process_write(parsed.content.as_deref(), cx)
133                                    {
134                                        log::error!("Failed to process write: {}", error);
135                                        return EditSessionResult::Failed { error, session };
136                                    }
137                                }
138                            }
139                            ToolInputPayload::Full(full_input) => {
140                                let mut session = if let Some(session) = session {
141                                    session
142                                } else {
143                                    match EditSession::new(
144                                        full_input.path.clone(),
145                                        EditSessionMode::Write,
146                                        Self::NAME,
147                                        self.session_context.clone(),
148                                        event_stream,
149                                        cx,
150                                    )
151                                    .await
152                                    {
153                                        Ok(created_session) => created_session,
154                                        Err(error) => {
155                                            log::error!("Failed to create edit session: {}", error);
156                                            return EditSessionResult::Failed {
157                                                error,
158                                                session: None,
159                                            };
160                                        }
161                                    }
162                                };
163
164                                return match session.finalize_write(&full_input.content, cx).await {
165                                    Ok(()) => EditSessionResult::Completed(session),
166                                    Err(error) => {
167                                        log::error!("Failed to finalize write: {}", error);
168                                        EditSessionResult::Failed {
169                                            error,
170                                            session: Some(session),
171                                        }
172                                    }
173                                };
174                            }
175                            ToolInputPayload::InvalidJson { error_message } => {
176                                log::error!("Received invalid JSON: {error_message}");
177                                return EditSessionResult::Failed {
178                                    error: error_message,
179                                    session,
180                                };
181                            }
182                        },
183                        Err(error) => {
184                            return EditSessionResult::Failed {
185                                error: error.to_string(),
186                                session,
187                            };
188                        }
189                    }
190                }
191                _ = event_stream.cancelled_by_user().fuse() => {
192                    return EditSessionResult::Failed {
193                        error: "Write cancelled by user".to_string(),
194                        session,
195                    };
196                }
197            }
198        }
199    }
200}
201
202impl AgentTool for WriteFileTool {
203    type Input = WriteFileToolInput;
204    type Output = EditSessionOutput;
205
206    const NAME: &'static str = "write_file";
207
208    fn supports_input_streaming() -> bool {
209        true
210    }
211
212    fn kind() -> acp::ToolKind {
213        acp::ToolKind::Edit
214    }
215
216    fn initial_title(
217        &self,
218        input: Result<Self::Input, serde_json::Value>,
219        cx: &mut App,
220    ) -> SharedString {
221        match input {
222            Ok(input) => {
223                self.session_context
224                    .initial_title_from_path(&input.path, DEFAULT_UI_TEXT, cx)
225            }
226            Err(raw_input) => initial_title_from_partial_path::<WriteFileToolPartialInput>(
227                &self.session_context,
228                raw_input,
229                |partial| partial.path.clone(),
230                DEFAULT_UI_TEXT,
231                cx,
232            ),
233        }
234    }
235
236    fn run(
237        self: Arc<Self>,
238        mut input: ToolInput<Self::Input>,
239        event_stream: ToolCallEventStream,
240        cx: &mut App,
241    ) -> Task<Result<Self::Output, Self::Output>> {
242        cx.spawn(async move |cx: &mut AsyncApp| {
243            run_session(
244                self.process_streaming_writes(&mut input, &event_stream, cx)
245                    .await,
246                &event_stream,
247                cx,
248            )
249            .await
250        })
251    }
252
253    fn replay(
254        &self,
255        _input: Self::Input,
256        output: Self::Output,
257        event_stream: ToolCallEventStream,
258        cx: &mut App,
259    ) -> anyhow::Result<()> {
260        self.session_context.replay_output(output, event_stream, cx)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::{
268        AgentTool, ContextServerRegistry, Templates, Thread, ToolCallEventStream, ToolInput,
269        ToolInputSender,
270    };
271    use acp_thread::Diff;
272    use action_log::ActionLog;
273    use fs::Fs as _;
274    use futures::StreamExt as _;
275    use gpui::{AppContext as _, Entity, TestAppContext, UpdateGlobal};
276    use language::language_settings::FormatOnSave;
277    use language_model::fake_provider::FakeLanguageModel;
278    use project::{Project, ProjectPath};
279    use prompt_store::ProjectContext;
280    use serde_json::json;
281    use settings::{Settings, SettingsStore};
282    use std::{path::PathBuf, sync::Arc};
283    use util::path;
284    use util::rel_path::{RelPath, rel_path};
285
286    #[gpui::test]
287    async fn test_streaming_write_create_file(cx: &mut TestAppContext) {
288        let (write_tool, _project, _action_log, _fs, _thread) =
289            setup_test(cx, json!({"dir": {}})).await;
290        let result = cx
291            .update(|cx| {
292                write_tool.clone().run(
293                    ToolInput::resolved(WriteFileToolInput {
294                        path: "root/dir/new_file.txt".into(),
295                        content: "Hello, World!".into(),
296                    }),
297                    ToolCallEventStream::test().0,
298                    cx,
299                )
300            })
301            .await;
302
303        let EditSessionOutput::Success { new_text, diff, .. } = result.unwrap() else {
304            panic!("expected success");
305        };
306        assert_eq!(new_text, "Hello, World!");
307        assert!(!diff.is_empty());
308    }
309
310    #[gpui::test]
311    async fn test_streaming_write_overwrite_file(cx: &mut TestAppContext) {
312        let (write_tool, _project, _action_log, _fs, _thread) =
313            setup_test(cx, json!({"file.txt": "old content"})).await;
314        let result = cx
315            .update(|cx| {
316                write_tool.clone().run(
317                    ToolInput::resolved(WriteFileToolInput {
318                        path: "root/file.txt".into(),
319                        content: "new content".into(),
320                    }),
321                    ToolCallEventStream::test().0,
322                    cx,
323                )
324            })
325            .await;
326
327        let EditSessionOutput::Success {
328            new_text, old_text, ..
329        } = result.unwrap()
330        else {
331            panic!("expected success");
332        };
333        assert_eq!(new_text, "new content");
334        assert_eq!(*old_text, "old content");
335    }
336
337    #[gpui::test]
338    async fn test_streaming_write_global_skill_file(cx: &mut TestAppContext) {
339        init_test(cx);
340
341        let fs = project::FakeFs::new(cx.executor());
342        fs.insert_tree(path!("/root"), json!({})).await;
343        let skill_dir = agent_skills::global_skills_dir().join("my-skill");
344        fs.insert_tree(&skill_dir, json!({})).await;
345        let (write_tool, _project, _action_log, fs, _thread) =
346            setup_test_with_fs(cx, fs, &[path!("/root").as_ref()]).await;
347
348        let input_path = PathBuf::from("~")
349            .join(".agents")
350            .join("skills")
351            .join("my-skill")
352            .join("SKILL.md");
353        let skill_file = agent_skills::global_skills_dir()
354            .join("my-skill")
355            .join("SKILL.md");
356
357        let (event_stream, mut event_rx) = ToolCallEventStream::test();
358        let task = cx.update(|cx| {
359            write_tool.clone().run(
360                ToolInput::resolved(WriteFileToolInput {
361                    path: input_path,
362                    content: "# My Skill\n".into(),
363                }),
364                event_stream,
365                cx,
366            )
367        });
368
369        event_rx.expect_update_fields().await;
370        let auth = event_rx.expect_authorization().await;
371        let title = auth.tool_call.fields.title.as_deref().unwrap_or("");
372        assert!(
373            title.contains("agent skills"),
374            "Authorization title should mention agent skills, got: {title}",
375        );
376        assert!(
377            auth.options
378                .first_option_of_kind(acp::PermissionOptionKind::AllowAlways)
379                .is_none(),
380            "agent skills prompt must not offer an \"Always allow\" option: {:?}",
381            auth.options,
382        );
383        auth.response
384            .send(acp_thread::SelectedPermissionOutcome::new(
385                acp::PermissionOptionId::new("allow"),
386                acp::PermissionOptionKind::AllowOnce,
387            ))
388            .expect("authorization response should send");
389
390        let EditSessionOutput::Success { new_text, .. } = task.await.unwrap() else {
391            panic!("expected success");
392        };
393        assert_eq!(new_text, "# My Skill\n");
394        assert_eq!(
395            fs.load(&skill_file).await.unwrap().replace("\r\n", "\n"),
396            "# My Skill\n"
397        );
398    }
399
400    #[gpui::test]
401    async fn test_streaming_path_completeness_heuristic(cx: &mut TestAppContext) {
402        let (write_tool, _project, _action_log, _fs, _thread) =
403            setup_test(cx, json!({"file.txt": "hello world"})).await;
404        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
405        let (event_stream, _receiver) = ToolCallEventStream::test();
406        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
407
408        // Send partial with path but NO mode — path should NOT be treated as complete
409        sender.send_partial(json!({
410            "path": "root/file"
411        }));
412        cx.run_until_parked();
413
414        // Now the path grows and mode appears
415        sender.send_partial(json!({
416            "path": "root/file.txt",
417        }));
418        cx.run_until_parked();
419
420        // Send final
421        sender.send_full(json!({
422            "path": "root/file.txt",
423            "content": "new content"
424        }));
425
426        let result = task.await;
427        let EditSessionOutput::Success { new_text, .. } = result.unwrap() else {
428            panic!("expected success");
429        };
430        assert_eq!(new_text, "new content");
431    }
432
433    #[gpui::test]
434    async fn test_streaming_create_file_with_partials(cx: &mut TestAppContext) {
435        let (write_tool, _project, _action_log, _fs, _thread) =
436            setup_test(cx, json!({"dir": {}})).await;
437        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
438        let (event_stream, _receiver) = ToolCallEventStream::test();
439        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
440
441        // Stream partials for create mode
442        sender.send_partial(json!({}));
443        cx.run_until_parked();
444
445        sender.send_partial(json!({
446            "path": "root/dir/new_file.txt",
447        }));
448        cx.run_until_parked();
449
450        sender.send_partial(json!({
451            "path": "root/dir/new_file.txt",
452            "content": "Hello, "
453        }));
454        cx.run_until_parked();
455
456        // Final with full content
457        sender.send_full(json!({
458            "path": "root/dir/new_file.txt",
459            "content": "Hello, World!"
460        }));
461
462        let result = task.await;
463        let EditSessionOutput::Success { new_text, .. } = result.unwrap() else {
464            panic!("expected success");
465        };
466        assert_eq!(new_text, "Hello, World!");
467    }
468
469    #[gpui::test]
470    async fn test_streaming_input_recv_drains_partials(cx: &mut TestAppContext) {
471        let (write_tool, _project, _action_log, _fs, _thread) =
472            setup_test(cx, json!({"dir": {}})).await;
473        // Create a channel and send multiple partials before a final, then use
474        // ToolInput::resolved-style immediate delivery to confirm recv() works
475        // when partials are already buffered.
476        let (mut sender, input): (ToolInputSender, ToolInput<WriteFileToolInput>) =
477            ToolInput::test();
478        let (event_stream, _event_rx) = ToolCallEventStream::test();
479        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
480
481        // Buffer several partials before sending the final
482        sender.send_partial(json!({}));
483        sender.send_partial(json!({"path": "root/dir/new.txt"}));
484        sender.send_partial(json!({
485            "path": "root/dir/new.txt",
486        }));
487        sender.send_full(json!({
488            "path": "root/dir/new.txt",
489            "content": "streamed content"
490        }));
491
492        let result = task.await;
493        let EditSessionOutput::Success { new_text, .. } = result.unwrap() else {
494            panic!("expected success");
495        };
496        assert_eq!(new_text, "streamed content");
497    }
498
499    #[gpui::test]
500    async fn test_streaming_resolve_path_for_creating_file(cx: &mut TestAppContext) {
501        let mode = EditSessionMode::Write;
502
503        let result = test_resolve_path(&mode, "root/new.txt", cx);
504        assert_resolved_path_eq(result.await, rel_path("new.txt"));
505
506        let result = test_resolve_path(&mode, "new.txt", cx);
507        assert_resolved_path_eq(result.await, rel_path("new.txt"));
508
509        let result = test_resolve_path(&mode, "dir/new.txt", cx);
510        assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
511
512        let result = test_resolve_path(&mode, "root/dir/subdir/existing.txt", cx);
513        assert_resolved_path_eq(result.await, rel_path("dir/subdir/existing.txt"));
514
515        let result = test_resolve_path(&mode, "root/dir/subdir", cx);
516        assert_eq!(
517            result.await.unwrap_err(),
518            "Can't write to file: path is a directory"
519        );
520
521        let result = test_resolve_path(&mode, "root/dir/nonexistent_dir/new.txt", cx);
522        assert_eq!(
523            result.await.unwrap_err(),
524            "Can't create file: parent directory doesn't exist"
525        );
526    }
527
528    #[gpui::test]
529    async fn test_streaming_format_on_save(cx: &mut TestAppContext) {
530        init_test(cx);
531
532        let fs = project::FakeFs::new(cx.executor());
533        fs.insert_tree("/root", json!({"src": {}})).await;
534        let (write_tool, project, action_log, fs, thread) =
535            setup_test_with_fs(cx, fs, &[path!("/root").as_ref()]).await;
536
537        let rust_language = Arc::new(language::Language::new(
538            language::LanguageConfig {
539                name: "Rust".into(),
540                matcher: (language::LanguageMatcher {
541                    path_suffixes: vec!["rs".to_string()],
542                    ..Default::default()
543                })
544                .into(),
545                ..Default::default()
546            },
547            None,
548        ));
549
550        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
551        language_registry.add(rust_language);
552
553        let mut fake_language_servers = language_registry.register_fake_lsp(
554            "Rust",
555            language::FakeLspAdapter {
556                capabilities: lsp::ServerCapabilities {
557                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
558                    ..Default::default()
559                },
560                ..Default::default()
561            },
562        );
563
564        fs.save(
565            path!("/root/src/main.rs").as_ref(),
566            &"initial content".into(),
567            language::LineEnding::Unix,
568        )
569        .await
570        .unwrap();
571
572        // Open the buffer to trigger LSP initialization
573        let buffer = project
574            .update(cx, |project, cx| {
575                project.open_local_buffer(path!("/root/src/main.rs"), cx)
576            })
577            .await
578            .unwrap();
579
580        // Register the buffer with language servers
581        let _handle = project.update(cx, |project, cx| {
582            project.register_buffer_with_language_servers(&buffer, cx)
583        });
584
585        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\
586";
587        const FORMATTED_CONTENT: &str = "This file was formatted by the fake formatter in the test.\
588";
589
590        // Get the fake language server and set up formatting handler
591        let fake_language_server = fake_language_servers.next().await.unwrap();
592        fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
593            |_, _| async move {
594                Ok(Some(vec![lsp::TextEdit {
595                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
596                    new_text: FORMATTED_CONTENT.to_string(),
597                }]))
598            }
599        });
600
601        // Test with format_on_save enabled
602        cx.update(|cx| {
603            SettingsStore::update_global(cx, |store, cx| {
604                store.update_user_settings(cx, |settings| {
605                    settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
606                    settings.project.all_languages.defaults.formatter =
607                        Some(language::language_settings::FormatterList::default());
608                });
609            });
610        });
611
612        // Use streaming pattern so executor can pump the LSP request/response
613        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
614        let (event_stream, _receiver) = ToolCallEventStream::test();
615
616        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
617
618        sender.send_partial(json!({
619            "path": "root/src/main.rs",
620        }));
621        cx.run_until_parked();
622
623        sender.send_full(json!({
624            "path": "root/src/main.rs",
625            "content": UNFORMATTED_CONTENT
626        }));
627
628        let result = task.await;
629        assert!(result.is_ok());
630
631        cx.executor().run_until_parked();
632
633        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
634        assert_eq!(
635            new_content.replace("\r\n", "\n"),
636            FORMATTED_CONTENT,
637            "Code should be formatted when format_on_save is enabled"
638        );
639
640        let stale_buffer_count = thread
641            .read_with(cx, |thread, _cx| thread.action_log.clone())
642            .read_with(cx, |log, cx| log.stale_buffers(cx).count());
643
644        assert_eq!(
645            stale_buffer_count, 0,
646            "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers.",
647            stale_buffer_count
648        );
649
650        // Test with format_on_save disabled
651        cx.update(|cx| {
652            SettingsStore::update_global(cx, |store, cx| {
653                store.update_user_settings(cx, |settings| {
654                    settings.project.all_languages.defaults.format_on_save =
655                        Some(FormatOnSave::Off);
656                });
657            });
658        });
659
660        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
661        let (event_stream, _receiver) = ToolCallEventStream::test();
662
663        let tool2 = Arc::new(WriteFileTool::new(
664            project.clone(),
665            thread.downgrade(),
666            action_log.clone(),
667            language_registry,
668        ));
669
670        let task = cx.update(|cx| tool2.run(input, event_stream, cx));
671
672        sender.send_partial(json!({
673            "path": "root/src/main.rs",
674        }));
675        cx.run_until_parked();
676
677        sender.send_full(json!({
678            "path": "root/src/main.rs",
679            "content": UNFORMATTED_CONTENT
680        }));
681
682        let result = task.await;
683        assert!(result.is_ok());
684
685        cx.executor().run_until_parked();
686
687        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
688        assert_eq!(
689            new_content.replace("\r\n", "\n"),
690            UNFORMATTED_CONTENT,
691            "Code should not be formatted when format_on_save is disabled"
692        );
693    }
694
695    #[gpui::test]
696    async fn test_streaming_remove_trailing_whitespace(cx: &mut TestAppContext) {
697        init_test(cx);
698
699        let fs = project::FakeFs::new(cx.executor());
700        fs.insert_tree("/root", json!({"src": {}})).await;
701        fs.save(
702            path!("/root/src/main.rs").as_ref(),
703            &"initial content".into(),
704            language::LineEnding::Unix,
705        )
706        .await
707        .unwrap();
708        let (write_tool, project, action_log, fs, thread) =
709            setup_test_with_fs(cx, fs, &[path!("/root").as_ref()]).await;
710        let language_registry = project.read_with(cx, |p, _cx| p.languages().clone());
711
712        // Test with remove_trailing_whitespace_on_save enabled
713        cx.update(|cx| {
714            SettingsStore::update_global(cx, |store, cx| {
715                store.update_user_settings(cx, |settings| {
716                    settings
717                        .project
718                        .all_languages
719                        .defaults
720                        .remove_trailing_whitespace_on_save = Some(true);
721                });
722            });
723        });
724
725        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
726            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
727
728        let result = cx
729            .update(|cx| {
730                write_tool.clone().run(
731                    ToolInput::resolved(WriteFileToolInput {
732                        path: "root/src/main.rs".into(),
733                        content: CONTENT_WITH_TRAILING_WHITESPACE.into(),
734                    }),
735                    ToolCallEventStream::test().0,
736                    cx,
737                )
738            })
739            .await;
740        assert!(result.is_ok());
741
742        cx.executor().run_until_parked();
743
744        assert_eq!(
745            fs.load(path!("/root/src/main.rs").as_ref())
746                .await
747                .unwrap()
748                .replace("\r\n", "\n"),
749            "fn main() {\n    println!(\"Hello!\");\n}\n",
750            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
751        );
752
753        // Test with remove_trailing_whitespace_on_save disabled
754        cx.update(|cx| {
755            SettingsStore::update_global(cx, |store, cx| {
756                store.update_user_settings(cx, |settings| {
757                    settings
758                        .project
759                        .all_languages
760                        .defaults
761                        .remove_trailing_whitespace_on_save = Some(false);
762                });
763            });
764        });
765
766        let tool2 = Arc::new(WriteFileTool::new(
767            project.clone(),
768            thread.downgrade(),
769            action_log.clone(),
770            language_registry,
771        ));
772
773        let result = cx
774            .update(|cx| {
775                tool2.run(
776                    ToolInput::resolved(WriteFileToolInput {
777                        path: "root/src/main.rs".into(),
778                        content: CONTENT_WITH_TRAILING_WHITESPACE.into(),
779                    }),
780                    ToolCallEventStream::test().0,
781                    cx,
782                )
783            })
784            .await;
785        assert!(result.is_ok());
786
787        cx.executor().run_until_parked();
788
789        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
790        assert_eq!(
791            final_content.replace("\r\n", "\n"),
792            CONTENT_WITH_TRAILING_WHITESPACE,
793            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
794        );
795    }
796
797    #[gpui::test]
798    async fn test_streaming_diff_finalization(cx: &mut TestAppContext) {
799        init_test(cx);
800        let fs = project::FakeFs::new(cx.executor());
801        fs.insert_tree("/", json!({"main.rs": ""})).await;
802        let (write_tool, project, action_log, _fs, thread) =
803            setup_test_with_fs(cx, fs, &[path!("/").as_ref()]).await;
804        let language_registry = project.read_with(cx, |p, _cx| p.languages().clone());
805
806        // Ensure the diff is finalized after the edit completes.
807        {
808            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
809            let edit = cx.update(|cx| {
810                write_tool.clone().run(
811                    ToolInput::resolved(WriteFileToolInput {
812                        path: path!("/main.rs").into(),
813                        content: "new content".into(),
814                    }),
815                    stream_tx,
816                    cx,
817                )
818            });
819            stream_rx.expect_update_fields().await;
820            let diff = stream_rx.expect_diff().await;
821            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
822            cx.run_until_parked();
823            edit.await.unwrap();
824            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
825        }
826
827        // Ensure the diff is finalized if the tool call gets dropped.
828        {
829            let tool = Arc::new(WriteFileTool::new(
830                project.clone(),
831                thread.downgrade(),
832                action_log,
833                language_registry,
834            ));
835            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
836            let edit = cx.update(|cx| {
837                tool.run(
838                    ToolInput::resolved(WriteFileToolInput {
839                        path: path!("/main.rs").into(),
840                        content: "dropped content".into(),
841                    }),
842                    stream_tx,
843                    cx,
844                )
845            });
846            stream_rx.expect_update_fields().await;
847            let diff = stream_rx.expect_diff().await;
848            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
849            drop(edit);
850            cx.run_until_parked();
851            diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
852        }
853    }
854
855    #[gpui::test]
856    async fn test_streaming_create_content_streamed(cx: &mut TestAppContext) {
857        let (write_tool, project, _action_log, _fs, _thread) =
858            setup_test(cx, json!({"dir": {}})).await;
859        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
860        let (event_stream, _receiver) = ToolCallEventStream::test();
861        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
862
863        // Transition to BufferResolved
864        sender.send_partial(json!({
865            "path": "root/dir/new_file.txt",
866        }));
867        cx.run_until_parked();
868
869        // Stream content incrementally
870        sender.send_partial(json!({
871            "path": "root/dir/new_file.txt",
872            "content": "line 1\n"
873        }));
874        cx.run_until_parked();
875
876        // Verify buffer has partial content
877        let buffer = project.update(cx, |project, cx| {
878            let path = project
879                .find_project_path("root/dir/new_file.txt", cx)
880                .unwrap();
881            project.get_open_buffer(&path, cx).unwrap()
882        });
883        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\n");
884
885        // Stream more content
886        sender.send_partial(json!({
887            "path": "root/dir/new_file.txt",
888            "content": "line 1\nline 2\n"
889        }));
890        cx.run_until_parked();
891        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "line 1\nline 2\n");
892
893        // Stream final chunk
894        sender.send_partial(json!({
895            "path": "root/dir/new_file.txt",
896            "content": "line 1\nline 2\nline 3\n"
897        }));
898        cx.run_until_parked();
899        assert_eq!(
900            buffer.read_with(cx, |b, _| b.text()),
901            "line 1\nline 2\nline 3\n"
902        );
903
904        // Send final input
905        sender.send_full(json!({
906            "path": "root/dir/new_file.txt",
907            "content": "line 1\nline 2\nline 3\n"
908        }));
909
910        let result = task.await;
911        let EditSessionOutput::Success { new_text, .. } = result.unwrap() else {
912            panic!("expected success");
913        };
914        assert_eq!(new_text, "line 1\nline 2\nline 3\n");
915    }
916
917    #[gpui::test]
918    async fn test_streaming_overwrite_diff_revealed_during_streaming(cx: &mut TestAppContext) {
919        let (write_tool, _project, _action_log, _fs, _thread) = setup_test(
920            cx,
921            json!({"file.txt": "old line 1\nold line 2\nold line 3\n"}),
922        )
923        .await;
924        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
925        let (event_stream, mut receiver) = ToolCallEventStream::test();
926        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
927
928        // Transition to BufferResolved
929        sender.send_partial(json!({
930            "path": "root/file.txt",
931        }));
932        cx.run_until_parked();
933
934        sender.send_partial(json!({
935            "path": "root/file.txt",
936        }));
937        cx.run_until_parked();
938
939        // Get the diff entity from the event stream
940        receiver.expect_update_fields().await;
941        let diff = receiver.expect_diff().await;
942
943        // Diff starts pending with no revealed ranges
944        diff.read_with(cx, |diff, cx| {
945            assert!(matches!(diff, Diff::Pending(_)));
946            assert!(!diff.has_revealed_range(cx));
947        });
948
949        // Stream first content chunk
950        sender.send_partial(json!({
951            "path": "root/file.txt",
952            "content": "new line 1\n"
953        }));
954        cx.run_until_parked();
955
956        // Diff should now have revealed ranges showing the new content
957        diff.read_with(cx, |diff, cx| {
958            assert!(diff.has_revealed_range(cx));
959        });
960
961        // Send final input
962        sender.send_full(json!({
963            "path": "root/file.txt",
964            "content": "new line 1\nnew line 2\n"
965        }));
966
967        let result = task.await;
968        let EditSessionOutput::Success {
969            new_text, old_text, ..
970        } = result.unwrap()
971        else {
972            panic!("expected success");
973        };
974        assert_eq!(new_text, "new line 1\nnew line 2\n");
975        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
976
977        // Diff is finalized after completion
978        diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
979    }
980
981    #[gpui::test]
982    async fn test_streaming_overwrite_content_streamed(cx: &mut TestAppContext) {
983        let (write_tool, project, _action_log, _fs, _thread) = setup_test(
984            cx,
985            json!({"file.txt": "old line 1\nold line 2\nold line 3\n"}),
986        )
987        .await;
988        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
989        let (event_stream, _receiver) = ToolCallEventStream::test();
990        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
991
992        // Transition to BufferResolved
993        sender.send_partial(json!({
994            "path": "root/file.txt",
995        }));
996        cx.run_until_parked();
997
998        // Verify buffer still has old content (no content partial yet)
999        let buffer = project.update(cx, |project, cx| {
1000            let path = project.find_project_path("root/file.txt", cx).unwrap();
1001            project.open_buffer(path, cx)
1002        });
1003        let buffer = buffer.await.unwrap();
1004        assert_eq!(
1005            buffer.read_with(cx, |b, _| b.text()),
1006            "old line 1\nold line 2\nold line 3\n"
1007        );
1008
1009        // First content partial replaces old content
1010        sender.send_partial(json!({
1011            "path": "root/file.txt",
1012            "content": "new line 1\n"
1013        }));
1014        cx.run_until_parked();
1015        assert_eq!(buffer.read_with(cx, |b, _| b.text()), "new line 1\n");
1016
1017        // Subsequent content partials append
1018        sender.send_partial(json!({
1019            "path": "root/file.txt",
1020            "content": "new line 1\nnew line 2\n"
1021        }));
1022        cx.run_until_parked();
1023        assert_eq!(
1024            buffer.read_with(cx, |b, _| b.text()),
1025            "new line 1\nnew line 2\n"
1026        );
1027
1028        // Send final input with complete content
1029        sender.send_full(json!({
1030            "path": "root/file.txt",
1031            "content": "new line 1\nnew line 2\nnew line 3\n"
1032        }));
1033
1034        let result = task.await;
1035        let EditSessionOutput::Success {
1036            new_text, old_text, ..
1037        } = result.unwrap()
1038        else {
1039            panic!("expected success");
1040        };
1041        assert_eq!(new_text, "new line 1\nnew line 2\nnew line 3\n");
1042        assert_eq!(*old_text, "old line 1\nold line 2\nold line 3\n");
1043    }
1044
1045    #[gpui::test]
1046    async fn test_streaming_write_file_tool_registers_changed_buffers(cx: &mut TestAppContext) {
1047        let (write_tool, _project, action_log, _fs, _thread) =
1048            setup_test(cx, json!({"file.txt": "original content"})).await;
1049        cx.update(|cx| {
1050            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1051            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
1052            agent_settings::AgentSettings::override_global(settings, cx);
1053        });
1054
1055        let (event_stream, _rx) = ToolCallEventStream::test();
1056        let task = cx.update(|cx| {
1057            write_tool.clone().run(
1058                ToolInput::resolved(WriteFileToolInput {
1059                    path: "root/file.txt".into(),
1060                    content: "completely new content".into(),
1061                }),
1062                event_stream,
1063                cx,
1064            )
1065        });
1066
1067        let result = task.await;
1068        assert!(result.is_ok(), "write should succeed: {:?}", result.err());
1069
1070        cx.run_until_parked();
1071
1072        let changed =
1073            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).collect::<Vec<_>>());
1074        assert!(
1075            !changed.is_empty(),
1076            "action_log.changed_buffers() should be non-empty after streaming write, \
1077             but no changed buffers were found \u{2014} Accept All / Reject All will not appear"
1078        );
1079    }
1080
1081    #[gpui::test]
1082    async fn test_streaming_write_file_tool_fields_out_of_order(cx: &mut TestAppContext) {
1083        let (write_tool, _project, _action_log, _fs, _thread) =
1084            setup_test(cx, json!({"file.txt": "old_content"})).await;
1085        let (mut sender, input) = ToolInput::<WriteFileToolInput>::test();
1086        let (event_stream, _receiver) = ToolCallEventStream::test();
1087        let task = cx.update(|cx| write_tool.clone().run(input, event_stream, cx));
1088
1089        sender.send_partial(json!({
1090            "content": "new_content"
1091        }));
1092        cx.run_until_parked();
1093
1094        sender.send_partial(json!({
1095            "content": "new_content",
1096            "path": "root"
1097        }));
1098        cx.run_until_parked();
1099
1100        // Send final.
1101        sender.send_full(json!({
1102            "content": "new_content",
1103            "path": "root/file.txt"
1104        }));
1105
1106        let result = task.await;
1107        let EditSessionOutput::Success { new_text, .. } = result.unwrap() else {
1108            panic!("expected success");
1109        };
1110        assert_eq!(new_text, "new_content");
1111    }
1112
1113    #[gpui::test]
1114    async fn test_streaming_reject_created_file_deletes_it(cx: &mut TestAppContext) {
1115        let (write_tool, _project, action_log, fs, _thread) =
1116            setup_test(cx, json!({"dir": {}})).await;
1117        cx.update(|cx| {
1118            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1119            settings.tool_permissions.default = settings::ToolPermissionMode::Allow;
1120            agent_settings::AgentSettings::override_global(settings, cx);
1121        });
1122
1123        // Create a new file via the streaming write file tool
1124        let (event_stream, _rx) = ToolCallEventStream::test();
1125        let task = cx.update(|cx| {
1126            write_tool.clone().run(
1127                ToolInput::resolved(WriteFileToolInput {
1128                    path: "root/dir/new_file.txt".into(),
1129                    content: "Hello, World!".into(),
1130                }),
1131                event_stream,
1132                cx,
1133            )
1134        });
1135        let result = task.await;
1136        assert!(result.is_ok(), "create should succeed: {:?}", result.err());
1137        cx.run_until_parked();
1138
1139        assert!(
1140            fs.is_file(path!("/root/dir/new_file.txt").as_ref()).await,
1141            "file should exist after creation"
1142        );
1143
1144        // Reject all edits — this should delete the newly created file
1145        let changed =
1146            action_log.read_with(cx, |log, cx| log.changed_buffers(cx).collect::<Vec<_>>());
1147        assert!(
1148            !changed.is_empty(),
1149            "action_log should track the created file as changed"
1150        );
1151
1152        action_log
1153            .update(cx, |log, cx| log.reject_all_edits(None, cx))
1154            .await;
1155        cx.run_until_parked();
1156
1157        assert!(
1158            !fs.is_file(path!("/root/dir/new_file.txt").as_ref()).await,
1159            "file should be deleted after rejecting creation, but an empty file was left behind"
1160        );
1161    }
1162
1163    /// When the buffer has unsaved user edits and the user picks
1164    /// "Discard my edits", the pending edits are reverted to match disk
1165    /// and the agent's overwrite proceeds.
1166    #[gpui::test]
1167    async fn test_streaming_write_dirty_buffer_discard(cx: &mut TestAppContext) {
1168        let (write_tool, project, _action_log, fs, _thread) =
1169            setup_test(cx, json!({"file.txt": "on disk content"})).await;
1170
1171        let project_path = project
1172            .read_with(cx, |project, cx| {
1173                project.find_project_path("root/file.txt", cx)
1174            })
1175            .expect("Should find project path");
1176        let buffer = project
1177            .update(cx, |project, cx| project.open_buffer(project_path, cx))
1178            .await
1179            .unwrap();
1180        buffer.update(cx, |buffer, cx| {
1181            let end_point = buffer.max_point();
1182            buffer.edit([(end_point..end_point, " plus user edit")], None, cx);
1183        });
1184        assert!(buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1185
1186        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1187        let task = cx.update(|cx| {
1188            write_tool.clone().run(
1189                ToolInput::resolved(WriteFileToolInput {
1190                    path: "root/file.txt".into(),
1191                    content: "agent overwrote it".into(),
1192                }),
1193                stream_tx,
1194                cx,
1195            )
1196        });
1197
1198        let _update = stream_rx.expect_update_fields().await;
1199        let auth = stream_rx.expect_authorization().await;
1200
1201        // Verify the prompt is the overwrite-mode prompt.
1202        let content = auth.tool_call.fields.content.as_deref().unwrap_or(&[]);
1203        let acp::ToolCallContent::Content(text) = content.first().expect("expected message body")
1204        else {
1205            panic!("expected text body, got: {:?}", content.first());
1206        };
1207        let acp::ContentBlock::Text(text) = &text.content else {
1208            panic!("expected text body, got: {:?}", text.content);
1209        };
1210        assert!(
1211            text.text.contains("overwrite"),
1212            "expected overwrite-mode prompt, got: {:?}",
1213            text.text,
1214        );
1215
1216        // Verify both option ids are present (option_id is the stable contract).
1217        let option_ids: Vec<&str> = match &auth.options {
1218            acp_thread::PermissionOptions::Flat(opts) => {
1219                opts.iter().map(|o| o.option_id.0.as_ref()).collect()
1220            }
1221            other => panic!("expected flat options, got: {other:?}"),
1222        };
1223        assert!(option_ids.contains(&"keep"), "options: {option_ids:?}");
1224        assert!(option_ids.contains(&"discard"), "options: {option_ids:?}");
1225
1226        auth.response
1227            .send(acp_thread::SelectedPermissionOutcome::new(
1228                acp::PermissionOptionId::new("discard"),
1229                acp::PermissionOptionKind::AllowOnce,
1230            ))
1231            .unwrap();
1232
1233        let EditSessionOutput::Success { new_text, .. } = task.await.unwrap() else {
1234            panic!("expected success");
1235        };
1236        assert_eq!(new_text, "agent overwrote it");
1237        assert!(!buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1238        let on_disk = fs.load(path!("/root/file.txt").as_ref()).await.unwrap();
1239        assert_eq!(on_disk, "agent overwrote it");
1240    }
1241
1242    /// When the buffer has unsaved user edits and the user picks
1243    /// "Keep my edits", the overwrite is cancelled with an error and the
1244    /// user's pending edits are preserved.
1245    #[gpui::test]
1246    async fn test_streaming_write_dirty_buffer_keep(cx: &mut TestAppContext) {
1247        let (write_tool, project, _action_log, fs, _thread) =
1248            setup_test(cx, json!({"file.txt": "on disk content"})).await;
1249
1250        let project_path = project
1251            .read_with(cx, |project, cx| {
1252                project.find_project_path("root/file.txt", cx)
1253            })
1254            .expect("Should find project path");
1255        let buffer = project
1256            .update(cx, |project, cx| project.open_buffer(project_path, cx))
1257            .await
1258            .unwrap();
1259        buffer.update(cx, |buffer, cx| {
1260            let end_point = buffer.max_point();
1261            buffer.edit([(end_point..end_point, " plus user edit")], None, cx);
1262        });
1263        assert!(buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1264
1265        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1266        let task = cx.update(|cx| {
1267            write_tool.clone().run(
1268                ToolInput::resolved(WriteFileToolInput {
1269                    path: "root/file.txt".into(),
1270                    content: "agent overwrote it".into(),
1271                }),
1272                stream_tx,
1273                cx,
1274            )
1275        });
1276
1277        let _update = stream_rx.expect_update_fields().await;
1278        let auth = stream_rx.expect_authorization().await;
1279        auth.response
1280            .send(acp_thread::SelectedPermissionOutcome::new(
1281                acp::PermissionOptionId::new("keep"),
1282                acp::PermissionOptionKind::RejectOnce,
1283            ))
1284            .unwrap();
1285
1286        let EditSessionOutput::Error { error, .. } = task.await.unwrap_err() else {
1287            panic!("expected error");
1288        };
1289        assert!(
1290            error.contains("keep") || error.contains("cancelled"),
1291            "expected cancel-style error message, got: {error:?}",
1292        );
1293
1294        // The user's in-memory edits are preserved.
1295        assert!(buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1296        let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
1297        assert_eq!(buffer_text, "on disk content plus user edit");
1298
1299        // The on-disk content is untouched.
1300        let on_disk = fs.load(path!("/root/file.txt").as_ref()).await.unwrap();
1301        assert_eq!(on_disk, "on disk content");
1302    }
1303
1304    /// When the user manually saves the buffer (e.g. cmd-s) while the
1305    /// overwrite prompt is visible, that's treated as "Keep my edits":
1306    /// the user just deliberately persisted their work, so we cancel the
1307    /// agent's overwrite to avoid clobbering it.
1308    #[gpui::test]
1309    async fn test_streaming_write_dirty_buffer_resolved_externally(cx: &mut TestAppContext) {
1310        let (write_tool, project, _action_log, fs, _thread) =
1311            setup_test(cx, json!({"file.txt": "on disk content"})).await;
1312
1313        let project_path = project
1314            .read_with(cx, |project, cx| {
1315                project.find_project_path("root/file.txt", cx)
1316            })
1317            .expect("Should find project path");
1318        let buffer = project
1319            .update(cx, |project, cx| project.open_buffer(project_path, cx))
1320            .await
1321            .unwrap();
1322        buffer.update(cx, |buffer, cx| {
1323            let end_point = buffer.max_point();
1324            buffer.edit([(end_point..end_point, " plus user edit")], None, cx);
1325        });
1326        assert!(buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1327
1328        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1329        let task = cx.update(|cx| {
1330            write_tool.clone().run(
1331                ToolInput::resolved(WriteFileToolInput {
1332                    path: "root/file.txt".into(),
1333                    content: "agent overwrote it".into(),
1334                }),
1335                stream_tx,
1336                cx,
1337            )
1338        });
1339
1340        let _update = stream_rx.expect_update_fields().await;
1341        let auth = stream_rx.expect_authorization().await;
1342
1343        // User saves manually while the prompt is up.
1344        project
1345            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1346            .await
1347            .unwrap();
1348
1349        // The prompt is dismissed by resolving the pending authorization.
1350        let (_, outcome) = stream_rx.expect_authorization_resolved().await;
1351        assert_eq!(outcome.option_id, acp::PermissionOptionId::new("keep"));
1352        assert_eq!(outcome.option_kind, acp::PermissionOptionKind::RejectOnce);
1353        drop(auth);
1354
1355        // The overwrite is cancelled with an error.
1356        let EditSessionOutput::Error { error, .. } = task.await.unwrap_err() else {
1357            panic!("expected error");
1358        };
1359        assert!(
1360            error.contains("saved") || error.contains("cancelled"),
1361            "expected cancel-on-manual-save error, got: {error:?}",
1362        );
1363
1364        // The user's edits were saved to disk and not clobbered.
1365        assert!(!buffer.read_with(cx, |buffer, _| buffer.is_dirty()));
1366        let on_disk = fs.load(path!("/root/file.txt").as_ref()).await.unwrap();
1367        assert_eq!(on_disk, "on disk content plus user edit");
1368    }
1369
1370    async fn setup_test_with_fs(
1371        cx: &mut TestAppContext,
1372        fs: Arc<project::FakeFs>,
1373        worktree_paths: &[&std::path::Path],
1374    ) -> (
1375        Arc<WriteFileTool>,
1376        Entity<Project>,
1377        Entity<ActionLog>,
1378        Arc<project::FakeFs>,
1379        Entity<Thread>,
1380    ) {
1381        let project = Project::test(fs.clone(), worktree_paths.iter().copied(), cx).await;
1382        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
1383        let context_server_registry =
1384            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1385        let model = Arc::new(FakeLanguageModel::default());
1386        let thread = cx.new(|cx| {
1387            crate::Thread::new(
1388                project.clone(),
1389                cx.new(|_cx| ProjectContext::default()),
1390                context_server_registry,
1391                Templates::new(),
1392                Some(model),
1393                cx,
1394            )
1395        });
1396        let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
1397        let write_tool = Arc::new(WriteFileTool::new(
1398            project.clone(),
1399            thread.downgrade(),
1400            action_log.clone(),
1401            language_registry,
1402        ));
1403        (write_tool, project, action_log, fs, thread)
1404    }
1405
1406    async fn setup_test(
1407        cx: &mut TestAppContext,
1408        initial_tree: serde_json::Value,
1409    ) -> (
1410        Arc<WriteFileTool>,
1411        Entity<Project>,
1412        Entity<ActionLog>,
1413        Arc<project::FakeFs>,
1414        Entity<Thread>,
1415    ) {
1416        init_test(cx);
1417        let fs = project::FakeFs::new(cx.executor());
1418        fs.insert_tree("/root", initial_tree).await;
1419        setup_test_with_fs(cx, fs, &[path!("/root").as_ref()]).await
1420    }
1421
1422    async fn test_resolve_path(
1423        mode: &EditSessionMode,
1424        path: &str,
1425        cx: &mut TestAppContext,
1426    ) -> Result<ProjectPath, String> {
1427        init_test(cx);
1428        let fs = project::FakeFs::new(cx.executor());
1429        fs.insert_tree(
1430            "/root",
1431            json!({
1432                "dir": {
1433                    "subdir": {
1434                        "existing.txt": "content"
1435                    }
1436                }
1437            }),
1438        )
1439        .await;
1440        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1441
1442        crate::tools::edit_session::test_resolve_path(mode, path, &project, cx).await
1443    }
1444
1445    #[track_caller]
1446    fn assert_resolved_path_eq(path: Result<ProjectPath, String>, expected: &RelPath) {
1447        let actual = path.expect("Should return valid path").path;
1448        assert_eq!(actual.as_ref(), expected);
1449    }
1450
1451    fn init_test(cx: &mut TestAppContext) {
1452        cx.update(|cx| {
1453            let settings_store = SettingsStore::test(cx);
1454            cx.set_global(settings_store);
1455            SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1456                store.update_user_settings(cx, |settings| {
1457                    settings
1458                        .project
1459                        .all_languages
1460                        .defaults
1461                        .ensure_final_newline_on_save = Some(false);
1462                });
1463            });
1464        });
1465    }
1466}
1467
Served at tenant.openagents/omega Member data and write actions are omitted.