AX Agent ?? ?? ?? ??? ??? ?? ??? ????

- Code ??? ?? ???? ?? ??? ??? ? ?? ?? ???? ???? no-progress ??? ???
- ??? ?? ??? 1~2? ????? ????? ToolCall/ToolResult ?? ??? ?? ????? ????? ???
- ??? Thinking/LLM ?? ??? ??? ???? ??? ?? ?? ??? ??? ??? ????? ????
- Cowork/Code ??? ??? ?? ??? ???? ??? ??? ??? ?? ???? ?
- README.md, docs/DEVELOPMENT.md ??? 2026-04-15 18:30 (KST) ???? ???

??
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_ui_logs\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_agent_ui_logs_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs_tests\\
This commit is contained in:
2026-04-15 18:33:06 +09:00
parent 232d5457d5
commit 53838a046b
18 changed files with 550 additions and 425 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -16,7 +15,7 @@ public partial class ChatWindow
private DateTime _v2LiveStartTime;
private TextBlock? _v2LiveElapsedText;
/// <summary>V2: 스트리밍 시작 시 라이브 진행 컨테이너 생성</summary>
/// <summary>V2: 스트리밍 시작 시 상단 라이브 진행 요약 카드를 생성합니다.</summary>
private void ShowAgentLiveCardV2(string runTab)
{
if (MessageList == null) return;
@@ -25,11 +24,13 @@ public partial class ChatWindow
RemoveAgentLiveCardV2(animated: false);
_v2LiveStartTime = DateTime.UtcNow;
_v2LiveToolCards.Clear();
_v2LastLiveToolCallId = null;
var msgMaxWidth = GetMessageMaxWidth();
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
_v2LiveContainer = new StackPanel
{
@@ -39,7 +40,6 @@ public partial class ChatWindow
Margin = new Thickness(0, 4, 0, 6),
};
// 에이전트 헤더 (아이콘 + 이름 + 경과시간)
var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 4) };
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
@@ -52,8 +52,12 @@ public partial class ChatWindow
{
var animState = new ChatIconAnimState
{
Host = liveIconHost, Canvas = canvas, Pixels = livePixels,
Glows = liveGlows, Rotate = liveRotate, Scale = liveScale,
Host = liveIconHost,
Canvas = canvas,
Pixels = livePixels,
Glows = liveGlows,
Rotate = liveRotate,
Scale = liveScale,
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
};
StartChatIconAnimation(animState);
@@ -79,7 +83,7 @@ public partial class ChatWindow
Text = "",
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.70,
Opacity = 0.7,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
@@ -88,10 +92,42 @@ public partial class ChatWindow
_v2LiveContainer.Children.Add(headerGrid);
var summaryCard = new Border
{
Background = hintBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(12, 10, 12, 10),
};
var summaryStack = new StackPanel();
_v2LiveStatusText = new TextBlock
{
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
};
summaryStack.Children.Add(_v2LiveStatusText);
_v2LiveDetailText = new TextBlock
{
FontSize = 10.5,
Foreground = secondaryText,
Opacity = 0.86,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 4, 0, 0),
};
summaryStack.Children.Add(_v2LiveDetailText);
summaryCard.Child = summaryStack;
_v2LiveContainer.Children.Add(summaryCard);
ApplyV2LiveNarrative(AgentStatusNarrativeCatalog.BuildInitial(runTab));
AddTranscriptElement(_v2LiveContainer);
ForceScrollToEnd();
// 경과 시간 타이머
_v2LiveElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_v2LiveElapsedTimer.Tick += (_, _) =>
{
@@ -102,242 +138,41 @@ public partial class ChatWindow
_v2LiveElapsedTimer.Start();
}
/// <summary>V2: 에이전트 이벤트 수신 시 라이브 카드 업데이트</summary>
/// <summary>V2: 라이브 카드는 1~2줄 요약만 유지하고, 상세 실행 이력은 본문 타임라인에 누적합니다.</summary>
private void UpdateAgentLiveCardV2(AgentEvent agentEvent)
{
if (_v2LiveContainer == null) return;
if (_v2LiveContainer == null)
return;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var msgMaxWidth = GetMessageMaxWidth();
switch (agentEvent.Type)
if (agentEvent.Type == AgentEventType.Thinking
&& !string.Equals(agentEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(agentEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
&& AgentProgressSummarySanitizer.IsLowSignalStatusSummary(agentEvent.Summary, agentEvent.ToolName))
{
case AgentEventType.ToolCall:
{
var (icon, iconColor) = GetV2ToolIcon(agentEvent.ToolName);
var toolId = $"{agentEvent.ToolName}_{agentEvent.Timestamp.Ticks}";
_v2LastLiveToolCallId = toolId;
var outerGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) };
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 왼쪽 세로선 (진행중 = 맥박)
var accentColor = ResolveLiveProgressAccentColor(
TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue);
var pulseLine = new Border
{
Width = 2,
Background = new SolidColorBrush(Color.FromArgb(0x80, accentColor.R, accentColor.G, accentColor.B)),
CornerRadius = new CornerRadius(1),
Margin = new Thickness(12, 0, 8, 0),
};
// 맥박 애니메이션
var pulseAnim = new DoubleAnimation(0.4, 1.0, TimeSpan.FromMilliseconds(800))
{
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever,
EasingFunction = new SineEase(),
};
pulseLine.BeginAnimation(UIElement.OpacityProperty, pulseAnim);
Grid.SetColumn(pulseLine, 0);
outerGrid.Children.Add(pulseLine);
var card = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x1C, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
Tag = "pending",
};
Grid.SetColumn(card, 1);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = icon,
FontSize = 12,
Foreground = iconColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
sp.Children.Add(new TextBlock
{
Text = GetV2ToolDisplayName(agentEvent.ToolName),
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
});
if (!string.IsNullOrWhiteSpace(agentEvent.FilePath))
{
sp.Children.Add(new TextBlock
{
Text = TruncateFilePath(agentEvent.FilePath, 50),
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.7,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
});
}
// 스피너 대신 "..." 텍스트
sp.Children.Add(new TextBlock
{
Text = "...",
FontSize = 11,
Foreground = secondaryText,
Opacity = 0.5,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(6, 0, 0, 0),
});
card.Child = sp;
outerGrid.Children.Add(card);
_v2LiveToolCards[toolId] = card;
_v2LiveContainer.Children.Add(outerGrid);
// 사용자가 수동 스크롤 중이면 강제 스크롤하지 않음
AutoScrollIfNeeded();
break;
}
case AgentEventType.ToolResult:
{
// 마지막 pending 카드를 완료 상태로 변환
if (_v2LastLiveToolCallId != null && _v2LiveToolCards.TryGetValue(_v2LastLiveToolCallId, out var pendingCard))
{
var isSuccess = agentEvent.Success;
var statusIcon = isSuccess ? "\uE73E" : "\uE711";
var statusColor = isSuccess
? new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50));
// 카드 배경/테두리를 성공/실패 색으로 변경
pendingCard.Background = isSuccess
? new SolidColorBrush(Color.FromArgb(0x0A, 0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromArgb(0x0A, 0xEF, 0x53, 0x50));
pendingCard.BorderBrush = isSuccess
? new SolidColorBrush(Color.FromArgb(0x30, 0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x53, 0x50));
pendingCard.Tag = "complete";
// "..." 텍스트를 상태 아이콘 + 소요시간으로 교체
if (pendingCard.Child is StackPanel sp)
{
// 마지막 "..." 제거
if (sp.Children.Count > 0 && sp.Children[^1] is TextBlock lastTb && lastTb.Text == "...")
sp.Children.RemoveAt(sp.Children.Count - 1);
sp.Children.Add(new TextBlock
{
Text = statusIcon,
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = statusColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(6, 0, 0, 0),
});
var elapsed = NormalizeProgressElapsedMs(agentEvent.ElapsedMs);
if (elapsed > 0)
{
sp.Children.Add(new TextBlock
{
Text = $"{elapsed / 1000.0:F1}s",
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.6,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 0, 0),
});
}
}
// 세로선 맥박 애니메이션 중지
var parent = pendingCard.Parent as Grid;
if (parent?.Children[0] is Border pulseLine)
{
pulseLine.BeginAnimation(UIElement.OpacityProperty, null);
pulseLine.Opacity = 1;
pulseLine.Background = isSuccess
? new SolidColorBrush(Color.FromArgb(0x60, 0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50));
}
// ★ 섹션 종료 후 자동 접기 — 완료된 카드는 1.2초 뒤 컴팩트 형태로 축소
// 얇은 줄이 빠르게 누적되어 공간 낭비되는 문제 해결
var cardToCollapse = pendingCard;
var outerGridToCollapse = parent;
var collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1200) };
collapseTimer.Tick += (_, _) =>
{
collapseTimer.Stop();
if (cardToCollapse == null) return;
// Padding 축소 + Opacity 감소로 "접힌" 느낌 연출
var padAnim = new ThicknessAnimation(
cardToCollapse.Padding,
new Thickness(8, 2, 8, 2),
TimeSpan.FromMilliseconds(200))
{ EasingFunction = new QuadraticEase() };
var opAnim = new DoubleAnimation(1.0, 0.55, TimeSpan.FromMilliseconds(200))
{ EasingFunction = new QuadraticEase() };
cardToCollapse.BeginAnimation(Border.PaddingProperty, padAnim);
cardToCollapse.BeginAnimation(UIElement.OpacityProperty, opAnim);
if (outerGridToCollapse != null)
outerGridToCollapse.Margin = new Thickness(0, 1, 0, 1);
};
collapseTimer.Start();
_v2LastLiveToolCallId = null;
}
break;
}
case AgentEventType.Thinking:
{
var thinkText = AgentProgressSummarySanitizer.NormalizeThinkingSummary(
agentEvent.Summary,
agentEvent.ToolName,
maxLength: 100);
if (string.IsNullOrWhiteSpace(thinkText)) break;
// 사고 과정을 간략히 표시
var thinkRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(22, 2, 0, 2),
};
thinkRow.Children.Add(new TextBlock
{
Text = "\uE915",
FontFamily = s_segoeIconFont,
FontSize = 10,
Foreground = new SolidColorBrush(Color.FromRgb(0x59, 0xA5, 0xF5)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 4, 0),
});
thinkRow.Children.Add(new TextBlock
{
Text = thinkText,
FontSize = 10.5,
FontStyle = FontStyles.Italic,
Foreground = secondaryText,
Opacity = 0.82,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = msgMaxWidth - 60,
});
_v2LiveContainer.Children.Add(thinkRow);
// 사용자가 수동 스크롤 중이면 강제 스크롤하지 않음
AutoScrollIfNeeded();
break;
}
return;
}
ApplyV2LiveNarrative(AgentStatusNarrativeCatalog.BuildFromEvent(agentEvent, _activeTab));
AutoScrollIfNeeded();
}
private void ApplyV2LiveNarrative(AgentStatusNarrative narrative)
{
if (_v2LiveStatusText != null)
_v2LiveStatusText.Text = narrative.Message;
if (_v2LiveDetailText == null)
return;
if (string.IsNullOrWhiteSpace(narrative.Detail))
{
_v2LiveDetailText.Text = string.Empty;
_v2LiveDetailText.Visibility = Visibility.Collapsed;
return;
}
_v2LiveDetailText.Text = narrative.Detail;
_v2LiveDetailText.Visibility = Visibility.Visible;
}
/// <summary>V2: 스트리밍 종료 시 라이브 카드 제거</summary>
@@ -346,16 +181,15 @@ public partial class ChatWindow
_v2LiveElapsedTimer?.Stop();
_v2LiveElapsedTimer = null;
_v2LiveElapsedText = null;
_v2LiveStatusText = null;
_v2LiveDetailText = null;
if (_v2LiveContainer == null) return;
// 아이콘 애니메이션 상태 정리
_chatIconAnimStates.RemoveAll(s => s.Host != null && !s.Host.IsVisible);
var toRemove = _v2LiveContainer;
_v2LiveContainer = null;
_v2LiveToolCards.Clear();
_v2LastLiveToolCallId = null;
if (animated && ContainsTranscriptElement(toRemove))
{