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:
@@ -1380,3 +1380,6 @@ MIT License
|
|||||||
- 전체 코드 기준 오류/성능 점검 중 발견된 런타임 핫패스를 정리했습니다. [SettingsService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SettingsService.cs) 에서 AX Agent 표현 수준을 매번 `rich`로 덮어쓰던 버그를 수정해, 저장된 `balanced/simple/rich` 값이 실제로 유지되도록 했습니다.
|
- 전체 코드 기준 오류/성능 점검 중 발견된 런타임 핫패스를 정리했습니다. [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를 완화하는 목적입니다.
|
- [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 백그라운드 부담을 줄였습니다.
|
- [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처럼 보이고, 실제 장기 대기/압축 상태만 은은한 강조 배경과 펄스 마커를 유지해 “지금 살아 있는 작업”만 더 잘 드러나게 맞췄습니다.
|
||||||
|
|||||||
@@ -5097,3 +5097,16 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)
|
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)
|
||||||
- AX Agent 창이 숨겨져 있거나 최소화된 동안에는 execution history rerender를 즉시 수행하지 않고, 다시 보일 때 한 번만 flush 하도록 바꿨다.
|
- AX Agent 창이 숨겨져 있거나 최소화된 동안에는 execution history rerender를 즉시 수행하지 않고, 다시 보일 때 한 번만 flush 하도록 바꿨다.
|
||||||
- 스트리밍/이벤트가 계속 들어와도 백그라운드 창에서 `RenderMessages()`가 반복 호출되는 비용을 줄이는 목적이다.
|
- 스트리밍/이벤트가 계속 들어와도 백그라운드 창에서 `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`처럼 “기다릴 수 있는 라이브 진행 줄”에 더 가까워졌다.
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
return new Border
|
return new Border
|
||||||
{
|
{
|
||||||
Background = Brushes.Transparent,
|
Background = liveWaitingStyle
|
||||||
BorderBrush = Brushes.Transparent,
|
? new SolidColorBrush(Color.FromArgb(0x20, 0xF5, 0x73, 0x16))
|
||||||
BorderThickness = new Thickness(0),
|
: hintBg,
|
||||||
CornerRadius = new CornerRadius(0),
|
BorderBrush = liveWaitingStyle
|
||||||
Padding = new Thickness(0),
|
? new SolidColorBrush(Color.FromArgb(0x4D, 0xF5, 0x73, 0x16))
|
||||||
Margin = new Thickness(12, 4, 12, 2),
|
: borderBrush,
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
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
|
Child = new Grid
|
||||||
{
|
{
|
||||||
ColumnDefinitions =
|
ColumnDefinitions =
|
||||||
@@ -86,11 +90,11 @@ public partial class ChatWindow
|
|||||||
BorderBrush = Brushes.Transparent,
|
BorderBrush = Brushes.Transparent,
|
||||||
BorderThickness = new Thickness(0),
|
BorderThickness = new Thickness(0),
|
||||||
Padding = new Thickness(0),
|
Padding = new Thickness(0),
|
||||||
Margin = new Thickness(26, 3, 12, 8),
|
Margin = new Thickness(28, 4, 12, 10),
|
||||||
Child = new TextBlock
|
Child = new TextBlock
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
FontSize = 13.5,
|
FontSize = 14,
|
||||||
FontWeight = FontWeights.Medium,
|
FontWeight = FontWeights.Medium,
|
||||||
Foreground = primaryText,
|
Foreground = primaryText,
|
||||||
TextWrapping = TextWrapping.Wrap,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
@@ -145,8 +149,8 @@ public partial class ChatWindow
|
|||||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||||
|
|
||||||
var processMeta = BuildProcessFeedMetaText(evt);
|
var processMeta = BuildReadableProgressMetaText(evt);
|
||||||
var summary = BuildProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim();
|
var summary = BuildReadableProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim();
|
||||||
if (string.IsNullOrWhiteSpace(summary))
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
summary = transcriptBadgeLabel;
|
summary = transcriptBadgeLabel;
|
||||||
|
|
||||||
@@ -158,7 +162,7 @@ public partial class ChatWindow
|
|||||||
var liveWaitingStyle = evt.Type == AgentEventType.Thinking
|
var liveWaitingStyle = evt.Type == AgentEventType.Thinking
|
||||||
&& (string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase));
|
|| string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase));
|
||||||
var summaryRow = CreateCompactEventPill(
|
var summaryRow = CreateReadableProgressFeedCard(
|
||||||
summary,
|
summary,
|
||||||
primaryText,
|
primaryText,
|
||||||
secondaryText,
|
secondaryText,
|
||||||
@@ -166,11 +170,12 @@ public partial class ChatWindow
|
|||||||
borderBrush,
|
borderBrush,
|
||||||
accentBrush,
|
accentBrush,
|
||||||
processMeta,
|
processMeta,
|
||||||
liveWaitingStyle);
|
liveWaitingStyle,
|
||||||
|
out var pulseMarker);
|
||||||
summaryRow.Opacity = 0;
|
summaryRow.Opacity = 0;
|
||||||
summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140)));
|
summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140)));
|
||||||
if (liveWaitingStyle)
|
if (liveWaitingStyle)
|
||||||
ApplyLiveWaitingPulse(summaryRow);
|
ApplyLiveWaitingPulseToMarker(pulseMarker);
|
||||||
stack.Children.Add(summaryRow);
|
stack.Children.Add(summaryRow);
|
||||||
|
|
||||||
var body = (eventSummaryText ?? string.Empty).Trim();
|
var body = (eventSummaryText ?? string.Empty).Trim();
|
||||||
@@ -188,7 +193,7 @@ public partial class ChatWindow
|
|||||||
var compactPathRow = new StackPanel
|
var compactPathRow = new StackPanel
|
||||||
{
|
{
|
||||||
Orientation = Orientation.Horizontal,
|
Orientation = Orientation.Horizontal,
|
||||||
Margin = new Thickness(26, 0, 12, 8),
|
Margin = new Thickness(28, 0, 12, 8),
|
||||||
ToolTip = evt.FilePath,
|
ToolTip = evt.FilePath,
|
||||||
};
|
};
|
||||||
compactPathRow.Children.Add(new TextBlock
|
compactPathRow.Children.Add(new TextBlock
|
||||||
@@ -203,7 +208,7 @@ public partial class ChatWindow
|
|||||||
compactPathRow.Children.Add(new TextBlock
|
compactPathRow.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = System.IO.Path.GetFileName(evt.FilePath),
|
Text = System.IO.Path.GetFileName(evt.FilePath),
|
||||||
FontSize = 10.5,
|
FontSize = 11,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
@@ -275,6 +280,235 @@ public partial class ChatWindow
|
|||||||
return string.Join(" · ", parts);
|
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)
|
private Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush)
|
||||||
{
|
{
|
||||||
return new Border
|
return new Border
|
||||||
|
|||||||
@@ -28,9 +28,32 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
|
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
|
||||||
{
|
{
|
||||||
return (conversation?.ShowExecutionHistory ?? true)
|
var events = conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>();
|
||||||
? conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>()
|
if (conversation?.ShowExecutionHistory ?? true)
|
||||||
: new List<ChatExecutionEvent>();
|
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(
|
private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions(
|
||||||
@@ -48,6 +71,10 @@ public partial class ChatWindow
|
|||||||
timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var liveProgressHint = GetLiveAgentProgressHint();
|
||||||
|
if (liveProgressHint != null)
|
||||||
|
timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
|
||||||
|
|
||||||
return timeline
|
return timeline
|
||||||
.OrderBy(x => x.Timestamp)
|
.OrderBy(x => x.Timestamp)
|
||||||
.ThenBy(x => x.Order)
|
.ThenBy(x => x.Order)
|
||||||
|
|||||||
@@ -6170,7 +6170,7 @@ public partial class ChatWindow : Window
|
|||||||
// 그래야 중간 배너 잔상과 최종 재렌더 중복이 줄어듭니다.
|
// 그래야 중간 배너 잔상과 최종 재렌더 중복이 줄어듭니다.
|
||||||
var shouldShowExecutionHistory = _currentConversation?.ShowExecutionHistory ?? false;
|
var shouldShowExecutionHistory = _currentConversation?.ShowExecutionHistory ?? false;
|
||||||
AppendConversationExecutionEvent(evt, eventTab);
|
AppendConversationExecutionEvent(evt, eventTab);
|
||||||
if (shouldShowExecutionHistory
|
if ((shouldShowExecutionHistory || ShouldRenderProgressEventWhenHistoryCollapsed(evt))
|
||||||
&& string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
&& string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||||
ScheduleExecutionHistoryRender(autoScroll: true);
|
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)
|
private void OnSubAgentStatusChanged(SubAgentStatusEvent evt)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user