Merge pull request #4160 from nesquena/stage-4158
Some checks failed
Release & Docker / release (push) Has been cancelled

Release NR (v0.51.405): Transparent Stream live prose stays chronological (#4096)
This commit is contained in:
nesquena-hermes
2026-06-13 20:06:37 -07:00
committed by GitHub
3 changed files with 76 additions and 0 deletions

View File

@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.405] — 2026-06-14 — Release NR (Transparent Stream live prose stays chronological, #4096)
### Fixed
- **Transparent Stream: assistant prose no longer bunches at the top of a live multi-round turn (#4096).** During a streaming turn that alternates prose with tool calls (narrate → tools → narrate → tools …), every round's prose visually stacked at the top of the turn while all tool/thinking rows clustered below it; the transcript only re-interleaved correctly once the turn settled. Root cause: `_syncLiveWorklogReasonsForAnchor()` runs on every live segment render and unconditionally built the top-anchored Worklog rail — mirroring each round's prose into a `wl-reason` row there and hiding the real, chronologically-placed inline `assistant-segment` (`assistant-segment-worklog-source``display:none`). That prose-folding is the **Compact Worklog** presentation (#3401) and must not run in **Transparent Stream** mode, where prose is meant to stay as visible inline segments interleaved with tool rows. The helper is now gated on `isCompactWorklogMode()`, so Transparent Stream keeps prose in chronological position live (matching the already-correct settled render), while Compact Worklog folding is unchanged. (#4096)
## [v0.51.404] — 2026-06-14 — Release NQ (PWA multi-window connection-pool saturation fix, #4151)
### Fixed

View File

@@ -7631,6 +7631,17 @@ function _appendWorklogStep(group, anchor, cards, thinkingText, opts){
}
}
function _syncLiveWorklogReasonsForAnchor(anchor, displayTextOverride){
// Worklog reason-mirroring (folding intermediate prose into a top Worklog rail
// and hiding the inline `assistant-segment` via `assistant-segment-worklog-source`
// → display:none) is the Compact Worklog presentation (#3401). In Transparent
// Stream mode prose must stay as visible, chronologically-placed inline segments
// interleaved with tool rows — so do NOT build the worklog rail or hide the
// inline segment here. Without this gate every round's prose mirror piles into
// the single top rail while tool rows append at the bottom, so all prose bunches
// above all tools during a live multi-round turn (#4096); it only self-heals when
// the turn settles and renderMessages() rebuilds with the compact-only
// `messageBelongsInWorklog` gate (which is already isCompactWorklogMode()-only).
if(typeof isCompactWorklogMode==='function' && !isCompactWorklogMode()) return;
if(!anchor||!anchor.matches||!anchor.matches('[data-live-assistant="1"]')) return;
const blocks=anchor.parentElement;
if(!blocks) return;
@@ -7759,6 +7770,13 @@ function ensureActivityGroup(inner, opts){
function normalizeLiveActivityGroupPlacement(turn){
const blocks=_assistantTurnBlocks(turn);
if(!blocks) return;
// Compact Worklog only: this reorders `.tool-call-group`/`.tool-worklog-group`
// containers, which exist solely on the Compact Worklog live path. Transparent
// Stream renders tool rows as flat `.transparent-event-row`s and never builds
// these group containers (see appendLiveToolCard's transparent branch), and the
// worklog prose-rail is gated off in transparent mode (#4096), so the selector
// below matches nothing and this is a no-op there. Kept implicit (empty match)
// rather than an early return so reconnect/restore behavior is unchanged.
const groups=Array.from(
blocks.querySelectorAll('.tool-worklog-group[data-live-tool-worklog-group="1"],.tool-call-group[data-live-tool-worklog-group="1"],.tool-call-group[data-live-tool-call-group="1"]')
);

View File

@@ -639,3 +639,55 @@ def test_transparent_entrance_animation_is_live_turn_only():
"""The entrance animation must be scoped to the live turn so it doesn't
replay across the whole transcript on every renderMessages. (Trifecta V9.)"""
assert "#liveAssistantTurn .transparent-event-row{animation:transparent-event-enter" in STYLE_CSS
def test_live_worklog_reason_mirror_is_gated_to_compact_mode():
"""#4096: during a live multi-round turn in Transparent Stream mode, all
assistant prose visually bunched at the top while every tool row clustered
below, self-healing only when the turn settled.
Root cause: _syncLiveWorklogReasonsForAnchor() runs on every live segment
render (from _flushPendingSegmentRender + the RAF _doRender in messages.js).
It builds the top-anchored `live-worklog` rail, mirrors each round's prose
into a `wl-reason` row there, AND tags the real chronological inline
`assistant-segment` as `assistant-segment-worklog-source` (-> display:none,
style.css). That worklog-folding is the Compact Worklog presentation (#3401)
and must NOT run in Transparent Stream mode, where prose stays as visible,
chronologically-placed inline segments interleaved with tool rows.
The fix gates the whole function on isCompactWorklogMode(). Assert the guard
is the FIRST statement in the function body (before it touches
ensureLiveWorklogContainer / _syncWorklogReasonFromAnchor) so it actually
short-circuits in transparent mode rather than running the rail-build first.
"""
start = UI_JS.index("function _syncLiveWorklogReasonsForAnchor(anchor, displayTextOverride){")
end = UI_JS.index("\nfunction ", start + 1)
body = UI_JS[start:end]
# The compact-mode gate exists and short-circuits non-compact (transparent) mode.
guard = "if(typeof isCompactWorklogMode==='function' && !isCompactWorklogMode()) return;"
assert guard in body, "missing transparent-mode gate on _syncLiveWorklogReasonsForAnchor"
# The guard must come BEFORE the rail is built / prose is mirrored, otherwise
# it would not actually prevent the bunching.
guard_idx = body.index(guard)
assert guard_idx < body.index("ensureLiveWorklogContainer("), (
"compact-mode gate must precede ensureLiveWorklogContainer() so transparent "
"mode never builds the top worklog rail"
)
assert guard_idx < body.index("_syncWorklogReasonFromAnchor("), (
"compact-mode gate must precede _syncWorklogReasonFromAnchor() so transparent "
"mode never hides the inline assistant-segment or appends a wl-reason mirror"
)
# Both live-render call sites still invoke the (now-gated) helper — the gate
# lives in the helper, not at the call sites, so live rendering is unchanged
# in compact mode.
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
assert MESSAGES_JS.count("_syncLiveWorklogReasonsForAnchor(assistantRow") >= 2
# The settled-render worklog-folding gate is also compact-only (regression
# guard against the symmetric settled-path bug).
render_message_start = UI_JS.index("const messageBelongsInWorklog=")
render_message_end = UI_JS.index("if(messageBelongsInWorklog)", render_message_start)
assert "isCompactWorklogMode()" in UI_JS[render_message_start:render_message_end]