storytellurx/reports/receipt-collapse-investigation.md

5.5 KiB

Receipt close: native scheduling investigation

Status: the preceding anchored-mode defect was reproduced; its experimental scheduling patch was rejected. The user subsequently selected REUI ai-chat-9, and product source now enables bottom-following with every row anchor disabled. That public configuration avoids the implicated anchored-spacer mode; final app verification remains pending the external local-S3 dependency. @shadcn/react@0.3.1 remains unpatched. See the REUI comparison. No live model requests, push or deployment.

Measured cause

The actual app's offline neutral fixture moved the receipt trigger from y=382.5 to 488.5 and back to 382.5: a 106px excursion over two animation-frame samples. Closing Base UI's natural-height panel reduced the transcript scrollHeight from 1545 to 483. The browser clamped scrollTop from 106 to zero. MessageScroller's deferred content ResizeObserver then added its native 217px spacer and restored scrollTop=106.

The disclosure itself unmounts normally. The visible return jump comes from the delay before native anchored-turn spacer recalculation, not new model text or the transport.

Public API and documented layout comparison

MessageScroller documentation was compared with installed 0.3.1. A temporary isolated Vite harness used native Items for every Content child, default Provider, user scrollAnchor, and Base UI Collapsible with natural panel height. There was no application scroll handler.

Harness variant Anchored trigger excursion After manual wheel intent
Current viewport/content CSS 48px 0px
Documented flex viewport; no Content min-height or max-content height 48px 0px
Experimental synchronous native Content resize callback 0px 0px

The first two variants used an exact unpatched copy of the installed distribution. Removing contain: content and Content sizing did not remove the failure. Frame data is in receipt-collapse-evidence.json. Each sample follows requestAnimationFrame plus a zero-delay task, after that rendering cycle's ResizeObserver work. Twenty cycles were captured per case. This is a bounded 390px-wide harness, not whole-app acceptance.

The documented API has no resize-scheduling policy. autoScroll, default positioning and previous-item peek change navigation behavior; they do not synchronize spacer recalculation with panel removal. Calling scrollToMessage after every close would add application scroll coordination and was not adopted. Manual reading remains an essential separate control because the package correctly stops reanchoring after reader intent.

Rejected candidate

The candidate changed only the package Content observer, leaving native anchored/free-reading checks intact:

- const observer = new ResizeObserver(() => {
-   cancelAnimationFrame(frame)
-   frame = requestAnimationFrame(handleResize)
- })
+ const observer = new ResizeObserver(handleResize)

It eliminated the sampled movement, but the Vite harness log reported ResizeObserver loop completed with undelivered notifications at 15:07:52, 15:07:53 and twice at 15:08:40. The six runs' Playwright pageerror arrays were empty; Vite's client error handling logged these observer errors separately. Empty pageerror arrays therefore do not establish an error-free fix. The error lines are preserved in receipt-collapse-observer-errors.log.

The provisional patch was removed from the workspace and lockfile. Published 0.3.1 supplies only the JavaScript distribution, so retaining such a patch would also require reviewing that distribution change on upgrades. No broader mutation-observer workaround was substituted.

Reproduction and remaining work

The fixture-only reproducer added to the workspace uses the real public route and samples a mid-viewport receipt through close, with anchored and manually scrolled variants:

node node_modules/@playwright/test/cli.js test \
  src/tests/public-receipt-close.spec.ts --project=phone --reporter=json

It is now active for the selected REUI policy: short-answer and manual-reading cases explicitly reveal the opened receipt before closing it mid-viewport. This differs from the historical anchored-mode reproducer used above. All API calls are intercepted by native local response fixtures. The temporary isolation harness lives under .cache/receipt-harness: its server uses createServer({configFile:false, root: harnessPath, server:{host:"127.0.0.1",port:3199}}), with original/docs/patched layout comparisons and no app server imports.

Fresh whole-app verification became blocked when the external dream-api @bureaudouble/local-s3 link target lost its package files. The harness bypassed that missing server dependency; it cannot replace final public-app regression checks. Next verification is the active public fixture matrix for the selected REUI policy once the external dependency is restored. An upstream scheduling correction would remain relevant if user-turn anchoring is selected again; no such patch is shipped.