Skip to content

Commit 8815559

Browse files
committed
fix(levelcode-ai): tear down group + label state on reset / checkpoint restore
Review caught that the new calm-transcript state outlives a log wipe: - The `reset` handler cleared the log HTML and the card/step maps but left `curGroup` pointing at the now-detached `.groupbody` and `turnLabeled` set. The next run would append into disconnected DOM (nothing shows), and the first assistant line would render as an unlabeled continuation. Reset both now. - `applyCheckpointRestored` removes this turn's (and later turns') DOM and pruned only the card maps — the step maps and a possibly-stranded `curGroup` were left behind. It now prunes editSteps/termSteps/verifySteps too and drops the group if its element was removed. (This also completes the earlier request to prune step maps on checkpoint restore, not just on reset.) - `pruneDetached` is now dual-mode: it follows `.card` for step-map values, so the same helper works for both card and step maps. webviewCss +2 static guards. Gate green (22 suites).
1 parent 569a320 commit 8815559

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

‎extensions/levelcode-ai/media/chat.html‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2814,12 +2814,16 @@
28142814
let wrap = u.querySelector('.ckwrap'); if (!wrap){ wrap = document.createElement('div'); wrap.className = 'ckwrap'; u.appendChild(wrap); }
28152815
renderRestoreIdle(wrap, turnId, fileCount);
28162816
}
2817-
function pruneDetached(map){ for (const k in map){ const el = map[k]; if (el && !log.contains(el)){ delete map[k]; } } }
2817+
// Drop entries whose DOM is gone. Works for card maps (value IS the element) and step maps (value
2818+
// is a {card} object) — a checkpoint restore removes the cards, so their step state must go too.
2819+
function pruneDetached(map){ for (const k in map){ const v = map[k]; const el = (v && v.nodeType) ? v : (v && v.card); if (!el || !log.contains(el)){ delete map[k]; } } }
28182820
function applyCheckpointRestored(m){
28192821
const u = ckBubble(m.turnId); if (!u){ return; }
28202822
let n = u.nextSibling; while (n){ const next = n.nextSibling; n.remove(); n = next; } // drop this turn's output + every later turn
28212823
const wrap = u.querySelector('.ckwrap'); if (wrap){ wrap.innerHTML = '<span class="ckmuted">' + REVERT + ' Restored to here · ' + (m.filesRestored || 0) + ' file' + (m.filesRestored === 1 ? '' : 's') + ' reverted</span>'; }
28222824
pruneDetached(editCards); pruneDetached(termCards); pruneDetached(verifyCards);
2825+
pruneDetached(editSteps); pruneDetached(termSteps); pruneDetached(verifySteps);
2826+
if (curGroup && !log.contains(curGroup.el)){ curGroup = null; } // don't append into a removed group
28232827
agentBubble = null; agentRaw = ''; setStreaming(false); forceStick();
28242828
}
28252829
function checkpointBusyNotice(m){
@@ -2914,6 +2918,10 @@
29142918
else if (m.type === 'clearContext'){ selLabel = null; renderChips(); }
29152919
else if (m.type === 'reset'){
29162920
log.innerHTML = '<div id="empty"><div class="big">New chat</div>Ask about your code, or describe what to build.</div>';
2921+
// Wiping the log detaches any open group; drop the dangling refs so the next run starts clean —
2922+
// otherwise groupAppend() appends into a disconnected .groupbody (nothing shows), and a stale
2923+
// turnLabeled makes the first assistant line render as an unlabeled continuation.
2924+
curGroup = null; turnLabeled = false;
29172925
selLabel = null; ctxFiles = []; renderChips();
29182926
for (const k in editCards) { delete editCards[k]; } for (const k in termCards) { delete termCards[k]; } for (const k in verifyCards) { delete verifyCards[k]; } for (const k in bgTasks) { delete bgTasks[k]; }
29192927
for (const k in editSteps) { delete editSteps[k]; } for (const k in termSteps) { delete termSteps[k]; } for (const k in verifySteps) { delete verifySteps[k]; } // step maps ride with their cards

‎extensions/levelcode-ai/test/webviewCss.test.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,21 @@ test('every check migrated to the ring-check — no bare codicon(\'check\') anyw
114114
'and drops its own border so there is a single ring, not two');
115115
});
116116

117+
// New calm-transcript state (curGroup, turnLabeled) must be torn down wherever the log is wiped,
118+
// or the next run appends into a detached group (nothing shows) and the first line renders unlabeled.
119+
test('the reset handler clears the group + speaker-label state, not just the card maps', () => {
120+
const reset = html.slice(html.indexOf("m.type === 'reset'"), html.indexOf("m.type === 'reset'") + 900);
121+
assert.ok(/curGroup = null/.test(reset), 'reset drops the open-group ref');
122+
assert.ok(/turnLabeled = false/.test(reset), 'reset re-arms the speaker label');
123+
});
124+
125+
test('a checkpoint restore prunes step maps and drops a group whose DOM was removed', () => {
126+
const fn = html.slice(html.indexOf('function applyCheckpointRestored'), html.indexOf('function applyCheckpointRestored') + 900);
127+
assert.ok(/pruneDetached\(editSteps\)/.test(fn) && /pruneDetached\(termSteps\)/.test(fn) && /pruneDetached\(verifySteps\)/.test(fn),
128+
'the step maps are pruned alongside the card maps');
129+
assert.ok(/curGroup && !log\.contains\(curGroup\.el\)/.test(fn), 'a stranded (detached) group is dropped');
130+
// pruneDetached must handle both an element value and a {card} step value
131+
assert.ok(/function pruneDetached\(map\)\{[^}]*\.card/.test(html), 'pruneDetached also follows step.card');
132+
});
133+
117134
console.log('webviewCss: ' + n + ' tests passed');

0 commit comments

Comments
 (0)