using System.IO; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// 파일 변경 사항을 미리 보여주고 사용자 승인을 받는 도구. /// 통합 diff를 생성하여 "[PREVIEW_PENDING]" 접두사와 함께 반환합니다. /// public class DiffPreviewTool : IAgentTool { public string Name => "diff_preview"; public string Description => "Preview file changes before applying them. Shows a unified diff and waits for user approval. " + "If approved, writes the new content to the file. If rejected, no changes are made. " + "The diff output is prefixed with [PREVIEW_PENDING] so the UI can show an approval panel."; public ToolParameterSchema Parameters => new() { Properties = new() { ["path"] = new() { Type = "string", Description = "File path to modify", }, ["new_content"] = new() { Type = "string", Description = "Proposed new content for the file", }, ["description"] = new() { Type = "string", Description = "Description of the changes (optional)", }, }, Required = ["path", "new_content"], }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var rawPath = args.GetProperty("path").GetString() ?? ""; var newContent = args.GetProperty("new_content").GetString() ?? ""; var description = args.TryGetProperty("description", out var d) ? d.GetString() ?? "" : ""; var path = Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath); if (!context.IsPathAllowed(path)) return ToolResult.Fail($"경로 접근 차단: {path}"); try { // 원본 파일 읽기 (없으면 새 파일 생성으로 처리) var originalContent = ""; var isNewFile = !File.Exists(path); var sourceEncoding = TextFileCodec.Utf8NoBom; var sourceHasBom = false; if (!isNewFile) { var read = await TextFileCodec.ReadAllTextAsync(path, ct); originalContent = read.Text; sourceEncoding = read.Encoding; sourceHasBom = read.HasBom; } // 통합 diff 생성 var originalLines = originalContent.Split('\n').Select(l => l.TrimEnd('\r')).ToArray(); var newLines = newContent.Split('\n').Select(l => l.TrimEnd('\r')).ToArray(); var diff = GenerateUnifiedDiff(originalLines, newLines, path); var sb = new StringBuilder(); sb.AppendLine("[PREVIEW_PENDING]"); if (!string.IsNullOrEmpty(description)) sb.AppendLine($"변경 설명: {description}"); sb.AppendLine($"파일: {path}"); sb.AppendLine(isNewFile ? "상태: 새 파일 생성" : "상태: 기존 파일 수정"); sb.AppendLine(); if (string.IsNullOrEmpty(diff)) sb.AppendLine("변경 사항 없음 — 내용이 동일합니다."); else sb.Append(diff); // 쓰기 권한 확인 (AskPermission 콜백 사용 — CustomMessageBox) if (!await context.CheckWritePermissionAsync("diff_preview", path)) return ToolResult.Ok($"사용자가 파일 변경을 거부했습니다.\n\n{sb}"); // 디렉토리 생성 var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); var writeEncoding = isNewFile ? TextFileCodec.Utf8NoBom : TextFileCodec.ResolveWriteEncoding(sourceEncoding, sourceHasBom); await TextFileCodec.WriteAllTextAsync(path, newContent, writeEncoding, ct); return ToolResult.Ok($"변경 사항이 적용되었습니다: {path}\n\n{sb}", path); } catch (Exception ex) { return ToolResult.Fail($"미리보기 오류: {ex.Message}"); } } private static string GenerateUnifiedDiff(string[] original, string[] modified, string filePath) { var sb = new StringBuilder(); sb.AppendLine($"--- {filePath} (원본)"); sb.AppendLine($"+++ {filePath} (수정)"); // 간단한 LCS 기반 diff var lcs = ComputeLcs(original, modified); int oi = 0, mi = 0, ci = 0; var hunks = new List<(int os, int oe, int ms, int me)>(); while (oi < original.Length || mi < modified.Length) { if (ci < lcs.Count && oi < original.Length && mi < modified.Length && original[oi] == lcs[ci] && modified[mi] == lcs[ci]) { oi++; mi++; ci++; } else { var hos = oi; var hms = mi; while (oi < original.Length && (ci >= lcs.Count || original[oi] != lcs[ci])) oi++; while (mi < modified.Length && (ci >= lcs.Count || modified[mi] != lcs[ci])) mi++; hunks.Add((hos, oi, hms, mi)); } } if (hunks.Count == 0) return ""; foreach (var (os, oe, ms, me) in hunks) { sb.AppendLine($"@@ -{os + 1},{oe - os} +{ms + 1},{me - ms} @@"); for (var i = os; i < oe; i++) sb.AppendLine($"-{original[i]}"); for (var i = ms; i < me; i++) sb.AppendLine($"+{modified[i]}"); } return sb.ToString(); } private static List ComputeLcs(string[] a, string[] b) { var m = a.Length; var n = b.Length; // 메모리 절약: 큰 파일은 전체를 diff로 표시 if ((long)m * n > 10_000_000) return []; var dp = new int[m + 1, n + 1]; for (var i = m - 1; i >= 0; i--) for (var j = n - 1; j >= 0; j--) dp[i, j] = a[i] == b[j] ? dp[i + 1, j + 1] + 1 : Math.Max(dp[i + 1, j], dp[i, j + 1]); var result = new List(); int x = 0, y = 0; while (x < m && y < n) { if (a[x] == b[y]) { result.Add(a[x]); x++; y++; } else if (dp[x + 1, y] >= dp[x, y + 1]) x++; else y++; } return result; } }