stage #4540 + CHANGELOG

This commit is contained in:
nesquena-hermes
2026-06-20 06:47:01 +00:00
parent 3d91defb66
commit 2f3abb00be
4 changed files with 83 additions and 11 deletions
+3 -3
View File
@@ -3,13 +3,13 @@
## [Unreleased]
## [v0.51.536] — 2026-06-20 — Release SU (transparent-stream tool-call rows persist after settle)
## [v0.51.537] — 2026-06-20 — Release SV (queued card clears on session switch)
### 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.
- **The queued-turn card from one session no longer bleeds into another when switching sessions (#4533).** The card-clear logic was only called for the *active* session's drain; session switch had no explicit clear, so a queued card from session A could persist visually when loading session B. `_clearQueueCardDisplay` is now called during `loadSession` on the session being left. Thanks @rodboev.
## [v0.51.535] — 2026-06-20 — Release ST (open model picker stays stable during stream sync)
## [v0.51.536] — 2026-06-20 — Release SU (transparent-stream tool-call rows persist after settle)
### Fixed
+1
View File
@@ -915,6 +915,7 @@ async function loadSession(sid){
// draft survives page refresh and syncs across clients.
if (currentSid && currentSid !== sid) {
if(typeof window._clearPendingSelections==='function') window._clearPendingSelections();
if(typeof _clearQueueCardDisplay==='function') _clearQueueCardDisplay(currentSid);
await _saveComposerDraftNow(currentSid, ($('msg') || {}).value || '', S.pendingFiles ? [...S.pendingFiles] : []);
// The awaited draft save above yields the event loop. If another
// loadSession() started for a different session while we were waiting
+26 -8
View File
@@ -5414,6 +5414,29 @@ function setBusy(v){
// normal user bubble in the chat — the chip is removed at drain time.
const _queueRenderKeys={}; // per-session fingerprint to avoid redundant rebuilds
const _queueCollapsed={}; // per-session: true when user explicitly collapsed the card
let _queueRenderEpoch=0;
function _clearQueueCardDisplay(sid){
const card=document.getElementById('queueCard');
const chips=document.getElementById('queueChips');
if(sid) delete _queueRenderKeys[sid];
if(card) card.classList.remove('visible');
if(chips){
const _chips=chips;
const _card=card;
const _sid=String(sid||'');
const _epoch=_chips.getAttribute('data-queue-render-epoch')||'';
setTimeout(()=>{
if((_card&&!_card.classList.contains('visible'))||!_card){
if((_chips.getAttribute('data-queue-render-sid')||'')===_sid&&(_chips.getAttribute('data-queue-render-epoch')||'')===_epoch){
_chips.innerHTML='';
}
}
},360);
}
const _msgsEl=document.getElementById('messages');
if(_msgsEl) _msgsEl.classList.remove('queue-open');
_updateQueuePill(sid,0);
}
function _renderQueueChips(sid){
const card=document.getElementById('queueCard');
@@ -5425,6 +5448,8 @@ function _renderQueueChips(sid){
// Skip re-render if user is actively editing inside the queue panel
if(inner.contains(document.activeElement)&&document.activeElement!==inner) return;
_queueRenderKeys[sid]=key;
inner.setAttribute('data-queue-render-sid',sid);
inner.setAttribute('data-queue-render-epoch',String(++_queueRenderEpoch));
inner.innerHTML='';
if(!q.length){
card.classList.remove('visible');
@@ -5663,14 +5688,7 @@ function updateQueueBadge(sessionId){
// Only wipe global DOM if this is the currently active session
const isActive=S.session&&sid===S.session.session_id;
if(isActive){
const card=document.getElementById('queueCard');
const chips=document.getElementById('queueChips');
if(card) card.classList.remove('visible');
// Defer clear until after slide-out transition so content doesn't vanish mid-animation
if(chips){const _chips=chips;const _card=card;setTimeout(()=>{if(!_card||!_card.classList.contains('visible'))_chips.innerHTML='';},360);}
const _msgsEl=document.getElementById('messages');
if(_msgsEl) _msgsEl.classList.remove('queue-open');
_updateQueuePill(sid,0);
_clearQueueCardDisplay(sid);
}
}
}
+53
View File
@@ -534,6 +534,59 @@ def test_session_scoped_message_queue_frontend_wiring(cleanup_test_sessions):
assert "updateQueueBadge(sid);" in sessions_src
def test_queue_card_cross_session_clear_called_before_draft_save(cleanup_test_sessions):
"""R15c: switching away from one session to another should clear the old
session's queue card before the async draft-save await, so stale DOM cannot
survive into the destination session.
"""
src = (REPO_ROOT / "static/sessions.js").read_text()
block_pattern = re.compile(
r"if \(currentSid && currentSid !== sid\) \{\s*"
r"if\(typeof window\._clearPendingSelections==='function'\) window\._clearPendingSelections\(\);\s*"
r"if\(typeof _clearQueueCardDisplay==='function'\) _clearQueueCardDisplay\(currentSid\);\s*"
r"await _saveComposerDraftNow\(currentSid",
re.S,
)
assert block_pattern.search(src), (
"cross-session loadSession path must clear queue card display via"
" _clearQueueCardDisplay(currentSid) before awaiting _saveComposerDraftNow"
)
def test_queue_card_cross_session_helper_used_only_for_session_change(cleanup_test_sessions):
"""R15c: _clearQueueCardDisplay(sid) must only fire on cross-session switches,
not on same-session navigation/force-reload code paths.
"""
src = (REPO_ROOT / "static/sessions.js").read_text()
load_start = src.find("async function loadSession(sid){")
assert load_start >= 0
load_end = src.find(" // Sync context usage indicator from session data", load_start)
load_body = src[load_start:load_end]
cross_start = load_body.find("if (currentSid && currentSid !== sid) {")
cross_end = load_body.find("if (currentSid !== sid || forceReload) {", cross_start)
assert cross_start >= 0 and cross_end >= 0
assert "_clearQueueCardDisplay(currentSid);" in load_body[cross_start:cross_end], (
"queue-card clear helper must be inside the cross-session branch"
)
same_session_idx = load_body.find("if(currentSid===sid && !forceReload && !_loadingSessionId) return;")
assert same_session_idx >= 0
assert same_session_idx < cross_start
def test_queue_card_clear_helper_tracks_render_epoch(cleanup_test_sessions):
"""R15d: the delayed queue clear must only wipe the chips from the render it
was asked to dismiss, not a later render for the same shared queue DOM.
"""
src = (REPO_ROOT / "static/ui.js").read_text()
assert "let _queueRenderEpoch=0;" in src
assert "if(sid) delete _queueRenderKeys[sid];" in src
assert "inner.setAttribute('data-queue-render-sid',sid);" in src
assert "inner.setAttribute('data-queue-render-epoch',String(++_queueRenderEpoch));" in src
assert "const _epoch=_chips.getAttribute('data-queue-render-epoch')||'';" in src
assert "(_chips.getAttribute('data-queue-render-sid')||'')===_sid" in src
assert "(_chips.getAttribute('data-queue-render-epoch')||'')===_epoch" in src
def test_chat_start_persists_pending_turn_metadata_for_reload_recovery(cleanup_test_sessions):
"""R15c: chat/start must expose enough pending-turn metadata for a reload to
rebuild the in-flight conversation instead of showing a blank session.