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

@@ -123,8 +123,93 @@ public partial class ChatWindow
}
}
/// <summary>
/// React Virtual DOM reconciliation 방식의 diff 렌더.
/// Incremental(prefix-match)이 실패해도, 키 기반 diff로 삭제/추가만 처리하여
/// 전체 재빌드(Full Render)를 회피합니다.
/// StreamingAppend → Incremental → DiffRender → FullRender 순으로 호출됩니다.
/// </summary>
private bool TryApplyDiffRender(TranscriptRenderPlan renderPlan)
{
// 이전 렌더 기록이 없으면 diff 불가
if (_lastRenderedTimelineKeys.Count == 0 || renderPlan.NewKeys.Count == 0)
return false;
// hiddenCount가 다르면 visible 범위 자체가 달라진 것 — diff 신뢰 불가
if (renderPlan.HiddenCount != _lastRenderedHiddenCount)
return false;
var oldKeys = _lastRenderedTimelineKeys;
var newKeys = renderPlan.NewKeys;
// 변화가 없으면 빠른 경로
if (oldKeys.Count == newKeys.Count && oldKeys.SequenceEqual(newKeys, StringComparer.Ordinal))
{
_lastRenderedTimelineKeys = renderPlan.NewKeys;
return true;
}
try
{
// 1. 기존 키 → 인덱스 매핑
var oldKeyIndex = new Dictionary<string, int>(oldKeys.Count, StringComparer.Ordinal);
for (var i = 0; i < oldKeys.Count; i++)
oldKeyIndex[oldKeys[i]] = i;
var newKeySet = new HashSet<string>(newKeys, StringComparer.Ordinal);
// 2. 라이브 컨테이너 임시 분리
var hadLiveContainer = _agentLiveContainer != null && ContainsTranscriptElement(_agentLiveContainer);
if (hadLiveContainer)
RemoveTranscriptElement(_agentLiveContainer!);
// "더보기" 카드가 있으면 오프셋 1
var loadMoreOffset = renderPlan.HiddenCount > 0 ? 1 : 0;
// 3. 삭제할 항목 제거 (뒤에서부터 — 인덱스 안정성 유지)
for (var i = oldKeys.Count - 1; i >= 0; i--)
{
if (!newKeySet.Contains(oldKeys[i]))
{
var elementIndex = i + loadMoreOffset;
if (elementIndex < GetTranscriptElementCount())
RemoveTranscriptElementAt(elementIndex);
}
}
// 4. 새 항목만 생성·삽입 — 이미 존재하는 키는 건너뜀
foreach (var item in renderPlan.VisibleTimeline)
{
if (!oldKeyIndex.ContainsKey(item.Key))
item.Render();
}
// 5. 라이브 컨테이너 재삽입
if (hadLiveContainer && _agentLiveContainer != null)
AddTranscriptElement(_agentLiveContainer);
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
return true;
}
catch (Exception ex)
{
Services.LogService.Warn($"Diff 렌더 실패, 전체 렌더로 전환: {ex.Message}");
_lastRenderedTimelineKeys.Clear();
return false;
}
}
private void ApplyFullTranscriptRender(TranscriptRenderPlan renderPlan)
{
// 스트리밍 중에는 ItemsSource 분리/재연결을 하지 않음
// — 전체 시각적 트리 파괴 + VirtualizingStackPanel 컨테이너 재생성이 UI 렉의 핵심 원인
// 비스트리밍 시에만 분리/재연결 (대량 초기 로드 시 레이아웃 패스 1회 축소 효과)
var disconnectItemsSource = !_isStreaming;
if (disconnectItemsSource)
MessageList.ItemsSource = null;
ClearTranscriptElements();
_runBannerAnchors.Clear();
@@ -137,6 +222,14 @@ public partial class ChatWindow
if (_agentLiveContainer != null && !ContainsTranscriptElement(_agentLiveContainer))
AddTranscriptElement(_agentLiveContainer);
if (disconnectItemsSource)
{
// ItemsSource 재연결 — 단일 레이아웃 패스
MessageList.ItemsSource = _transcriptElements;
// ItemsSource 변경 시 ScrollViewer가 재생성될 수 있으므로 훅 재연결
AttachTranscriptScrollChanged();
}
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
}