AX Agent 진행 시간·글로우 경로 정리 및 최근 로컬 변경 일괄 반영

- AX Agent 스트리밍 경과 시간을 공용 helper로 통일해 비정상적인 수천만 시간 표시를 방지함

- 채팅 입력창 글로우를 런처와 같은 표시/숨김 중심의 얇은 외곽 글로우로 정리하고 런처 글로우 설정은 일반 설정에 유지함

- README와 DEVELOPMENT 문서를 2026-04-08 12:02 (KST) 기준으로 갱신하고 Release 빌드 경고 0 / 오류 0을 확인함
This commit is contained in:
2026-04-08 23:20:53 +09:00
parent 6e99837a4c
commit 1b4a2bfb1c
24 changed files with 1103 additions and 173 deletions

View File

@@ -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)