diff --git a/README.md b/README.md index 6282496..946c0db 100644 --- a/README.md +++ b/README.md @@ -2285,3 +2285,9 @@ MIT License - 검증: - `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 +업데이트: 2026-04-15 22:07 (KST) +- AX Agent에서 같은 탭 안의 다른 대화를 눌렀을 때 즉시 실행 중 대화로 되돌아가던 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`의 `SaveLastConversations()`가 세션 상태를 저장할 때 발생하는 `SettingsChanged`를 다시 UI 전체 갱신으로 연결하지 않도록 억제해, 대화 선택 직후 `RefreshFromSavedSettings() -> UpdateTabUI() -> SwitchToTabConversation()`이 현재 선택을 덮어쓰지 않게 했습니다. +- 탭 복귀 시에도 스트리밍 대화 우선 노출 기준을 완화했습니다. `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`의 `ShouldPreferStreamingConversation(...)`와 `SwitchToTabConversation()`이 이제 사용자가 같은 탭에서 다른 대화를 명시적으로 선택해 기억해둔 경우 그 선택을 유지하고, 상단 라이브 가이드만 background conversation 모드로 보여줍니다. +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_selection_persist\\ -p:IntermediateOutputPath=obj\\verify_conversation_selection_persist\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatSessionStateServiceTests" -p:OutputPath=bin\\verify_conversation_selection_persist_tests\\ -p:IntermediateOutputPath=obj\\verify_conversation_selection_persist_tests\\` 통과 55 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a70d648..04be60a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1608,3 +1608,11 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫 - 검증: - `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 +업데이트: 2026-04-15 22:07 (KST) +- AX Agent 동일 탭 내 대화 선택 회귀를 수정했습니다. 원인은 `src/AxCopilot/Views/ChatWindow.xaml.cs`의 `SaveLastConversations()`가 세션 상태(`LastActiveTab`, `LastConversationIds`)를 저장할 때마다 `SettingsChanged`를 다시 태워 `RefreshFromSavedSettings() -> UpdateTabUI() -> SwitchToTabConversation()`가 연쇄 호출되고, 실행 중 탭에서는 스트리밍 대화를 다시 현재 대화로 강제 복귀시키던 흐름이었습니다. +- `ChatWindow.xaml.cs`에 `_suppressSettingsRefreshForSessionSave`를 추가해 세션 상태 저장으로 발생한 설정 변경 이벤트는 UI 전체 재적용에서 제외했습니다. 이로써 같은 탭 안의 다른 대화를 클릭해도 선택 직후 다시 원래 실행 대화로 튕기지 않습니다. +- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`에는 `ShouldPreferStreamingConversation(...)` 정책을 추가했습니다. 탭 복귀 시 스트리밍 대화가 있더라도, 사용자가 해당 탭에서 다른 대화를 명시적으로 선택해 기억해둔 상태라면 그 선택을 유지하고 라이브 가이드만 `BackgroundConversation`으로 노출하도록 `SwitchToTabConversation()` 분기를 조정했습니다. +- 테스트: `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`에 스트리밍 대화 우선 노출 정책 회귀 케이스를 추가했습니다. +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_selection_persist\\ -p:IntermediateOutputPath=obj\\verify_conversation_selection_persist\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatSessionStateServiceTests" -p:OutputPath=bin\\verify_conversation_selection_persist_tests\\ -p:IntermediateOutputPath=obj\\verify_conversation_selection_persist_tests\\` 통과 55 diff --git a/src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs b/src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs index 8975e1b..69470a4 100644 --- a/src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs +++ b/src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs @@ -51,4 +51,22 @@ public class ChatStreamingUiPolicyTests result.Should().Be(expected); } + + [Theory] + [InlineData(null, null, false)] + [InlineData("run-1", null, true)] + [InlineData("run-1", "", true)] + [InlineData("run-1", "run-1", true)] + [InlineData("run-1", "other-conversation", false)] + public void ShouldPreferStreamingConversation_ShouldKeepExplicitConversationSelection( + string? streamingConversationId, + string? rememberedConversationId, + bool expected) + { + var result = ChatStreamingUiPolicy.ShouldPreferStreamingConversation( + streamingConversationId, + rememberedConversationId); + + result.Should().Be(expected); + } } diff --git a/src/AxCopilot/Views/ChatStreamingUiPolicy.cs b/src/AxCopilot/Views/ChatStreamingUiPolicy.cs index e2f7d72..d40d883 100644 --- a/src/AxCopilot/Views/ChatStreamingUiPolicy.cs +++ b/src/AxCopilot/Views/ChatStreamingUiPolicy.cs @@ -1,5 +1,7 @@ namespace AxCopilot.Views; +using System; + internal enum StreamingGuideVisibility { Hidden, @@ -26,4 +28,15 @@ internal static class ChatStreamingUiPolicy internal static bool ShouldRenderConversationBoundContent(StreamingGuideVisibility visibility) => visibility == StreamingGuideVisibility.ActiveConversation; + + internal static bool ShouldPreferStreamingConversation( + string? streamingConversationId, + string? rememberedConversationId) + { + if (string.IsNullOrWhiteSpace(streamingConversationId)) + return false; + + return string.IsNullOrWhiteSpace(rememberedConversationId) + || string.Equals(streamingConversationId, rememberedConversationId, StringComparison.Ordinal); + } } diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index f26bab2..db01301 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -151,6 +151,7 @@ public partial class ChatWindow : Window private StackPanel? _selectedMessageActionBar; private Border? _selectedMessageBorder; private bool _isRefreshingFromSettings; + private bool _suppressSettingsRefreshForSessionSave; private int? _lastCompactionBeforeTokens; private int? _lastCompactionAfterTokens; private DateTime? _lastCompactionAt; @@ -600,7 +601,7 @@ public partial class ChatWindow : Window private void Settings_SettingsChanged(object? sender, EventArgs e) { - if (_forceClose || !IsLoaded || _isRefreshingFromSettings) + if (_forceClose || !IsLoaded || _isRefreshingFromSettings || _suppressSettingsRefreshForSessionSave) return; Dispatcher.BeginInvoke(new Action(() => @@ -1899,13 +1900,20 @@ public partial class ChatWindow : Window Services.LogService.Info($"[SwitchTab] START tab={_activeTab}, emptyState={EmptyState.Visibility}, streaming={_isStreaming}"); - // 현재 탭에 실행 중인 대화가 있으면 차단하지 않고 해당 대화를 그대로 보여줍니다. + // 현재 탭에 실행 중인 대화가 있더라도, 사용자가 같은 탭 안에서 다른 대화를 선택한 상태라면 + // 해당 선택을 유지하고 상단 가이드만 background conversation 모드로 보여줍니다. + var session = ChatSession; var streamingConversation = GetStreamingConversation(_activeTab); - if (_streamingTabs.Contains(_activeTab) && streamingConversation != null) + var rememberedConversationId = session?.GetConversationId(_activeTab); + var shouldPreferStreamingConversation = _streamingTabs.Contains(_activeTab) + && ChatStreamingUiPolicy.ShouldPreferStreamingConversation( + streamingConversation?.Id, + rememberedConversationId); + if (shouldPreferStreamingConversation && streamingConversation != null) { Services.LogService.Info($"[SwitchTab] STREAMING_CONV path: tab={_activeTab}, convId={streamingConversation.Id[..Math.Min(8, streamingConversation.Id.Length)]}"); lock (_convLock) - _currentConversation = ChatSession?.SetCurrentConversation(_activeTab, streamingConversation, _storage) ?? streamingConversation; + _currentConversation = session?.SetCurrentConversation(_activeTab, streamingConversation, _storage) ?? streamingConversation; SyncTabConversationIdsFromSession(); SaveLastConversations(); ClearTranscriptElements(); @@ -1919,7 +1927,6 @@ public partial class ChatWindow : Window return; } - var session = ChatSession; if (session != null) { var conv = session.LoadOrCreateConversation(_activeTab, _storage, _settings); @@ -3391,7 +3398,15 @@ public partial class ChatWindow : Window { SyncTabConversationIdsToSession(); session.ActiveTab = _activeTab; - session.Save(_settings); + _suppressSettingsRefreshForSessionSave = true; + try + { + session.Save(_settings); + } + finally + { + _suppressSettingsRefreshForSessionSave = false; + } SyncTabConversationIdsFromSession(); return; }