AX Agent 라이브 진행 문구 지속 표시 회귀 수정

- V2 라이브 진행 카드에 고정 상태 본문/상세/메타 영역을 추가해 실행 로그만 남고 상단 메시지가 비어 보이던 문제를 줄임
- 스트리밍 시작 시 초기 내러티브를 채우고 각 AgentEvent 처리마다 상태 카드를 갱신하도록 정리함
- README.md와 docs/DEVELOPMENT.md에 2026-04-15 22:18 (KST) 기준 변경 이력과 검증 결과를 반영함

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_live_message_persistence2\ -p:IntermediateOutputPath=obj\verify_live_message_persistence2\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests -p:OutputPath=bin\verify_live_message_persistence_tests\ -p:IntermediateOutputPath=obj\verify_live_message_persistence_tests\ : 통과 69
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_live_message_persistence3\ -p:IntermediateOutputPath=obj\verify_live_message_persistence3\ : 경고 0 / 오류 0
This commit is contained in:
2026-04-15 22:02:55 +09:00
parent f8067a1f9b
commit 76afb9d5c8
3 changed files with 128 additions and 0 deletions

View File

@@ -15,6 +15,10 @@ public partial class ChatWindow
private DispatcherTimer? _v2LiveElapsedTimer;
private DateTime _v2LiveStartTime;
private TextBlock? _v2LiveElapsedText;
private Border? _v2LiveStatusCard;
private TextBlock? _v2LiveStatusText;
private TextBlock? _v2LiveStatusDetailText;
private TextBlock? _v2LiveStatusMetaText;
/// <summary>V2: 스트리밍 시작 시 라이브 진행 컨테이너 생성</summary>
private void ShowAgentLiveCardV2(string runTab)
@@ -29,7 +33,10 @@ public partial class ChatWindow
_v2LastLiveToolCallId = null;
var msgMaxWidth = GetMessageMaxWidth();
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var accentColor = ResolveLiveProgressAccentColor(accentBrush);
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
_v2LiveContainer = new StackPanel
{
@@ -87,6 +94,52 @@ public partial class ChatWindow
_v2LiveContainer.Children.Add(headerGrid);
_v2LiveStatusText = new TextBlock
{
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
LineHeight = 20,
};
_v2LiveStatusDetailText = new TextBlock
{
FontSize = 11,
Foreground = secondaryText,
Opacity = 0.88,
TextWrapping = TextWrapping.Wrap,
LineHeight = 18,
Margin = new Thickness(0, 4, 0, 0),
};
_v2LiveStatusMetaText = new TextBlock
{
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.72,
Margin = new Thickness(0, 8, 0, 0),
};
_v2LiveStatusCard = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x12, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x36, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(14, 12, 14, 12),
Margin = new Thickness(0, 4, 0, 6),
Child = new StackPanel
{
Children =
{
_v2LiveStatusText,
_v2LiveStatusDetailText,
_v2LiveStatusMetaText,
}
}
};
_v2LiveContainer.Children.Add(_v2LiveStatusCard);
RefreshV2LiveStatusCard(runTab);
AddTranscriptElement(_v2LiveContainer);
ForceScrollToEnd();
@@ -108,6 +161,7 @@ public partial class ChatWindow
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var msgMaxWidth = GetMessageMaxWidth();
UpdateV2LiveStatusCardFromEvent(agentEvent);
switch (agentEvent.Type)
{
@@ -327,6 +381,10 @@ public partial class ChatWindow
_v2LiveElapsedTimer?.Stop();
_v2LiveElapsedTimer = null;
_v2LiveElapsedText = null;
_v2LiveStatusCard = null;
_v2LiveStatusText = null;
_v2LiveStatusDetailText = null;
_v2LiveStatusMetaText = null;
if (_v2LiveContainer == null) return;
@@ -347,4 +405,62 @@ public partial class ChatWindow
RemoveTranscriptElement(toRemove);
}
private void RefreshV2LiveStatusCard(string runTab)
{
if (_v2LiveStatusText == null)
return;
var hint = GetLiveAgentProgressHint();
if (hint != null)
{
var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(hint, runTab);
var hintSummary = string.IsNullOrWhiteSpace(hint.Summary)
? narrative.Message
: hint.Summary;
UpdateV2LiveStatusCard(hintSummary, narrative.Detail, BuildReadableProgressMetaText(hint));
return;
}
var initial = AgentStatusNarrativeCatalog.BuildInitial(runTab);
UpdateV2LiveStatusCard(initial.Message, initial.Detail, null);
}
private void UpdateV2LiveStatusCardFromEvent(AgentEvent agentEvent)
{
if (_v2LiveStatusText == null)
return;
var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(agentEvent, _activeTab);
var normalizedThinking = agentEvent.Type == AgentEventType.Thinking
? AgentProgressSummarySanitizer.NormalizeThinkingSummary(
agentEvent.Summary,
agentEvent.ToolName,
maxLength: 160)
: string.Empty;
var message = string.IsNullOrWhiteSpace(normalizedThinking)
? narrative.Message
: normalizedThinking;
UpdateV2LiveStatusCard(message, narrative.Detail, BuildReadableProgressMetaText(agentEvent));
}
private void UpdateV2LiveStatusCard(string message, string? detail, string? meta)
{
if (_v2LiveStatusText == null || _v2LiveStatusDetailText == null || _v2LiveStatusMetaText == null)
return;
_v2LiveStatusText.Text = string.IsNullOrWhiteSpace(message)
? "작업을 이어가고 있습니다..."
: message;
_v2LiveStatusDetailText.Text = detail ?? string.Empty;
_v2LiveStatusDetailText.Visibility = string.IsNullOrWhiteSpace(detail)
? Visibility.Collapsed
: Visibility.Visible;
_v2LiveStatusMetaText.Text = meta ?? string.Empty;
_v2LiveStatusMetaText.Visibility = string.IsNullOrWhiteSpace(meta)
? Visibility.Collapsed
: Visibility.Visible;
}
}