Skip to content

fix(frontend): stop reduced-motion transcript guards from compounding / 修复关闭窗口动画后会话滚动大跳 - #9753

Merged
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/reduced-motion-transcript-scroll
Sep 3, 2026
Merged

fix(frontend): stop reduced-motion transcript guards from compounding / 修复关闭窗口动画后会话滚动大跳#9753
SivanCola merged 2 commits into
esengine:main-v2from
SivanCola:fix/reduced-motion-transcript-scroll

Conversation

@SivanCola

@SivanCola SivanCola commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

User report: on Win10 LTSC 1809 (8th-gen i7, GTX 1060) with all Windows visual effects disabled, the transcript still jumps by thousands of pixels while scrolling and the question-jump slider is unusable. Enabling "Animate controls and elements inside windows" makes both symptoms disappear.

That setting is what WebView2 maps to prefers-reduced-motion: reduce (the attached diagnostics manifest has reducedMotion: true). Our global reduced-motion reset turned every transition into a 0.01ms transition: all. A near-zero transition still starts from the old value, so transform/padding writes are not visible to same-frame getBoundingClientRect / scrollHeight reads; they land one frame later. Verified in headless Chromium: normal mode reads the new value synchronously, the 0.01ms rule reads the old value until the next frame, transition: none reads the new value synchronously again.

The transcript scroll guards measure geometry right after their own writes, so:

  • The reader-extent guard wrote its visual-guard transform, re-measured the anchor row, saw "no change", and treated its own unapplied transform as fresh displacement. The diagnostics show reverseDisplacement 7088.5 → 14177 → 21265.5 → 28354 → 35442.5 (exact multiples), followed by a correction to scrollTop 0. That is the reported jump.
  • Virtuoso's padding writes also lagged a frame, so scrollHeight oscillated 9057 ↔ 16369 during the history prepend and the question-jump intent ended degraded. That is the "slider unusable" report.

Changes

  • styles.css: the reduced-motion block uses transition: none !important instead of transition-duration: 0.01ms. Animations keep the 0.01ms squash so fill-mode: forwards end states survive.
  • transcriptReaderExtentStability.ts: new transcriptAppliedVisualOffset() reads the item list's applied computed transform (matrix/matrix3d translateY) and falls back to the remembered offset only when nothing can be read.
  • useTranscriptReaderExtentStability.ts (observe + tick correction) and transcriptAnchorCompensation.ts subtract the applied offset rather than the remembered one, so a lagging or externally cleared guard cannot compound.
  • Race tests: two new scenarios (transform not yet applied; transform applied but remembered offset gone). Both fail against the previous hook (-2043px guard, 1154px correction) and pass now (-681px, 681px).
  • Bundle budgets step one decimal each, measured on the rebased tree with the worktree's own install: gzip 462.827 KiB (budget 462.7 → 462.9), raw 2469.815 KiB (budget 2469.4 → 2469.9).
  • Follow-up d8840832f: the two visual-guard race scenarios live in transcript-reader-visual-guard-race.test.tsx (the extent race file was back over the 800-line repolint ceiling), and check-motion-ci-contract.mjs now asserts the global reduced-motion reset uses transition: none !important and never shortens transitions, so the CSS half of the fix cannot regress silently.

Verification

  • pnpm test (251 suites), typecheck, test:typecheck, lint:hooks, check:css, test:motion, pnpm build all pass.
  • bench/transcript-scroll-stability.mjs with reducedMotion: "reduce" emulation: 94/94 pass, including the reduced-motion tail and return-to-bottom gates.
  • bench/transcript-reader-transaction.mjs: Chromium 20/20 rounds pass; the WebKit leg did not run locally (WebKit not installed).
  • transcript-reader-visual-guard-race: 9 pass on the head; 2 fail against the pre-fix hook. The motion contract check fails when the reset is reverted to transition-duration.
  • go run ./tools/repolint clean.
  • Not yet verified on the reporter's Windows machine; a canary test build from this head is with the tester.

Merge order

Merge this PR before #9754. Both touch useTranscriptReaderExtentStability.ts (adjacent hunks in observe()) and check-bundle-budget.mjs; a dry-run merge in either order conflicts on those two files, so #9754 should rebase onto main-v2 after this lands. The changes are semantically independent: this PR replaces the remembered visualOffset with the applied transform when deriving physicalAnchorDrift; #9754 widens the rejected predicate two lines below.

Documentation-impact: none - behavior change is a bug fix in the scroll guards and a reduced-motion CSS reset; no documented user-facing behavior changes.

Windows reports prefers-reduced-motion: reduce whenever "Animate controls
and elements inside windows" is off. The global reduced-motion reset turned
every transition into a 0.01ms `transition: all`, which still starts from
the old value: transform and padding writes were invisible to same-frame
getBoundingClientRect/scrollHeight reads. The reader-extent guard then
re-measured its own unapplied transform as fresh displacement and
compounded the visual offset on every scroll event (7088 → 14177 → 21265
px in the reported diagnostics), ending in a correction to scrollTop 0 and
degraded question-jump navigation.

- Reduced-motion CSS now removes transitions (`transition: none`) instead
  of shortening them, so geometry lands synchronously. Animations keep the
  0.01ms squash so fill-mode: forwards end states survive.
- The reader-extent guard and the anchor-compensation loop derive the
  physical drift from the item list's applied computed transform rather
  than the remembered offset, so a lagging or externally cleared guard can
  no longer compound.
- Race tests cover the unapplied and applied transform cases; both fail
  against the previous hook.
- Bundle budgets step one decimal: 462.7 → 462.9 KiB gzip (462.827
  measured), 2469.4 → 2469.9 KiB raw (2469.815 measured).
@SivanCola
SivanCola requested a review from esengine as a code owner September 3, 2026 06:24
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Sep 3, 2026
… reset

Problem: repolint rejected the PR head because the two new visual-guard
scenarios pushed transcript-reader-extent-race.test.tsx to 853 lines, over
the 800-line test-file ceiling. The CSS half of the fix also had no
automated guard: nothing stopped a later edit from restoring
`transition-duration: 0.01ms` and silently reintroducing the lagging
same-frame geometry reads.

Fix:
- Move the unapplied/applied visual-guard scenarios into
  transcript-reader-visual-guard-race.test.tsx with the same JSDOM + fake
  rAF harness, restore the extent race file to its base contents, and add
  the new file to test:transcript and the motion CI contract.
- Extend check-motion-ci-contract.mjs so the global universal
  prefers-reduced-motion reset must use `transition: none !important` and
  must not shorten transitions.

Verification:
- transcript-reader-visual-guard-race: 9 pass on the PR head; 2 fail
  against the pre-fix hook (-2043px guard, 1154px correction).
- check-motion-ci-contract passes on the PR head and fails when the reset
  is reverted to transition-duration.
- go run ./tools/repolint clean; pnpm test:transcript, test:motion,
  test:typecheck, lint:hooks pass; git diff --check clean.
@SivanCola SivanCola changed the title fix(frontend): stop reduced-motion transcript guards from compounding fix(frontend): stop reduced-motion transcript guards from compounding / 修复关闭窗口动画后会话滚动大跳 Sep 3, 2026
@SivanCola
SivanCola merged commit 44b5664 into esengine:main-v2 Sep 3, 2026
43 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant