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