1use super::truncation::{TruncationLimits, truncate_dir_listing, truncate_file_content};
19use crate::agent::ide::IdeClient;
20use crate::agent::ui::confirmation::ConfirmationResult;
21use crate::agent::ui::diff::{confirm_file_write, confirm_file_write_with_ide};
22use rig::completion::ToolDefinition;
23use rig::tool::Tool;
24use serde::{Deserialize, Serialize};
25use serde_json::json;
26use std::collections::HashSet;
27use std::fs;
28use std::path::PathBuf;
29use std::sync::Mutex;
30
31#[derive(Debug, Deserialize)]
36pub struct ReadFileArgs {
37 pub path: String,
38 pub start_line: Option<u64>,
39 pub end_line: Option<u64>,
40}
41
42#[derive(Debug, thiserror::Error)]
43#[error("Read file error: {0}")]
44pub struct ReadFileError(String);
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ReadFileTool {
48 project_path: PathBuf,
49}
50
51impl ReadFileTool {
52 pub fn new(project_path: PathBuf) -> Self {
53 Self { project_path }
54 }
55
56 fn validate_path(&self, requested: &PathBuf) -> Result<PathBuf, ReadFileError> {
57 let canonical_project = self
58 .project_path
59 .canonicalize()
60 .map_err(|e| ReadFileError(format!("Invalid project path: {}", e)))?;
61
62 let target = if requested.is_absolute() {
63 requested.clone()
64 } else {
65 self.project_path.join(requested)
66 };
67
68 let canonical_target = target
69 .canonicalize()
70 .map_err(|e| ReadFileError(format!("File not found: {}", e)))?;
71
72 if !canonical_target.starts_with(&canonical_project) {
73 return Err(ReadFileError(
74 "Access denied: path is outside project directory".to_string(),
75 ));
76 }
77
78 Ok(canonical_target)
79 }
80}
81
82impl Tool for ReadFileTool {
83 const NAME: &'static str = "read_file";
84
85 type Error = ReadFileError;
86 type Args = ReadFileArgs;
87 type Output = String;
88
89 async fn definition(&self, _prompt: String) -> ToolDefinition {
90 ToolDefinition {
91 name: Self::NAME.to_string(),
92 description: "Read the contents of a file in the project. Use this to examine source code, configuration files, or any text file.".to_string(),
93 parameters: json!({
94 "type": "object",
95 "properties": {
96 "path": {
97 "type": "string",
98 "description": "Path to the file to read (relative to project root)"
99 },
100 "start_line": {
101 "type": "integer",
102 "description": "Optional starting line number (1-based)"
103 },
104 "end_line": {
105 "type": "integer",
106 "description": "Optional ending line number (1-based, inclusive)"
107 }
108 },
109 "required": ["path"]
110 }),
111 }
112 }
113
114 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
115 let requested_path = PathBuf::from(&args.path);
116 let file_path = self.validate_path(&requested_path)?;
117
118 let metadata = fs::metadata(&file_path)
119 .map_err(|e| ReadFileError(format!("Cannot read file: {}", e)))?;
120
121 const MAX_SIZE: u64 = 1024 * 1024;
122 if metadata.len() > MAX_SIZE {
123 return Ok(json!({
124 "error": format!("File too large ({} bytes). Maximum size is {} bytes.", metadata.len(), MAX_SIZE)
125 }).to_string());
126 }
127
128 let content = fs::read_to_string(&file_path)
129 .map_err(|e| ReadFileError(format!("Failed to read file: {}", e)))?;
130
131 let output = if let Some(start) = args.start_line {
132 let lines: Vec<&str> = content.lines().collect();
134 let start_idx = (start as usize).saturating_sub(1);
135 let end_idx = args
136 .end_line
137 .map(|e| (e as usize).min(lines.len()))
138 .unwrap_or(lines.len());
139
140 if start_idx >= lines.len() {
141 return Ok(json!({
142 "error": format!("Start line {} exceeds file length ({})", start, lines.len())
143 })
144 .to_string());
145 }
146
147 let end_idx = end_idx.max(start_idx);
149
150 let selected: Vec<String> = lines[start_idx..end_idx]
151 .iter()
152 .enumerate()
153 .map(|(i, line)| format!("{:>4} | {}", start_idx + i + 1, line))
154 .collect();
155
156 json!({
157 "file": args.path,
158 "lines": format!("{}-{}", start, end_idx),
159 "total_lines": lines.len(),
160 "content": selected.join("\n")
161 })
162 } else {
163 let limits = TruncationLimits::default();
165 let truncated = truncate_file_content(&content, &limits);
166
167 json!({
168 "file": args.path,
169 "total_lines": truncated.total_lines,
170 "lines_returned": truncated.returned_lines,
171 "truncated": truncated.was_truncated,
172 "content": truncated.content
173 })
174 };
175
176 serde_json::to_string_pretty(&output)
177 .map_err(|e| ReadFileError(format!("Failed to serialize: {}", e)))
178 }
179}
180
181#[derive(Debug, Deserialize)]
186pub struct ListDirectoryArgs {
187 pub path: Option<String>,
188 pub recursive: Option<bool>,
189}
190
191#[derive(Debug, thiserror::Error)]
192#[error("List directory error: {0}")]
193pub struct ListDirectoryError(String);
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ListDirectoryTool {
197 project_path: PathBuf,
198}
199
200impl ListDirectoryTool {
201 pub fn new(project_path: PathBuf) -> Self {
202 Self { project_path }
203 }
204
205 fn validate_path(&self, requested: &PathBuf) -> Result<PathBuf, ListDirectoryError> {
206 let canonical_project = self
207 .project_path
208 .canonicalize()
209 .map_err(|e| ListDirectoryError(format!("Invalid project path: {}", e)))?;
210
211 let target = if requested.is_absolute() {
212 requested.clone()
213 } else {
214 self.project_path.join(requested)
215 };
216
217 let canonical_target = target
218 .canonicalize()
219 .map_err(|e| ListDirectoryError(format!("Directory not found: {}", e)))?;
220
221 if !canonical_target.starts_with(&canonical_project) {
222 return Err(ListDirectoryError(
223 "Access denied: path is outside project directory".to_string(),
224 ));
225 }
226
227 Ok(canonical_target)
228 }
229
230 fn list_entries(
231 &self,
232 base_path: &PathBuf,
233 current_path: &PathBuf,
234 recursive: bool,
235 depth: usize,
236 max_depth: usize,
237 entries: &mut Vec<serde_json::Value>,
238 ) -> Result<(), ListDirectoryError> {
239 let skip_dirs = [
240 "node_modules",
241 ".git",
242 "target",
243 "__pycache__",
244 ".venv",
245 "venv",
246 "dist",
247 "build",
248 ];
249
250 let dir_name = current_path
251 .file_name()
252 .and_then(|n| n.to_str())
253 .unwrap_or("");
254
255 if depth > 0 && skip_dirs.contains(&dir_name) {
256 return Ok(());
257 }
258
259 let read_dir = fs::read_dir(current_path)
260 .map_err(|e| ListDirectoryError(format!("Cannot read directory: {}", e)))?;
261
262 for entry in read_dir {
263 let entry =
264 entry.map_err(|e| ListDirectoryError(format!("Error reading entry: {}", e)))?;
265 let path = entry.path();
266 let metadata = entry.metadata().ok();
267
268 let relative_path = path
269 .strip_prefix(base_path)
270 .unwrap_or(&path)
271 .to_string_lossy()
272 .to_string();
273 let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
274 let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
275
276 entries.push(json!({
277 "name": entry.file_name().to_string_lossy(),
278 "path": relative_path,
279 "type": if is_dir { "directory" } else { "file" },
280 "size": if is_dir { None::<u64> } else { Some(size) }
281 }));
282
283 if recursive && is_dir && depth < max_depth {
284 self.list_entries(base_path, &path, recursive, depth + 1, max_depth, entries)?;
285 }
286 }
287
288 Ok(())
289 }
290}
291
292impl Tool for ListDirectoryTool {
293 const NAME: &'static str = "list_directory";
294
295 type Error = ListDirectoryError;
296 type Args = ListDirectoryArgs;
297 type Output = String;
298
299 async fn definition(&self, _prompt: String) -> ToolDefinition {
300 ToolDefinition {
301 name: Self::NAME.to_string(),
302 description: "List the contents of a directory in the project. Returns file and subdirectory names with their types and sizes.".to_string(),
303 parameters: json!({
304 "type": "object",
305 "properties": {
306 "path": {
307 "type": "string",
308 "description": "Path to the directory to list (relative to project root). Use '.' for root."
309 },
310 "recursive": {
311 "type": "boolean",
312 "description": "If true, list contents recursively (max depth 3). Default is false."
313 }
314 }
315 }),
316 }
317 }
318
319 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
320 let path_str = args.path.as_deref().unwrap_or(".");
321
322 let requested_path = if path_str.is_empty() || path_str == "." {
323 self.project_path.clone()
324 } else {
325 PathBuf::from(path_str)
326 };
327
328 let dir_path = self.validate_path(&requested_path)?;
329 let recursive = args.recursive.unwrap_or(false);
330
331 let mut entries = Vec::new();
332 self.list_entries(&dir_path, &dir_path, recursive, 0, 3, &mut entries)?;
333
334 let limits = TruncationLimits::default();
336 let truncated = truncate_dir_listing(entries, limits.max_dir_entries);
337
338 let result = if truncated.was_truncated {
339 json!({
340 "path": path_str,
341 "entries": truncated.entries,
342 "entries_returned": truncated.entries.len(),
343 "total_count": truncated.total_entries,
344 "truncated": true,
345 "note": format!("Showing first {} of {} entries. Use a more specific path to see others.", truncated.entries.len(), truncated.total_entries)
346 })
347 } else {
348 json!({
349 "path": path_str,
350 "entries": truncated.entries,
351 "total_count": truncated.total_entries
352 })
353 };
354
355 serde_json::to_string_pretty(&result)
356 .map_err(|e| ListDirectoryError(format!("Failed to serialize: {}", e)))
357 }
358}
359
360#[derive(Debug, Deserialize)]
365pub struct WriteFileArgs {
366 pub path: String,
368 pub content: String,
370 pub create_dirs: Option<bool>,
372}
373
374#[derive(Debug, thiserror::Error)]
375#[error("Write file error: {0}")]
376pub struct WriteFileError(String);
377
378#[derive(Debug)]
380pub struct AllowedFilePatterns {
381 patterns: Mutex<HashSet<String>>,
382}
383
384impl AllowedFilePatterns {
385 pub fn new() -> Self {
386 Self {
387 patterns: Mutex::new(HashSet::new()),
388 }
389 }
390
391 pub fn is_allowed(&self, filename: &str) -> bool {
393 let patterns = self.patterns.lock().unwrap();
394 patterns.contains(filename)
395 }
396
397 pub fn allow(&self, pattern: String) {
399 let mut patterns = self.patterns.lock().unwrap();
400 patterns.insert(pattern);
401 }
402}
403
404impl Default for AllowedFilePatterns {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410#[derive(Debug, Clone)]
411pub struct WriteFileTool {
412 project_path: PathBuf,
413 require_confirmation: bool,
415 allowed_patterns: std::sync::Arc<AllowedFilePatterns>,
417 ide_client: Option<std::sync::Arc<tokio::sync::Mutex<IdeClient>>>,
419}
420
421impl WriteFileTool {
422 pub fn new(project_path: PathBuf) -> Self {
423 Self {
424 project_path,
425 require_confirmation: true,
426 allowed_patterns: std::sync::Arc::new(AllowedFilePatterns::new()),
427 ide_client: None,
428 }
429 }
430
431 pub fn with_allowed_patterns(
433 project_path: PathBuf,
434 allowed_patterns: std::sync::Arc<AllowedFilePatterns>,
435 ) -> Self {
436 Self {
437 project_path,
438 require_confirmation: true,
439 allowed_patterns,
440 ide_client: None,
441 }
442 }
443
444 pub fn with_ide_client(
446 mut self,
447 ide_client: std::sync::Arc<tokio::sync::Mutex<IdeClient>>,
448 ) -> Self {
449 self.ide_client = Some(ide_client);
450 self
451 }
452
453 pub fn without_confirmation(mut self) -> Self {
455 self.require_confirmation = false;
456 self
457 }
458
459 fn validate_path(&self, requested: &PathBuf) -> Result<PathBuf, WriteFileError> {
460 let canonical_project = self
461 .project_path
462 .canonicalize()
463 .map_err(|e| WriteFileError(format!("Invalid project path: {}", e)))?;
464
465 let target = if requested.is_absolute() {
466 requested.clone()
467 } else {
468 self.project_path.join(requested)
469 };
470
471 let parent = target
473 .parent()
474 .ok_or_else(|| WriteFileError("Invalid path: no parent directory".to_string()))?;
475
476 let is_within_project = if parent.exists() {
478 let canonical_parent = parent
479 .canonicalize()
480 .map_err(|e| WriteFileError(format!("Invalid parent path: {}", e)))?;
481 canonical_parent.starts_with(&canonical_project)
482 } else {
483 let normalized = self.project_path.join(requested);
485 !normalized
486 .components()
487 .any(|c| c == std::path::Component::ParentDir)
488 };
489
490 if !is_within_project {
491 return Err(WriteFileError(
492 "Access denied: path is outside project directory".to_string(),
493 ));
494 }
495
496 Ok(target)
497 }
498}
499
500impl Tool for WriteFileTool {
501 const NAME: &'static str = "write_file";
502
503 type Error = WriteFileError;
504 type Args = WriteFileArgs;
505 type Output = String;
506
507 async fn definition(&self, _prompt: String) -> ToolDefinition {
508 ToolDefinition {
509 name: Self::NAME.to_string(),
510 description: r#"Write content to a file in the project. Creates the file if it doesn't exist, or overwrites if it does.
511
512**IMPORTANT**: Use this tool IMMEDIATELY when the user asks you to:
513- Create ANY file (Dockerfile, .tf, .yaml, .md, .json, etc.)
514- Generate configuration files
515- Write documentation to a specific location
516- "Put content in" or "under" a directory
517- Save analysis results or findings
518- Document anything in a file
519
520**DO NOT** just describe what you would write - actually call this tool with the content.
521
522Use cases:
523- Generate Dockerfiles for applications
524- Create Terraform configuration files (.tf)
525- Write Helm chart templates and values
526- Create docker-compose.yml files
527- Generate CI/CD configuration files (.github/workflows, .gitlab-ci.yml)
528- Write Kubernetes manifests
529- Save analysis findings to markdown files
530- Create any text file the user requests
531
532The tool will create parent directories automatically if they don't exist."#.to_string(),
533 parameters: json!({
534 "type": "object",
535 "properties": {
536 "path": {
537 "type": "string",
538 "description": "Path to the file to write (relative to project root). Example: 'Dockerfile', 'terraform/main.tf', 'helm/values.yaml'"
539 },
540 "content": {
541 "type": "string",
542 "description": "The complete content to write to the file"
543 },
544 "create_dirs": {
545 "type": "boolean",
546 "description": "If true (default), create parent directories if they don't exist"
547 }
548 },
549 "required": ["path", "content"]
550 }),
551 }
552 }
553
554 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
555 let requested_path = PathBuf::from(&args.path);
556 let file_path = self.validate_path(&requested_path)?;
557
558 let old_content = if file_path.exists() {
560 fs::read_to_string(&file_path).ok()
561 } else {
562 None
563 };
564
565 let filename = std::path::Path::new(&args.path)
567 .file_name()
568 .map(|n| n.to_string_lossy().to_string())
569 .unwrap_or_else(|| args.path.clone());
570
571 let needs_confirmation =
573 self.require_confirmation && !self.allowed_patterns.is_allowed(&filename);
574
575 if needs_confirmation {
576 let ide_client_guard = if let Some(ref client) = self.ide_client {
578 Some(client.lock().await)
579 } else {
580 None
581 };
582 let ide_client_ref = ide_client_guard.as_deref();
583
584 let confirmation = confirm_file_write_with_ide(
586 &args.path,
587 old_content.as_deref(),
588 &args.content,
589 ide_client_ref,
590 )
591 .await;
592
593 match confirmation {
594 ConfirmationResult::Proceed => {
595 }
597 ConfirmationResult::ProceedAlways(pattern) => {
598 self.allowed_patterns.allow(pattern);
600 }
601 ConfirmationResult::Modify(feedback) => {
602 let result = json!({
604 "cancelled": true,
605 "STOP": "Do NOT create this file or any similar files. Wait for user instruction.",
606 "reason": "User requested changes",
607 "user_feedback": feedback,
608 "original_path": args.path,
609 "action_required": "Read the user_feedback and respond accordingly. Do NOT try to create alternative files."
610 });
611 return serde_json::to_string_pretty(&result)
612 .map_err(|e| WriteFileError(format!("Failed to serialize: {}", e)));
613 }
614 ConfirmationResult::Cancel => {
615 let result = json!({
617 "cancelled": true,
618 "STOP": "User has rejected this operation. Do NOT create this file or any alternative files.",
619 "reason": "User cancelled the operation",
620 "original_path": args.path,
621 "action_required": "Stop creating files. Ask the user what they want instead."
622 });
623 return serde_json::to_string_pretty(&result)
624 .map_err(|e| WriteFileError(format!("Failed to serialize: {}", e)));
625 }
626 }
627 } else {
628 use crate::agent::ui::diff::{render_diff, render_new_file};
630 use colored::Colorize;
631
632 if let Some(old) = &old_content {
633 render_diff(old, &args.content, &args.path);
634 } else {
635 render_new_file(&args.content, &args.path);
636 }
637 println!(" {} Auto-accepted", "✓".green());
638 }
639
640 let create_dirs = args.create_dirs.unwrap_or(true);
642 if create_dirs
643 && let Some(parent) = file_path.parent()
644 && !parent.exists()
645 {
646 fs::create_dir_all(parent)
647 .map_err(|e| WriteFileError(format!("Failed to create directories: {}", e)))?;
648 }
649
650 let file_existed = file_path.exists();
652
653 fs::write(&file_path, &args.content)
655 .map_err(|e| WriteFileError(format!("Failed to write file: {}", e)))?;
656
657 let action = if file_existed { "Updated" } else { "Created" };
658 let lines = args.content.lines().count();
659
660 let result = json!({
661 "success": true,
662 "action": action,
663 "path": args.path,
664 "lines_written": lines,
665 "bytes_written": args.content.len()
666 });
667
668 serde_json::to_string_pretty(&result)
669 .map_err(|e| WriteFileError(format!("Failed to serialize: {}", e)))
670 }
671}
672
673#[derive(Debug, Deserialize)]
678pub struct FileToWrite {
679 pub path: String,
681 pub content: String,
683}
684
685#[derive(Debug, Deserialize)]
686pub struct WriteFilesArgs {
687 pub files: Vec<FileToWrite>,
689 pub create_dirs: Option<bool>,
691}
692
693#[derive(Debug, thiserror::Error)]
694#[error("Write files error: {0}")]
695pub struct WriteFilesError(String);
696
697#[derive(Debug, Clone)]
698pub struct WriteFilesTool {
699 project_path: PathBuf,
700 require_confirmation: bool,
702 allowed_patterns: std::sync::Arc<AllowedFilePatterns>,
704 ide_client: Option<std::sync::Arc<tokio::sync::Mutex<IdeClient>>>,
706}
707
708impl WriteFilesTool {
709 pub fn new(project_path: PathBuf) -> Self {
710 Self {
711 project_path,
712 require_confirmation: true,
713 allowed_patterns: std::sync::Arc::new(AllowedFilePatterns::new()),
714 ide_client: None,
715 }
716 }
717
718 pub fn with_allowed_patterns(
720 project_path: PathBuf,
721 allowed_patterns: std::sync::Arc<AllowedFilePatterns>,
722 ) -> Self {
723 Self {
724 project_path,
725 require_confirmation: true,
726 allowed_patterns,
727 ide_client: None,
728 }
729 }
730
731 pub fn without_confirmation(mut self) -> Self {
733 self.require_confirmation = false;
734 self
735 }
736
737 pub fn with_ide_client(
739 mut self,
740 ide_client: std::sync::Arc<tokio::sync::Mutex<IdeClient>>,
741 ) -> Self {
742 self.ide_client = Some(ide_client);
743 self
744 }
745
746 fn validate_path(&self, requested: &PathBuf) -> Result<PathBuf, WriteFilesError> {
747 let canonical_project = self
748 .project_path
749 .canonicalize()
750 .map_err(|e| WriteFilesError(format!("Invalid project path: {}", e)))?;
751
752 let target = if requested.is_absolute() {
753 requested.clone()
754 } else {
755 self.project_path.join(requested)
756 };
757
758 let parent = target
759 .parent()
760 .ok_or_else(|| WriteFilesError("Invalid path: no parent directory".to_string()))?;
761
762 let is_within_project = if parent.exists() {
763 let canonical_parent = parent
764 .canonicalize()
765 .map_err(|e| WriteFilesError(format!("Invalid parent path: {}", e)))?;
766 canonical_parent.starts_with(&canonical_project)
767 } else {
768 let normalized = self.project_path.join(requested);
769 !normalized
770 .components()
771 .any(|c| c == std::path::Component::ParentDir)
772 };
773
774 if !is_within_project {
775 return Err(WriteFilesError(
776 "Access denied: path is outside project directory".to_string(),
777 ));
778 }
779
780 Ok(target)
781 }
782}
783
784impl Tool for WriteFilesTool {
785 const NAME: &'static str = "write_files";
786
787 type Error = WriteFilesError;
788 type Args = WriteFilesArgs;
789 type Output = String;
790
791 async fn definition(&self, _prompt: String) -> ToolDefinition {
792 ToolDefinition {
793 name: Self::NAME.to_string(),
794 description: r#"Write multiple files at once. Ideal for creating complete infrastructure configurations.
795
796**IMPORTANT**: Use this tool when you need to create multiple related files together.
797
798**USE THIS TOOL** (not just describe files) when the user asks for:
799- Complete Terraform modules (main.tf, variables.tf, outputs.tf, providers.tf)
800- Full Helm charts (Chart.yaml, values.yaml, templates/*.yaml)
801- Kubernetes manifests (deployment.yaml, service.yaml, configmap.yaml)
802- Multi-file docker-compose setups
803- Multiple documentation files in a directory
804- Any set of related files
805
806**DO NOT** just describe the files - actually call this tool to create them.
807
808All files are written atomically. Parent directories are created automatically."#.to_string(),
809 parameters: json!({
810 "type": "object",
811 "properties": {
812 "files": {
813 "type": "array",
814 "description": "List of files to write",
815 "items": {
816 "type": "object",
817 "properties": {
818 "path": {
819 "type": "string",
820 "description": "Path to the file (relative to project root)"
821 },
822 "content": {
823 "type": "string",
824 "description": "Content to write to the file"
825 }
826 },
827 "required": ["path", "content"]
828 }
829 },
830 "create_dirs": {
831 "type": "boolean",
832 "description": "If true (default), create parent directories if they don't exist"
833 }
834 },
835 "required": ["files"]
836 }),
837 }
838 }
839
840 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
841 let create_dirs = args.create_dirs.unwrap_or(true);
842 let mut results = Vec::new();
843 let mut total_bytes = 0usize;
844 let mut total_lines = 0usize;
845
846 for file in &args.files {
847 let requested_path = PathBuf::from(&file.path);
848 let file_path = self.validate_path(&requested_path)?;
849
850 let old_content = if file_path.exists() {
852 fs::read_to_string(&file_path).ok()
853 } else {
854 None
855 };
856
857 let filename = std::path::Path::new(&file.path)
859 .file_name()
860 .map(|n| n.to_string_lossy().to_string())
861 .unwrap_or_else(|| file.path.clone());
862
863 let needs_confirmation =
865 self.require_confirmation && !self.allowed_patterns.is_allowed(&filename);
866
867 if needs_confirmation {
868 let confirmation = if let Some(ref client) = self.ide_client {
870 let guard = client.lock().await;
871 if guard.is_connected() {
872 confirm_file_write_with_ide(
873 &file.path,
874 old_content.as_deref(),
875 &file.content,
876 Some(&*guard),
877 )
878 .await
879 } else {
880 drop(guard);
881 confirm_file_write(&file.path, old_content.as_deref(), &file.content)
882 }
883 } else {
884 confirm_file_write(&file.path, old_content.as_deref(), &file.content)
885 };
886
887 match confirmation {
888 ConfirmationResult::Proceed => {
889 }
891 ConfirmationResult::ProceedAlways(pattern) => {
892 self.allowed_patterns.allow(pattern);
893 }
894 ConfirmationResult::Modify(feedback) => {
895 let result = json!({
897 "cancelled": true,
898 "STOP": "User provided feedback. Stop creating all remaining files in this batch.",
899 "reason": "User requested changes",
900 "user_feedback": feedback,
901 "skipped_file": file.path,
902 "files_written_before_cancel": results.len(),
903 "action_required": "Read the user_feedback. Do NOT continue with remaining files."
904 });
905 return serde_json::to_string_pretty(&result)
906 .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e)));
907 }
908 ConfirmationResult::Cancel => {
909 let result = json!({
911 "cancelled": true,
912 "STOP": "User cancelled. Stop creating all files immediately.",
913 "reason": "User cancelled the operation",
914 "skipped_file": file.path,
915 "files_written_before_cancel": results.len(),
916 "action_required": "Stop all file creation. Ask the user what they want instead."
917 });
918 return serde_json::to_string_pretty(&result)
919 .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e)));
920 }
921 }
922 } else {
923 use crate::agent::ui::diff::{render_diff, render_new_file};
925 use colored::Colorize;
926
927 if let Some(old) = &old_content {
928 render_diff(old, &file.content, &file.path);
929 } else {
930 render_new_file(&file.content, &file.path);
931 }
932 println!(" {} Auto-accepted", "✓".green());
933 }
934
935 if create_dirs
937 && let Some(parent) = file_path.parent()
938 && !parent.exists()
939 {
940 fs::create_dir_all(parent).map_err(|e| {
941 WriteFilesError(format!(
942 "Failed to create directories for {}: {}",
943 file.path, e
944 ))
945 })?;
946 }
947
948 let file_existed = file_path.exists();
949
950 fs::write(&file_path, &file.content)
951 .map_err(|e| WriteFilesError(format!("Failed to write {}: {}", file.path, e)))?;
952
953 let lines = file.content.lines().count();
954 total_bytes += file.content.len();
955 total_lines += lines;
956
957 results.push(json!({
958 "path": file.path,
959 "action": if file_existed { "updated" } else { "created" },
960 "lines": lines,
961 "bytes": file.content.len()
962 }));
963 }
964
965 let result = json!({
968 "success": true,
969 "files_written": results.len(),
970 "total_lines": total_lines,
971 "total_bytes": total_bytes,
972 "files": results
973 });
974
975 serde_json::to_string_pretty(&result)
976 .map_err(|e| WriteFilesError(format!("Failed to serialize: {}", e)))
977 }
978}