AX Agent 코워크·코드 흐름과 컨텍스트 관리를 claude-code 기준으로 대폭 정리
- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함 - OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함 - AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함 - 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함 - README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
@@ -68,8 +69,15 @@ public class LspClientService : IDisposable
|
||||
textDocument = new
|
||||
{
|
||||
definition = new { dynamicRegistration = false },
|
||||
implementation = new { dynamicRegistration = false },
|
||||
references = new { dynamicRegistration = false },
|
||||
documentSymbol = new { dynamicRegistration = false },
|
||||
hover = new { dynamicRegistration = false },
|
||||
callHierarchy = new { dynamicRegistration = false },
|
||||
},
|
||||
workspace = new
|
||||
{
|
||||
symbol = new { dynamicRegistration = false },
|
||||
}
|
||||
}
|
||||
}, ct);
|
||||
@@ -126,6 +134,79 @@ public class LspClientService : IDisposable
|
||||
return ParseSymbols(result);
|
||||
}
|
||||
|
||||
/// <summary>심볼의 hover 정보를 가져옵니다.</summary>
|
||||
public async Task<string?> HoverAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/hover", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseHover(result);
|
||||
}
|
||||
|
||||
/// <summary>인터페이스/추상 메서드 등의 구현 위치를 찾습니다.</summary>
|
||||
public async Task<List<LspLocation>> GotoImplementationAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/implementation", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseLocations(result);
|
||||
}
|
||||
|
||||
/// <summary>워크스페이스 전체 심볼을 검색합니다.</summary>
|
||||
public async Task<List<LspWorkspaceSymbol>> SearchWorkspaceSymbolsAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("workspace/symbol", new { query }, ct);
|
||||
return ParseWorkspaceSymbols(result);
|
||||
}
|
||||
|
||||
/// <summary>호출 계층의 기준 아이템을 준비합니다.</summary>
|
||||
public async Task<List<LspCallHierarchyItem>> PrepareCallHierarchyAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var result = await SendRequestAsync("textDocument/prepareCallHierarchy", new
|
||||
{
|
||||
textDocument = new { uri = FileToUri(filePath) },
|
||||
position = new { line, character }
|
||||
}, ct);
|
||||
|
||||
return ParseCallHierarchyItems(result);
|
||||
}
|
||||
|
||||
/// <summary>해당 심볼을 호출하는 상위 호출자를 찾습니다.</summary>
|
||||
public async Task<List<LspCallHierarchyEntry>> GetIncomingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||
if (items.Count == 0)
|
||||
return new List<LspCallHierarchyEntry>();
|
||||
|
||||
var result = await SendRequestAsync("callHierarchy/incomingCalls", new
|
||||
{
|
||||
item = items[0].RawItem
|
||||
}, ct);
|
||||
|
||||
return ParseIncomingCalls(result);
|
||||
}
|
||||
|
||||
/// <summary>해당 심볼이 호출하는 하위 호출 대상을 찾습니다.</summary>
|
||||
public async Task<List<LspCallHierarchyEntry>> GetOutgoingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||
{
|
||||
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||
if (items.Count == 0)
|
||||
return new List<LspCallHierarchyEntry>();
|
||||
|
||||
var result = await SendRequestAsync("callHierarchy/outgoingCalls", new
|
||||
{
|
||||
item = items[0].RawItem
|
||||
}, ct);
|
||||
|
||||
return ParseOutgoingCalls(result);
|
||||
}
|
||||
|
||||
// ─── JSON-RPC 통신 ──────────────────────────────────────────────────────
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
@@ -249,16 +330,7 @@ public class LspClientService : IDisposable
|
||||
var elem = result.Value;
|
||||
if (elem.ValueKind == JsonValueKind.Array && elem.GetArrayLength() > 0)
|
||||
elem = elem[0];
|
||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
||||
{
|
||||
var start = range.GetProperty("start");
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
};
|
||||
}
|
||||
return ParseLocationElement(elem);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
@@ -267,21 +339,56 @@ public class LspClientService : IDisposable
|
||||
private static List<LspLocation> ParseLocations(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspLocation>();
|
||||
if (result?.ValueKind != JsonValueKind.Array) return list;
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
if (result == null)
|
||||
return list;
|
||||
|
||||
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
var parsed = ParseLocationElement(elem);
|
||||
if (parsed != null)
|
||||
list.Add(parsed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsed = ParseLocationElement(result.Value);
|
||||
if (parsed != null)
|
||||
list.Add(parsed);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static LspLocation? ParseLocationElement(JsonElement elem)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (elem.TryGetProperty("targetUri", out var targetUri) && elem.TryGetProperty("targetSelectionRange", out var targetSelectionRange))
|
||||
{
|
||||
var start = targetSelectionRange.GetProperty("start");
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(targetUri.SafeGetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
};
|
||||
}
|
||||
|
||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
||||
{
|
||||
var start = range.GetProperty("start");
|
||||
list.Add(new LspLocation
|
||||
return new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
||||
FilePath = UriToFile(uri.SafeGetString() ?? ""),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
return list;
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<LspSymbol> ParseSymbols(JsonElement? result)
|
||||
@@ -311,6 +418,193 @@ public class LspClientService : IDisposable
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string? ParseHover(JsonElement? result)
|
||||
{
|
||||
if (result == null || result.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
if (!result.Value.TryGetProperty("contents", out var contents))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return contents.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => contents.SafeGetString(),
|
||||
JsonValueKind.Object => contents.TryGetProperty("value", out var value)
|
||||
? value.SafeGetString()
|
||||
: contents.GetRawText(),
|
||||
JsonValueKind.Array => string.Join(
|
||||
"\n\n",
|
||||
contents.EnumerateArray()
|
||||
.Select(item => item.ValueKind == JsonValueKind.Object && item.TryGetProperty("value", out var v)
|
||||
? v.SafeGetString()
|
||||
: item.SafeGetString())
|
||||
.Where(text => !string.IsNullOrWhiteSpace(text))),
|
||||
_ => contents.SafeGetString()
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LspWorkspaceSymbol> ParseWorkspaceSymbols(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspWorkspaceSymbol>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||
var location = elem.TryGetProperty("location", out var locationEl)
|
||||
? ParseLocationElement(locationEl)
|
||||
: null;
|
||||
|
||||
list.Add(new LspWorkspaceSymbol
|
||||
{
|
||||
Name = name,
|
||||
Kind = kind,
|
||||
Location = location,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyItem> ParseCallHierarchyItems(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyItem>();
|
||||
if (result == null)
|
||||
return list;
|
||||
|
||||
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
var item = ParseCallHierarchyItem(elem);
|
||||
if (item != null)
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
else if (result.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var item = ParseCallHierarchyItem(result.Value);
|
||||
if (item != null)
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static LspCallHierarchyItem? ParseCallHierarchyItem(JsonElement elem)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = elem.TryGetProperty("uri", out var uriEl) ? uriEl.SafeGetString() ?? "" : "";
|
||||
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||
var selectionRange = elem.TryGetProperty("selectionRange", out var selectionRangeEl)
|
||||
? selectionRangeEl
|
||||
: elem.GetProperty("range");
|
||||
var start = selectionRange.GetProperty("start");
|
||||
|
||||
return new LspCallHierarchyItem
|
||||
{
|
||||
Name = name,
|
||||
Kind = kind,
|
||||
Location = new LspLocation
|
||||
{
|
||||
FilePath = UriToFile(uri),
|
||||
Line = start.GetProperty("line").GetInt32(),
|
||||
Character = start.GetProperty("character").GetInt32(),
|
||||
},
|
||||
RawItem = elem.Clone(),
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyEntry> ParseIncomingCalls(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyEntry>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!elem.TryGetProperty("from", out var fromEl))
|
||||
continue;
|
||||
|
||||
var item = ParseCallHierarchyItem(fromEl);
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||
? ranges.GetArrayLength()
|
||||
: 0;
|
||||
|
||||
list.Add(new LspCallHierarchyEntry
|
||||
{
|
||||
Name = item.Name,
|
||||
Kind = item.Kind,
|
||||
Location = item.Location,
|
||||
RangeCount = count,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<LspCallHierarchyEntry> ParseOutgoingCalls(JsonElement? result)
|
||||
{
|
||||
var list = new List<LspCallHierarchyEntry>();
|
||||
if (result?.ValueKind != JsonValueKind.Array)
|
||||
return list;
|
||||
|
||||
foreach (var elem in result.Value.EnumerateArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!elem.TryGetProperty("to", out var toEl))
|
||||
continue;
|
||||
|
||||
var item = ParseCallHierarchyItem(toEl);
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||
? ranges.GetArrayLength()
|
||||
: 0;
|
||||
|
||||
list.Add(new LspCallHierarchyEntry
|
||||
{
|
||||
Name = item.Name,
|
||||
Kind = item.Kind,
|
||||
Location = item.Location,
|
||||
RangeCount = count,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string SymbolKindName(int kind) => kind switch
|
||||
{
|
||||
1 => "file", 2 => "module", 3 => "namespace", 4 => "package",
|
||||
@@ -408,3 +702,34 @@ public class LspSymbol
|
||||
public int Line { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} (line {Line + 1})";
|
||||
}
|
||||
|
||||
/// <summary>워크스페이스 심볼 검색 결과.</summary>
|
||||
public class LspWorkspaceSymbol
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation? Location { get; init; }
|
||||
public override string ToString() => Location == null
|
||||
? $"[{Kind}] {Name}"
|
||||
: $"[{Kind}] {Name} @ {Location}";
|
||||
}
|
||||
|
||||
/// <summary>호출 계층 기준 아이템.</summary>
|
||||
public class LspCallHierarchyItem
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation Location { get; init; } = new();
|
||||
public JsonElement RawItem { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} @ {Location}";
|
||||
}
|
||||
|
||||
/// <summary>호출 계층의 incoming/outgoing 엔트리.</summary>
|
||||
public class LspCallHierarchyEntry
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Kind { get; init; } = "";
|
||||
public LspLocation Location { get; init; } = new();
|
||||
public int RangeCount { get; init; }
|
||||
public override string ToString() => $"[{Kind}] {Name} @ {Location} (matches: {RangeCount})";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user