stage #4542 @696bf798 (incl perf: pre-scan burst IDs) + CHANGELOG
This commit is contained in:
1 parent
5b96d28139
commit
348381bda3
4 files changed
+111
-3
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.536] — 2026-06-20 — Release SU (transparent-stream tool-call rows persist after settle)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Transparent-stream tool-call rows no longer vanish on turn settle (#4539).** The settled-node cleanup sweep in `renderMessages` was removing `.tool-card-row` elements regardless of type; it now excludes rows with `data-event-type="tool"` (the transparent-stream rows) so they survive settlement. The burst-key assignment also pre-scans burst IDs into a `Set` before the tool-card loop (O(1) lookup instead of O(n) spread per card), and only uses a burst key when the matching assistant segment is present. Thanks @rodboev.
|
||||
|
||||
## [v0.51.535] — 2026-06-20 — Release ST (open model picker stays stable during stream sync)
|
||||
|
||||
### Fixed
|
||||
|
||||
+5
-2
@@ -10796,7 +10796,7 @@ function renderMessages(options){
|
||||
// regression vs master; same content-loss-on-switch class as #3668). The
|
||||
// `:not([data-live-thinking="1"])` / live-card guards below keep the active
|
||||
// turn's own live nodes from being double-built.
|
||||
inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking:not([data-live-thinking="1"]):not([data-event-type="thinking"]),.wl-reason[data-worklog-anchor-reason="1"],.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove());
|
||||
inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]):not([data-event-type="tool"]),.agent-activity-thinking:not([data-live-thinking="1"]):not([data-event-type="thinking"]),.wl-reason[data-worklog-anchor-reason="1"],.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove());
|
||||
const byActivity = new Map();
|
||||
const assistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b);
|
||||
const _assistantAnchorForActivity=(aIdx,segmentSeq,burstId)=>{
|
||||
@@ -10847,13 +10847,16 @@ function renderMessages(options){
|
||||
const hasValue=value!==undefined&&value!==null&&String(value)!==''&&String(value)!=='0';
|
||||
return hasValue?String(value):'';
|
||||
};
|
||||
const knownBurstIds=new Set();
|
||||
for(const s of assistantSegments.values()) if(s){const b=s.getAttribute('data-activity-burst-id');if(b)knownBurstIds.add(b);}
|
||||
for(const tc of (S.toolCalls||[])){
|
||||
if(!tc) continue;
|
||||
const aIdx=tc.assistant_msg_idx!==undefined?parseInt(tc.assistant_msg_idx):-1;
|
||||
if(virtualWindow.virtualized&&renderableRawIdxs.has(aIdx)&&!renderedRawIdxs.has(aIdx)) continue;
|
||||
const segmentSeq=normalizeToken(tc.activitySegmentSeq);
|
||||
const burstId=normalizeToken(tc.activityBurstId);
|
||||
const key=segmentSeq?`segment:${segmentSeq}`:(burstId?`burst:${burstId}`:`assistant:${aIdx}`);
|
||||
const burstResolvable=burstId&&knownBurstIds.has(burstId);
|
||||
const key=segmentSeq?`segment:${segmentSeq}`:(burstResolvable?`burst:${burstId}`:`assistant:${aIdx}`);
|
||||
const entry=ensureActivityBucket(key,aIdx,segmentSeq,burstId);
|
||||
entry.cards.push(tc);
|
||||
entry.includeAnchorReason=true;
|
||||
|
||||
@@ -420,7 +420,7 @@ def test_thinking_blocks_persist_after_renderMessages():
|
||||
assert inner_sweep in UI_JS
|
||||
# The promoted-row guard is the only thing that changed; the rest of
|
||||
# the selector must remain intact.
|
||||
head = ".tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking"
|
||||
head = ".tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]):not([data-event-type=\"tool\"]),.agent-activity-thinking"
|
||||
tail = ".wl-reason[data-worklog-reason-source=\"reasoning\"]"
|
||||
assert head in UI_JS
|
||||
assert tail in UI_JS
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Regression tests for issue #4539: transparent-stream tool-call rows
|
||||
vanish on turn settle, reappear only after tab/session switch."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _settled_cleanup_selector():
|
||||
"""Extract the settled-node cleanup querySelectorAll string."""
|
||||
anchor = ".tool-worklog-group:not([data-compression-card])"
|
||||
idx = UI_JS.index(anchor)
|
||||
marker = ".forEach(el=>el.remove())"
|
||||
end = UI_JS.index(marker, idx) + len(marker)
|
||||
line_start = UI_JS.rfind("\n", 0, idx) + 1
|
||||
return UI_JS[line_start:end]
|
||||
|
||||
|
||||
def _tool_bucketing_block():
|
||||
"""Extract the tool-call bucketing region including burst-ID pre-scan."""
|
||||
start = UI_JS.index("const knownBurstIds=")
|
||||
end = UI_JS.index("\n }", UI_JS.index("for(const tc of (S.toolCalls||[])){", start)) + len("\n }")
|
||||
return UI_JS[start:end]
|
||||
|
||||
|
||||
class TestCleanupSelectorProtectsTransparentToolRows:
|
||||
"""Fix A: the settled-node cleanup must not remove transparent tool rows."""
|
||||
|
||||
def test_tool_card_row_arm_excludes_event_type_tool(self):
|
||||
selector = _settled_cleanup_selector()
|
||||
assert ".tool-card-row:not([data-compression-card]):not([data-event-type=\"tool\"])" in selector, (
|
||||
"The .tool-card-row cleanup arm must exclude [data-event-type='tool'] "
|
||||
"rows to protect transparent tool-call rows from deletion"
|
||||
)
|
||||
|
||||
def test_thinking_arm_still_excludes_event_type_thinking(self):
|
||||
selector = _settled_cleanup_selector()
|
||||
assert ":not([data-event-type=\"thinking\"])" in selector, (
|
||||
"The .agent-activity-thinking arm must still exclude "
|
||||
"[data-event-type='thinking'] rows"
|
||||
)
|
||||
|
||||
def test_tool_and_thinking_arms_follow_same_event_type_pattern(self):
|
||||
selector = _settled_cleanup_selector()
|
||||
assert ":not([data-event-type=\"tool\"])" in selector
|
||||
assert ":not([data-event-type=\"thinking\"])" in selector
|
||||
|
||||
|
||||
class TestBurstIdFallbackWhenSegmentLacksBurstAttribute:
|
||||
"""Fix B: bucketing must fall back to assistant:${aIdx} when no segment
|
||||
carries a matching data-activity-burst-id."""
|
||||
|
||||
def test_burst_resolvable_check_before_burst_key(self):
|
||||
block = _tool_bucketing_block()
|
||||
assert "burstResolvable" in block, (
|
||||
"The tool-call bucketing loop must check burst-ID resolvability "
|
||||
"before using 'burst:' as the bucket key"
|
||||
)
|
||||
|
||||
def test_burst_ids_pre_scanned_from_assistant_segments(self):
|
||||
block = _tool_bucketing_block()
|
||||
assert "assistantSegments.values()" in block, (
|
||||
"knownBurstIds must be built from assistantSegments before the loop"
|
||||
)
|
||||
assert "knownBurstIds.has(burstId)" in block, (
|
||||
"burstResolvable must use the pre-scanned knownBurstIds set"
|
||||
)
|
||||
|
||||
def test_fallback_uses_burst_resolvable_not_raw_burst_id(self):
|
||||
block = _tool_bucketing_block()
|
||||
assert re.search(r"burstResolvable\?.*burst:", block), (
|
||||
"The bucket key must use burstResolvable (not raw burstId) "
|
||||
"as the guard for the burst: key path"
|
||||
)
|
||||
|
||||
def test_assistant_fallback_preserved(self):
|
||||
block = _tool_bucketing_block()
|
||||
assert "`assistant:${aIdx}`" in block, (
|
||||
"The assistant:${aIdx} fallback must still be present"
|
||||
)
|
||||
|
||||
|
||||
class TestDecorateTransparentEventRowSetsEventType:
|
||||
"""Confirms the decorator still sets the attributes the cleanup
|
||||
selector depends on for its exclusion."""
|
||||
|
||||
def test_decorator_sets_data_event_type(self):
|
||||
start = UI_JS.index("function _decorateTransparentEventRow(row, opts){")
|
||||
end = UI_JS.index("\nfunction ", start + 1)
|
||||
block = UI_JS[start:end]
|
||||
assert "row.setAttribute('data-event-type',type)" in block
|
||||
|
||||
def test_tool_type_triggers_transparent_event_card(self):
|
||||
start = UI_JS.index("function _decorateTransparentEventRow(row, opts){")
|
||||
end = UI_JS.index("\nfunction ", start + 1)
|
||||
block = UI_JS[start:end]
|
||||
assert "if(type==='tool')" in block
|
||||
Reference in new issue
Block a user