AX Agent transcript 렌더 계획/적용 구조를 분리해 유지보수성을 높이고 문서를 갱신한다
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow transcript 렌더를 planner/execution 단계로 분리해 RenderMessages 오케스트레이션을 단순화함
- TranscriptRenderPlanner/TranscriptRenderExecution partial을 추가해 planning과 host 적용 책임을 나눔
- README와 DEVELOPMENT 문서에 2026-04-09 09:37 (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:
2026-04-09 00:39:47 +09:00
parent 8643562319
commit 594d38e4a9
5 changed files with 151 additions and 88 deletions

View File

@@ -0,0 +1,60 @@
namespace AxCopilot.Views;
public partial class ChatWindow
{
private bool TryApplyIncrementalTranscriptRender(TranscriptRenderPlan renderPlan)
{
if (!renderPlan.CanIncremental)
return false;
var prefixMatch = true;
for (var i = 0; i < renderPlan.PreviousStableCount; i++)
{
if (i >= renderPlan.NewKeys.Count || !string.Equals(_lastRenderedTimelineKeys[i], renderPlan.NewKeys[i], StringComparison.Ordinal))
{
prefixMatch = false;
break;
}
}
if (!prefixMatch)
return false;
try
{
for (var removeIndex = 0; removeIndex < renderPlan.PreviousLiveCount && GetTranscriptElementCount() > 0; removeIndex++)
RemoveTranscriptElementAt(GetTranscriptElementCount() - 1);
for (var i = renderPlan.PreviousStableCount; i < renderPlan.VisibleTimeline.Count; i++)
renderPlan.VisibleTimeline[i].Render();
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
return true;
}
catch (Exception ex)
{
Services.LogService.Warn($"증분 transcript 렌더 실패, 전체 렌더로 전환: {ex.Message}");
_lastRenderedTimelineKeys.Clear();
return false;
}
}
private void ApplyFullTranscriptRender(TranscriptRenderPlan renderPlan)
{
ClearTranscriptElements();
_runBannerAnchors.Clear();
if (renderPlan.HiddenCount > 0)
AddTranscriptElement(CreateTimelineLoadMoreCard(renderPlan.HiddenCount));
foreach (var item in renderPlan.VisibleTimeline)
item.Render();
if (_agentLiveContainer != null && !ContainsTranscriptElement(_agentLiveContainer))
AddTranscriptElement(_agentLiveContainer);
_lastRenderedTimelineKeys = renderPlan.NewKeys;
_lastRenderedHiddenCount = renderPlan.HiddenCount;
}
}

View File

@@ -0,0 +1,65 @@
using AxCopilot.Models;
namespace AxCopilot.Views;
public partial class ChatWindow
{
private sealed class TranscriptRenderPlan
{
public required List<(string Key, DateTime Timestamp, int Order, Action Render)> VisibleTimeline { get; init; }
public required List<string> NewKeys { get; init; }
public required bool ShowHistory { get; init; }
public required int HiddenCount { get; init; }
public required bool CanIncremental { get; init; }
public required int PreviousLiveCount { get; init; }
public required int PreviousStableCount { get; init; }
}
private TranscriptRenderPlan BuildTranscriptRenderPlan(
ChatConversation conv,
IReadOnlyList<ChatMessage> visibleMessages,
IReadOnlyList<ChatExecutionEvent> visibleEvents)
{
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 item in visibleTimeline)
newKeys.Add(item.Key);
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;
var previousLiveCount = 0;
if (canIncremental)
{
for (var i = _lastRenderedTimelineKeys.Count - 1; i >= 0; i--)
{
if (_lastRenderedTimelineKeys[i].StartsWith("_live_", StringComparison.Ordinal))
previousLiveCount++;
else
break;
}
}
return new TranscriptRenderPlan
{
VisibleTimeline = visibleTimeline,
NewKeys = newKeys,
ShowHistory = conv.ShowExecutionHistory,
HiddenCount = hiddenCount,
CanIncremental = canIncremental,
PreviousLiveCount = previousLiveCount,
PreviousStableCount = _lastRenderedTimelineKeys.Count - previousLiveCount,
};
}
}

View File

@@ -1,6 +1,5 @@
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
@@ -58,96 +57,15 @@ public partial class ChatWindow
InvalidateTimelineCache();
}
var showHistory = conv.ShowExecutionHistory;
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
var renderPlan = BuildTranscriptRenderPlan(conv, visibleMessages, visibleEvents);
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);
if (!TryApplyIncrementalTranscriptRender(renderPlan))
ApplyFullTranscriptRender(renderPlan);
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;
}
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
_lastRenderedShowHistory = renderPlan.ShowHistory;
if (!preserveViewport)
{