fix: tool call cards persist correctly after page reload (#141)
* fix: tool call cards persist correctly after page reload Root cause: the insertion logic looked for the NEXT assistant row to insert BEFORE, but when the triggering assistant message contained only tool_use blocks (no text), it was filtered from the DOM by msgContent() and no anchor row existed. Cards were silently dropped. Fix: find the row AT the assistant_msg_idx first (exact match), fall back to the nearest preceding visible row, then insert AFTER it (not before the next). This handles both text+tool and tool-only assistant messages correctly. Closes #140 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: tool card ordering — role-filter fallback anchor, preserve order for same-anchor groups Two bugs in the initial PR: - Fallback 'nearest row' had no role filter, could anchor to a user message row causing cards to appear after a user bubble. Fixed: check role==='assistant' in the fallback loop. - Back-to-back tool-card groups sharing a filtered anchor were inserted in reverse order because each group re-read anchorRow.nextSibling from the live DOM. Fixed: track the last inserted node per anchor via anchorInsertAfter Map, so each subsequent group appends after the previous one. Also restores the 'Collect card elements before they get moved to DOM' comment explaining why Array.from() is used on frag before DOM insertion. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
42
static/ui.js
42
static/ui.js
@@ -522,18 +522,35 @@ function renderMessages(){
|
||||
byAssistant[key].push(tc);
|
||||
}
|
||||
const allRows = Array.from(inner.querySelectorAll('.msg-row[data-msg-idx]'));
|
||||
// Track the last inserted node per anchor so back-to-back groups for the
|
||||
// same (filtered) anchor row are inserted in chronological order.
|
||||
const anchorInsertAfter = new Map();
|
||||
for(const [key, cards] of Object.entries(byAssistant)){
|
||||
const aIdx = parseInt(key);
|
||||
let insertBefore = null;
|
||||
if(aIdx === -1){
|
||||
for(let i=allRows.length-1;i>=0;i--){
|
||||
const ri=parseInt(allRows[i].dataset.msgIdx||'-1',10);
|
||||
if(ri>=0&&S.messages[ri]&&S.messages[ri].role==='assistant'){insertBefore=allRows[i];break;}
|
||||
}
|
||||
} else {
|
||||
// Find the right insertion point: cards go AFTER the assistant message
|
||||
// that triggered them. We look for the row at aIdx, or the nearest
|
||||
// visible ASSISTANT row at or before aIdx (the assistant message may be
|
||||
// filtered out if it contained only tool_use blocks with no text response).
|
||||
let anchorRow = null;
|
||||
if(aIdx >= 0){
|
||||
// First: exact match for the assistant row
|
||||
for(const r of allRows){
|
||||
const ri=parseInt(r.dataset.msgIdx||'-1');
|
||||
if(ri>aIdx&&S.messages[ri]&&S.messages[ri].role==='assistant'){insertBefore=r;break;}
|
||||
if(ri===aIdx){anchorRow=r;break;}
|
||||
}
|
||||
// Fallback: nearest visible ASSISTANT row at or before aIdx
|
||||
if(!anchorRow){
|
||||
for(let i=allRows.length-1;i>=0;i--){
|
||||
const ri=parseInt(allRows[i].dataset.msgIdx||'-1');
|
||||
if(ri<=aIdx&&S.messages[ri]&&S.messages[ri].role==='assistant'){anchorRow=allRows[i];break;}
|
||||
}
|
||||
}
|
||||
}
|
||||
// aIdx === -1 or no assistant anchor found: attach after the last assistant row
|
||||
if(!anchorRow){
|
||||
for(let i=allRows.length-1;i>=0;i--){
|
||||
const ri=parseInt(allRows[i].dataset.msgIdx||'-1',10);
|
||||
if(ri>=0&&S.messages[ri]&&S.messages[ri].role==='assistant'){anchorRow=allRows[i];break;}
|
||||
}
|
||||
}
|
||||
const frag=document.createDocumentFragment();
|
||||
@@ -554,8 +571,15 @@ function renderMessages(){
|
||||
toggle.appendChild(collapseBtn);
|
||||
frag.insertBefore(toggle,frag.firstChild);
|
||||
}
|
||||
if(insertBefore) inner.insertBefore(frag,insertBefore);
|
||||
// Insert after the anchor row (or after any previously inserted group for
|
||||
// the same anchor), preserving chronological order for multi-step chains.
|
||||
const insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow;
|
||||
const refNode = insertAfterNode ? insertAfterNode.nextSibling : null;
|
||||
if(refNode) inner.insertBefore(frag,refNode);
|
||||
else inner.appendChild(frag);
|
||||
// Record the last child we inserted so the next group for this anchor
|
||||
// goes after it rather than back at anchorRow.nextSibling.
|
||||
anchorInsertAfter.set(anchorRow, inner.lastChild);
|
||||
}
|
||||
}
|
||||
// Render usage badge on the last assistant message row (if enabled and usage data exists)
|
||||
|
||||
Reference in New Issue
Block a user