Skip to content

fix(chat): re-anchor the transcript when the queue band resizes - #7764

Merged
iamwhatever merged 1 commit into
mainfrom
fix/queue-band-mask-overlap
Sep 2, 2026
Merged

fix(chat): re-anchor the transcript when the queue band resizes#7764
iamwhatever merged 1 commit into
mainfrom
fix/queue-band-mask-overlap

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

A send that lands behind a busy turn does two things in one commit window: it mounts the queue stack between the transcript and the composer (spring-animated, so the scroller's box shrinks over ~400ms), and it appends a queued row to the message array, which regroups and remounts tail rows. The virtualizer's viewport branch does re-pin for the shrink — but that re-pin can land on interior heights that are still settling from the remount. Measured frame-by-frame on the pre-fix build: the scroller's own math reports "at bottom" while the content sits a card-height (~21px) low, and whether it recovers depends on which re-render lands last — the defect reads as intermittent. The offset eats the transcript's tail clearance; wherever the budget is thinner than the offset (phones most of all), the reply's last line renders as sliced glyphs directly above the queue card (reported live from a session: half-cut CJK glyphs above a queued document-feedback card).

The tip card and the survey card have similar geometry and are compensated by the existing [activeTip, surveyLayoutTick] effect. The queue stack cannot ride that effect: its height is spring-animated, so a one-shot re-anchor at mount time measures a half-grown band.

How

A ResizeObserver on the existing composer-status-stack wrapper (the element #6912's guard already pins as the band area) re-anchors via scrollBottom(true) at every layout step while FOLLOW holds — the observer fires post-layout, so the final write always follows the last height change instead of racing it, and it covers every band in the stack (progress bars, sub-agent delivery line, queue stack) rather than enumerating them as effect deps. Attached with a callback ref so it survives the chat column unmounting and remounting. Gated on vGetFollowRef exactly like the tip/survey effect: a reader parked above the bottom is never yanked.

This is a page-level mitigation by design: the virtualizer-level root fix (why the viewport re-pin can settle low when a tail-row remount coincides with an animated viewport shrink) is diagnosed and tracked in #7769 — landing it would make this observer AND the tip/survey effect deletable, and would cover the remaining bands (folder-suggestion card, follow-up options, knowledge card) for free.

  • website/src/pages/ChatPage.tsx: the callback ref + observer; ref attribute on the stack wrapper.
  • website/src/test/ChatPage.queueBandReanchor.test.tsx: source-contract pin beside the two existing mask guards (happy-dom has no layout, so a real observer can never fire there).
  • website/src/test/ChatPage.statusStackAboveMask.test.tsx, website/src/test/ChatPage.composerChromeOcclusion.test.tsx: their stack-wrapper regexes track the added ref attribute; every assertion unchanged.
  • website/scripts/capture-queue-band-reanchor.mjs: self-checking harness — boots the real built SPA parked at the bottom, pushes a queue_push frame through the stubbed websocket, samples clearance across the spring. Pre-fix: deepest anchor dip ~21px (sometimes permanent, sometimes recovered — the race; the BEFORE expectation allows up to 3 runs). Post-fix: dip ≤ 3px, single-run strict, desktop + phone.

Evidence

Same fixture, same crop; BEFORE captured at the dip (the transcript sits a card-height lower; on thin tail budgets that is the sliced line):

BEFORE (main) AFTER (this fix)
desktop dark before-dark after-dark
desktop light before-light after-light
phone dark before-phone after-phone
Harness numbers (deepest dip while the band animates): pre-fix 20.9 / 21.6 / 19.6 px (desktop-dark / desktop-light / phone) — post-fix 2.5 / 3.0 / 0.0 px.

Tested

  • npx tsc -b clean; targeted vitest run on ChatPage.queueBandReanchor / statusStackAboveMask / composerChromeOcclusion / fadeClearance — 29 passed (re-run after the rebase onto current main).
  • eslint on the touched files: 0 errors, warning count identical to main (9 vs 9 on ChatPage.tsx).
  • Harness run against both builds as above; frame-by-frame geometry probe confirmed the mechanism (scroller reports at-bottom while content sits low).

no linked issue: reported live from the dashboard (queued document-feedback card slicing the reply's last line); root-cause follow-up filed as #7769.

Pattern harvest

Rule candidate: any in-flow band that mounts between a scroll container and the content it anchors must re-anchor the scroller on RESIZE, not on mount — a one-shot compensation effect measures an animated band mid-flight. Prefer one ResizeObserver on the band's shared wrapper over enumerating each band as effect deps; the dependency-list route is how the queue band was missed after the tip/survey effect shipped. When a follow re-pin exists but still loses, probe frame-by-frame before claiming the mechanism is absent — the loss here was settling interior heights, not a missing observer (#7769).

@CrysisDeu
CrysisDeu requested a review from a team September 1, 2026 23:56
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 1, 2026 23:56
@CrysisDeu
CrysisDeu requested a review from dwu96 September 1, 2026 23:56
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 054b4bdc030555708ed5e4605e1e6715aebf6824 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Invisible-when-correct fix: the transcript tail no longer clips behind the queue band, no strings or controls change, and re-anchoring is gated on FOLLOW so parked readers are never yanked.

The only behavioral surface a user touches is scroll position: the observer fires scrollBottom(true) only while vGetFollowRef.current() holds, which preserves reading position for anyone scrolled up — the right locus-of-control call. Desktop before/after screenshots show the claimed re-anchor (tail clearance restored); phone pair corroborates via the harness numbers. Nothing else in the diff renders.

[UX-REVIEWED] 054b4bd

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 054b4bdc030555708ed5e4605e1e6715aebf6824 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Convention check done — temp-screenshots/ and capture-*.mjs harness scripts are established repo patterns (337 prior capture scripts), the source-text pin test mirrors two neighboring guards, and the mitigation-vs-root-cause split is explicitly diagnosed, disclosed, and tracked in #7769 with the observer designed to be deletable. The change is small, additive, FOLLOW-gated like the existing tip/survey compensation, and fully reversible.

Design-Verdict: PASS

A measured, disclosed page-level mitigation — resize-driven re-anchor beats the mount-time effect it generalizes, and the tracked root fix (#7769) makes it deletable.

[DESIGN-REVIEWED] 054b4bd

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 054b4bdc030555708ed5e4605e1e6715aebf6824 — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 054b4bd

Verdict parsed from the review's SHA-scoped output markers for commit 054b4bdc030555708ed5e4605e1e6715aebf6824.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 054b4bdc030555708ed5e4605e1e6715aebf6824: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 054b4bdc030555708ed5e4605e1e6715aebf6824 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 054b4bd

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 054b4bdc030555708ed5e4605e1e6715aebf6824: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 054b4bdc030555708ed5e4605e1e6715aebf6824 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All checks are done. The change is fully declared, grounded in a live-reported defect, and every artifact follows established repo conventions (337 sibling capture scripts, 571 committed screenshots, source-contract tests matching the two neighbouring mask guards). The uncovered bands and the coexisting tip/survey effect are declared and deferred to a filed root-cause issue (#7769).

First-Principles-Verdict: PASS

A reported sliced-glyph defect gets the smallest fix that survives an animated band, with the root cause named, measured, and filed rather than hidden.

What this change ships

Intent: stop the reply's last line rendering sliced above a queued-message card when a send lands behind a busy turn — a FIX.

  1. Transcript stays pinned to bottom while the queue band spring-animates in — justified (live-reported defect, frame-by-frame measured)
  2. Any status-stack band resize (progress bars, delivery line) now re-anchors while following — declared broadening; one observer is smaller than enumerating bands as effect deps
  3. Readers parked above the bottom are never yanked (FOLLOW gate) — justified, mirrors the existing tip/survey gate
  4. New source-contract test pinning the ref/observer wiring — justified; same shape as the two sibling mask guards
  5. Two existing test regexes track the added ref attribute, assertions unchanged — mechanical, declared
  6. Self-checking screenshot harness, not run by CI — declared; convention (337 capture-*.mjs siblings counted)
  7. Six BEFORE/AFTER screenshots committed — convention (571 files under temp-screenshots/)

Watch

[FIRST-PRINCIPLES-REVIEWED] 054b4bd

A send that lands behind a busy turn mounts the queue stack between the
transcript and the composer AND appends a queued row to the message
array. The virtualizer's viewport branch does re-pin for the scroller
shrink, but the append regroups and remounts tail rows while the band's
spring animates, so the re-pin can land on interior heights that are
still settling: the scroller reports at-bottom while the content sits a
card-height (~21px) low, recovering or not depending on which re-render
lands last. Users saw the reply's last line sliced at the clip edge
directly above the queue card, intermittently.

A ResizeObserver on the composer status stack re-anchors while FOLLOW
holds, after every layout step of the band's animation, so the final
write always follows the last height change instead of racing it. The
one-shot tip/survey compensation effect cannot do this: a mount-time
re-anchor measures a half-grown band. Pinned by a source-contract test
beside the two existing mask guards; the geometry is asserted and
photographed by scripts/capture-queue-band-reanchor.mjs against the
real built SPA (deepest anchor dip pre-fix ~21px, post-fix <= 3px,
desktop and phone). The virtualizer-level root fix that would make this
observer deletable is tracked in issue #7769.
@CrysisDeu
CrysisDeu force-pushed the fix/queue-band-mask-overlap branch from cbd8a4d to 054b4bd Compare September 2, 2026 00:37
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 2, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

ai-review-disposition — first-principles

Finding 1 (framing vs code: "the virtualizer does not know about it" contradicted by the viewport branch)accepted, fixed in place. The lane was right and the probe proves it: a frame-by-frame geometry trace on the pre-fix build shows the viewport re-pin firing and scrollHeight − scrollTop − clientHeight settling to 0, while the content sits ~21px low — the loss is interior heights still settling from the queued-row append's tail-row remount, not a missing observer. The PR body, the code comment, the test header, and the harness header were all rewritten to state that mechanism (head 054b4bd); the commit message carries it too.

Finding 2 (second page-level compensation; sibling bands uncovered)accepted-and-deferred to #7769. The subtraction is taken at face value: the root fix belongs inside the virtualizer's follow mechanism, and once it lands, this observer, the [activeTip, surveyLayoutTick] effect, and the uncovered bands (folder-suggestion card, follow-up options row, knowledge-fetch card) all resolve together. That diagnosis is written into #7769 with the probe evidence and a repro recipe. It is genuinely larger than this fix: it sits in regroup identity / height-cache / pin-write ordering, and this PR's observer is measured safe (dip ≤3px, FOLLOW-gated, no writes for a scrolled-up reader) while remaining deletable when #7769 lands.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 2, 2026
@iamwhatever
iamwhatever merged commit a71114e into main Sep 2, 2026
69 checks passed
@iamwhatever
iamwhatever deleted the fix/queue-band-mask-overlap branch September 2, 2026 05:33
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants