From dc2574bb17c9ccdf181e91f7e5eaf4e5932b7dbb Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 23:29:08 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=A7=84=ED=96=89=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=A5=BC=20Claude=20Code=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=EC=97=90=20=EA=B0=80=EA=B9=9D=EA=B2=8C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - execution history를 접은 상태에서도 대기/압축/중요 진행 이벤트가 transcript에 계속 노출되도록 필터를 조정함 - 진행 줄 메타를 경과 시간 · 누적 토큰 형식으로 통일하고 일반 진행 이벤트를 평평한 line 스타일로 정리함 - 장기 대기/컨텍스트 압축 상태만 강조 배경과 펄스 마커를 유지해 살아 있는 작업이 더 잘 보이도록 개선함 - README와 DEVELOPMENT 문서에 2026-04-06 23:26 (KST) 기준 이력을 반영함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 3 + docs/DEVELOPMENT.md | 13 + .../Views/ChatWindow.AgentEventRendering.cs | 266 ++++++++++++++++-- .../Views/ChatWindow.TimelinePresentation.cs | 33 ++- src/AxCopilot/Views/ChatWindow.xaml.cs | 20 +- 5 files changed, 315 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a97d4ec..14357ec 100644 --- a/README.md +++ b/README.md @@ -1380,3 +1380,6 @@ MIT License - 전체 코드 기준 오류/성능 점검 중 발견된 런타임 핫패스를 정리했습니다. [SettingsService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SettingsService.cs) 에서 AX Agent 표현 수준을 매번 `rich`로 덮어쓰던 버그를 수정해, 저장된 `balanced/simple/rich` 값이 실제로 유지되도록 했습니다. - [IndexService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/IndexService.cs) 에는 `tmp/cache/log/bak/crdownload` 같은 임시 파일과 숨김/시스템 경로, `~$` Office 임시 파일을 색인/감시 대상에서 제외하는 규칙을 추가했습니다. 불필요한 증분 갱신과 재색인 노이즈를 줄여 런처가 백그라운드에서 먹는 CPU와 디스크 I/O를 완화하는 목적입니다. - [LauncherWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs)의 인덱스 상태 타이머는 매 호출마다 새 인스턴스를 만들지 않고 재사용하도록 바꿨고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)는 창이 숨김/최소화된 동안 transcript 재렌더를 지연했다가 다시 보일 때 한 번만 반영하도록 정리해 AX Agent 백그라운드 부담을 줄였습니다. +- 업데이트: 2026-04-06 23:26 (KST) + - AX Agent의 중간 진행 메시지를 `claw-code`에 더 가깝게 마무리했습니다. execution history를 접어 둔 상태에서도 `처리 중...`, `컨텍스트 압축 중...`, 중요한 thinking/tool 진행 이벤트는 transcript에 계속 보이도록 필터를 조정했습니다. + - 진행 줄 스타일도 카드형 박스보다 더 평평한 요약줄 위주로 정리했습니다. 일반 진행 이벤트는 borderless line처럼 보이고, 실제 장기 대기/압축 상태만 은은한 강조 배경과 펄스 마커를 유지해 “지금 살아 있는 작업”만 더 잘 드러나게 맞췄습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5ba577d..a7131c5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5097,3 +5097,16 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) - AX Agent 창이 숨겨져 있거나 최소화된 동안에는 execution history rerender를 즉시 수행하지 않고, 다시 보일 때 한 번만 flush 하도록 바꿨다. - 스트리밍/이벤트가 계속 들어와도 백그라운드 창에서 `RenderMessages()`가 반복 호출되는 비용을 줄이는 목적이다. + +## 2026-04-06 23:26 (KST) + +- [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) + - execution history를 접어 둔 상태에서도 기다릴 근거가 필요한 진행 이벤트는 transcript에 남도록 `ShouldShowCollapsedProgressEvent(...)`를 추가했다. + - `Complete`, `Error`, `agent_wait`, `context_compaction`, 요약이 있는 thinking, process feed 계열 이벤트는 collapsed 상태에서도 계속 렌더된다. +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) + - `OnAgentEvent(...)`가 execution history가 꺼져 있어도 중요한 progress 이벤트면 rerender를 예약하도록 `ShouldRenderProgressEventWhenHistoryCollapsed(...)`를 도입했다. + - 이제 Cowork/Code에서 긴 대기나 압축이 발생할 때, hover로 우연히만 보이는 것이 아니라 transcript 기본 흐름에서 진행 상태를 읽을 수 있다. +- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) + - 진행 줄 메타 표기를 `경과 시간 · 누적 토큰` 형식으로 통일하는 `BuildReadableProgressMetaText(...)`를 추가했다. + - 일반 진행 이벤트는 borderless에 가까운 평평한 line 스타일로, 실제 장기 대기/압축 상태만 강조 배경/테두리를 유지하는 `CreateReadableProgressFeedCard(...)`를 추가했다. + - 결과적으로 AX Agent의 중간 처리 피드가 `claw-code`처럼 “기다릴 수 있는 라이브 진행 줄”에 더 가까워졌다. diff --git a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs index 23d7d8b..d76fcda 100644 --- a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs +++ b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs @@ -22,13 +22,17 @@ public partial class ChatWindow { return new Border { - Background = Brushes.Transparent, - BorderBrush = Brushes.Transparent, - BorderThickness = new Thickness(0), - CornerRadius = new CornerRadius(0), - Padding = new Thickness(0), - Margin = new Thickness(12, 4, 12, 2), - HorizontalAlignment = HorizontalAlignment.Left, + Background = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x20, 0xF5, 0x73, 0x16)) + : hintBg, + BorderBrush = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x4D, 0xF5, 0x73, 0x16)) + : borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(10), + Padding = new Thickness(10, 8, 10, 8), + Margin = new Thickness(12, 6, 12, 2), + HorizontalAlignment = HorizontalAlignment.Stretch, Child = new Grid { ColumnDefinitions = @@ -86,11 +90,11 @@ public partial class ChatWindow BorderBrush = Brushes.Transparent, BorderThickness = new Thickness(0), Padding = new Thickness(0), - Margin = new Thickness(26, 3, 12, 8), + Margin = new Thickness(28, 4, 12, 10), Child = new TextBlock { Text = text, - FontSize = 13.5, + FontSize = 14, FontWeight = FontWeights.Medium, Foreground = primaryText, TextWrapping = TextWrapping.Wrap, @@ -145,8 +149,8 @@ public partial class ChatWindow var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; - var processMeta = BuildProcessFeedMetaText(evt); - var summary = BuildProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim(); + var processMeta = BuildReadableProgressMetaText(evt); + var summary = BuildReadableProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim(); if (string.IsNullOrWhiteSpace(summary)) summary = transcriptBadgeLabel; @@ -158,7 +162,7 @@ public partial class ChatWindow var liveWaitingStyle = evt.Type == AgentEventType.Thinking && (string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) || string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)); - var summaryRow = CreateCompactEventPill( + var summaryRow = CreateReadableProgressFeedCard( summary, primaryText, secondaryText, @@ -166,11 +170,12 @@ public partial class ChatWindow borderBrush, accentBrush, processMeta, - liveWaitingStyle); + liveWaitingStyle, + out var pulseMarker); summaryRow.Opacity = 0; summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140))); if (liveWaitingStyle) - ApplyLiveWaitingPulse(summaryRow); + ApplyLiveWaitingPulseToMarker(pulseMarker); stack.Children.Add(summaryRow); var body = (eventSummaryText ?? string.Empty).Trim(); @@ -188,7 +193,7 @@ public partial class ChatWindow var compactPathRow = new StackPanel { Orientation = Orientation.Horizontal, - Margin = new Thickness(26, 0, 12, 8), + Margin = new Thickness(28, 0, 12, 8), ToolTip = evt.FilePath, }; compactPathRow.Children.Add(new TextBlock @@ -203,7 +208,7 @@ public partial class ChatWindow compactPathRow.Children.Add(new TextBlock { Text = System.IO.Path.GetFileName(evt.FilePath), - FontSize = 10.5, + FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, @@ -275,6 +280,235 @@ public partial class ChatWindow return string.Join(" · ", parts); } + private static string BuildReadableProcessFeedSummary(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName) + { + return evt.Type switch + { + AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) + => "처리 중...", + AgentEventType.Thinking when string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase) + => "컨텍스트 압축 중...", + AgentEventType.Planning when evt.Steps is { Count: > 0 } + => $"계획 {evt.Steps.Count}단계 정리", + AgentEventType.StepStart when evt.StepTotal > 0 + => $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행", + AgentEventType.StepDone when evt.StepTotal > 0 + => $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료", + AgentEventType.Thinking when !string.IsNullOrWhiteSpace(evt.Summary) + => evt.Summary, + AgentEventType.ToolCall + => string.IsNullOrWhiteSpace(itemDisplayName) + ? $"{transcriptBadgeLabel} 실행" + : $"{itemDisplayName} 실행", + AgentEventType.SkillCall + => string.IsNullOrWhiteSpace(itemDisplayName) + ? "스킬 실행" + : $"{itemDisplayName} 실행", + _ => string.IsNullOrWhiteSpace(evt.Summary) ? transcriptBadgeLabel : evt.Summary, + }; + } + + private Border CreateReadableProcessFeedCard( + string summary, + Brush primaryText, + Brush secondaryText, + Brush hintBg, + Brush borderBrush, + Brush accentBrush, + string? metaText, + bool liveWaitingStyle, + out Border pulseMarker) + { + var cardBackground = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x20, 0xF5, 0x73, 0x16)) + : hintBg; + var cardBorder = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x4D, 0xF5, 0x73, 0x16)) + : borderBrush; + + pulseMarker = new Border + { + Width = liveWaitingStyle ? 9 : 8, + Height = liveWaitingStyle ? 9 : 8, + CornerRadius = new CornerRadius(999), + Background = liveWaitingStyle ? accentBrush : secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + + var summaryText = new TextBlock + { + Text = summary, + FontSize = liveWaitingStyle ? 13.5 : 12.5, + FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal, + Foreground = liveWaitingStyle ? primaryText : secondaryText, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap, + }; + + var metaTextBlock = new TextBlock + { + Text = metaText ?? "", + FontSize = 10.5, + FontWeight = liveWaitingStyle ? FontWeights.Medium : FontWeights.Normal, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(10, 0, 0, 0), + Visibility = string.IsNullOrWhiteSpace(metaText) ? Visibility.Collapsed : Visibility.Visible, + }; + + var contentGrid = new Grid(); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + contentGrid.Children.Add(pulseMarker); + Grid.SetColumn(summaryText, 1); + contentGrid.Children.Add(summaryText); + Grid.SetColumn(metaTextBlock, 2); + contentGrid.Children.Add(metaTextBlock); + + return new Border + { + Background = cardBackground, + BorderBrush = cardBorder, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(10), + Padding = new Thickness(10, 8, 10, 8), + Margin = new Thickness(12, 6, 12, 2), + HorizontalAlignment = HorizontalAlignment.Stretch, + Child = contentGrid, + }; + } + + private string BuildReadableProgressMetaText(AgentEvent evt) + { + var parts = new List(); + + if (evt.ElapsedMs > 0) + { + var elapsed = TimeSpan.FromMilliseconds(evt.ElapsedMs); + if (elapsed.TotalHours >= 1) + parts.Add($"{(int)elapsed.TotalHours}h {elapsed.Minutes}m"); + else if (elapsed.TotalMinutes >= 1) + parts.Add($"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s"); + else + parts.Add($"{Math.Max(1, elapsed.Seconds)}s"); + } + + var totalTokens = Math.Max(0, evt.InputTokens) + Math.Max(0, evt.OutputTokens); + if (totalTokens > 0) + parts.Add($"{FormatTokenCount(totalTokens)} tokens"); + + return string.Join(" · ", parts); + } + + private Border CreateReadableProgressFeedCard( + string summary, + Brush primaryText, + Brush secondaryText, + Brush hintBg, + Brush borderBrush, + Brush accentBrush, + string? metaText, + bool liveWaitingStyle, + out Border pulseMarker) + { + var cardBackground = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x20, 0xF5, 0x73, 0x16)) + : Brushes.Transparent; + var cardBorder = liveWaitingStyle + ? new SolidColorBrush(Color.FromArgb(0x4D, 0xF5, 0x73, 0x16)) + : Brushes.Transparent; + + pulseMarker = new Border + { + Width = liveWaitingStyle ? 9 : 8, + Height = liveWaitingStyle ? 9 : 8, + CornerRadius = new CornerRadius(999), + Background = liveWaitingStyle + ? accentBrush + : new SolidColorBrush(Color.FromArgb(0xA6, 0x94, 0xA3, 0xB8)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + + var summaryText = new TextBlock + { + Text = summary, + FontSize = liveWaitingStyle ? 13.5 : 12.75, + FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Medium, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap, + }; + + var metaTextBlock = new TextBlock + { + Text = metaText ?? string.Empty, + FontSize = 10.5, + FontWeight = liveWaitingStyle ? FontWeights.Medium : FontWeights.Normal, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(12, 0, 0, 0), + Visibility = string.IsNullOrWhiteSpace(metaText) ? Visibility.Collapsed : Visibility.Visible, + }; + + var contentGrid = new Grid(); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + contentGrid.Children.Add(pulseMarker); + Grid.SetColumn(summaryText, 1); + contentGrid.Children.Add(summaryText); + Grid.SetColumn(metaTextBlock, 2); + contentGrid.Children.Add(metaTextBlock); + + return new Border + { + Background = cardBackground, + BorderBrush = cardBorder, + BorderThickness = liveWaitingStyle ? new Thickness(1) : new Thickness(0), + CornerRadius = new CornerRadius(liveWaitingStyle ? 10 : 6), + Padding = liveWaitingStyle ? new Thickness(10, 8, 10, 8) : new Thickness(2, 2, 2, 2), + Margin = liveWaitingStyle ? new Thickness(12, 6, 12, 2) : new Thickness(12, 4, 12, 0), + HorizontalAlignment = HorizontalAlignment.Stretch, + Child = contentGrid, + }; + } + + private static void ApplyLiveWaitingPulseToMarker(Border marker) + { + marker.RenderTransformOrigin = new Point(0.5, 0.5); + marker.RenderTransform = new ScaleTransform(1, 1); + + var pulse = new DoubleAnimation + { + From = 0.42, + To = 1.0, + Duration = TimeSpan.FromMilliseconds(780), + AutoReverse = true, + RepeatBehavior = RepeatBehavior.Forever, + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut }, + }; + marker.BeginAnimation(UIElement.OpacityProperty, pulse); + + if (marker.RenderTransform is ScaleTransform scale) + { + var scaleAnimX = new DoubleAnimation + { + From = 0.92, + To = 1.08, + Duration = TimeSpan.FromMilliseconds(780), + AutoReverse = true, + RepeatBehavior = RepeatBehavior.Forever, + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut }, + }; + var scaleAnimY = scaleAnimX.Clone(); + scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimX); + scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimY); + } + } + private Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush) { return new Border diff --git a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs index 812c80e..252e9a8 100644 --- a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs @@ -28,9 +28,32 @@ public partial class ChatWindow private List GetVisibleTimelineEvents(ChatConversation? conversation) { - return (conversation?.ShowExecutionHistory ?? true) - ? conversation?.ExecutionEvents?.ToList() ?? new List() - : new List(); + var events = conversation?.ExecutionEvents?.ToList() ?? new List(); + if (conversation?.ShowExecutionHistory ?? true) + return events; + + return events + .Where(ShouldShowCollapsedProgressEvent) + .ToList(); + } + + private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent) + { + var restoredEvent = ToAgentEvent(executionEvent); + if (restoredEvent.Type == AgentEventType.Complete || restoredEvent.Type == AgentEventType.Error) + return true; + + if (restoredEvent.Type == AgentEventType.Thinking) + { + if (string.Equals(restoredEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) + || string.Equals(restoredEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)) + return true; + + if (!string.IsNullOrWhiteSpace(restoredEvent.Summary)) + return true; + } + + return IsProcessFeedEvent(restoredEvent); } private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions( @@ -48,6 +71,10 @@ public partial class ChatWindow timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent))); } + var liveProgressHint = GetLiveAgentProgressHint(); + if (liveProgressHint != null) + timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint))); + return timeline .OrderBy(x => x.Timestamp) .ThenBy(x => x.Order) diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 21e2db7..848a52c 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -6170,7 +6170,7 @@ public partial class ChatWindow : Window // 그래야 중간 배너 잔상과 최종 재렌더 중복이 줄어듭니다. var shouldShowExecutionHistory = _currentConversation?.ShowExecutionHistory ?? false; AppendConversationExecutionEvent(evt, eventTab); - if (shouldShowExecutionHistory + if ((shouldShowExecutionHistory || ShouldRenderProgressEventWhenHistoryCollapsed(evt)) && string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase)) ScheduleExecutionHistoryRender(autoScroll: true); @@ -6315,6 +6315,24 @@ public partial class ChatWindow : Window }; } + private static bool ShouldRenderProgressEventWhenHistoryCollapsed(AgentEvent evt) + { + if (evt.Type == AgentEventType.Complete || evt.Type == AgentEventType.Error) + return true; + + if (evt.Type == AgentEventType.Thinking) + { + if (string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) + || string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)) + return true; + + if (!string.IsNullOrWhiteSpace(evt.Summary)) + return true; + } + + return IsProcessFeedEvent(evt); + } + private void OnSubAgentStatusChanged(SubAgentStatusEvent evt) { Dispatcher.Invoke(() =>