AX Agent 구조를 claw-code 기준으로 추가 정리해 transcript 렌더와 tool streaming 책임을 분리함
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.TranscriptRendering partial을 추가해 transcript windowing, 증분 렌더, 스크롤 보존 로직을 메인 ChatWindow.xaml.cs에서 분리 - StreamingToolExecutionCoordinator를 도입해 read-only 도구 prefetch, tool-use 스트리밍 수신, context overflow/transient error 복구를 별도 계층으로 이동 - AgentLoopRuntimeThresholds helper를 추가해 no-tool, plan retry, terminal evidence gate 임계값 계산을 AgentLoopService에서 분리 - AgentLoopTransitions.Execution은 coordinator thin wrapper 중심 구조로 정리해 이후 executor 고도화와 정책 변경이 덜 위험하도록 개선 - README와 docs/DEVELOPMENT.md를 2026-04-09 09:14 (KST) 기준으로 갱신 - 검증: 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:
169
src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs
Normal file
169
src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private int GetActiveTimelineRenderLimit()
|
||||
{
|
||||
if (!_isStreaming)
|
||||
return _timelineRenderLimit;
|
||||
|
||||
var streamingLimit = IsLightweightLiveProgressMode()
|
||||
? TimelineLightweightStreamingRenderLimit
|
||||
: TimelineStreamingRenderLimit;
|
||||
return Math.Min(_timelineRenderLimit, streamingLimit);
|
||||
}
|
||||
|
||||
private void RenderMessages(bool preserveViewport = false)
|
||||
{
|
||||
var previousScrollableHeight = GetTranscriptScrollableHeight();
|
||||
var previousVerticalOffset = GetTranscriptVerticalOffset();
|
||||
|
||||
ChatConversation? conv;
|
||||
lock (_convLock) conv = _currentConversation;
|
||||
_appState.RestoreAgentRunHistory(conv?.AgentRunHistory);
|
||||
|
||||
var visibleMessages = GetVisibleTimelineMessages(conv);
|
||||
var visibleEvents = GetVisibleTimelineEvents(conv);
|
||||
|
||||
if (_isStreaming && preserveViewport
|
||||
&& visibleMessages.Count == _lastRenderedMessageCount
|
||||
&& visibleEvents.Count == _lastRenderedEventCount
|
||||
&& (conv?.ShowExecutionHistory ?? true) == _lastRenderedShowHistory
|
||||
&& string.Equals(_lastRenderedConversationId, conv?.Id, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (conv == null || (visibleMessages.Count == 0 && visibleEvents.Count == 0))
|
||||
{
|
||||
ClearTranscriptElements();
|
||||
_runBannerAnchors.Clear();
|
||||
_lastRenderedTimelineKeys.Clear();
|
||||
_lastRenderedMessageCount = 0;
|
||||
_lastRenderedEventCount = 0;
|
||||
EmptyState.Visibility = System.Windows.Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(_lastRenderedConversationId, conv.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_lastRenderedConversationId = conv.Id;
|
||||
_timelineRenderLimit = TimelineRenderPageSize;
|
||||
_elementCache.Clear();
|
||||
_lastRenderedTimelineKeys.Clear();
|
||||
_lastRenderedMessageCount = 0;
|
||||
_lastRenderedEventCount = 0;
|
||||
InvalidateTimelineCache();
|
||||
}
|
||||
|
||||
var showHistory = conv.ShowExecutionHistory;
|
||||
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
|
||||
|
||||
var orderedTimeline = BuildTimelineRenderActions(visibleMessages, visibleEvents);
|
||||
var effectiveRenderLimit = GetActiveTimelineRenderLimit();
|
||||
var hiddenCount = Math.Max(0, orderedTimeline.Count - effectiveRenderLimit);
|
||||
var visibleTimeline = hiddenCount > 0
|
||||
? orderedTimeline.GetRange(hiddenCount, orderedTimeline.Count - hiddenCount)
|
||||
: orderedTimeline;
|
||||
var newKeys = new List<string>(visibleTimeline.Count);
|
||||
foreach (var t in visibleTimeline)
|
||||
newKeys.Add(t.Key);
|
||||
|
||||
var incremented = false;
|
||||
var hasExternalChildren = _agentLiveContainer != null && ContainsTranscriptElement(_agentLiveContainer);
|
||||
var expectedChildCount = _lastRenderedTimelineKeys.Count + (_lastRenderedHiddenCount > 0 ? 1 : 0);
|
||||
var canIncremental = !hasExternalChildren
|
||||
&& _lastRenderedTimelineKeys.Count > 0
|
||||
&& newKeys.Count >= _lastRenderedTimelineKeys.Count
|
||||
&& _lastRenderedHiddenCount == hiddenCount
|
||||
&& GetTranscriptElementCount() == expectedChildCount;
|
||||
|
||||
if (canIncremental)
|
||||
{
|
||||
var prevLiveCount = 0;
|
||||
for (var i = _lastRenderedTimelineKeys.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_lastRenderedTimelineKeys[i].StartsWith("_live_", StringComparison.Ordinal))
|
||||
prevLiveCount++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var prevStableCount = _lastRenderedTimelineKeys.Count - prevLiveCount;
|
||||
var prefixMatch = true;
|
||||
for (var i = 0; i < prevStableCount; i++)
|
||||
{
|
||||
if (i >= newKeys.Count || !string.Equals(_lastRenderedTimelineKeys[i], newKeys[i], StringComparison.Ordinal))
|
||||
{
|
||||
prefixMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefixMatch)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var r = 0; r < prevLiveCount && GetTranscriptElementCount() > 0; r++)
|
||||
RemoveTranscriptElementAt(GetTranscriptElementCount() - 1);
|
||||
|
||||
for (var i = prevStableCount; i < visibleTimeline.Count; i++)
|
||||
visibleTimeline[i].Render();
|
||||
|
||||
_lastRenderedTimelineKeys = newKeys;
|
||||
_lastRenderedHiddenCount = hiddenCount;
|
||||
_lastRenderedMessageCount = visibleMessages.Count;
|
||||
_lastRenderedEventCount = visibleEvents.Count;
|
||||
_lastRenderedShowHistory = showHistory;
|
||||
incremented = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"증분 렌더링 실패, 전체 재빌드로 전환: {ex.Message}");
|
||||
_lastRenderedTimelineKeys.Clear();
|
||||
incremented = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!incremented)
|
||||
{
|
||||
ClearTranscriptElements();
|
||||
_runBannerAnchors.Clear();
|
||||
|
||||
if (hiddenCount > 0)
|
||||
AddTranscriptElement(CreateTimelineLoadMoreCard(hiddenCount));
|
||||
|
||||
foreach (var item in visibleTimeline)
|
||||
item.Render();
|
||||
|
||||
if (_agentLiveContainer != null && !ContainsTranscriptElement(_agentLiveContainer))
|
||||
AddTranscriptElement(_agentLiveContainer);
|
||||
|
||||
_lastRenderedTimelineKeys = newKeys;
|
||||
_lastRenderedHiddenCount = hiddenCount;
|
||||
_lastRenderedMessageCount = visibleMessages.Count;
|
||||
_lastRenderedEventCount = visibleEvents.Count;
|
||||
_lastRenderedShowHistory = showHistory;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user