Skip to content

Commit fcc7477

Browse files
perf: virtualize chat timeline rows (#898)
Virtualize native chat rows with stable recycled containers, bounded renderer state, and cleanup for removed component trees. Preserve history, update, append, and follow-to-bottom behavior with native WinUI proof. Thanks @TurboTheTurtle for the contribution. Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
1 parent dae64b6 commit fcc7477

6 files changed

Lines changed: 690 additions & 23 deletions

File tree

src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public record OpenClawChatTimelineProps(
7575
public class OpenClawChatTimeline : Component<OpenClawChatTimelineProps>
7676
{
7777
const double FollowThreshold = 60;
78+
const int FollowToBottomMaxStabilizationPasses = 4;
79+
const double FollowToBottomExtentEpsilon = 0.5;
7880

7981
/// <summary>
8082
/// Static scroll-offset store shared across all timeline instances so that
@@ -466,7 +468,7 @@ public override Element Render()
466468

467469
var scrollViewRef = UseRef<Microsoft.UI.Xaml.Controls.ScrollViewer?>(null);
468470
var isFollowingRef = UseRef(true);
469-
var contentRef = UseRef<Microsoft.UI.Xaml.Controls.StackPanel?>(null);
471+
var contentRef = UseRef<FrameworkElement?>(null);
470472
var prevEntryCountRef = UseRef(0);
471473
var prevSessionIdRef = UseRef<string?>(null);
472474
var prevFirstEntryIdRef = UseRef<string?>(null);
@@ -509,7 +511,10 @@ public override Element Render()
509511
if (prevShowToolCallsRef.Current != showToolCalls)
510512
{
511513
prevShowToolCallsRef.Current = showToolCalls;
512-
contentRef.Current?.Children.Clear();
514+
if (contentRef.Current is ItemsRepeater repeater)
515+
repeater.ItemsSource = Array.Empty<object>();
516+
else if (contentRef.Current is StackPanel stackPanel)
517+
stackPanel.Children.Clear();
513518
}
514519

515520
// Hover state — set of entry ids currently under the pointer. Used to
@@ -636,15 +641,27 @@ void UpdateScrollMetrics(Microsoft.UI.Xaml.Controls.ScrollViewer sv)
636641
void QueueScrollToBottom(Microsoft.UI.Xaml.Controls.ScrollViewer sv, string? sessionId, bool disableAnimation)
637642
{
638643
isFollowingRef.Current = true;
639-
sv.DispatcherQueue.TryEnqueue(() =>
644+
645+
void QueuePass(int pass, double previousScrollableHeight, bool passDisableAnimation)
640646
{
641-
var bottom = sv.ScrollableHeight;
642-
sv.ChangeView(null, bottom, null, disableAnimation);
643-
lastVerticalOffsetRef.Current = bottom;
644-
lastScrollableHeightRef.Current = sv.ScrollableHeight;
645-
isFollowingRef.Current = true;
646-
StoreSessionOffset(sessionId, bottom);
647-
});
647+
sv.DispatcherQueue.TryEnqueue(() =>
648+
{
649+
sv.UpdateLayout();
650+
var bottom = sv.ScrollableHeight;
651+
sv.ChangeView(null, bottom, null, passDisableAnimation);
652+
lastVerticalOffsetRef.Current = bottom;
653+
lastScrollableHeightRef.Current = sv.ScrollableHeight;
654+
isFollowingRef.Current = true;
655+
StoreSessionOffset(sessionId, bottom);
656+
657+
var extentStable = Math.Abs(bottom - previousScrollableHeight) <= FollowToBottomExtentEpsilon;
658+
var atBottom = sv.ScrollableHeight - sv.VerticalOffset <= FollowThreshold;
659+
if (pass < FollowToBottomMaxStabilizationPasses && (!extentStable || !atBottom))
660+
QueuePass(pass + 1, sv.ScrollableHeight, passDisableAnimation: true);
661+
});
662+
}
663+
664+
QueuePass(0, double.NaN, disableAnimation);
648665
}
649666

650667
void QueuePreservePrependOffset(Microsoft.UI.Xaml.Controls.ScrollViewer sv, string? sessionId, double oldOffset, double oldScrollableHeight)
@@ -2490,12 +2507,12 @@ bool BurstIsNestable(System.Collections.Generic.List<ChatTimelineItem> b)
24902507
Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto],
24912508
loadMoreButton.Grid(row: 0, column: 0),
24922509
Border(Empty()).Height(20).Grid(row: 1, column: 0),
2493-
VStack(2, timelineRows).Set(sp =>
2510+
VirtualVStack(2, timelineRows).Set(host =>
24942511
{
2495-
if (contentRef.Current != sp)
2512+
if (contentRef.Current != host)
24962513
{
2497-
contentRef.Current = (Microsoft.UI.Xaml.Controls.StackPanel)sp;
2498-
sp.SizeChanged += (_, _) =>
2514+
contentRef.Current = host;
2515+
host.SizeChanged += (_, _) =>
24992516
{
25002517
if (scrollViewRef.Current is not { } sv) return;
25012518

0 commit comments

Comments
 (0)