fix(frontend): clear programmatic scroll flag when a content-fits send is a no-op - #238
Open
detail-app[bot] wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Bug
On a short/new conversation whose content fits the viewport (
scrollHeight ≤ clientHeight),scrollOnce()callsperformScroll(), whosescrollTo({ top: scrollHeight })clamps to the current position — a no-op that fires 0scroll/scrollendevents. With no event to clear it viaonUserScroll's near-bottom branch,programmaticScrollActivestays stucktruefor the rest of the stream. Any laterscrollevent with no intent — e.g. keyboardTabfocus into an off-screen link, which the browserscrollIntoViews without firing wheel/touch/pointer or aUSER_SCROLL_KEYSkeydown — hitsonUserScroll's branch 3 but is gated off by the stuck flag, so follow never disengages. At stream end,revealFinalContentIfFollowing()yanks the viewport back to the bottom and suppresses the unseen-content indicator. Introduced in 30ac922.Fix
In
performScroll(), track a stickymovedDuringScrollaccumulator across reconciliation passes — settrueonly when a pass will actually move the container (scrollTop + clientHeight < scrollHeight). After the loop, clearprogrammaticScrollActiveonly when every pass was a no-op. Real smooth scrolls keep the flag raised so their in-flightscrollevents stay shielded (preserving the intentional design guarded by the existing intermediate-smooth-scroll test); no-op sends — which fire no events at all — get the flag cleared so a later branch-3 entry can disengage follow. This mirrors the synchronous resetfollowActiveStreamalready uses for itsbehavior:"instant"scrolls, adapted forperformScroll's smooth-scroll shielding requirement.Testing
createScrollAnchor.svelte.test.ts— a regression guard for the no-op send + no-intent scroll-away (the exact bug), a mixed-pass guard (no-op pass 0 + real-move pass 1 keeps the flag raised to shield in-flight events), and an all-no-op guard (every pass clamps → flag clears → follow disengages). Verified the two no-op guards fail on the unmodified source (yank to the bottom) and pass with the fix; the mixed-pass guard passes both ways, so it is not a tautology. The full existing suite stays green, and the entire frontend unit suite passes (355 tests across 31 files).npm run validateis clean — svelte-check reports 0 errors/0 warnings, oxlint (regular and type-aware), eslint (zod), and ast-grep all pass, andoxfmt --checkreports all files correctly formatted.<a>→ realTabfocus into the link (no-intent scroll) → stream-end reveal. On the unmodified source it yanksscrollTop1882 → 2463 (the bottom) with the indicator suppressed; on the fixed source the viewport stays at 1882 and the indicator rises. Two supporting probes confirmed the preconditions jsdom can't establish: a content-fits no-opscrollTofires 0scroll/scrollendevents, andTabinto an off-screen<a>fires ascrollwith no pointerdown/wheel/touchstart intent and lands below the 95% near-bottom threshold. These probes were run and removed per the repo's probes-are-not-tests policy.build -x testboth succeed; the changed composable bundles cleanly into the Spring Boot jar.make dev) — it requires a live LLM gateway credential, a running Qdrant, and CSRF/Clerk auth unavailable in this sandbox. The real-browser probe against the actual composable above covers the same end-to-end guarantee (no end-of-stream yank after a Tab-focus scroll-away).Closes #225
Automatic Fixes PRs can be configured here.