Skip to content

Fix chat scrollbar not reaching bottom during streaming via scroll anchoring - #1014

Merged
shanselman merged 6 commits into
openclaw:mainfrom
karkarl:karkarl-fix-chat-scrollbar-reconciliation
Jul 21, 2026
Merged

Fix chat scrollbar not reaching bottom during streaming via scroll anchoring#1014
shanselman merged 6 commits into
openclaw:mainfrom
karkarl:karkarl-fix-chat-scrollbar-reconciliation

Conversation

@karkarl

@karkarl karkarl commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fix chat scrollbar not reaching bottom during streaming via scroll anchoring

Problem

During streaming (agent thinking + token growth), the chat scrollbar doesn't reach the bottom and gets stuck ~30% from the newest row. Root cause: as WinUI's ItemsRepeater re-estimates row heights during virtualized realization, the extent climbs a few px per frame with no reliable SizeChanged/ViewChanged event to drive a reactive re-pin.

Solution

Replace the old reactive post-layout ChangeView re-pin with WinUI ScrollViewer scroll anchoring (sv.VerticalAnchorRatio = 1.0), which keeps the bottom-most realized row glued to the viewport bottom pre-paint as the extent grows — zero jitter, no intermediate short frame.

The anchoring is dynamic: enabled (1.0) while the view is in the bottom band (follow mode), disabled (NaN) when the user scrolls up to read earlier history. This prevents extent re-estimation during full-remount from drifting the reader's held position.

Three cooperating mechanisms

  1. WinUI scroll anchoring — pins the bottom row pre-paint as extent grows in-place (streaming/thinking growth)
  2. Self-terminating settle timer — chases late virtualization extent corrections on discrete scroll-to-bottom events, converges on being at the bottom (not on a never-settling stable extent)
  3. Coalesced reactive follows + UserScrolledAway guard — genuine user scroll-away is authoritative; follow machinery never clobbers the user's own scroll

Settle timer moderate-scroll fix (f791c2f7ee0d1b)

When a user scrolls above FollowThreshold (60px) but below the large abandonGap (900px) during an active settle timer, the timer's periodic PinToBottom overrode their scroll intent. Fixed by checking isFollowingRef.Current at the top of each timer tick AND after UpdateLayout:

  • When the user scrolls, ChangeView fires ViewChangedUpdateScrollMetrics sets isFollowingRef=false (gap > 60px)
  • The next timer tick sees isFollowingRef=false → stops timer, disables anchoring, never calls PinToBottom
  • The ViewChanged moderate-cancel clause (existing) provides supplementary early cancellation

This is the ViewChanged source of truth directly encoding user intent — no offset heuristics, no risk of misclassifying ItemsRepeater re-estimation drift.

Key behaviors preserved


Validation

Head: 7ee0d1b63e4b4ccc0b2cecb133738f6bf3f2d1e4

Build:build.ps1 — all 5 projects pass (Shared, Cli, WinNodeCli, SetupEngine, WinUI)

Shared tests: ✅ 2863 passed, 31 skipped

Tray tests: ✅ 1716 passed

Tray UI tests (non-accessibility): ✅ 83/83 passed × 2 runs (no flakes, no hangs)

Accessibility tests: ✅ 18/18 passed

Focused anchoring tests (6/6):

  • ✅ ThinkingAndStreamingGrowth_KeepsViewPinnedToBottom
  • ✅ UserScrolledUpDuringStreaming_ViewStaysStable
  • ✅ ModerateScrollUpDuringSettleTimer_IsNotRepinned (NEW — 5/5 repeat stable)
  • ✅ PrependHistory_PreservesViewport
  • ✅ LargeNativeTimeline_PinsBottom
  • ✅ RealizedComponent_IntegratesWithRenderer

Contract tests: ✅ 3/3 passed (ChatTimelineVirtualizationContractTests)

Red/green mutation proof

Mutation Result Evidence
Both timer-tick isFollowingRef checks + ViewChanged moderate-cancel disabled ❌ FAIL (gap=0, timer re-pins) Confirms test exercises real bug
ViewChanged moderate-cancel disabled, timer-tick checks active ✅ PASS Timer-tick isFollowingRef is the essential fix
Both mechanisms active (shipped code) ✅ PASS Full defense-in-depth

Residual risk

  • 61–200px band timing edge: If a user's ChangeView is processed AFTER a timer tick fires but BEFORE the tick's UpdateLayout, the tick could call PinToBottom once before observing the user's intent on the following tick. In practice, continuous scroll gestures generate multiple ViewChanged events per tick, making single-tick lag non-observable.
  • Pre-existing flake: QueuedSend_InFlightAckWithoutLifecycle is timing-sensitive (passed on re-run, unrelated to scroll anchoring).

…choring

While the agent is thinking/streaming, the last assistant row grows in place
(no new Props.Entries, no ScrollToBottomToken bump, no reliable SizeChanged).
The ItemsRepeater extent estimate climbs across realization frames, leaving the
offset a few px short of the bottom with nothing to re-pin it -- the "scrollbar
doesn't reach the bottom while the agent is thinking" bug.

Replace the earlier reactive ViewChanged re-pin (which painted an intermediate
short frame -> jitter, and whose programmatic ChangeView fought the user's own
drag) with WinUI ScrollViewer scroll anchoring:

- Set sv.VerticalAnchorRatio = 1.0 in the one-time ScrollViewer init so the
  bottom-most realized row stays glued to the viewport bottom BEFORE paint as the
  extent grows. The ViewChanged handler no longer re-pins; it only refreshes
  metrics and drives the load-earlier trigger.
- QueuePreservePrependOffset toggles anchoring to NaN during the manual
  load-earlier delta correction and restores it to 1.0 afterward, with a
  fail-safe restore if a dispatcher enqueue is rejected or the correction throws.

Discrete events (append, session switch, initial load, ScrollToBottom token)
still use QueueScrollToBottom; anchoring only covers the in-place growth case.

Known residual (acceptable temporary fix): slight jitter as the viewport expands
during virtualization/streaming. Tracked on issue openclaw#996 (Reactor port).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. labels Jul 17, 2026
@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed July 20, 2026, 2:24 PM ET / 18:24 UTC.

Summary
The PR changes the WinUI chat timeline to combine bottom scroll anchoring with bounded settling and user-scroll cancellation, and adds virtualization/UI-thread regression coverage for streaming, prepend history, and reading-position stability.

Reproducibility: yes. at source level: in-place growth of the final virtualized assistant row bypasses ordinary append/token follow paths, and the PR adds focused UI proofs for that behavior. The supplied video also demonstrates the affected streaming path, although the current-head full UI test result is still pending.

Review metrics: 2 noteworthy metrics.

  • Changed surface: 4 files affected; 750 additions, 36 deletions. Most of the patch is focused behavioral proof and test infrastructure around one chat timeline production path.
  • Behavioral scenarios: 3 recorded scenarios; 3 targeted UI regression cases. The evidence covers follow-to-bottom, deliberate user scroll-up, and prepend restoration, while exposing the remaining resize limitation.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Let the current-head CI matrix complete, including the tray UI test job.
  • [P2] Have a maintainer explicitly accept or reject the documented resize-during-streaming limitation.

Risk before merge

  • [P1] The supplied proof shows that resizing during streaming can still drift from the bottom band (reported as 100% to about 91%); merging accepts that bounded residual until the broader follow-up at Port OpenClawTray.FunctionalUI to Reactor to stop hand-maintaining bespoke UI primitives #996 changes the underlying UI architecture.
  • [P1] The current-head CI matrix is still in progress, so its UI-test execution remains a merge-time gate rather than evidence that the new branch has already passed every suite.

Maintainer options:

  1. Accept the documented resize limitation (recommended)
    Merge after the current-head checks complete if maintainers accept that ordinary streaming and user scroll-up are fixed while viewport-resize convergence remains tracked separately.
  2. Require full resize follow behavior
    Keep the branch open until resize-during-streaming remains in the bottom band rather than drifting after recovery.

Next step before merge

  • [P2] No discrete mechanical defect is identified for a repair lane; remaining work is current-head validation and maintainer acceptance of the known runtime limitation.

Maintainer decision needed

  • Question: Is the documented resize-during-streaming drift acceptable for this focused bottom-follow fix, with the fuller architectural remedy remaining in Port OpenClawTray.FunctionalUI to Reactor to stop hand-maintaining bespoke UI primitives #996?
  • Rationale: The recording demonstrates meaningful improvement but also a remaining user-visible limitation that tests and CI cannot decide is acceptable product behavior.
  • Likely owner: karkarl — They are the strongest available recent contributor for the related chat UI behavior and can provide the technical context for the trade-off.
  • Options:
    • Accept the bounded fix (recommended): Merge after current-head checks pass, retain the resize limitation as explicit follow-up work in the Reactor migration discussion.
    • Require resize convergence first: Keep this PR open until the timeline also remains reliably bottom-pinned through viewport resizing during streaming.

Security
Cleared: The four-file WinUI behavior and test change adds no dependency, secret, permission, workflow, package-resolution, or external code-execution surface.

Review details

Best possible solution:

Land the bounded scroll-follow improvement only after the current-head test matrix succeeds and maintainers explicitly accept the documented resize residual as tracked follow-up work in #996.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: in-place growth of the final virtualized assistant row bypasses ordinary append/token follow paths, and the PR adds focused UI proofs for that behavior. The supplied video also demonstrates the affected streaming path, although the current-head full UI test result is still pending.

Is this the best way to solve the issue?

Yes, provisionally: native scroll anchoring plus bounded settling is narrower than post-layout scroll fighting and the PR tests user intent and prepend restoration. The remaining resize drift means maintainers must decide whether this partial fix is acceptable before merge.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against bd6c4bf8e575.

Label changes

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (recording): The supplied current-head runtime recording provides live scroll diagnostics for streaming follow and deliberate scroll-up, and it transparently shows the remaining resize limitation; redact any private endpoints or identifiers if the proof is reposted.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: This is a bounded chat scrolling regression fix with meaningful but non-emergency user impact.
  • merge-risk: 🚨 other: The supplied real-behavior proof shows a known user-visible resize residual that correctness checks alone cannot resolve.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (recording): The supplied current-head runtime recording provides live scroll diagnostics for streaming follow and deliberate scroll-up, and it transparently shows the remaining resize limitation; redact any private endpoints or identifiers if the proof is reposted.
  • proof: sufficient: Contributor real behavior proof is sufficient. The supplied current-head runtime recording provides live scroll diagnostics for streaming follow and deliberate scroll-up, and it transparently shows the remaining resize limitation; redact any private endpoints or identifiers if the proof is reposted.
  • proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. The supplied current-head runtime recording provides live scroll diagnostics for streaming follow and deliberate scroll-up, and it transparently shows the remaining resize limitation; redact any private endpoints or identifiers if the proof is reposted.
Evidence reviewed

What I checked:

  • Prior review feedback addressed: The latest branch commit replaces the prior multi-pass approach with coalesced follow-to-bottom handling, dynamic anchoring, user-scroll cancellation, and prepend preservation; its stated scope directly addresses the previous P1 concern about safely suspending anchoring. (src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs, f0e99b839c6f)
  • Focused behavioral coverage: The diff adds UI proofs for bottom pinning during in-place thinking/streaming growth, preserving a user’s scrolled-up reading position, and restoring anchoring after prepended history; the source contract test guards the three cooperating follow mechanisms. (tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs, f0e99b839c6f)
  • Real behavior proof: The PR provides a 58-second multi-scenario recording with live ScrollPattern diagnostics showing 100% pinning during streaming and a stable 28.3% user-selected scroll position; it also candidly records the remaining resize drift. (f0e99b839c6f)
  • Adjacent merged chat history: The related merged chat UI work established a prior FunctionalUI fix path, but this PR explicitly distinguishes the present issue as ItemsRepeater follow-to-bottom convergence rather than control recreation. (src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs, a0042affb8c0)

Likely related people:

  • karkarl: They previously delivered the merged FunctionalUI chat stability work and authored the current timeline change, providing the strongest available history signal for this chat/UI area. (role: recent adjacent chat contributor; confidence: medium; commits: a0042affb8c0, f0e99b839c6f; files: src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs, tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (6 earlier review cycles)
  • reviewed 2026-07-17T20:53:40.270Z sha 214a47c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-17T22:32:51.287Z sha 214a47c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-17T23:15:14.289Z sha 214a47c :: needs real behavior proof before merge. :: [P2] Replace fixed compositor delays with eventual assertions
  • reviewed 2026-07-17T23:50:26.834Z sha 8acd924 :: needs real behavior proof before merge. :: [P1] Use a supported way to suspend scroll anchoring
  • reviewed 2026-07-17T23:56:56.875Z sha 8acd924 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T00:38:53.956Z sha 8acd924 :: needs changes before merge. :: [P1] Use a supported way to suspend scroll anchoring

@shanselman

Copy link
Copy Markdown
Collaborator

GitHub Copilot maintainer-assistant review — automated feedback, not written personally by Scott.

Reviewed head: 214a47ce270cc967fa80d49c7d1ea9fc8b05a5ba

Using native VerticalAnchorRatio = 1.0 for in-place streaming growth is the right architectural direction; it is preferable to continuously fighting layout with post-render ChangeView calls. The implementation also carefully disables/restores anchoring around prepend-offset correction.

The current proof/test coverage is not sufficient to merge yet:

  1. The new ThinkingAndStreamingGrowth_KeepsViewPinnedToBottom UI test was reportedly not run. Please run it on current-head Windows CI/local UI and include actual gap values, not only a pass claim.
  2. Add a real UI test for user intent: scroll above FollowThreshold, stream multiple revisions into the active assistant row, and prove VerticalOffset remains near the user-selected position rather than snapping to bottom.
  3. Add a prepend-history test: with HasMoreHistory=true, prepend entries, verify offset preservation, then append/stream again and prove anchoring was restored rather than left disabled or double-correcting.
  4. Replace fixed Task.Delay(50) compositor drains with bounded polling/eventual assertions. The test should wait for stable extent/offset conditions with a timeout; a tight fixed delay can be flaky or pass before the intended anchor adjustment occurs.
  5. Provide a short current-head recording showing: bottom-follow during streaming, user scroll-up remaining stable, and resize/DPI change during streaming.

Please also document or verify the double.NaN anchoring-disable sentinel. VerticalAnchorRatio documents normalized values; if NaN is retained as a WinUI implementation pattern, pin it with the prepend test and a safe restoration path. Otherwise prefer a documented value/strategy.

The code approach looks promising, but the tests currently prove only the happy bottom-pinned path and have not themselves been demonstrated to run. This is a UI behavior change, so current-head native proof is the remaining gate.

@shanselman

Copy link
Copy Markdown
Collaborator

GitHub Copilot maintainer-assistant note — automated, not written personally by Scott.

@karkarl, thank you for working on this—the native anchoring approach looks promising. Could you please add a short current-head screen recording? A static screenshot cannot demonstrate the timing/scroll behavior we need to verify.

Ideally the recording would show:

  1. an assistant response actively streaming while the view remains pinned to the bottom;
  2. scrolling up during that same stream and remaining at the chosen position without being pulled back down;
  3. loading earlier history without a viewport jump, followed by streaming/appending to show anchoring was restored;
  4. resizing the window during streaming without losing the expected position.

Please also include the exact PR SHA and the command/result for the new Tray UI tests. If the UI test suite is difficult to run locally, a link to the current-head CI job/artifact is fine once it demonstrably executes the new test.

That proof plus deterministic eventual/polling assertions in place of fixed compositor delays would give maintainers a much stronger basis to land the change. Thanks!

@clawsweeper clawsweeper Bot added proof: 🎥 video Contributor real behavior proof includes video or recording evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jul 17, 2026
…ic drain

Address PR review feedback (shanselman + clawsweeper) requesting deeper test
coverage of the VerticalAnchorRatio=1.0 scroll-anchoring fix.

Tests (tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs):
- UserScrolledUpDuringStreaming_KeepsReadingPositionStable: proves streaming
  growth of the off-screen newest row does NOT yank a scrolled-up user to the
  bottom (gap stays > FollowThreshold, reading position drift <= 24px across 4
  revisions) - the "don't fight the scrollbar" half of the fix.
- PrependHistory_PreservesOffset_AndRestoresBottomAnchoring: proves load-earlier
  prepend preserves the visible offset (shifts by inserted height) AND restores
  VerticalAnchorRatio to 1.0 (not left at the NaN disable sentinel), then a
  scroll-to-bottom token follows again end-to-end. Guards the highest-risk path
  that a fresh live recording cannot exercise.
- Gap-value diagnostics logging on the streaming pin test (CHAT_TIMELINE_*).

Determinism (UIThreadFixture.cs + test drain):
- Add YieldToRenderAsync(): awaits one Low-priority dispatcher callback as a
  bounded render-queue drain primitive; completes immediately if the dispatcher
  rejects the enqueue so callers can never block.
- Replace DrainRenderQueueAsync's single Task.Delay(50) with 6 bounded passes of
  UpdateLayout + YieldToRenderAsync + a 5ms floor.

Docs (OpenClawChatTimeline.cs):
- Expand the QueuePreservePrependOffset comment to document that double.NaN is
  the documented WinUI sentinel that disables anchoring (default value; 0.0 would
  anchor to the TOP edge, not disable), per the VerticalAnchorRatio contract.

Validation: build.ps1 green; UITests project compiles clean (0 warn/err);
Shared 2863 passed; Tray 1717 passed. UITests run on CI (they time out in the
local harness); the 2 new tests are exercised by the ci.yml UITests job.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 17, 2026
… prepend preservation

The ItemsRepeater SizeChanged storm during row realization fired a flood of
reactive QueueScrollToBottom calls that each superseded the user's own
ChangeView, so the proof tests' scroll-up never "took" and the view stayed
pinned. Replace the multi-pass QueuePass stabilization with:

- A coalescing guard (scrollPinPendingRef) so host.SizeChanged enqueues at most
  one reactive pin at a time. Held across the synchronous pin window (which can
  re-enter SizeChanged) and released only in terminal paths.
- An authoritative ViewChanged handler: a genuine user scroll past the abandon
  gap cancels the in-flight settle timer/pending pin and disables anchoring
  immediately; symmetric re-arm to 1.0 when back in the bottom band.
- A single settle timer (converges on atBottom, hands off to VerticalAnchorRatio
  = 1.0) instead of parallel timers fighting over ChangeView.
- A per-render anchoring gate: VerticalAnchorRatio = following ? 1.0 : NaN,
  deferring to any pin/prepend that owns the ratio for its async window.

Prepend: our FunctionalUI reconciler full-remounts every Entry (openclaw#996), which
destroys the element the platform chose as the bottom anchor, so leaving
VerticalAnchorRatio = 1.0 re-pinned a scrolled-up reader to the NEW bottom
(offset 2528 -> 8531, gap 0). QueuePreservePrependOffset now disables anchoring
and manually re-seats the offset by the measured inserted height instead, keeping
the reader in the middle band (not reset to top, not dragged to bottom).

Also update the ChatTimelineVirtualizationContractTests source-text assertions to
the current mechanism and relax the prepend proof's anchoring assertion to the
dynamic model (NaN while scrolled up, or 1.0 at bottom). Removed dead DIAG
diagnostics and the unused FollowToBottomExtentEpsilon constant.

Validation: build.ps1 green; Shared 2863 passed/31 skipped; Tray 1717 passed;
UITests ChatTimelineVirtualizationProofTests 5/5; contract tests 3/3.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 21, 2026
shanselman and others added 2 commits July 20, 2026 17:48
When a user scrolls above FollowThreshold (60px) but below the large
abandonGap (900px) during an active settle timer, the timer's periodic
PinToBottom could override their scroll intent. This happened because:

1. The timer ticks every 16ms and calls PinToBottom each tick
2. PinToBottom's ChangeView could apply before ViewChanged processed
   the user's scroll, masking the user's intent
3. The existing ViewChanged moderate-cancel clause races with timer ticks

Fix: add a two-layer detection in the timer tick handler:

Layer 1 (isFollowingRef gate): Check isFollowingRef.Current at the top
of each tick AND after UpdateLayout. When the user's ChangeView applies,
ViewChanged fires → UpdateScrollMetrics sets isFollowing=false (gap > 60).
If this happens before/during the tick, we catch it immediately and
abandon without calling PinToBottom.

Layer 2 (pinConfirmed + delta): After confirming that PinToBottom
successfully converged (offset reached within FollowThreshold of
ScrollableHeight), track the confirmed offset. If a subsequent tick sees
the pre-layout offset dropped by > 200px from the confirmed position,
the user scrolled between ticks. The 200px threshold exceeds the maximum
observed WinUI ItemsRepeater re-estimation drift (~140px) to avoid
false-positive abandonment during normal extent convergence.

Add WinUI UI proof test: ModerateScrollUpDuringSettleTimer_IsNotRepinned
verifies that a 400px scroll-up during active settle timer is not
re-pinned across 4 streaming revisions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b969653-0736-4a50-8ca8-f7cf06f0d38c
Remove the pinConfirmed/lastConfirmedOffset/offsetPreLayout delta heuristic
from the timer tick. The isFollowingRef check at the top of each tick (and
after UpdateLayout) is the correct source of truth: when the user scrolls,
ViewChanged → UpdateScrollMetrics sets isFollowingRef=false, and the next
tick observes it immediately. The offset-based delta duplicated this signal
with worse reliability (could misclassify ItemsRepeater re-estimation drift).

Keep both mechanisms:
1. Timer-tick isFollowingRef gate (essential, proven by red/green mutation)
2. ViewChanged moderate-cancel clause (supplementary early cancellation)

Red/green mutation proof:
- Both disabled → test FAILS (gap=0, timer re-pins user scroll) ❌
- Timer-tick isFollowingRef alone → test PASSES ✅
- Both together → test PASSES ✅

Strengthen test: bounded poll confirms settle timer is active (anchoring=NaN)
before simulating user scroll, eliminating timing dependency on Task.Delay.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b969653-0736-4a50-8ca8-f7cf06f0d38c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4f0e93ff-d320-454d-b00a-865dac727d0c
@shanselman

Copy link
Copy Markdown
Collaborator

GitHub Copilot maintainer-assistant handoff — automated, not written personally by Scott.

@karkarl, thank you for this work. Your original change identified the right root mechanism: WinUI's native VerticalAnchorRatio = 1.0 is the correct way to keep the bottom row anchored while an in-place streaming response grows. That insight remains the foundation of the merged design; the later work hardened the lifecycle around it rather than replacing it.

During review we extended the branch around cases that native anchoring alone does not cover reliably under the current FunctionalUI/ItemsRepeater full-remount behavior: discrete new-entry/session-switch moves, prepend offset restoration, user scroll intent during a pending bottom settle, stale session/view callbacks, resize, and cleanup. The final implementation uses native anchoring for streaming plus a bounded settle for late virtualized extent correction, with user intent cancelling that settle.

We challenged the added complexity rather than assuming it was necessary. In a separate unpushed A/B branch we replaced the bounded settle with a much smaller two-dispatch one-shot design. It was simpler, but in the ordered/full UI suite it reproducibly stopped 208px short of the true bottom after earlier virtualization activity. That experiment is why we retained bounded settling. We also removed an extra independent 200px heuristic after adversarial review, so the final cancellation behavior uses the existing follow-intent signal instead of stacking another distance rule.

The final proof now covers real WinUI behavior for bottom-follow during streaming, modest and larger user scroll-up, resume-to-bottom, prepend/history behavior, resize, session/visual lifecycle, accessibility, and full-suite ordering. The selected tree was revalidated after the unrelated localization baseline repair, and all hosted checks passed.

The most important takeaway is that your native-anchor direction was correct. The extra machinery exists because the current virtualizer reports its final extent across later frames; once keyed reconciliation work such as #996 removes that remount limitation, some of this settling code may become removable. Thanks again for establishing the platform-correct approach and for the detailed proof work around it.

@shanselman
shanselman merged commit 653327a into openclaw:main Jul 21, 2026
12 checks passed
@shanselman

Copy link
Copy Markdown
Collaborator

Follow-up regression work is available in #1044 at 12b85917e0cd0e4683d63fa3315a474ba588cee7.

Automated WinUI proof now distinguishes explicit user intent from generation-scoped programmatic scrolling. On the original regression baseline, an upward user-equivalent request reversed from 12741.8 to 12769.8. The fixed head passed repeated monotonic top/bottom, exact bottom, streaming follow, user-hold, prepend/history, and resize proofs. Current-main normal-profile feedback is more nuanced: scrolling is a little jittery in the middle, but bottom is reachable.

The focused PR is intentionally not merge-ready until the user completes a direct normal-profile A/B and confirms a material improvement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. proof: 🎥 video Contributor real behavior proof includes video or recording evidence. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants