AX Agent 진행 표시를 Claude Code 스타일에 가깝게 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 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)
This commit is contained in:
@@ -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<string>();
|
||||
|
||||
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
|
||||
|
||||
@@ -28,9 +28,32 @@ public partial class ChatWindow
|
||||
|
||||
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
|
||||
{
|
||||
return (conversation?.ShowExecutionHistory ?? true)
|
||||
? conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>()
|
||||
: new List<ChatExecutionEvent>();
|
||||
var events = conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>();
|
||||
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)
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
Reference in New Issue
Block a user