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

@@ -8,6 +8,11 @@ namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 렌더링 쓰로틀: 스트리밍 중 최소 간격 보장 ───────────────────────
private long _lastRenderTicks;
private const long MinStreamingRenderIntervalMs = 1500; // 스트리밍 중 최소 1.5초 간격
private const long MinIdleRenderIntervalMs = 300; // 비스트리밍(유휴) 시 최소 300ms 간격
private int GetActiveTimelineRenderLimit()
{
if (!_isStreaming)
@@ -19,12 +24,49 @@ public partial class ChatWindow
return Math.Min(_timelineRenderLimit, streamingLimit);
}
private void RenderMessages(bool preserveViewport = false)
private void RenderMessages(bool preserveViewport = false, [System.Runtime.CompilerServices.CallerMemberName] string? caller = null)
{
// B-4: 비가시 상태일 때 렌더링 차단 — 최소화/숨김 시 불필요한 UI 재구축 방지
if (this.WindowState == System.Windows.WindowState.Minimized || !IsVisible)
return;
var now = Environment.TickCount64;
// B-5: 스트리밍 중 쓰로틀 — preserveViewport=true (타이머 기반) 호출만 제한
// preserveViewport=false는 사용자 메시지 전송 등 중요 렌더이므로 항상 허용
if (_isStreaming && preserveViewport)
{
if (now - _lastRenderTicks < MinStreamingRenderIntervalMs)
return;
}
// B-7: 유휴 상태 렌더 쓰로틀 — 빈 대화에서 반복 호출 방지 (UI 프리징 원인)
// 대화 내용이 바뀌지 않았는데 짧은 간격으로 반복 호출되면 무시
if (!_isStreaming && now - _lastRenderTicks < MinIdleRenderIntervalMs)
{
ChatConversation? quickConv;
lock (_convLock) quickConv = _currentConversation;
var quickMsgCount = quickConv?.Messages?.Count ?? 0;
var quickEvtCount = quickConv?.ExecutionEvents?.Count ?? 0;
// 대화 내용이 마지막 렌더와 같으면 스킵 (빈 대화 반복 렌더 차단)
if (quickMsgCount == _lastRenderedMessageCount
&& quickEvtCount == _lastRenderedEventCount
&& string.Equals(_lastRenderedConversationId, quickConv?.Id, StringComparison.OrdinalIgnoreCase))
{
return;
}
// B-7b: 빈 대화 간 convId 플래핑 방지 — 둘 다 메시지 0개면
// convId가 달라도 빈 화면 렌더를 반복할 이유 없음 (SwitchToTabConversation 스팸 차단)
// 단, preserveViewport=false(탭 전환 등 명시적 렌더)는 차단하지 않음
// — 탭 전환 시 EmptyState/마스코트 표시에 필요
if (preserveViewport
&& quickMsgCount == 0 && quickEvtCount == 0
&& _lastRenderedMessageCount == 0 && _lastRenderedEventCount == 0)
{
return;
}
}
var renderStopwatch = Stopwatch.StartNew();
var previousScrollableHeight = GetTranscriptScrollableHeight();
var previousVerticalOffset = GetTranscriptVerticalOffset();
@@ -36,6 +78,13 @@ public partial class ChatWindow
var visibleMessages = GetVisibleTimelineMessages(conv);
var visibleEvents = GetVisibleTimelineEvents(conv);
// 진단 로그: 렌더링 호출 시점의 상태 추적
Services.LogService.Info($"[Render] caller={caller}, preserveViewport={preserveViewport}, streaming={_isStreaming}, " +
$"convId={conv?.Id?[..Math.Min(8, conv?.Id?.Length ?? 0)]}, " +
$"rawMsgCount={conv?.Messages?.Count ?? 0}, visibleMsg={visibleMessages.Count}, " +
$"visibleEvt={visibleEvents.Count}, emptyState={EmptyState.Visibility}, " +
$"transcriptElements={GetTranscriptElementCount()}");
if (_isStreaming && preserveViewport
&& visibleMessages.Count == _lastRenderedMessageCount
&& visibleEvents.Count == _lastRenderedEventCount
@@ -45,12 +94,25 @@ public partial class ChatWindow
if (conv == null || (visibleMessages.Count == 0 && visibleEvents.Count == 0))
{
ClearTranscriptElements();
_runBannerAnchors.Clear();
_lastRenderedTimelineKeys.Clear();
_lastRenderedMessageCount = 0;
_lastRenderedEventCount = 0;
EmptyState.Visibility = System.Windows.Visibility.Visible;
// 스트리밍 중이거나 대화에 원본 메시지가 있으면 EmptyState를 표시하지 않음
// (GetVisibleTimelineMessages의 필터링으로 visibleMessages가 0이 되어도 원본은 존재)
bool hasRawMessages = (conv?.Messages?.Count ?? 0) > 0;
if (!_isStreaming && !hasRawMessages)
{
ClearTranscriptElements();
_runBannerAnchors.Clear();
_lastRenderedTimelineKeys.Clear();
_lastRenderedMessageCount = 0;
_lastRenderedEventCount = 0;
EmptyState.Visibility = System.Windows.Visibility.Visible;
StartMascotAnimation();
}
else
{
// 메시지가 있거나 스트리밍 중 → EmptyState 강제 숨김
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
StopMascotAnimation();
}
return;
}
@@ -66,19 +128,43 @@ public partial class ChatWindow
}
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
var renderPlan = BuildTranscriptRenderPlan(conv, visibleMessages, visibleEvents);
StopMascotAnimation();
// B-3: 스트리밍 전용 빠른 경로 → 일반 인크리멘탈 → 전체 재빌드
if (!TryApplyStreamingAppendRender(renderPlan)
&& !TryApplyIncrementalTranscriptRender(renderPlan))
ApplyFullTranscriptRender(renderPlan);
PruneTranscriptElementCache(renderPlan.NewKeys);
// V2 렌더링 분기 — 설정 토글로 Claude Code 스타일 상세 이력 UI 활성화
if (_settings.Settings.Llm.EnableNewChatRendering)
{
RenderMessagesV2(conv, visibleMessages, visibleEvents, preserveViewport,
previousScrollableHeight, previousVerticalOffset, renderStopwatch, caller);
return;
}
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
_lastRenderedShowHistory = renderPlan.ShowHistory;
try
{
var renderPlan = BuildTranscriptRenderPlan(conv, visibleMessages, visibleEvents);
Services.LogService.Info($"[Render] plan: items={renderPlan.VisibleTimeline.Count}, hidden={renderPlan.HiddenCount}, " +
$"canIncremental={renderPlan.CanIncremental}, keys={renderPlan.NewKeys.Count}");
// B-3: 스트리밍 전용 빠른 경로 → 일반 인크리멘탈 → Diff(Virtual DOM) → 전체 재빌드
if (!TryApplyStreamingAppendRender(renderPlan)
&& !TryApplyIncrementalTranscriptRender(renderPlan)
&& !TryApplyDiffRender(renderPlan))
ApplyFullTranscriptRender(renderPlan);
PruneTranscriptElementCache(renderPlan.NewKeys);
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
_lastRenderedShowHistory = renderPlan.ShowHistory;
}
catch (Exception renderEx)
{
Services.LogService.Error($"[Render] 렌더링 파이프라인 예외: {renderEx.GetType().Name}: {renderEx.Message}\n{renderEx.StackTrace}");
}
_lastRenderTicks = Environment.TickCount64; // 쓰로틀 타임스탬프 갱신
renderStopwatch.Stop();
if (renderStopwatch.ElapsedMilliseconds >= 24 || _isStreaming)
// B-6: 스트리밍 중 로깅 빈도 축소 — 100ms 미만 렌더는 기록하지 않음 (UI 부하 감소)
if (renderStopwatch.ElapsedMilliseconds >= (_isStreaming ? 100 : 24))
{
AgentPerformanceLogService.LogMetric(
"transcript",
@@ -93,14 +179,7 @@ public partial class ChatWindow
lightweight = IsLightweightLiveProgressMode(),
visibleMessages = visibleMessages.Count,
visibleEvents = visibleEvents.Count,
renderedItems = renderPlan.NewKeys.Count,
hiddenCount = renderPlan.HiddenCount,
transcriptElements = GetTranscriptElementCount(),
processFeedAppends = _processFeedAppendCount,
processFeedMerges = _processFeedMergeCount,
rowKindCounts = _transcriptRowKindCounts.ToDictionary(
pair => pair.Key.ToString(),
pair => pair.Value),
});
}