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:
@@ -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))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user