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;
}
}