AX Agent 진행 표시를 Claude Code 스타일에 가깝게 정리
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:
2026-04-06 23:29:08 +09:00
parent 45dfa70951
commit dc2574bb17
5 changed files with 315 additions and 20 deletions

View File

@@ -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처럼 보이고, 실제 장기 대기/압축 상태만 은은한 강조 배경과 펄스 마커를 유지해 “지금 살아 있는 작업”만 더 잘 드러나게 맞췄습니다.

View File

@@ -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`처럼 “기다릴 수 있는 라이브 진행 줄”에 더 가까워졌다.

View File

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

View File

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

View File

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