Merge pull request #4160 from nesquena/stage-4158
Some checks failed
Release & Docker / release (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
18
static/ui.js
18
static/ui.js
@@ -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"]')
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user