AX Agent 진행 시간·글로우 경로 정리 및 최근 로컬 변경 일괄 반영
- AX Agent 스트리밍 경과 시간을 공용 helper로 통일해 비정상적인 수천만 시간 표시를 방지함 - 채팅 입력창 글로우를 런처와 같은 표시/숨김 중심의 얇은 외곽 글로우로 정리하고 런처 글로우 설정은 일반 설정에 유지함 - README와 DEVELOPMENT 문서를 2026-04-08 12:02 (KST) 기준으로 갱신하고 Release 빌드 경고 0 / 오류 0을 확인함
This commit is contained in:
@@ -11,9 +11,28 @@ namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
// 스트리밍 중 LINQ 재실행 방지용 캐시
|
||||
private List<ChatMessage>? _cachedVisibleMessages;
|
||||
private int _cachedVisibleMessagesSourceCount = -1;
|
||||
private List<ChatExecutionEvent>? _cachedVisibleEvents;
|
||||
private int _cachedVisibleEventsSourceCount = -1;
|
||||
private bool _cachedVisibleEventsShowHistory;
|
||||
|
||||
private void InvalidateTimelineCache()
|
||||
{
|
||||
_cachedVisibleMessages = null;
|
||||
_cachedVisibleMessagesSourceCount = -1;
|
||||
_cachedVisibleEvents = null;
|
||||
_cachedVisibleEventsSourceCount = -1;
|
||||
}
|
||||
|
||||
private List<ChatMessage> GetVisibleTimelineMessages(ChatConversation? conversation)
|
||||
{
|
||||
return conversation?.Messages?.Where(msg =>
|
||||
var sourceCount = conversation?.Messages?.Count ?? 0;
|
||||
if (_cachedVisibleMessages != null && sourceCount == _cachedVisibleMessagesSourceCount)
|
||||
return _cachedVisibleMessages;
|
||||
|
||||
var result = conversation?.Messages?.Where(msg =>
|
||||
{
|
||||
if (string.Equals(msg.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
@@ -24,15 +43,36 @@ public partial class ChatWindow
|
||||
|
||||
return true;
|
||||
}).ToList() ?? new List<ChatMessage>();
|
||||
|
||||
_cachedVisibleMessages = result;
|
||||
_cachedVisibleMessagesSourceCount = sourceCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
|
||||
{
|
||||
var events = conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>();
|
||||
if (conversation?.ShowExecutionHistory ?? true)
|
||||
return events;
|
||||
var sourceCount = conversation?.ExecutionEvents?.Count ?? 0;
|
||||
var showHistory = conversation?.ShowExecutionHistory ?? true;
|
||||
if (_cachedVisibleEvents != null
|
||||
&& sourceCount == _cachedVisibleEventsSourceCount
|
||||
&& showHistory == _cachedVisibleEventsShowHistory)
|
||||
return _cachedVisibleEvents;
|
||||
|
||||
return events.Where(ShouldShowCollapsedProgressEvent).ToList();
|
||||
List<ChatExecutionEvent> result;
|
||||
if (showHistory)
|
||||
{
|
||||
result = conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (conversation?.ExecutionEvents ?? Enumerable.Empty<ChatExecutionEvent>())
|
||||
.Where(ShouldShowCollapsedProgressEvent).ToList();
|
||||
}
|
||||
|
||||
_cachedVisibleEvents = result;
|
||||
_cachedVisibleEventsSourceCount = sourceCount;
|
||||
_cachedVisibleEventsShowHistory = showHistory;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent)
|
||||
@@ -64,17 +104,17 @@ public partial class ChatWindow
|
||||
or "csv_create" or "markdown_create" or "md_create" or "script_create"
|
||||
or "pptx_create";
|
||||
|
||||
private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions(
|
||||
private List<(string Key, DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions(
|
||||
IReadOnlyCollection<ChatMessage> visibleMessages,
|
||||
IReadOnlyCollection<ChatExecutionEvent> visibleEvents)
|
||||
{
|
||||
var timeline = new List<(DateTime Timestamp, int Order, Action Render)>(visibleMessages.Count + visibleEvents.Count);
|
||||
var timeline = new List<(string Key, DateTime Timestamp, int Order, Action Render)>(visibleMessages.Count + visibleEvents.Count);
|
||||
|
||||
foreach (var msg in visibleMessages)
|
||||
{
|
||||
var capturedMsg = msg;
|
||||
var cacheKey = $"m_{msg.MsgId}";
|
||||
timeline.Add((msg.Timestamp, 0, () =>
|
||||
timeline.Add((cacheKey, msg.Timestamp, 0, () =>
|
||||
{
|
||||
// 캐시된 버블이 있으면 재생성 없이 재사용 (O(1) Add vs 전체 재생성)
|
||||
if (_elementCache.TryGetValue(cacheKey, out var cached))
|
||||
@@ -88,6 +128,9 @@ public partial class ChatWindow
|
||||
var showFullHistory = _currentConversation?.ShowExecutionHistory ?? true;
|
||||
var activeRunId = _isStreaming ? (_appState.AgentRun.RunId ?? "") : "";
|
||||
|
||||
var eventIndex = 0;
|
||||
string? prevToolCallName = null;
|
||||
int consecutiveToolCallCount = 0;
|
||||
foreach (var executionEvent in visibleEvents)
|
||||
{
|
||||
// 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시
|
||||
@@ -101,7 +144,28 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
var restoredEvent = ToAgentEvent(executionEvent);
|
||||
timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
||||
|
||||
// 접힌 모드: 연속 동일 ToolCall 병합 (예: document_read 3회 → 1개 pill)
|
||||
if (!showFullHistory && restoredEvent.Type == AgentEventType.ToolCall
|
||||
&& !string.IsNullOrWhiteSpace(restoredEvent.ToolName))
|
||||
{
|
||||
if (string.Equals(prevToolCallName, restoredEvent.ToolName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
consecutiveToolCallCount++;
|
||||
continue; // 연속 중복 스킵
|
||||
}
|
||||
// 이전 연속 카운트가 있었으면 이전 pill에 반영됨
|
||||
prevToolCallName = restoredEvent.ToolName;
|
||||
consecutiveToolCallCount = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevToolCallName = null;
|
||||
consecutiveToolCallCount = 0;
|
||||
}
|
||||
|
||||
var eventKey = $"e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
|
||||
timeline.Add((eventKey, executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
||||
}
|
||||
|
||||
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
|
||||
@@ -109,14 +173,32 @@ public partial class ChatWindow
|
||||
{
|
||||
var capturedSteps = _currentRunProgressSteps.ToList();
|
||||
var cardTimestamp = capturedSteps[^1].Timestamp;
|
||||
timeline.Add((cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
|
||||
// 통합 진행 카드는 매번 내용이 바뀌므로 volatile 키 사용
|
||||
timeline.Add(("_live_progress", cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
|
||||
}
|
||||
|
||||
var liveProgressHint = GetLiveAgentProgressHint();
|
||||
if (liveProgressHint != null)
|
||||
timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
|
||||
timeline.Add(("_live_hint", liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
|
||||
|
||||
return timeline.OrderBy(x => x.Timestamp).ThenBy(x => x.Order).ToList();
|
||||
// 대부분 이미 시간순이므로 정렬 필요 여부를 먼저 확인 — O(n) 스캔으로 O(n log n) 정렬 회피
|
||||
var needsSort = false;
|
||||
for (int i = 1; i < timeline.Count; i++)
|
||||
{
|
||||
var cmp = timeline[i].Timestamp.CompareTo(timeline[i - 1].Timestamp);
|
||||
if (cmp < 0 || (cmp == 0 && timeline[i].Order < timeline[i - 1].Order))
|
||||
{
|
||||
needsSort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (needsSort)
|
||||
timeline.Sort((a, b) =>
|
||||
{
|
||||
var cmp = a.Timestamp.CompareTo(b.Timestamp);
|
||||
return cmp != 0 ? cmp : a.Order.CompareTo(b.Order);
|
||||
});
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private Border CreateTimelineLoadMoreCard(int hiddenCount)
|
||||
|
||||
Reference in New Issue
Block a user