From 1091062a0987dafe940cea5c2baf6049124b410d Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Sat, 12 Sep 2026 11:30:01 -0700 Subject: [PATCH] fix(chat): work around WinUI session-switch crash Use non-animated ScrollView extent navigation instead of retaining an ItemsView bring target across history replacement. Workaround for microsoft/microsoft-ui-xaml#11865; related to #1167. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/TEST_COVERAGE.md | 1 + .../Chat/ReactorItemsViewScrollController.cs | 14 ++++++++------ .../ChatTimelinePresentationTests.cs | 9 ++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/TEST_COVERAGE.md b/docs/TEST_COVERAGE.md index b5263e569..aadfeac95 100644 --- a/docs/TEST_COVERAGE.md +++ b/docs/TEST_COVERAGE.md @@ -50,6 +50,7 @@ authoritative runtime totals. ### OpenClaw.Tray.Tests - **Tray UI and state** - app state, menu display/position/sizing, tray tooltip formatting, activity streams, async list loading, diagnostics contracts, markup regressions, and chat timeline/markdown handling. +- **Chat scroll lifetime** - `ChatTimelinePresentationTests` guards the workaround for [microsoft/microsoft-ui-xaml#11865](https://github.com/microsoft/microsoft-ui-xaml/issues/11865): automatic tail navigation must not use `ItemsView.StartBringItemIntoView`, which can retain a recycled row as an invalid anchor and throw `E_INVALIDARG` during subsequent layout. Tail requests use non-animated `ScrollView` extent navigation with stable bottom anchoring. Runtime proof should repeatedly switch between large, mixed-height histories, verify the final message is visible, and check that a scrolled-up reader is not pulled to the bottom by new messages. - **Connection and pairing** - connection manager node connector tests, connection page approval/channel metrics/row state, operator and Windows tray node pairing approval, and gateway action transport. - **Settings and startup** - settings round-trip/isolation, consent and settings save, auto-start defaults, startup setup state, existing config guard policy, and local setup progress stage mapping. - **Onboarding and local gateway setup** - onboarding completion/chat bootstrapper/existing config guard, wizard flow/selection/error/step parsing, setup code decoding, local gateway setup diagnostics, uninstall, WSL keep-alive, and auto-pair flags. diff --git a/src/OpenClaw.Tray.WinUI/Chat/ReactorItemsViewScrollController.cs b/src/OpenClaw.Tray.WinUI/Chat/ReactorItemsViewScrollController.cs index c861cb14a..4cfe44a95 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/ReactorItemsViewScrollController.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/ReactorItemsViewScrollController.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Reactor.Core.V1Protocol; using Microsoft.UI.Reactor.Input; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using System.Runtime.CompilerServices; using WinUIAnnotatedScrollBar = Microsoft.UI.Xaml.Controls.AnnotatedScrollBar; using WinUIItemsView = Microsoft.UI.Xaml.Controls.ItemsView; @@ -253,7 +254,7 @@ private void QueueTailRequest(int version, TailNavigationRequest request) private bool StartTailRequest(TailNavigationRequest request) { - if (itemsView.ScrollView is not { IsLoaded: true }) + if (itemsView.ScrollView is not { IsLoaded: true } scrollView) return false; if (!TailNavigationPolicy.CanExecute( @@ -266,11 +267,12 @@ private bool StartTailRequest(TailNavigationRequest request) } _following = true; - itemsView.StartBringItemIntoView(request.Index, new BringIntoViewOptions - { - AnimationDesired = false, - VerticalAlignmentRatio = 1.0, - }); + // Work around microsoft/microsoft-ui-xaml#11865 without retaining a row + // as a bring-into-view anchor across history replacement. + scrollView.ScrollTo( + scrollView.HorizontalOffset, + scrollView.ScrollableHeight, + new ScrollingScrollOptions(ScrollingAnimationMode.Disabled, ScrollingSnapPointsMode.Ignore)); return true; } diff --git a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs index e258ba47a..80483283f 100644 --- a/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs +++ b/tests/OpenClaw.Tray.Tests/ChatTimelinePresentationTests.cs @@ -66,8 +66,11 @@ public void ReactorTimeline_UsesStableBottomAnchoringAndDiscreteTailRequests() Assert.Contains("itemsView.Loaded += OnLoaded", binding); Assert.Contains("itemsView.LayoutUpdated += OnLayoutUpdated", binding); Assert.Contains("itemsView.DispatcherQueue.TryEnqueue", binding); - Assert.Contains("itemsView.StartBringItemIntoView(", binding); - Assert.Contains("VerticalAlignmentRatio = 1.0", binding); + Assert.Contains("scrollView.ScrollTo(", binding); + Assert.Contains("scrollView.HorizontalOffset,", binding); + Assert.Contains("scrollView.ScrollableHeight,", binding); + Assert.Contains("ScrollingAnimationMode.Disabled, ScrollingSnapPointsMode.Ignore", binding); + Assert.DoesNotContain("StartBringItemIntoView", binding); Assert.Contains("!string.Equals(_displayedTailKey, displayedTailKey, StringComparison.Ordinal)", binding); Assert.Contains("_following = IsNearBottom(sender)", binding); Assert.Contains("_scrollView.VerticalAnchorRatio = 1.0", binding); @@ -83,7 +86,6 @@ public void ReactorTimeline_UsesStableBottomAnchoringAndDiscreteTailRequests() Assert.DoesNotContain("ChangeView", binding); Assert.DoesNotContain("UpdateLayout", binding); Assert.DoesNotContain("TailSettle", binding); - Assert.DoesNotContain("ScrollTo(", binding); Assert.DoesNotContain("ScrollCompleted", binding); Assert.DoesNotContain("DispatcherTimer", binding); Assert.DoesNotContain("TextLength != current.TextLength", binding); @@ -96,6 +98,7 @@ public void ReactorTimeline_UsesStableBottomAnchoringAndDiscreteTailRequests() var viewChanged = binding[viewChangedStart..tailRequestStart]; Assert.DoesNotContain("VerticalAnchorRatio", viewChanged); Assert.DoesNotContain("StartBringItemIntoView", viewChanged); + Assert.DoesNotContain("ScrollTo(", viewChanged); } [Fact]