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:
2026-04-12 22:02:14 +09:00
parent b8f4df1892
commit fb0bea41f7
137 changed files with 18532 additions and 1144 deletions

View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── V2 렌더 상태 ───────────────────────────────────────────────────
private string? _v2LastRenderedConversationId;
private int _v2LastRenderedMessageCount;
private int _v2LastRenderedEventCount;
private readonly List<string> _v2LastRenderedKeys = new();
// V2 라이브 프로그레스 상태
private StackPanel? _v2LiveContainer;
private readonly Dictionary<string, Border> _v2LiveToolCards = new();
private string? _v2LastLiveToolCallId;
private void RenderMessagesV2(
ChatConversation conv,
IReadOnlyList<ChatMessage> visibleMessages,
IReadOnlyList<ChatExecutionEvent> visibleEvents,
bool preserveViewport,
double previousScrollableHeight,
double previousVerticalOffset,
Stopwatch renderStopwatch,
string? caller)
{
try
{
// 대화 전환 감지 → 캐시 초기화
if (!string.Equals(_v2LastRenderedConversationId, conv.Id, StringComparison.OrdinalIgnoreCase))
{
_v2LastRenderedConversationId = conv.Id;
_v2LastRenderedKeys.Clear();
_v2LastRenderedMessageCount = 0;
_v2LastRenderedEventCount = 0;
_elementCache.Clear();
}
// 통합 타임라인 빌드 (메시지 + 이벤트를 시간순 병합)
var timeline = BuildV2Timeline(visibleMessages, visibleEvents);
// 새 키 목록 생성
var newKeys = new List<string>(timeline.Count);
foreach (var item in timeline)
newKeys.Add(item.Key);
// 인크리멘탈 렌더 시도: 기존 키가 새 키의 접두사인 경우 추가분만 렌더
var canIncremental = _v2LastRenderedKeys.Count > 0
&& newKeys.Count >= _v2LastRenderedKeys.Count
&& KeysArePrefixMatch(_v2LastRenderedKeys, newKeys);
if (canIncremental)
{
// 라이브 컨테이너가 있으면 임시 제거 (맨 끝에 다시 추가)
if (_v2LiveContainer != null && ContainsTranscriptElement(_v2LiveContainer))
RemoveTranscriptElement(_v2LiveContainer);
for (int i = _v2LastRenderedKeys.Count; i < timeline.Count; i++)
{
var item = timeline[i];
AddDeferredTranscriptElement(item.Key, () =>
{
if (_elementCache.TryGetValue(item.Key, out var cached))
return cached;
var element = item.CreateElement();
_elementCache[item.Key] = element;
return element;
});
}
// 라이브 컨테이너 재삽입
if (_v2LiveContainer != null && _isStreaming)
AddTranscriptElement(_v2LiveContainer);
}
else
{
// 전체 재빌드
ClearTranscriptElements();
foreach (var item in timeline)
{
var capturedItem = item;
AddDeferredTranscriptElement(capturedItem.Key, () =>
{
if (_elementCache.TryGetValue(capturedItem.Key, out var cached))
return cached;
var element = capturedItem.CreateElement();
_elementCache[capturedItem.Key] = element;
return element;
});
}
// 라이브 컨테이너 재삽입
if (_v2LiveContainer != null && _isStreaming)
AddTranscriptElement(_v2LiveContainer);
}
_v2LastRenderedKeys.Clear();
_v2LastRenderedKeys.AddRange(newKeys);
_v2LastRenderedMessageCount = visibleMessages.Count;
_v2LastRenderedEventCount = visibleEvents.Count;
}
catch (Exception ex)
{
LogService.Error($"[V2Render] 렌더링 예외: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}");
}
_lastRenderTicks = Environment.TickCount64;
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
renderStopwatch.Stop();
if (renderStopwatch.ElapsedMilliseconds >= (_isStreaming ? 100 : 24))
{
AgentPerformanceLogService.LogMetric(
"transcript", "render_messages_v2", conv.Id, _activeTab ?? "",
renderStopwatch.ElapsedMilliseconds,
new { preserveViewport, streaming = _isStreaming, visibleMessages = visibleMessages.Count, visibleEvents = visibleEvents.Count });
}
if (!preserveViewport)
{
_ = Dispatcher.InvokeAsync(ScrollTranscriptToEnd, DispatcherPriority.Background);
return;
}
_ = Dispatcher.InvokeAsync(() =>
{
if (_transcriptScrollViewer == null) return;
var newScrollableHeight = GetTranscriptScrollableHeight();
var delta = newScrollableHeight - previousScrollableHeight;
var targetOffset = Math.Max(0, previousVerticalOffset + Math.Max(0, delta));
ScrollTranscriptToVerticalOffset(targetOffset);
}, DispatcherPriority.Background);
}
private static bool KeysArePrefixMatch(List<string> oldKeys, List<string> newKeys)
{
for (int i = 0; i < oldKeys.Count; i++)
{
if (!string.Equals(oldKeys[i], newKeys[i], StringComparison.Ordinal))
return false;
}
return true;
}
private sealed record V2TimelineItem(string Key, DateTime Timestamp, Func<UIElement> CreateElement);
private List<V2TimelineItem> BuildV2Timeline(
IReadOnlyList<ChatMessage> visibleMessages,
IReadOnlyList<ChatExecutionEvent> visibleEvents)
{
var timeline = new List<V2TimelineItem>(visibleMessages.Count + visibleEvents.Count);
// 1. 메시지 추가
foreach (var msg in visibleMessages)
{
var capturedMsg = msg;
var key = $"v2m_{msg.MsgId}";
timeline.Add(new V2TimelineItem(key, msg.Timestamp, () => CreateV2MessageElement(capturedMsg)));
}
// 2. 실행 이벤트 추가 — ToolCall+ToolResult 쌍을 병합
var eventIndex = 0;
var events = visibleEvents.ToList();
for (int i = 0; i < events.Count; i++)
{
var executionEvent = events[i];
var agentEvent = ToAgentEvent(executionEvent);
// SessionStart / UserPromptSubmit 숨김
if (agentEvent.Type == AgentEventType.SessionStart || agentEvent.Type == AgentEventType.UserPromptSubmit)
continue;
var key = $"v2e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
// ToolCall → 바로 다음 같은 도구의 ToolResult를 찾아 병합
if (agentEvent.Type == AgentEventType.ToolCall && i + 1 < events.Count)
{
var nextEvent = ToAgentEvent(events[i + 1]);
if (nextEvent.Type == AgentEventType.ToolResult
&& string.Equals(nextEvent.ToolName, agentEvent.ToolName, StringComparison.OrdinalIgnoreCase))
{
var capturedCall = agentEvent;
var capturedResult = nextEvent;
timeline.Add(new V2TimelineItem(key, executionEvent.Timestamp,
() => CreateV2ToolExecutionCard(capturedCall, capturedResult)));
i++; // ToolResult 스킵
continue;
}
}
var capturedEvent = agentEvent;
timeline.Add(new V2TimelineItem(key, executionEvent.Timestamp,
() => CreateV2AgentEventElement(capturedEvent)));
}
// 시간순 정렬
var needsSort = false;
for (int i = 1; i < timeline.Count; i++)
{
if (timeline[i].Timestamp < timeline[i - 1].Timestamp)
{
needsSort = true;
break;
}
}
if (needsSort)
timeline.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
return timeline;
}
}