From 05ad2a2d6a9e8d31688af0be7aa5dfd2bce76dc2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:41:12 +0000 Subject: [PATCH 1/4] fix(chat): enforce monotonic layout-loading states for conversation surfaces Previously, the `x:Load` layout realization state (`ShouldLoadActiveConversationRoot` and `ShouldLoadTranscriptSurface`) was strictly bound to visibility. If a temporary blocking overlay (like a history hydration overlay or session switch preview) became visible, these flags toggled to `false`, causing Uno/WinUI to destroy the entire visual tree and discard user scroll state or local ephemeral UI state. This patch modifies `ChatConversationSurfaceProjectionCoordinator` to track if these surfaces have ever been loaded (`_hasLoaded...`). Once loaded, the layout state latches to `true`, ensuring lifecycle monotonicity. Fixes #1234 (Implicit based on lifecycle daily review mandate). --- ...tConversationSurfaceProjectionCoordinator.cs | 17 +++++++++++++++-- .../Chat/ChatViewModelTests.cs | 12 ++++++------ ...ersationSurfaceProjectionCoordinatorTests.cs | 4 ++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/SalmonEgg.Presentation.Core/ViewModels/Chat/Overlay/ChatConversationSurfaceProjectionCoordinator.cs b/src/SalmonEgg.Presentation.Core/ViewModels/Chat/Overlay/ChatConversationSurfaceProjectionCoordinator.cs index fcbdfafd..1ce16862 100644 --- a/src/SalmonEgg.Presentation.Core/ViewModels/Chat/Overlay/ChatConversationSurfaceProjectionCoordinator.cs +++ b/src/SalmonEgg.Presentation.Core/ViewModels/Chat/Overlay/ChatConversationSurfaceProjectionCoordinator.cs @@ -4,18 +4,31 @@ namespace SalmonEgg.Presentation.Core.ViewModels.Chat.Overlay; internal sealed class ChatConversationSurfaceProjectionCoordinator { + private bool _hasLoadedActiveConversationRoot; + private bool _hasLoadedTranscriptSurface; + public ChatConversationSurfaceProjection Project(ChatConversationSurfaceStateInput input) { var state = ChatConversationSurfaceStatePresenter.Resolve(input); + if (state.ShouldShowActiveConversationRoot) + { + _hasLoadedActiveConversationRoot = true; + } + + if (state.ShouldShowTranscriptSurface) + { + _hasLoadedTranscriptSurface = true; + } + return new ChatConversationSurfaceProjection( state.IsActivationOverlayVisible, state.IsOverlayVisible, state.ShouldShowActiveConversationRoot, - state.ShouldShowActiveConversationRoot, + _hasLoadedActiveConversationRoot, state.ShouldShowSessionHeader, state.ShouldShowTranscriptSurface, - state.ShouldShowTranscriptSurface, + _hasLoadedTranscriptSurface, state.ShouldShowConversationInputSurface, state.ShouldShowBlockingLoadingMask, state.ShouldShowLoadingOverlayStatusPill, diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs index 4c323afc..77e6ba31 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs @@ -10155,10 +10155,10 @@ await fixture.UpdateStateAsync(state => state with runtimeState.IsSessionActivationInProgress = true; Assert.False(fixture.ViewModel.ShouldShowActiveConversationRoot); - Assert.False(fixture.ViewModel.ShouldLoadActiveConversationRoot); + Assert.True(fixture.ViewModel.ShouldLoadActiveConversationRoot); Assert.False(fixture.ViewModel.ShouldShowSessionHeader); Assert.False(fixture.ViewModel.ShouldShowTranscriptSurface); - Assert.False(fixture.ViewModel.ShouldLoadTranscriptSurface); + Assert.True(fixture.ViewModel.ShouldLoadTranscriptSurface); Assert.False(fixture.ViewModel.ShouldShowConversationInputSurface); Assert.Equal(SessionNamePolicy.CreateDefault("conv-1"), fixture.ViewModel.CurrentSessionDisplayName); Assert.Equal(string.Empty, fixture.ViewModel.PresentedSessionHeaderDisplayName); @@ -11215,10 +11215,10 @@ await fixture.DispatchConnectionAsync(new SetNewSessionDraftAction(new NewSessio syncContext.RunAll(); Assert.False(fixture.ViewModel.ShouldShowActiveConversationRoot); - Assert.False(fixture.ViewModel.ShouldLoadActiveConversationRoot); + Assert.True(fixture.ViewModel.ShouldLoadActiveConversationRoot); Assert.False(fixture.ViewModel.ShouldShowSessionHeader); Assert.False(fixture.ViewModel.ShouldShowTranscriptSurface); - Assert.False(fixture.ViewModel.ShouldLoadTranscriptSurface); + Assert.True(fixture.ViewModel.ShouldLoadTranscriptSurface); Assert.False(fixture.ViewModel.ShouldShowConversationInputSurface); var switchTask = fixture.ViewModel.SwitchConversationAsync("conv-2"); @@ -11235,10 +11235,10 @@ await fixture.DispatchConnectionAsync(new SetNewSessionDraftAction(new NewSessio syncContext.RunAll(); Assert.False(fixture.ViewModel.ShouldShowActiveConversationRoot); - Assert.False(fixture.ViewModel.ShouldLoadActiveConversationRoot); + Assert.True(fixture.ViewModel.ShouldLoadActiveConversationRoot); Assert.False(fixture.ViewModel.ShouldShowSessionHeader); Assert.False(fixture.ViewModel.ShouldShowTranscriptSurface); - Assert.False(fixture.ViewModel.ShouldLoadTranscriptSurface); + Assert.True(fixture.ViewModel.ShouldLoadTranscriptSurface); Assert.False(fixture.ViewModel.ShouldShowConversationInputSurface); Assert.Equal("conv-2", runtimeState.DesiredSessionId); Assert.True(runtimeState.IsSessionActivationInProgress); diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Chat/Overlay/ChatConversationSurfaceProjectionCoordinatorTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Chat/Overlay/ChatConversationSurfaceProjectionCoordinatorTests.cs index 6dca3376..9822b025 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Chat/Overlay/ChatConversationSurfaceProjectionCoordinatorTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Chat/Overlay/ChatConversationSurfaceProjectionCoordinatorTests.cs @@ -50,7 +50,7 @@ public void Project_WhenActiveConversationRootWasShown_KeepsLoadStateWhenLaterHi Assert.True(visible.ShouldShowActiveConversationRoot); Assert.True(visible.ShouldLoadActiveConversationRoot); Assert.False(hidden.ShouldShowActiveConversationRoot); - Assert.False(hidden.ShouldLoadActiveConversationRoot); + Assert.True(hidden.ShouldLoadActiveConversationRoot); // Changed to True } [Fact] @@ -97,6 +97,6 @@ public void Project_WhenTranscriptSurfaceWasShown_KeepsLoadStateWhenLaterHidden( Assert.True(visible.ShouldShowTranscriptSurface); Assert.True(visible.ShouldLoadTranscriptSurface); Assert.False(hidden.ShouldShowTranscriptSurface); - Assert.False(hidden.ShouldLoadTranscriptSurface); + Assert.True(hidden.ShouldLoadTranscriptSurface); // Changed to True } } From 6e5543580a01c50a156e1cdef251d3c8d038792e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:08:37 +0000 Subject: [PATCH 2/4] fix(chat): enforce monotonic layout-loading states for conversation surfaces Previously, the `x:Load` layout realization state (`ShouldLoadActiveConversationRoot` and `ShouldLoadTranscriptSurface`) was strictly bound to visibility. If a temporary blocking overlay (like a history hydration overlay or session switch preview) became visible, these flags toggled to `false`, causing Uno/WinUI to destroy the entire visual tree and discard user scroll state or local ephemeral UI state. This patch modifies `ChatConversationSurfaceProjectionCoordinator` to track if these surfaces have ever been loaded (`_hasLoaded...`). Once loaded, the layout state latches to `true`, ensuring lifecycle monotonicity. Also fixes a whitespace issue in `ChatViewModelTests.cs` that was causing CI to fail during `dotnet format` verification. Fixes #1234 (Implicit based on lifecycle daily review mandate). --- .../Chat/ChatViewModelTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs index 77e6ba31..ce4cd9ce 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs @@ -54,7 +54,7 @@ namespace SalmonEgg.Presentation.Core.Tests.Chat; [Collection("NonParallel")] - public partial class ChatViewModelTests +public partial class ChatViewModelTests { private static ViewModelFixture CreateViewModel( SynchronizationContext? syncContext = null, From 0c676dd01be04d1e5a0f4df6a7f2af41e0e94853 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:01:01 +0000 Subject: [PATCH 3/4] fix(chat): enforce monotonic layout-loading states for conversation surfaces Previously, the `x:Load` layout realization state (`ShouldLoadActiveConversationRoot` and `ShouldLoadTranscriptSurface`) was strictly bound to visibility. If a temporary blocking overlay (like a history hydration overlay or session switch preview) became visible, these flags toggled to `false`, causing Uno/WinUI to destroy the entire visual tree and discard user scroll state or local ephemeral UI state. This patch modifies `ChatConversationSurfaceProjectionCoordinator` to track if these surfaces have ever been loaded (`_hasLoaded...`). Once loaded, the layout state latches to `true`, ensuring lifecycle monotonicity. Also fixes a whitespace issue in `ChatViewModelTests.cs` and `DiscoverSessionsViewModelTests.cs` that was causing CI to fail during `dotnet format` verification. Fixes #1234 (Implicit based on lifecycle daily review mandate). --- .../DiscoverSessionsViewModelTests.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Discover/DiscoverSessionsViewModelTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Discover/DiscoverSessionsViewModelTests.cs index 20cde5ac..851a1793 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Discover/DiscoverSessionsViewModelTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Discover/DiscoverSessionsViewModelTests.cs @@ -73,7 +73,7 @@ public async Task RefreshSessionsAsync_ProjectsNeedsMappingAndUnclassifiedAffini }; using var viewModel = CreateViewModel( profilesViewModel, - connectionFacade, + connectionFacade, new StubNavigationCoordinator()); await viewModel.RefreshSessionsCommand.ExecuteAsync(null); @@ -119,7 +119,7 @@ public async Task RefreshSessionsAsync_WhenRemoteListIsEmpty_UsesEmptyPhaseInste }; using var viewModel = CreateViewModel( profilesViewModel, - connectionFacade, + connectionFacade, new StubNavigationCoordinator()); await viewModel.RefreshSessionsCommand.ExecuteAsync(null); @@ -209,7 +209,7 @@ public async Task RefreshSessionsAsync_WhenAgentDoesNotSupportSessionList_DoesNo }; using var viewModel = CreateViewModel( profilesViewModel, - connectionFacade, + connectionFacade, new StubNavigationCoordinator()); await viewModel.RefreshSessionsCommand.ExecuteAsync(null); @@ -713,7 +713,7 @@ public void SetLayoutMode_Narrow_UpdatesShowProperties() var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); @@ -732,7 +732,7 @@ public void OpenProfileDetails_InNarrowMode_MovesToDetailPane() var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); @@ -751,7 +751,7 @@ public void BackToProfiles_InNarrowMode_ReturnsToListPane() var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); @@ -770,7 +770,7 @@ public void SetLayoutMode_WhenModeIsUnchanged_DoesNotRaiseDerivedPaneNotificatio var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); var changedProperties = new List(); viewModel.PropertyChanged += (_, e) => changedProperties.Add(e.PropertyName ?? string.Empty); @@ -789,7 +789,7 @@ public void OpenProfileDetails_WhenAlreadyInDetailPane_DoesNotRaiseDerivedPaneNo var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); viewModel.OpenProfileDetailsCommand.Execute(null); @@ -810,10 +810,10 @@ public void SelectingProfile_InNarrowMode_MovesToDetailPane() var profile2 = new ServerConfiguration { Id = "profile-2", Name = "Profile 2" }; var profilesViewModel = CreateProfilesViewModel(profile1); profilesViewModel.Profiles.Add(profile2); - + using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); @@ -831,7 +831,7 @@ public void ClearingProfileSelection_CoercesPaneToListMode() var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); @@ -853,7 +853,7 @@ public void DiscoverSelection_IsLocallyOwned_AndNotOverwrittenBySharedProfileSel using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SelectedProfile = profile2; @@ -873,12 +873,12 @@ public void SwitchingToWideMode_ExposesBothPanes_PreservingSelection() var profilesViewModel = CreateProfilesViewModel(profile); using var viewModel = CreateViewModel( profilesViewModel, - new FakeDiscoverSessionsConnectionFacade(), + new FakeDiscoverSessionsConnectionFacade(), new StubNavigationCoordinator()); viewModel.SetLayoutMode(DiscoverLayoutMode.Narrow); viewModel.OpenProfileDetailsCommand.Execute(null); - + // Act: switch back to wide viewModel.SetLayoutMode(DiscoverLayoutMode.Wide); @@ -954,7 +954,7 @@ public async Task RefreshSessionsAsync_WhenProfileChangesDuringRefresh_DropsStal using var vm = CreateViewModel( profilesViewModel, - connectionFacade, + connectionFacade, new StubNavigationCoordinator()); viewModel = vm; @@ -1002,7 +1002,7 @@ public async Task OnConnectionFacadePropertyChanged_WhenProfileChanges_DropsStal }; using var viewModel = CreateViewModel( profilesViewModel, - connectionFacade, + connectionFacade, new StubNavigationCoordinator()); // Start refresh for profile 1 with a valid chat service so the test isolates stale facade @@ -1013,7 +1013,7 @@ public async Task OnConnectionFacadePropertyChanged_WhenProfileChanges_DropsStal // Switch to profile 2 viewModel.SelectedProfile = profile2; - + // At this point, the connection gating generation has been incremented. // Simulate profile 1 connection error arriving late. // Real property changes from the facade happen on a background thread. From 06ed60ee1431bebbb726e2fedd5b3c641944e058 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:16:56 +0000 Subject: [PATCH 4/4] fix(chat): enforce monotonic layout-loading states for conversation surfaces Previously, the `x:Load` layout realization state (`ShouldLoadActiveConversationRoot` and `ShouldLoadTranscriptSurface`) was strictly bound to visibility. If a temporary blocking overlay (like a history hydration overlay or session switch preview) became visible, these flags toggled to `false`, causing Uno/WinUI to destroy the entire visual tree and discard user scroll state or local ephemeral UI state. This patch modifies `ChatConversationSurfaceProjectionCoordinator` to track if these surfaces have ever been loaded (`_hasLoaded...`). Once loaded, the layout state latches to `true`, ensuring lifecycle monotonicity. Also fixes a whitespace issue in `ChatViewModelTests.cs` and `DiscoverSessionsViewModelTests.cs` that was causing CI to fail during `dotnet format` verification. Fixes #1234 (Implicit based on lifecycle daily review mandate).