251 lines
7.7 KiB
C#
251 lines
7.7 KiB
C#
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<string, ToolProperty>
|
|
{
|
|
["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<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "path";
|
|
span[1] = "new_content";
|
|
obj.Required = list;
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> 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<string> 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<string> ComputeLcs(string[] a, string[] b)
|
|
{
|
|
int num = a.Length;
|
|
int num2 = b.Length;
|
|
if ((long)num * (long)num2 > 10000000)
|
|
{
|
|
return new List<string>();
|
|
}
|
|
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<string> list = new List<string>();
|
|
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;
|
|
}
|
|
}
|