diff --git a/README.md b/README.md index 50bf13f..597ef09 100644 --- a/README.md +++ b/README.md @@ -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 조작을 분리하는 방향에 더 가까워졌고, 이후 실제 가상화 윈도우 정책을 다듬을 때 변경 범위를 더 안전하게 제한할 수 있게 됐습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 86ce3c9..070ab41 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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 정교화 작업을 더 안전하게 진행할 수 있다. diff --git a/src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs b/src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs new file mode 100644 index 0000000..08d05b1 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.TranscriptRenderExecution.cs @@ -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; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs b/src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs new file mode 100644 index 0000000..a1b1e01 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.TranscriptRenderPlanner.cs @@ -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 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 visibleMessages, + IReadOnlyList 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(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, + }; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs b/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs index 43039a7..91c57b9 100644 --- a/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs +++ b/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs @@ -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(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) {