AX Agent 상단 라이브 안내 카드 유지 회귀를 수정한다
- 같은 탭에 실행이 살아 있는 동안에는 현재 대화와 실행 대화가 잠깐 어긋나도 상단 라이브 카드와 상태 바를 숨기지 않도록 ChatWindow 스트리밍 분기를 조정함 - ChatStreamingUiPolicy를 추가해 Hidden/ActiveConversation/BackgroundConversation 상태를 분리하고 본문 실행 이력 렌더와 상단 진행 안내 표시를 분리함 - ChatStreamingUiPolicyTests를 추가하고 README.md, docs/DEVELOPMENT.md 이력을 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_guide_persistence\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence\\ (경고 0, 오류 0) - 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests|ChatSessionStateServiceTests|AxAgentExecutionEngineTests -p:OutputPath=bin\\verify_live_guide_persistence_tests\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence_tests\\ (통과 98)
This commit is contained in:
54
src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs
Normal file
54
src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using AxCopilot.Views;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Views;
|
||||
|
||||
public class ChatStreamingUiPolicyTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false, false, nameof(StreamingGuideVisibility.Hidden))]
|
||||
[InlineData(false, true, nameof(StreamingGuideVisibility.Hidden))]
|
||||
[InlineData(true, true, nameof(StreamingGuideVisibility.ActiveConversation))]
|
||||
[InlineData(true, false, nameof(StreamingGuideVisibility.BackgroundConversation))]
|
||||
public void ResolveGuideVisibility_ShouldClassifyStreamingState(
|
||||
bool isOwningActiveTab,
|
||||
bool isVisibleStreamingConversation,
|
||||
string expectedName)
|
||||
{
|
||||
var result = ChatStreamingUiPolicy.ResolveGuideVisibility(
|
||||
isOwningActiveTab,
|
||||
isVisibleStreamingConversation);
|
||||
var expected = Enum.Parse<StreamingGuideVisibility>(expectedName);
|
||||
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(StreamingGuideVisibility.Hidden), false)]
|
||||
[InlineData(nameof(StreamingGuideVisibility.ActiveConversation), true)]
|
||||
[InlineData(nameof(StreamingGuideVisibility.BackgroundConversation), true)]
|
||||
public void ShouldShowTopLevelGuide_ShouldKeepGuideVisibleForBackgroundConversation(
|
||||
string visibilityName,
|
||||
bool expected)
|
||||
{
|
||||
var visibility = Enum.Parse<StreamingGuideVisibility>(visibilityName);
|
||||
var result = ChatStreamingUiPolicy.ShouldShowTopLevelGuide(visibility);
|
||||
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(StreamingGuideVisibility.Hidden), false)]
|
||||
[InlineData(nameof(StreamingGuideVisibility.ActiveConversation), true)]
|
||||
[InlineData(nameof(StreamingGuideVisibility.BackgroundConversation), false)]
|
||||
public void ShouldRenderConversationBoundContent_ShouldOnlyRenderVisibleConversationTimeline(
|
||||
string visibilityName,
|
||||
bool expected)
|
||||
{
|
||||
var visibility = Enum.Parse<StreamingGuideVisibility>(visibilityName);
|
||||
var result = ChatStreamingUiPolicy.ShouldRenderConversationBoundContent(visibility);
|
||||
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
29
src/AxCopilot/Views/ChatStreamingUiPolicy.cs
Normal file
29
src/AxCopilot/Views/ChatStreamingUiPolicy.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
internal enum StreamingGuideVisibility
|
||||
{
|
||||
Hidden,
|
||||
ActiveConversation,
|
||||
BackgroundConversation,
|
||||
}
|
||||
|
||||
internal static class ChatStreamingUiPolicy
|
||||
{
|
||||
internal static StreamingGuideVisibility ResolveGuideVisibility(
|
||||
bool isOwningActiveTab,
|
||||
bool isVisibleStreamingConversation)
|
||||
{
|
||||
if (!isOwningActiveTab)
|
||||
return StreamingGuideVisibility.Hidden;
|
||||
|
||||
return isVisibleStreamingConversation
|
||||
? StreamingGuideVisibility.ActiveConversation
|
||||
: StreamingGuideVisibility.BackgroundConversation;
|
||||
}
|
||||
|
||||
internal static bool ShouldShowTopLevelGuide(StreamingGuideVisibility visibility)
|
||||
=> visibility != StreamingGuideVisibility.Hidden;
|
||||
|
||||
internal static bool ShouldRenderConversationBoundContent(StreamingGuideVisibility visibility)
|
||||
=> visibility == StreamingGuideVisibility.ActiveConversation;
|
||||
}
|
||||
@@ -1536,7 +1536,10 @@ public partial class ChatWindow : Window
|
||||
private void RefreshStreamingControlsForActiveTab()
|
||||
{
|
||||
var isOwningTab = _streamingTabs.Contains(_activeTab);
|
||||
var hasVisibleStreamingConversation = isOwningTab && IsStreamingConversationVisible(_activeTab);
|
||||
var guideVisibility = ChatStreamingUiPolicy.ResolveGuideVisibility(
|
||||
isOwningTab,
|
||||
isOwningTab && IsStreamingConversationVisible(_activeTab));
|
||||
var shouldShowTopLevelGuide = ChatStreamingUiPolicy.ShouldShowTopLevelGuide(guideVisibility);
|
||||
|
||||
// 실행 중에도 Send 버튼 표시 — 메시지가 큐에 추가됨
|
||||
BtnSend.IsEnabled = true;
|
||||
@@ -1551,19 +1554,19 @@ public partial class ChatWindow : Window
|
||||
// 스트리밍 탭으로 복귀 시 자동 복원
|
||||
if (PulseDotBar != null)
|
||||
{
|
||||
if (!hasVisibleStreamingConversation)
|
||||
if (!shouldShowTopLevelGuide)
|
||||
PulseDotBar.Visibility = Visibility.Collapsed;
|
||||
else if (_streamingTabs.Count > 0)
|
||||
PulseDotBar.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (isOwningTab && !hasVisibleStreamingConversation)
|
||||
if (!shouldShowTopLevelGuide)
|
||||
{
|
||||
RemoveAgentLiveCard(animated: false);
|
||||
HideStreamingStatusBar();
|
||||
HideStickyProgress();
|
||||
}
|
||||
else if (hasVisibleStreamingConversation && IsAgentLiveCardEligibleTab(_activeTab))
|
||||
else if (IsAgentLiveCardEligibleTab(_activeTab))
|
||||
{
|
||||
EnsureAgentLiveCardVisible(_activeTab);
|
||||
if (_pendingAgentUiEvent != null)
|
||||
@@ -6799,24 +6802,32 @@ public partial class ChatWindow : Window
|
||||
{
|
||||
TouchLiveAgentProgressHints();
|
||||
var eventTab = runTab;
|
||||
var isVisibleRunConversation = IsStreamingConversationVisible(runTab);
|
||||
var normalizedRunTab = NormalizeTabName(runTab);
|
||||
var isOwningActiveTab = string.Equals(normalizedRunTab, _activeTab, StringComparison.OrdinalIgnoreCase)
|
||||
&& _streamingTabs.Contains(normalizedRunTab);
|
||||
var guideVisibility = ChatStreamingUiPolicy.ResolveGuideVisibility(
|
||||
isOwningActiveTab,
|
||||
isOwningActiveTab && IsStreamingConversationVisible(runTab));
|
||||
var shouldShowTopLevelGuide = ChatStreamingUiPolicy.ShouldShowTopLevelGuide(guideVisibility);
|
||||
var shouldRenderConversationBoundContent =
|
||||
ChatStreamingUiPolicy.ShouldRenderConversationBoundContent(guideVisibility);
|
||||
|
||||
// V2 라이브 카드 실시간 업데이트
|
||||
if (isVisibleRunConversation && IsAgentLiveCardEligibleTab(runTab))
|
||||
if (shouldShowTopLevelGuide && IsAgentLiveCardEligibleTab(runTab))
|
||||
{
|
||||
EnsureAgentLiveCardVisible(runTab);
|
||||
UpdateAgentLiveCardV2(evt);
|
||||
}
|
||||
|
||||
// ── 1단계: 경량 UI 피드백 (PulseDotBar 상태 텍스트만 갱신) ───────────
|
||||
if (isVisibleRunConversation)
|
||||
if (shouldShowTopLevelGuide)
|
||||
{
|
||||
if (evt.Type is AgentEventType.Complete or AgentEventType.Error)
|
||||
{
|
||||
HideStreamingStatusBar();
|
||||
FlushPendingAgentUiEvent();
|
||||
}
|
||||
else if (PulseDotBar?.Visibility == Visibility.Visible)
|
||||
else if (PulseDotBar != null)
|
||||
{
|
||||
var liveStatus = AgentStatusNarrativeCatalog.BuildFromEvent(evt, runTab);
|
||||
UpdateStreamingStatusBar(
|
||||
@@ -6827,7 +6838,7 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
|
||||
// 현재 실행 중인 스텝 추적 (통합 진행 카드용)
|
||||
if (IsProcessFeedEvent(evt) && isVisibleRunConversation)
|
||||
if (IsProcessFeedEvent(evt) && shouldShowTopLevelGuide)
|
||||
{
|
||||
_currentRunProgressSteps.Add(evt);
|
||||
if (_currentRunProgressSteps.Count > 8)
|
||||
@@ -6837,11 +6848,11 @@ public partial class ChatWindow : Window
|
||||
// ── 2단계: 무거운 작업을 백그라운드 큐로 위임 ──────────────────────────
|
||||
// AppendConversationExecutionEvent, AppendConversationAgentRun, 디스크 저장은
|
||||
// 백그라운드 스레드에서 배치 처리됩니다. UI 렌더 갱신도 배치 완료 후 1회만 호출됩니다.
|
||||
var shouldShowExecutionHistory = isVisibleRunConversation && (_currentConversation?.ShowExecutionHistory ?? false);
|
||||
var lightweightLiveMode = isVisibleRunConversation
|
||||
var shouldShowExecutionHistory = shouldRenderConversationBoundContent && (_currentConversation?.ShowExecutionHistory ?? false);
|
||||
var lightweightLiveMode = shouldRenderConversationBoundContent
|
||||
&& !shouldShowExecutionHistory
|
||||
&& IsLightweightLiveProgressMode(eventTab);
|
||||
var shouldRender = isVisibleRunConversation && (
|
||||
var shouldRender = shouldRenderConversationBoundContent && (
|
||||
shouldShowExecutionHistory
|
||||
|| (!lightweightLiveMode && ShouldRenderProgressEventWhenHistoryCollapsed(evt))
|
||||
|| evt.Type == AgentEventType.Complete
|
||||
@@ -6857,11 +6868,11 @@ public partial class ChatWindow : Window
|
||||
{
|
||||
_tabCumulativeInputTokens[runTab] = _tabCumulativeInputTokens.GetValueOrDefault(runTab) + evt.InputTokens;
|
||||
_tabCumulativeOutputTokens[runTab] = _tabCumulativeOutputTokens.GetValueOrDefault(runTab) + evt.OutputTokens;
|
||||
if (isVisibleRunConversation)
|
||||
if (shouldShowTopLevelGuide)
|
||||
UpdateStatusTokens((int)_tabCumulativeInputTokens[runTab], (int)_tabCumulativeOutputTokens[runTab]);
|
||||
}
|
||||
|
||||
if (isVisibleRunConversation)
|
||||
if (shouldShowTopLevelGuide)
|
||||
ScheduleAgentUiEvent(evt);
|
||||
if (!lightweightLiveMode
|
||||
|| evt.Type is AgentEventType.Complete
|
||||
|
||||
Reference in New Issue
Block a user