using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AxCopilot.Services.Agent; 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 { get { ToolParameterSchema obj = new ToolParameterSchema { Properties = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "File path to modify" }, ["new_content"] = new ToolProperty { Type = "string", Description = "Proposed new content for the file" }, ["description"] = new ToolProperty { Type = "string", Description = "Description of the changes (optional)" } } }; int num = 2; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "path"; span[1] = "new_content"; obj.Required = list; return obj; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken)) { string rawPath = args.GetProperty("path").GetString() ?? ""; string newContent = args.GetProperty("new_content").GetString() ?? ""; JsonElement d; string description = (args.TryGetProperty("description", out d) ? (d.GetString() ?? "") : ""); string path = (Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath)); if (!context.IsPathAllowed(path)) { return ToolResult.Fail("경로 접근 차단: " + path); } try { string originalContent = ""; bool isNewFile = !File.Exists(path); if (!isNewFile) { originalContent = await File.ReadAllTextAsync(path, ct); } string[] originalLines = (from l in originalContent.Split('\n') select l.TrimEnd('\r')).ToArray(); string[] newLines = (from l in newContent.Split('\n') select l.TrimEnd('\r')).ToArray(); string diff = GenerateUnifiedDiff(originalLines, newLines, path); StringBuilder sb = new StringBuilder(); sb.AppendLine("[PREVIEW_PENDING]"); StringBuilder stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler; if (!string.IsNullOrEmpty(description)) { stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder); handler.AppendLiteral("변경 설명: "); handler.AppendFormatted(description); stringBuilder2.AppendLine(ref handler); } stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder); handler.AppendLiteral("파일: "); handler.AppendFormatted(path); stringBuilder3.AppendLine(ref handler); sb.AppendLine(isNewFile ? "상태: 새 파일 생성" : "상태: 기존 파일 수정"); sb.AppendLine(); if (string.IsNullOrEmpty(diff)) { sb.AppendLine("변경 사항 없음 — 내용이 동일합니다."); } else { sb.Append(diff); } if (!(await context.CheckWritePermissionAsync("diff_preview", path))) { return ToolResult.Ok($"사용자가 파일 변경을 거부했습니다.\n\n{sb}"); } string dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } await File.WriteAllTextAsync(path, newContent, 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) { StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = stringBuilder; StringBuilder stringBuilder3 = stringBuilder2; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); handler.AppendLiteral("--- "); handler.AppendFormatted(filePath); handler.AppendLiteral(" (원본)"); stringBuilder3.AppendLine(ref handler); stringBuilder2 = stringBuilder; StringBuilder stringBuilder4 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); handler.AppendLiteral("+++ "); handler.AppendFormatted(filePath); handler.AppendLiteral(" (수정)"); stringBuilder4.AppendLine(ref handler); List list = ComputeLcs(original, modified); int i = 0; int j = 0; int num = 0; List<(int, int, int, int)> list2 = new List<(int, int, int, int)>(); while (i < original.Length || j < modified.Length) { if (num < list.Count && i < original.Length && j < modified.Length && original[i] == list[num] && modified[j] == list[num]) { i++; j++; num++; continue; } int item = i; int item2 = j; for (; i < original.Length && (num >= list.Count || original[i] != list[num]); i++) { } for (; j < modified.Length && (num >= list.Count || modified[j] != list[num]); j++) { } list2.Add((item, i, item2, j)); } if (list2.Count == 0) { return ""; } foreach (var item7 in list2) { int item3 = item7.Item1; int item4 = item7.Item2; int item5 = item7.Item3; int item6 = item7.Item4; stringBuilder2 = stringBuilder; StringBuilder stringBuilder5 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(11, 4, stringBuilder2); handler.AppendLiteral("@@ -"); handler.AppendFormatted(item3 + 1); handler.AppendLiteral(","); handler.AppendFormatted(item4 - item3); handler.AppendLiteral(" +"); handler.AppendFormatted(item5 + 1); handler.AppendLiteral(","); handler.AppendFormatted(item6 - item5); handler.AppendLiteral(" @@"); stringBuilder5.AppendLine(ref handler); for (int k = item3; k < item4; k++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder6 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral("-"); handler.AppendFormatted(original[k]); stringBuilder6.AppendLine(ref handler); } for (int l = item5; l < item6; l++) { stringBuilder2 = stringBuilder; StringBuilder stringBuilder7 = stringBuilder2; handler = new StringBuilder.AppendInterpolatedStringHandler(1, 1, stringBuilder2); handler.AppendLiteral("+"); handler.AppendFormatted(modified[l]); stringBuilder7.AppendLine(ref handler); } } return stringBuilder.ToString(); } private static List ComputeLcs(string[] a, string[] b) { int num = a.Length; int num2 = b.Length; if ((long)num * (long)num2 > 10000000) { return new List(); } int[,] array = new int[num + 1, num2 + 1]; for (int num3 = num - 1; num3 >= 0; num3--) { for (int num4 = num2 - 1; num4 >= 0; num4--) { array[num3, num4] = ((a[num3] == b[num4]) ? (array[num3 + 1, num4 + 1] + 1) : Math.Max(array[num3 + 1, num4], array[num3, num4 + 1])); } } List list = new List(); int num5 = 0; int num6 = 0; while (num5 < num && num6 < num2) { if (a[num5] == b[num6]) { list.Add(a[num5]); num5++; num6++; } else if (array[num5 + 1, num6] >= array[num5, num6 + 1]) { num5++; } else { num6++; } } return list; } }