AX Agent 라이브 진행 표시 회귀 복구 및 본문 선택 유지 정리
상단 라이브 진행 카드를 이전 단계형 구조로 복구하고 스트리밍 중 현재 실행 이벤트가 본문 타임라인에 중복 표시되지 않도록 V2 렌더 컷오프를 다시 적용했습니다. 사용자 말풍선은 기존 마크다운 렌더로 되돌려 세로로 깨지던 표시를 해결하고, 어시스턴트 본문과 스트리밍 완료 본문은 계속 드래그 선택/복사가 가능하도록 유지했습니다. 또한 SkillRuntime, allowed_tools, 메인 루프 요청, 읽기 도구 조기 실행 준비, 스트리밍 도구 감지 같은 저신호 내부 문구를 추가 필터링해 화면 노이즈를 줄였습니다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_restore\\ -p:IntermediateOutputPath=obj\\verify_live_restore\\ 에서 경고 0 오류 0을 확인했습니다. 검증: AgentLoopCodeQualityTests, AgentStatusNarrativeCatalogTests, AgentProgressSummarySanitizerTests 필터로 dotnet test를 실행해 131개 테스트 통과를 확인했습니다.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -15,7 +16,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;
|
||||
@@ -24,13 +25,11 @@ 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
|
||||
{
|
||||
@@ -52,12 +51,8 @@ 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);
|
||||
@@ -83,7 +78,7 @@ public partial class ChatWindow
|
||||
Text = "",
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.7,
|
||||
Opacity = 0.70,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
@@ -92,39 +87,6 @@ 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();
|
||||
|
||||
@@ -138,41 +100,225 @@ public partial class ChatWindow
|
||||
_v2LiveElapsedTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>V2: 라이브 카드는 1~2줄 요약만 유지하고, 상세 실행 이력은 본문 타임라인에 누적합니다.</summary>
|
||||
/// <summary>V2: 에이전트 이벤트 수신 시 라이브 카드 업데이트</summary>
|
||||
private void UpdateAgentLiveCardV2(AgentEvent agentEvent)
|
||||
{
|
||||
if (_v2LiveContainer == null)
|
||||
return;
|
||||
if (_v2LiveContainer == null) return;
|
||||
|
||||
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))
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var msgMaxWidth = GetMessageMaxWidth();
|
||||
|
||||
switch (agentEvent.Type)
|
||||
{
|
||||
return;
|
||||
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:
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
var cardToCollapse = pendingCard;
|
||||
var outerGridToCollapse = parent;
|
||||
var collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1200) };
|
||||
collapseTimer.Tick += (_, _) =>
|
||||
{
|
||||
collapseTimer.Stop();
|
||||
if (cardToCollapse == null) return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -181,8 +327,6 @@ public partial class ChatWindow
|
||||
_v2LiveElapsedTimer?.Stop();
|
||||
_v2LiveElapsedTimer = null;
|
||||
_v2LiveElapsedText = null;
|
||||
_v2LiveStatusText = null;
|
||||
_v2LiveDetailText = null;
|
||||
|
||||
if (_v2LiveContainer == null) return;
|
||||
|
||||
@@ -190,6 +334,8 @@ public partial class ChatWindow
|
||||
|
||||
var toRemove = _v2LiveContainer;
|
||||
_v2LiveContainer = null;
|
||||
_v2LiveToolCards.Clear();
|
||||
_v2LastLiveToolCallId = null;
|
||||
|
||||
if (animated && ContainsTranscriptElement(toRemove))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user