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:
@@ -1,5 +1,11 @@
|
||||
# AX Commander
|
||||
|
||||
- 업데이트: 2026-04-15 19:31 (KST)
|
||||
- AX Agent 상단 라이브 안내 카드가 실행 중인데도 사라지던 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`는 이제 같은 탭에 실행이 살아 있는 동안 상단 안내 카드와 상태 바를 유지하고, 현재 대화가 실행 대화와 다를 때는 본문 실행 이력만 숨긴 채 상단 진행 안내는 계속 보여주도록 분리합니다.
|
||||
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `숨김 / 현재 대화 / 같은 탭 백그라운드 대화` 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`로 상단 가이드 유지 정책 회귀 테스트를 고정했습니다.
|
||||
- 검증: `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
|
||||
|
||||
- 업데이트: 2026-04-15 19:21 (KST)
|
||||
- AX Agent 실행이 이제 같은 탭 안에서 다른 대화로 이동하거나 새 대화를 시작해도 원래 대화에 계속 귀속됩니다. `src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs`에서 대화 전환 시 즉시 취소하던 흐름을 제거했고, `src/AxCopilot/Views/ChatWindow.xaml.cs`는 실행 중인 대화를 탭별로 추적해 탭 복귀 시 진행 중이던 대화를 다시 보여주도록 정리했습니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.AgentEventProcessor.cs`, `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/ChatSessionStateService.cs`는 에이전트 이벤트와 완료 기록을 현재 화면의 `_currentConversation`이 아니라 실행이 시작된 원래 대화에 누적하도록 보강했습니다. 같은 탭에서 다른 대화를 보고 있어도 실행 로그와 완료 요약이 새 대화에 섞이지 않고, 선택 중인 대화 ID도 백그라운드 실행 때문에 되돌아가지 않습니다.
|
||||
|
||||
@@ -1511,3 +1511,9 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
||||
- 테스트는 `src/AxCopilot.Tests/Services/ChatSessionStateServiceTests.cs`, `src/AxCopilot.Tests/Services/AxAgentExecutionEngineTests.cs`에 같은 탭 백그라운드 실행 귀속 회귀 케이스를 추가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_background_resume\\ -p:IntermediateOutputPath=obj\\verify_conversation_background_resume\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatSessionStateServiceTests|AxAgentExecutionEngineTests" -p:OutputPath=bin\\verify_conversation_background_resume_tests\\ -p:IntermediateOutputPath=obj\\verify_conversation_background_resume_tests\\` 통과 39
|
||||
업데이트: 2026-04-15 19:31 (KST)
|
||||
- AX Agent 상단 라이브 안내 카드 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`의 `RefreshStreamingControlsForActiveTab()`와 `OnAgentEvent(...)`가 더 이상 `현재 대화가 실행 대화와 정확히 일치하지 않는다`는 이유만으로 상단 라이브 카드와 상태 바를 제거하지 않고, 같은 탭에 실행이 살아 있는 동안에는 상단 안내를 유지하도록 분기했습니다.
|
||||
- 본문 실행 이력과 상단 진행 안내를 분리했습니다. 같은 탭의 다른 대화를 보고 있을 때는 conversation-bound timeline 렌더만 멈추고, 상단 라이브 카드/펄스 상태/토큰 갱신은 계속 유지되도록 바꿨습니다.
|
||||
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `Hidden`, `ActiveConversation`, `BackgroundConversation` 세 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`에 상단 가이드 유지 및 본문 렌더 분리 회귀 테스트를 추가했습니다.
|
||||
- 검증: `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
|
||||
|
||||
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