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:
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user