AX Agent transcript 렌더 계획/적용 구조를 분리해 유지보수성을 높이고 문서를 갱신한다
Some checks failed
Release Gate / gate (push) Has been cancelled
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:
@@ -1527,3 +1527,7 @@ MIT License
|
||||
- [StreamingToolExecutionCoordinator.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/StreamingToolExecutionCoordinator.cs)를 추가해 read-only 도구 prefetch, tool-use 스트리밍 수신, context overflow/transient error 복구를 별도 coordinator 계층으로 분리했습니다.
|
||||
- [AgentLoopRuntimeThresholds.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopRuntimeThresholds.cs)를 추가해 no-tool, plan retry, terminal evidence gate 같은 임계값 계산을 `AgentLoopService`에서 분리했습니다.
|
||||
- 결과적으로 Cowork/Code의 핵심 루프는 정책 소비자에 더 가까워졌고, 이후 transcript 진짜 가상화와 모델별 실행 정책 조정도 덜 위험하게 진행할 수 있는 구조가 됐습니다.
|
||||
- 업데이트: 2026-04-09 09:37 (KST)
|
||||
- transcript 렌더 구조를 planning/execution 단계로 한 번 더 쪼갰습니다. [ChatWindow.TranscriptRenderPlanner.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs) 에서 visible window 계산, render key 집계, 전체/증분 렌더 계획 생성을 맡기고, [ChatWindow.TranscriptRenderExecution.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs) 에서 host 적용과 viewport 보존을 맡기도록 정리했습니다.
|
||||
- [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)의 `RenderMessages()`는 이제 `데이터 수집 -> render plan 생성 -> 증분/전체 적용`만 오케스트레이션하는 얇은 진입점이 됐습니다.
|
||||
- `claw-code`의 `Messages.tsx`와 `VirtualMessageList.tsx`처럼 transcript planning과 실제 host 조작을 분리하는 방향에 더 가까워졌고, 이후 실제 가상화 윈도우 정책을 다듬을 때 변경 범위를 더 안전하게 제한할 수 있게 됐습니다.
|
||||
|
||||
@@ -5520,3 +5520,19 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- 구조 효과
|
||||
- transcript 렌더링과 tool streaming 복구 정책의 책임 경계가 더 분명해졌다.
|
||||
- 이후 남은 큰 작업인 `진짜 transcript 가상화`와 `AgentLoopService 추가 분해`를 더 작은 변경 단위로 진행할 수 있는 기반을 마련했다.
|
||||
|
||||
## 2026-04-09 09:37 (KST)
|
||||
|
||||
- [ChatWindow.TranscriptRenderPlanner.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs)
|
||||
- `TranscriptRenderPlan`을 도입하고, transcript 렌더 계획 생성 책임을 별도 partial로 분리했다.
|
||||
- visible 메시지/실행 이벤트를 수집한 뒤 render key 리스트와 `FullRefresh` 필요 여부를 계산하도록 정리했다.
|
||||
- 결과적으로 `RenderMessages()`는 더 이상 timeline 조립과 diff 판정을 함께 하지 않고 planner에서 render intent를 받는다.
|
||||
- [ChatWindow.TranscriptRenderExecution.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs)
|
||||
- `TryApplyIncrementalTranscriptRender(...)`, `ApplyFullTranscriptRender(...)`를 분리해 render plan 적용 책임을 별도 partial로 이동했다.
|
||||
- transcript host에 대한 add/replace/remove와 viewport 보존 시점을 planner와 분리해, 이후 virtualization window 정책과 render batching을 독립적으로 조정할 수 있는 구조를 만들었다.
|
||||
- [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)
|
||||
- `RenderMessages()`는 `데이터 수집 -> planner 호출 -> incremental/full 적용 -> viewport finalize` 순서만 담당하는 orchestration 메서드로 축소했다.
|
||||
- `claw-code`의 `Messages.tsx` + `VirtualMessageList.tsx`처럼 planning과 host 조작을 분리하는 방향으로 더 가까워졌고, transcript 변경의 파급 범위를 줄일 수 있게 됐다.
|
||||
- 구조 효과
|
||||
- transcript 렌더의 변경 포인트가 `계획 생성`과 `실행 적용`으로 명확히 나뉘었다.
|
||||
- 향후 실제 가상화 강화, render batching, incremental diff 정교화 작업을 더 안전하게 진행할 수 있다.
|
||||
|
||||
60
src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs
Normal file
60
src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
65
src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs
Normal file
65
src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user