Skip to content

Stabilize transcript scrolling and Markdown surfaces / 稳定 transcript 滚动与 Markdown 展示 - #9754

Open
SivanCola wants to merge 15 commits into
esengine:main-v2from
SivanCola:fix/transcript-scroll-9711
Open

Stabilize transcript scrolling and Markdown surfaces / 稳定 transcript 滚动与 Markdown 展示#9754
SivanCola wants to merge 15 commits into
esengine:main-v2from
SivanCola:fix/transcript-scroll-9711

Conversation

@SivanCola

@SivanCola SivanCola commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the Reasonix transcript behavior reported in #9711: upward scrolling could jump to another location, the scrollbar could move independently of the viewport, and the question navigator could land on the wrong content. It also removes the empty fenced Markdown cards shown in the attached capture.

What changed

  • Added a tokenized transcript surface transaction spanning reader history prepends, question jumps, and scrollbar ownership.
  • Preserved reader position by logical rowKey and viewport offset across history prepends; stale generations and writers are rejected.
  • Removed the small-transcript full-mount path and retained a bounded reader corridor to avoid asynchronous Markdown/Virtualized height storms.
  • Rebased custom scrollbar drags after geometry changes and fenced native thumb completion by surface generation.
  • Kept question jumps masked until the target is loaded, mounted, visible, and stable; final placement uses a single writer with an approximately 16px top offset.
  • Added content-free transaction/anchor diagnostics and field replay coverage for the captured 27,104px reverse displacement.
  • Omitted whitespace-only fenced code blocks across shared Markdown rendering while preserving non-empty comment/code blocks and surrounding text.
  • Updated the native smoke contract to match bounded reader-corridor virtualization.

Verification

  • pnpm test:transcript — 177 tests passed.
  • pnpm test:typecheck, pnpm typecheck, pnpm check:scroll-writer — passed.
  • pnpm build — passed; initial JavaScript gzip 463.6 KiB, raw JavaScript/CSS 2473.3 KiB within the narrow calibrated budgets.
  • Chromium/WebKit transcript browser replay — passed with no blank sampled frames or reverse jumps.
  • Windows 11 WebView2 10.0.26200, runtime 152.0.4191.53: sustained native wheel smoke passed (936 rows, 4,459 frames, maximum reverse 0px, blank frames 0); native composer smoke passed (168 samples, maximum reverse 0px); native selection/compositor smoke passed 3/3 iterations.
  • After rebasing onto latest main-v2 (a1faaf5), macOS WKWebView smoke exposed delayed blank range replacement. The reader-lease writer fence plus a bounded visual hold for a temporarily unmounted anchor now cover that path; focused reader/anchor race suites and the corrected build pass.

Scope and compatibility

  • No Provider, persistence, Wails public API, or database changes.
  • No prompt/tool serialization changes; cache impact is none.

Cache-impact: none - this PR only changes transcript viewport ownership and shared Markdown presentation; provider-visible prompt/tool bytes are unchanged.
Cache-guard: no cache-sensitive files changed; frontend transcript and Markdown regression suites plus the existing cache guard remain green.
Documentation-impact: none - the change fixes internal transcript ownership and rendering behavior without changing user-facing commands, configuration, or public API documentation.

Problem: empty fenced code blocks rendered as bordered phantom cards in shared Markdown surfaces.\n\nRoot cause: the shared code component always mounted CodeViewer for block-shaped code, including whitespace-only fences.\n\nFix: omit whitespace-only fenced blocks while preserving non-empty code and add renderer parity coverage.\n\nVerification: pnpm exec tsx src/__tests__/markdown-pipeline.test.tsx; pnpm test:transcript; pnpm build.
Problem: history prepends, virtualized measurement, scrollbar drags, and question jumps could restore stale anchors and move the reader by multiple screens.\n\nRoot cause: small sessions were fully mounted during reader gestures, prepend coverage waited on the whole list, scrollbar mapping stayed on stale extents, and diagnostics did not classify restore-anchor reversals.\n\nFix: add tokenized surface transaction telemetry, use bounded reader corridors with logical-anchor coverage, rebase custom scrollbar drags after geometry changes, add a stable 16px question-jump offset correction, and report unauthorized scroll reversals.\n\nVerification: pnpm test:transcript; PLAYWRIGHT_BROWSERS_PATH=.pw-browsers pnpm test:transcript-browser; PLAYWRIGHT_BROWSERS_PATH=.pw-browsers pnpm test:transcript-reader-browser; pnpm test:typecheck; pnpm build.
Problem: a native thumb completion could outlive the transcript surface that started it.\n\nRoot cause: native scrollbar ownership tracked pointer identity but not the surface generation.\n\nFix: bind native thumb transactions to generationRef and reject stale observations, finishes, and activity checks.\n\nVerification: pnpm exec tsx src/__tests__/transcript-reader-extent-race.test.tsx; pnpm exec tsx src/__tests__/transcript-native-scrollbar.test.ts; pnpm exec eslint src/lib/useTranscriptNativeScrollbarOwnership.ts src/lib/useTranscriptScrollArbiter.ts.
Problem: the verified transcript transaction and native-thumb generation fence exceed the previous exact raw startup budget by the measured build delta.\n\nFix: retain the smallest decimal raw budget ratchet and document the attributable geometry/diagnostic additions.\n\nVerification: pnpm build.
Problem: the Windows field capture for esengine#9711 was not represented in deterministic replay coverage.\n\nFix: add a content-free fixture for all nine restore-anchor reversals and the 27,104px maximum, with build identity and tolerance assertions.\n\nVerification: pnpm exec tsx src/__tests__/transcript-geometry-replay.test.ts; pnpm exec eslint src/__tests__/transcript-geometry-replay.test.ts src/__tests__/transcript-diagnostic-replay.fixtures.ts.
Problem: stale native-scrollbar observations called the cancellation callback through a closure not owned by the observation callback.\n\nFix: clear the stale transaction inline, publish the terminal transition, and keep the generation fence dependency-complete.\n\nVerification: pnpm exec eslint src/lib/useTranscriptNativeScrollbarOwnership.ts; pnpm exec tsx src/__tests__/transcript-reader-extent-race.test.tsx.
Verification on the final native-thumb generation-fenced tree measured 463.5 KiB initial gzip and 2472.6 KiB initial raw JavaScript/CSS. Keep the smallest one-decimal ceilings above those measurements.
Problem: the native smoke contract waited for every logical row, which conflicts with the bounded reader corridor used to prevent measurement storms.

Root cause: the test encoded the removed full-mount behavior instead of the new corridor contract.

Fix: wait for a painted mounted corridor row and visible viewport, then retain the existing stability, blank-frame, reversal, and tail assertions.

Verification: Windows 11 WebView2 transcript smoke; native composer smoke; native selection compositor smoke; node --check desktop/transcript_native_smoke_contract.js.
Problem: the latest main-v2 anchor-compensation baseline raises the measured initial raw bundle to 2472.9 KiB.

Root cause: the existing 2472.7 KiB ceiling was calibrated before PR esengine#9746 landed on main-v2.

Fix: retain the narrow one-decimal raw budget ceiling at 2473.0 KiB and document the exact post-rebase measurement.

Verification: pnpm build.
@SivanCola
SivanCola requested a review from esengine as a code owner September 3, 2026 06:33
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) and removed v2 Go rewrite (1.x) — main-v2 branch, active development labels Sep 3, 2026
Problem: the integrated transcript arbiter crossed the repository 800-line file-size ceiling after adding transaction wiring.

Fix: remove redundant section separators without changing behavior or ownership logic.

Verification: pnpm exec eslint src/lib/useTranscriptScrollArbiter.ts; go run ./tools/repolint.
Problem: delayed WebKit geometry callbacks could start an anchor-compensation writer while an active reader gesture still owned the viewport.

Root cause: generic geometry scheduling and the imperative writer had no final reader-intent fence.

Fix: expose the live reader layout lease to geometry scheduling and reject anchor-compensation writes while the transcript declares reader intent. Add a deterministic writer regression for the ownership fence.

Verification: focused ESLint; transcript scroll-writer, anchor-compensation, and reader-extent race suites; repolint.
Problem: the reader-lease writer fence raises the measured initial raw bundle to 2473.1 KiB.

Fix: retain the smallest one-decimal raw budget ceiling at 2473.2 KiB and document the attributable fence.

Verification: pnpm build.
@SivanCola

Copy link
Copy Markdown
Collaborator Author

Merge-order note: #9753 (reduced-motion visual-guard fix) should land first. Both PRs touch adjacent hunks in useTranscriptReaderExtentStability.ts observe() and the budget constants in check-bundle-budget.mjs; a dry-run merge in either order conflicts on those two files. The changes are semantically independent — #9753 derives physicalAnchorDrift from the applied item-list transform instead of the remembered visualOffset; this PR widens the rejected predicate two lines below — so a rebase onto main-v2 after #9753 merges should be mechanical. Note that when rebasing, the physicalAnchorDrift line must keep #9753's appliedVisualOffset form.

Problem: WebKit can briefly unmount the logical reader anchor during a delayed range replacement, exposing a blank viewport before the corridor remounts it.

Root cause: reader visual guarding depended on an anchor row rect, while the replacement can remove that row for one or more paints.

Fix: retain a bounded visual hold from the accepted native scroll position when a rejected range is blank and the anchor row is temporarily unavailable. Keep the writer and reader ownership fences unchanged.

Verification: focused reader-extent and anchor-compensation race suites; pnpm build.
Problem: the blank-range visual hold adds a small raw bundle delta beyond the previous calibrated ceiling.

Fix: retain a narrow one-decimal 2473.5 KiB raw ceiling for the measured 2473.3 KiB build output.

Verification: pnpm check:bundle; go run ./tools/repolint; git diff --check.
SivanCola added a commit that referenced this pull request Sep 3, 2026
Problem: a fenced code block with no content (or only whitespace) renders
as a bordered one-line CodeViewer. In the #9711 capture these appear as
empty cards between paragraphs and add phantom height to transcript rows.

Root cause: createComponents() treats every fence as a block and hands the
empty value to CodeViewer; PlainMarkdownBlock likewise renders an empty
<pre> for whitespace-only plain blocks.

Fix: return null for a fenced block or plain block whose trimmed value is
empty. Comment-only and other non-empty code keeps its code-block surface.

Verification: markdown-pipeline.test.tsx gains four assertions (empty
fences render nothing, surrounding text survives, comment-only blocks keep
their surface). The new assertions fail against the previous component.
typecheck, lint:hooks, related transcript suites, vite build, and bundle
budgets pass without a budget change.

Extracted from #9754 so the display fix can land independently of the
scroll-ownership work.
SivanCola added a commit that referenced this pull request Sep 3, 2026
Problem (#9711): scrollbar interaction fights the app's own scroll
handling. Dragging the custom scrollbar while rows are still mounting
writes through a mapping frozen at pointerdown, so the next pointer move
lands on a stale extent. A native-thumb transaction is only ended by call
ordering (cancel before generation bump), not by the transaction itself.

Root cause: the custom-scrollbar drag captures overflow/maxThumbTop once
and never rebases when geometry changes mid-drag. The native-thumb
transaction carries no generation, so observe()/finish() cannot tell a
stale transaction from a live one and rely on every generation-advancing
caller to cancel first.

Fix:
- useCreationTranscriptScrollbar: rebase the frozen drag mapping at the
  current physical ratio whenever overflow/maxThumbTop/thumbHeight change,
  keeping the last pointer sample so the thumb does not visibly jump.
  The rail-click settle loop waits for two quiet frames but is bounded by
  a 1000 ms wall-clock budget (RAIL_SETTLE_BUDGET_MS) instead of spinning
  while geometry keeps changing.
- useTranscriptNativeScrollbarOwnership: bind each transaction to the
  surface generation. A stale-generation observe() ends ownership without
  claiming the tail; a stale finish() clears the marker and reports no
  transaction; isActive() is false. The arbiter passes generationRef.

Verification:
- transcript-native-scrollbar-generation.test.tsx (new): 14 assertions
  covering same-generation claim-tail, stale observe, stale finish with and
  without prior observe. Four fail against the previous hook.
- creation-transcript-scrollbar.test.ts: rebase preserves the physical
  ratio and later movement uses the current extent.
- transcript-reader-extent-race (native-thumb section), test:transcript,
  typecheck, test:typecheck, lint:hooks, check:scroll-writer, repolint,
  git diff --check pass.
- Bundle: gzip 463.102 KiB (budget 462.9 -> 463.2), raw 2470.932 KiB
  (2469.9 -> 2471.0).

The generation fence and drag rebase are extracted from #9754; the settle
deadline is new.
Problem: a delayed WebView2 range replacement can unmount the logical reader anchor for one paint, making stale transcript pixels appear duplicated during a running turn.

Root cause: the reader extent regression suite covered mounted-anchor corrections but not the blank-range interval where no anchor row is available.

Fix: add a deterministic race fixture that collapses the native extent, unmounts the logical anchor, and asserts that the visual guard is retained.

Verification: pnpm exec tsx src/__tests__/transcript-reader-extent-race.test.tsx (59 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