fix(composer): stop footer fit from jittering the transcript during SSE - #7275
ruizanthony wants to merge 2 commits into
Conversation
SummaryReading the full one-file diff at Code referenceThe important boundary is const prevVisibility=footer.style.visibility;
const prevHeight=footer.style.height;
const frozenHeight=footer.getBoundingClientRect().height;
if(frozenHeight>0){The The CSS contract supports the freeze. The global rule at Diagnosis / recommendationThe implementation fixes the right layer. Changing scroll pinning would only mask the layout source, while this keeps The remaining gap is automated coverage, not a code defect. VerificationA focused browser test should record the messages container |
_fitComposerFooter() measured overflow by REMOVING the .cf-icons/.cf-burger
stage classes, reading scrollWidth, then adding them back. Between those two
steps the footer is laid out at full width: the composer grows a few px and
#messages loses the same amount of clientHeight, then gets it back on the
next frame.
For a pinned reader that is a visible up/down jitter of the whole transcript,
because the fit pass runs on every context-indicator update — which fires
continuously while an SSE turn is streaming.
Measured with a real Chromium probe instrumenting the scrollTop setter,
scroll events, ResizeObserver and MutationObserver:
before: 4 non-JS backward jumps of exactly -8px per streamed turn,
each one ~30ms after a `ctx-indicator-wrap` -> `composer-footer`
class mutation pair
after: 0 backward jumps over 5 streamed turns on 2 different sessions
Fix: freeze the footer's layout box (explicit height + visibility:hidden)
for the duration of the measurement, so the intermediate expanded geometry
is never committed to the screen, and restore the resolved stage in the same
task. The adaptive behaviour is unchanged — verified across 8 viewport
widths (1600 -> 420px), with the correct full/icons/burger stage at each and
no residual inline style left behind.
Note: the guard added in the tail-jitter work is what keeps the reader pinned
here; before it, the same footer jitter unpinned the reader instead, which
masked the oscillation behind a worse bug.
17bcf37 to
57a75e8
Compare
SummaryI re-read the complete Code referenceThe critical ordering remains at const frozenHeight=footer.getBoundingClientRect().height;
if(frozenHeight>0){
footer.style.height=frozenHeight+'px';
footer.style.visibility='hidden';
}
let next='';
try{The corresponding The layout contract still supports this. Diagnosis / recommendationNo additional product-code change is requested from this re-review. The helper preserves prior caller-owned inline styles, skips the freeze when the measured height is zero, and restores state after exceptions. Since The remaining test gap from the first review is unchanged. VerificationCurrent exact-head GitHub metadata shows test, lint, browser-smoke, and all three lifecycle checks green. The implementation tree is unchanged from the prior review. No contributor-authored code was executed during this read-only re-review. |
Add a behavioural node driver test for _fitComposerFooter() that runs the actual function from static/ui.js against a small layout model where the stage classes dictate the footer's natural height and the left cluster's content width. Every class mutation, inline-style write and overflow measurement commits a layout sample, so the recorded footer heights and messages clientHeights are what a browser would have painted. The matrix starts from each stage (full, icons, burger), forces each overflow outcome, and runs with empty and caller-owned prior inline styles. It asserts the resolved stage classes, that the footer border-box height and messages clientHeight stay pinned while the ladder is probed (and do not move at all in the steady state), that no intermediate stage is ever painted, and that prior inline height/visibility are restored verbatim. It also covers the zero-height skip and an exception during measurement, and includes a control run with the freeze made ineffective to prove the harness reports the original jitter. Against the pre-fix ui.js the height, jitter, painted-stage and style-restoration assertions fail.
|
Added the behavioural DOM coverage requested in the re-review: It drives the actual Matrix: start stage ∈ {full, icons, burger} × forced outcome ∈ {full, icons, burger} × prior inline styles ∈ {empty, caller-owned
|
🔬 Gate certification — GREEN ✅Certified head: Integration: clean synthetic rebase Verdict: the synchronous footer freeze preserves transcript viewport geometry while fit-stage classes are probed, restores exact caller styles in Evidence
RecommendationReady to merge at |
* fix(composer): stop footer fit from jittering the transcript during SSE
_fitComposerFooter() measured overflow by REMOVING the .cf-icons/.cf-burger
stage classes, reading scrollWidth, then adding them back. Between those two
steps the footer is laid out at full width: the composer grows a few px and
#messages loses the same amount of clientHeight, then gets it back on the
next frame.
For a pinned reader that is a visible up/down jitter of the whole transcript,
because the fit pass runs on every context-indicator update — which fires
continuously while an SSE turn is streaming.
Measured with a real Chromium probe instrumenting the scrollTop setter,
scroll events, ResizeObserver and MutationObserver:
before: 4 non-JS backward jumps of exactly -8px per streamed turn,
each one ~30ms after a `ctx-indicator-wrap` -> `composer-footer`
class mutation pair
after: 0 backward jumps over 5 streamed turns on 2 different sessions
Fix: freeze the footer's layout box (explicit height + visibility:hidden)
for the duration of the measurement, so the intermediate expanded geometry
is never committed to the screen, and restore the resolved stage in the same
task. The adaptive behaviour is unchanged — verified across 8 viewport
widths (1600 -> 420px), with the correct full/icons/burger stage at each and
no residual inline style left behind.
Note: the guard added in the tail-jitter work is what keeps the reader pinned
here; before it, the same footer jitter unpinned the reader instead, which
masked the oscillation behind a worse bug.
* test(composer): cover footer fit freeze across full/icons/burger stages
Add a behavioural node driver test for _fitComposerFooter() that runs the
actual function from static/ui.js against a small layout model where the
stage classes dictate the footer's natural height and the left cluster's
content width. Every class mutation, inline-style write and overflow
measurement commits a layout sample, so the recorded footer heights and
messages clientHeights are what a browser would have painted.
The matrix starts from each stage (full, icons, burger), forces each
overflow outcome, and runs with empty and caller-owned prior inline
styles. It asserts the resolved stage classes, that the footer border-box
height and messages clientHeight stay pinned while the ladder is probed
(and do not move at all in the steady state), that no intermediate stage
is ever painted, and that prior inline height/visibility are restored
verbatim. It also covers the zero-height skip and an exception during
measurement, and includes a control run with the freeze made ineffective
to prove the harness reports the original jitter. Against the pre-fix
ui.js the height, jitter, painted-stage and style-restoration assertions
fail.
* docs(changelog): stamp #7275 composer footer-fit jitter fix
* ci: re-trigger (playwright browser install hung on GH runners)
* ci: re-trigger (google chrome apt mirror hash-sum mismatch, external infra)
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: n <a@n>
|
Shipped in Your composer footer-fit freeze is live: the fit-stage probe now runs synchronously with transcript geometry frozen and caller styles restored in Gate evidence at exact head It'll promote to the stable channel after the standard experimental soak. |
* fix(composer): stop footer fit from jittering the transcript during SSE
_fitComposerFooter() measured overflow by REMOVING the .cf-icons/.cf-burger
stage classes, reading scrollWidth, then adding them back. Between those two
steps the footer is laid out at full width: the composer grows a few px and
#messages loses the same amount of clientHeight, then gets it back on the
next frame.
For a pinned reader that is a visible up/down jitter of the whole transcript,
because the fit pass runs on every context-indicator update — which fires
continuously while an SSE turn is streaming.
Measured with a real Chromium probe instrumenting the scrollTop setter,
scroll events, ResizeObserver and MutationObserver:
before: 4 non-JS backward jumps of exactly -8px per streamed turn,
each one ~30ms after a `ctx-indicator-wrap` -> `composer-footer`
class mutation pair
after: 0 backward jumps over 5 streamed turns on 2 different sessions
Fix: freeze the footer's layout box (explicit height + visibility:hidden)
for the duration of the measurement, so the intermediate expanded geometry
is never committed to the screen, and restore the resolved stage in the same
task. The adaptive behaviour is unchanged — verified across 8 viewport
widths (1600 -> 420px), with the correct full/icons/burger stage at each and
no residual inline style left behind.
Note: the guard added in the tail-jitter work is what keeps the reader pinned
here; before it, the same footer jitter unpinned the reader instead, which
masked the oscillation behind a worse bug.
* test(composer): cover footer fit freeze across full/icons/burger stages
Add a behavioural node driver test for _fitComposerFooter() that runs the
actual function from static/ui.js against a small layout model where the
stage classes dictate the footer's natural height and the left cluster's
content width. Every class mutation, inline-style write and overflow
measurement commits a layout sample, so the recorded footer heights and
messages clientHeights are what a browser would have painted.
The matrix starts from each stage (full, icons, burger), forces each
overflow outcome, and runs with empty and caller-owned prior inline
styles. It asserts the resolved stage classes, that the footer border-box
height and messages clientHeight stay pinned while the ladder is probed
(and do not move at all in the steady state), that no intermediate stage
is ever painted, and that prior inline height/visibility are restored
verbatim. It also covers the zero-height skip and an exception during
measurement, and includes a control run with the freeze made ineffective
to prove the harness reports the original jitter. Against the pre-fix
ui.js the height, jitter, painted-stage and style-restoration assertions
fail.
* docs(changelog): stamp nesquena#7275 composer footer-fit jitter fix
* ci: re-trigger (playwright browser install hung on GH runners)
* ci: re-trigger (google chrome apt mirror hash-sum mismatch, external infra)
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: n <a@n>
* fix(composer): stop footer fit from jittering the transcript during SSE
_fitComposerFooter() measured overflow by REMOVING the .cf-icons/.cf-burger
stage classes, reading scrollWidth, then adding them back. Between those two
steps the footer is laid out at full width: the composer grows a few px and
#messages loses the same amount of clientHeight, then gets it back on the
next frame.
For a pinned reader that is a visible up/down jitter of the whole transcript,
because the fit pass runs on every context-indicator update — which fires
continuously while an SSE turn is streaming.
Measured with a real Chromium probe instrumenting the scrollTop setter,
scroll events, ResizeObserver and MutationObserver:
before: 4 non-JS backward jumps of exactly -8px per streamed turn,
each one ~30ms after a `ctx-indicator-wrap` -> `composer-footer`
class mutation pair
after: 0 backward jumps over 5 streamed turns on 2 different sessions
Fix: freeze the footer's layout box (explicit height + visibility:hidden)
for the duration of the measurement, so the intermediate expanded geometry
is never committed to the screen, and restore the resolved stage in the same
task. The adaptive behaviour is unchanged — verified across 8 viewport
widths (1600 -> 420px), with the correct full/icons/burger stage at each and
no residual inline style left behind.
Note: the guard added in the tail-jitter work is what keeps the reader pinned
here; before it, the same footer jitter unpinned the reader instead, which
masked the oscillation behind a worse bug.
* test(composer): cover footer fit freeze across full/icons/burger stages
Add a behavioural node driver test for _fitComposerFooter() that runs the
actual function from static/ui.js against a small layout model where the
stage classes dictate the footer's natural height and the left cluster's
content width. Every class mutation, inline-style write and overflow
measurement commits a layout sample, so the recorded footer heights and
messages clientHeights are what a browser would have painted.
The matrix starts from each stage (full, icons, burger), forces each
overflow outcome, and runs with empty and caller-owned prior inline
styles. It asserts the resolved stage classes, that the footer border-box
height and messages clientHeight stay pinned while the ladder is probed
(and do not move at all in the steady state), that no intermediate stage
is ever painted, and that prior inline height/visibility are restored
verbatim. It also covers the zero-height skip and an exception during
measurement, and includes a control run with the freeze made ineffective
to prove the harness reports the original jitter. Against the pre-fix
ui.js the height, jitter, painted-stage and style-restoration assertions
fail.
* docs(changelog): stamp nesquena#7275 composer footer-fit jitter fix
* ci: re-trigger (playwright browser install hung on GH runners)
* ci: re-trigger (google chrome apt mirror hash-sum mismatch, external infra)
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: n <a@n>
Problem
While an SSE response streams, the transcript visibly jitters: it shifts up a few pixels and immediately back down, over and over. It is most noticeable for a reader pinned to the bottom, and it repeats for the entire duration of the stream.
The cause is not in the scroll code. It is in
_fitComposerFooter().To decide whether the composer footer overflows, that function removes the compacting stage classes (
.cf-icons/.cf-burger), readsscrollWidth, then puts the classes back:Between those two statements the footer is laid out at full width. It grows by ~8px,
#messagesloses exactly that muchclientHeight, and regains it on the next frame. The browser reflows the transcript twice, and the user sees an 8px bounce.This fit pass runs on every context-indicator update, so during a stream it runs continuously — which is why the jitter is permanent rather than a one-off.
Fix
Freeze the footer for the duration of the measurement so the intermediate full-width state is never presented:
frozenHeight) so#messageskeeps itsclientHeight,visibility:hiddenduring the probe so the transient layout is never painted,The measurement itself is unchanged, so the adaptive behaviour is unchanged.
Evidence
Measured in Chromium with instrumented probes (
scrollTopwrites,scroll,ResizeObserver,MutationObserver), attributing every non-programmatic backward movement:scrollHeightgenuinely shrank (3229 → 3223), so the position follows. It did not reproduce on a second run.No regression in the adaptive behaviour — the footer stage was verified across 8 viewport widths (1600px → 420px): correct stage selected at each width, and no residual inline
height/visibilityleft on the element afterwards.Scope
One file,
static/ui.js, +35/−5. No change to the scroll/pinning logic, and no dependency on #7268 — this cherry-picks cleanly onto a pristinemaster.Note on #7268
This is deliberately a separate PR, not stacked. #7268 fixes a different root cause (browser tail drift silently unpinning readers) in different code. The two are independent and can be reviewed and merged in either order.