fix(chat): prevent stream-end worklog collapse jump - #5058
allenliang2022 wants to merge 4 commits into
Conversation
|
| Filename | Overview |
|---|---|
| static/messages.js | Adds stream-end worklog guards and scopes the keep-open render token to the just-settled stream. |
| static/ui.js | Updates settled activity rendering so pinned readers avoid collapse jumps while historical worklogs still collapse normally. |
| tests/test_issue4970_stream_done_shrink_regression.py | Adds tests for one-shot keep-open behavior and pinned versus unpinned rendering. |
| tests/test_pure_prose_turn_not_worklog.py | Adds tests that pure-prose scenes are not treated as worklogs while real worklog rows still are. |
Reviews (5): Last reviewed commit: "fix(chat): do not collapse a pure-prose ..." | Re-trigger Greptile
nesquena-hermes
left a comment
There was a problem hiding this comment.
Thanks for chasing the post-#4970 stream-end shrink jump @allenliang2022 — the diagnosis is right and the pinned-follower keep-open approach is the correct shape. The collapsed-override threading through _anchorSceneWorklogGroup/ensureActivityGroup is clean and byte-identical for every other caller (default path unchanged). But there's one reproduced issue that needs a fix before this can ship:
🔴 The keep-open exception leaks into historical settled turns
_shouldKeepSettledWorklogOpenForPinnedFollow() keys only on the global pin state:
return !!(_scrollPinned && !_messageUserUnpinned);But _renderSettledAnchorSceneForMessage() is called for every assistant message that has an _anchor_activity_scene, on every renderMessages() pass — static/ui.js:12665:
for(const [rawIdx,seg] of assistantSegments){
const msg=S.messages[rawIdx];
if(msg&&msg._anchor_activity_scene){
_renderSettledAnchorSceneForMessage(msg, seg, rawIdx); // ← every settled turn, every render
}
}So whenever the reader is pinned at the bottom — which is also true after a session switch/reset and on ordinary pinned re-renders, not just at STREAM_DONE — every historical settled turn's worklog re-expands, defeating the compact-worklog default for past turns. The one-turn "keep open so the live→settled swap is height-stable" exception you want only applies to the turn that just completed; right now it applies to all of them.
Fix-spec (scope it to the just-settled turn)
Gate the keep-open on a one-shot just-settled stream/turn token, not global pin state alone:
- In
static/messages.jsaround the STREAM_DONErenderMessages({preserveScroll:true})call (~messages.js:4802), set a one-shot token to the stream/turn id that just settled, and clear it right after the settled-render loop completes. - Pass the message's
streamId(e.g.message._anchor_stream_id) into_shouldKeepSettledWorklogOpenForPinnedFollow(streamId)and require it to match that one-shot token in addition to the pin flags. - Keep the unpinned path exactly as-is (compact collapsed settled worklog).
That keeps the height-stable swap for the turn that just finished (the actual jump source) while leaving every prior turn compact, regardless of pin state.
Heads-up on testing: the new test_issue4970_stream_done_shrink_regression.py cases are source-string assertions (they check the helper/markers exist), so they'll pass for any code containing those strings — they won't catch this historical-turn leak. Consider a behavioral assertion that drives a second settled turn while pinned and checks it stays collapsed.
Gate summary: rebased onto current master (clean, no conflicts — disjoint from the #4647 cancel-snapshot work that landed in the meantime), node --check clean, the anchor/worklog/scroll render-test slice is 586 passed / 0 failed, and Codex reproduced the finding above. Re-request review once the keep-open is scoped to the just-settled turn and I'll re-gate.
|
Pulled the branch into a read-only worktree and read Code referenceThe new pin authority (ui.js, return !!(_scrollPinned && !_messageUserUnpinned);Using the sticky pin flags instead of a transient bottom-distance check is the right call. The plumbing through collapsed:(opts&&opts.collapsed!==undefined)?opts.collapsed:!live,One thing I verified (and it checks out)The One subtlety worth a sanity check before merge: the disclosure-state overrides in TestThe source-lock style in Solid, minimal, well-evidenced. The pinned/unpinned split keeps the blast radius small. |
Follow-up to maintainer review on nesquena#5058: make the pinned-follower helper comment explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes precedence over a transient user-collapsed live worklog while the reader remains pinned. Add a source lock so that intent stays documented. Focused scroll locks remain green (22 passed), node --check clean.
|
@nesquena-hermes Addressed the sanity-check note in The helper comment now explicitly documents the precedence decision: while the reader remains pinned, avoiding the visible Focused verification remains green locally ( |
|
Confirmed — What changedThe helper comment now documents the tradeoff I flagged (static/ui.js, // settled worklogs and preserve their viewport normally. This intentionally
// wins over a transient user-collapsed live worklog while the reader remains
// pinned: avoiding the visible STREAM_DONE jump takes precedence for followers.That's the precise case I raised: a pinned reader who had explicitly collapsed the live worklog mid-stream will see it re-open as settled. The comment now makes clear this is deliberate (height-stability > preserving a transient collapse), so the next reader doesn't "fix" it back into a jump. The source lock at assert "avoiding the visible STREAM_DONE jump takes precedence" in helperVerificationThe behavioral logic is unchanged from the version I reviewed — Per policy I didn't execute the suite from the worktree, but the new assertion lines up with the diff and the three prior locks remain intact. Nothing further from my side — the changes-requested item is resolved. |
|
Thanks for the follow-up push @allenliang2022 — but the 🔴 must-fix from the last review is still open, so I can't re-gate to ship yet. The new commit added a test documenting keep-open precedence, but the actual scoping fix wasn't made:
return !!(_scrollPinned && !_messageUserUnpinned);and To converge, please implement the one-shot-token scoping from the prior review:
And please add a behavioral assertion (not a source-string check): drive a second settled turn while pinned and assert it stays collapsed — the current source-string tests pass for any code containing the helper name, so they don't catch this leak (this is exactly how the regression slipped through). The diagnosis and the pinned-follower keep-open shape are right — this is purely about scoping the exception to the just-settled turn. Re-request review once that's in and I'll re-gate immediately. |
Follow-up to maintainer review on nesquena#5058: make the pinned-follower helper comment explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes precedence over a transient user-collapsed live worklog while the reader remains pinned. Add a source lock so that intent stays documented. Focused scroll locks remain green (22 passed), node --check clean.
f70878b to
c4b1fb0
Compare
|
@nesquena-hermes Implemented the one-shot-token scoping exactly as requested — pushed in 1. One-shot token at STREAM_DONE (messages.js): capture 2. Helper gated on token match (ui.js): 3. Unpinned path unchanged (compact collapsed settled worklog). Behavioral test (replaced the source-string one): drives turn A settling while pinned → A keeps open, second turn B stays collapsed, A also collapses after disarm, unpinned always compact. Ran the real helper+arm/disarm via Node. Local: this file passes + 583 scroll/worklog/render-window locks pass, 0 fail. Re-requesting review. |
✅ Re-gate after convergence — bounce ANSWERED, engineering SAFE TO SHIP (pending maintainer visual sign-off)The 2026-06-27 Bounce fix-spec (a) — one-shot scoping: ✅ The keep-open exception is now gated on a one-shot token Bounce fix-spec (b) — behavioral test: ✅ Gate (this re-gate):
Status: converged → good-rework, contributor-court cleared. This touches the crown-jewel chat scroll/render path with a visible pinned-follower behavior change, so it wants a quick maintainer hidden-tail / pinned-follow visual check before merge (engineering is clean). Promoting to the priority review queue (sibling of #5172/#5189). Thanks @allenliang2022 for the precise rework. |
A new post-nesquena#4970 scroll-jump class surfaced after Release YH: when a streamed assistant turn with a large live worklog/tool trace settles, the settled compact worklog collapses hundreds of pixels at STREAM_DONE. The reader is still pinned and scroll state is correct, but the net scrollHeight shrink makes the browser clamp scrollTop down by the same amount, which is visible as a large backward jump. Fix by keeping the just-settled activity worklog open for pinned followers (_scrollPinned && !_messageUserUnpinned), so the live->settled DOM swap is height-stable. Unpinned readers still get compact settled worklogs and keep their viewport normally. Also make _anchorSceneWorklogGroup respect opts.collapsed; previously it hard-coded collapsed: !live so the caller could not request an open settled group. Regression locks assert the pinned-follow helper, that settled rendering passes collapsed:!keepSettledWorklogOpen, and that _anchorSceneWorklogGroup honors an explicit opts.collapsed. Verified locally with a Playwright repro: before the fix a large tool-worklog answer produced scrollHeight -367/-422px and scrollTop -367/-422px at STREAM_DONE; after the fix nShrinks=0 and nBack=0. Local focused scroll locks pass: 22 passed.
Follow-up to maintainer review on nesquena#5058: make the pinned-follower helper comment explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes precedence over a transient user-collapsed live worklog while the reader remains pinned. Add a source lock so that intent stays documented. Focused scroll locks remain green (22 passed), node --check clean.
…hot token Address maintainer must-fix: _shouldKeepSettledWorklogOpenForPinnedFollow() keyed only on global pin state, so every historical settled worklog re-expanded on any pinned re-render — defeating the compact default. Gate the keep-open exception on a one-shot token = the just-settled stream id (armed at STREAM_DONE renderMessages, disarmed right after), threaded as _shouldKeep...(streamId). Unpinned path unchanged. Replace source-string test with a behavioral Node test that drives a second pinned turn and asserts it collapses.
c4b1fb0 to
9634774
Compare
|
@nesquena Rebased onto current master (was BEHIND) — |
…one jump) A turn that streamed only prose (a long plain-text answer, or a degeneration burst that floods the body with repeated tokens) still projected an anchor activity scene whose activity_rows were all prose/terminal — zero tool/thinking rows. The settle path promoted it to a collapsed worklog anyway (the gate only checked activity_rows.length), hiding the whole answer and shrinking the transcript by the full streamed height at STREAM_DONE, so the browser clamped a bottom-pinned viewport back to the top (the 'jump back' report). Add a worklog-worthiness predicate at both gates: the generation gate (_anchorSceneHasWorklogWorthyRows in messages.js, decides whether to attach a scene at all) and the render gate (_anchorSceneSceneHasWorklogWorthyRows in ui.js, defense-in-depth for already-persisted all-prose scenes). A scene is worklog-worthy only if it has >=1 tool/thinking row or a compression lifecycle card; pure prose is not, so the turn renders as normal visible prose and the viewport stays pinned at the bottom. Behavioral + structural tests in tests/test_pure_prose_turn_not_worklog.py.
|
Pushed a follow-up commit ( Symptom: an assistant turn that streams only prose — a long plain-text answer, or a degeneration burst that floods the body with a repeated token — still jumped the viewport back to the top at STREAM_DONE for a bottom-pinned reader. Root cause: such a turn projects an anchor activity scene whose Fix: require the scene to be genuinely worklog-worthy (>=1
A pure-prose turn now renders as normal visible inline prose — no worklog, no hidden body, no settle-time height shrink — so the pinned viewport stays at the bottom. Turns with real tool/thinking work are unaffected. Verified live against a fresh transcript on a local source build: a 200-line plain-text turn reproduced the jump (-5258px backward, scrollHeight 5897→242) before and is clean after — Tests: |
…allenliang2022) Release v0.51.745 — stream-end worklog collapse fixes for pinned readers (#5058, @allenliang2022)
|
Shipped in v0.51.745 (via #5221). Thanks @allenliang2022 — the stream-end worklog collapse jump is fixed for pinned readers (one-shot keep-open token scoped to just the settled turn) AND a pure-prose turn is no longer wrongly hidden inside a collapsed worklog (worklog-worthiness predicate at both gates). Converged from the 06-27 court bounce + the later pure-prose addition; re-gated fully: Codex SAFE TO SHIP (gen-gate/render-gate predicates match, token truly one-shot), scroll/anchor sweep 812 passed, full suite 11016 passed. Maintainer-approved. |
Follow-up to maintainer review on nesquena#5058: make the pinned-follower helper comment explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes precedence over a transient user-collapsed live worklog while the reader remains pinned. Add a source lock so that intent stays documented. Focused scroll locks remain green (22 passed), node --check clean.
Summary
Fix a new post-#4970 stream-end jump class: when a pinned reader follows a streamed assistant turn with a large live worklog/tool trace,
STREAM_DONEcollapses the live worklog into a compact settled worklog. That can shrinkscrollHeightby hundreds of pixels in one frame, causing the browser to clampscrollTopdown by the same amount. Scroll state is still correct (pinned=true,gap=0), but the user sees a large backward jump.This is distinct from #4934/#4970's earlier classes (spurious upward scroll events / post-render artifacts). Here the page is correctly pinned; the DOM itself shrinks.
Evidence (Playwright repro on a source build)
Instrumented
#messageswith a 16ms sampler for everyscrollHeight(H) andscrollTop(T) delta while sending a prompt that triggers multiple tool calls/worklog rows:Before this fix:
The jump magnitude equals the live worklog collapse height. Small reasoning-only answers showed the same signature at ~28px; large tool-worklog answers reproduced the user's "much more than one line" jump at 367-422px.
DOM comparison showed the stream peak had an expanded
agent-activity-group tool-worklog-groupwhile the settled turn used a compact/collapsed activity group; the net live->settled height drop was the visible jump.After this fix:
Same multi-tool repro:
{ "nShrinks": 0, "shrinks": [], "nBack": 0, "backs": [], "gap": 0, "groups": [{ "cls": "agent-activity-group tool-worklog-group activity open", "h": 450, "body": [{"h": 402, "display": "block"}] }] }Fix
_scrollPinned && !_messageUserUnpinned), keep the just-settled worklog open. This keeps the live->settled DOM swap height-stable and prevents the browser from clampingscrollTopbackward._anchorSceneWorklogGroup()respect explicitopts.collapsed; previously it hard-codedcollapsed: !live, so callers could not request an open settled group.Tests
Added
tests/test_issue4970_stream_done_shrink_regression.pywith source locks for:_scrollPinned && !_messageUserUnpinnedas authority, not transient bottom distance),collapsed:!keepSettledWorklogOpen,_anchorSceneWorklogGroup()honoring explicitopts.collapsed.Focused local verification on upstream master worktree:
Suggested labels:
bug,scroll,streaming,size:S.