fix(ui): keep current summary visible next to stale compaction markers
The settled current-summary card was only created when no compaction marker was loaded at all. A transcript with older (stale) markers plus a newer `compression_anchor_summary` that no loaded marker references therefore lost the current summary, which master still rendered. Decide the fallback from `_latestCompressionReferenceMessage()` (`referenceMessageRawIdx < 0`) instead of marker presence, and feed that decision into `_selectCompactionCardPlacements()` before any card node is built so the fallback is the single preserved-task owner and marker cards never attach the task list a second time. Add a render-level regression that executes the real `renderMessages()` over 220 messages, three stale markers, one preserved task-list message and a newer summary: the current summary is visible and exactly one task owner is rendered, in full, virtualized and server-truncated windows.
This commit is contained in:
1 parent
44b2a57109
commit
8fe3325e12
4 files changed
+570
-12
No files matched your search
+27
-11
@@ -15373,7 +15373,11 @@ function _loadedCompactionMarkerRawIdxs(messages){
|
|||||||
// A settled compaction whose marker sits before the server-loaded tail must
|
// A settled compaction whose marker sits before the server-loaded tail must
|
||||||
// remain visible at the top of that tail. Anchoring it to an old assistant/tool
|
// remain visible at the top of that tail. Anchoring it to an old assistant/tool
|
||||||
// turn can bury it inside a hidden worklog, which makes compaction look absent.
|
// turn can bury it inside a hidden worklog, which makes compaction look absent.
|
||||||
function _selectCompactionCardPlacements(markerRawIdxs,firstRenderedRawIdx){
|
// `currentSummaryFallback` is true when the session's current summary matches
|
||||||
|
// no loaded marker and therefore renders as its own settled card. That card is
|
||||||
|
// the newest compaction the user can see, so it owns the preserved task cards
|
||||||
|
// and no marker card may attach them again.
|
||||||
|
function _selectCompactionCardPlacements(markerRawIdxs,firstRenderedRawIdx,currentSummaryFallback=false){
|
||||||
const preWindowMarkers=[];
|
const preWindowMarkers=[];
|
||||||
const inlineMarkers=[];
|
const inlineMarkers=[];
|
||||||
const boundary=Number.isFinite(firstRenderedRawIdx)?firstRenderedRawIdx:-1;
|
const boundary=Number.isFinite(firstRenderedRawIdx)?firstRenderedRawIdx:-1;
|
||||||
@@ -15383,11 +15387,13 @@ function _selectCompactionCardPlacements(markerRawIdxs,firstRenderedRawIdx){
|
|||||||
if(rawIdx<boundary) preWindowMarkers.push(rawIdx);
|
if(rawIdx<boundary) preWindowMarkers.push(rawIdx);
|
||||||
else inlineMarkers.push(rawIdx);
|
else inlineMarkers.push(rawIdx);
|
||||||
}
|
}
|
||||||
const taskOwner=inlineMarkers.length
|
const taskOwner=currentSummaryFallback
|
||||||
?{kind:'inline',rawIdx:inlineMarkers[inlineMarkers.length-1]}
|
?{kind:'current-summary',rawIdx:-1}
|
||||||
:!preWindowMarkers.length
|
:inlineMarkers.length
|
||||||
?null
|
?{kind:'inline',rawIdx:inlineMarkers[inlineMarkers.length-1]}
|
||||||
:{kind:'pre-window',rawIdx:preWindowMarkers[preWindowMarkers.length-1]};
|
:!preWindowMarkers.length
|
||||||
|
?null
|
||||||
|
:{kind:'pre-window',rawIdx:preWindowMarkers[preWindowMarkers.length-1]};
|
||||||
return {preWindowMarkers,inlineMarkers,taskOwner};
|
return {preWindowMarkers,inlineMarkers,taskOwner};
|
||||||
}
|
}
|
||||||
function _insertCompactionCardNodes(entries,taskOwner,insertNode){
|
function _insertCompactionCardNodes(entries,taskOwner,insertNode){
|
||||||
@@ -17072,7 +17078,17 @@ function renderMessages(options){
|
|||||||
// order; markers inside the window stay inline.
|
// order; markers inside the window stay inline.
|
||||||
const firstRenderedRawIdx=renderVisWithIdx.length?renderVisWithIdx[0].rawIdx:Infinity;
|
const firstRenderedRawIdx=renderVisWithIdx.length?renderVisWithIdx[0].rawIdx:Infinity;
|
||||||
const loadedCompactionRawIdxs=(!compressionState)?_loadedCompactionMarkerRawIdxs(S.messages):[];
|
const loadedCompactionRawIdxs=(!compressionState)?_loadedCompactionMarkerRawIdxs(S.messages):[];
|
||||||
const compactionPlacements=_selectCompactionCardPlacements(loadedCompactionRawIdxs,firstRenderedRawIdx);
|
// A loaded marker that matches the current summary already renders as its
|
||||||
|
// own card. When none matches (referenceMessageRawIdx<0) the current summary
|
||||||
|
// is still authoritative and must stay visible even next to stale markers,
|
||||||
|
// so the settled fallback is decided here, before any card node is built,
|
||||||
|
// and takes part in the single preserved-task owner selection.
|
||||||
|
const showCurrentSummaryFallback=!!(!compressionState && referenceMessageRawIdx<0 && _shouldShowSettledCompressionReference(referenceText) && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey || sessionCompressionSummary));
|
||||||
|
const compactionPlacements=_selectCompactionCardPlacements(loadedCompactionRawIdxs,firstRenderedRawIdx,showCurrentSummaryFallback);
|
||||||
|
const referenceNodeOwnsTasks=!!(showCurrentSummaryFallback
|
||||||
|
&& compactionPlacements.taskOwner
|
||||||
|
&& compactionPlacements.taskOwner.kind==='current-summary'
|
||||||
|
&& preservedCompressionTaskMessages.length);
|
||||||
const _compactionCardEntry=(markerRawIdx,kind)=>{
|
const _compactionCardEntry=(markerRawIdx,kind)=>{
|
||||||
const markerMsg=S.messages[markerRawIdx];
|
const markerMsg=S.messages[markerRawIdx];
|
||||||
let raw='';
|
let raw='';
|
||||||
@@ -17098,8 +17114,8 @@ function renderMessages(options){
|
|||||||
};
|
};
|
||||||
const preWindowCompactionCards=compactionPlacements.preWindowMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'pre-window'));
|
const preWindowCompactionCards=compactionPlacements.preWindowMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'pre-window'));
|
||||||
const compactionCardNodes=compactionPlacements.inlineMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'inline'));
|
const compactionCardNodes=compactionPlacements.inlineMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'inline'));
|
||||||
const referenceNode=(!compressionState && loadedCompactionRawIdxs.length===0 && _shouldShowSettledCompressionReference(referenceText) && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey || sessionCompressionSummary))
|
const referenceNode=showCurrentSummaryFallback
|
||||||
? (()=>{const row=document.createElement('div');row.innerHTML=`<div class="compression-turn"><div class="compression-turn-blocks">${_compressionReferenceCardHtml(referenceText,false)}${_preservedCompressionTaskListCardsHtml(preservedCompressionTaskMessages)}</div></div>`;const node=row.firstElementChild;if(node&&preservedCompressionTaskMessages.length) node.setAttribute('data-compaction-task-owner','1');return node;})()
|
? (()=>{const row=document.createElement('div');row.innerHTML=`<div class="compression-turn"><div class="compression-turn-blocks">${_compressionReferenceCardHtml(referenceText,false)}${referenceNodeOwnsTasks?_preservedCompressionTaskListCardsHtml(preservedCompressionTaskMessages):''}</div></div>`;const node=row.firstElementChild;if(node&&referenceNodeOwnsTasks) node.setAttribute('data-compaction-task-owner','1');return node;})()
|
||||||
: null;
|
: null;
|
||||||
let referenceNodePinnedAtTop=false;
|
let referenceNodePinnedAtTop=false;
|
||||||
let preservedCompressionTaskOwnerNode=null;
|
let preservedCompressionTaskOwnerNode=null;
|
||||||
@@ -17142,7 +17158,7 @@ function renderMessages(options){
|
|||||||
// Keep the settled compacted-context card immediately visible in a long,
|
// Keep the settled compacted-context card immediately visible in a long,
|
||||||
// tail-loaded conversation. Put it in flow (not inside an old tool turn).
|
// tail-loaded conversation. Put it in flow (not inside an old tool turn).
|
||||||
referenceNodePinnedAtTop=_pinSettledCompressionReferenceAtTop(inner,referenceNode,referenceMessageRawIdx);
|
referenceNodePinnedAtTop=_pinSettledCompressionReferenceAtTop(inner,referenceNode,referenceMessageRawIdx);
|
||||||
if(referenceNodePinnedAtTop&&referenceNode?.parentElement&&preservedCompressionTaskMessages.length){
|
if(referenceNodePinnedAtTop&&referenceNode?.parentElement&&referenceNodeOwnsTasks){
|
||||||
preservedCompressionTaskOwnerNode=referenceNode;
|
preservedCompressionTaskOwnerNode=referenceNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17780,7 +17796,7 @@ function renderMessages(options){
|
|||||||
const referenceInserted=referenceNode&&referenceMessageRawIdx>=0
|
const referenceInserted=referenceNode&&referenceMessageRawIdx>=0
|
||||||
?_insertCompressionLikeNodeByRawIdx(referenceNode,referenceMessageRawIdx)
|
?_insertCompressionLikeNodeByRawIdx(referenceNode,referenceMessageRawIdx)
|
||||||
:_insertCompressionLikeNode(referenceNode);
|
:_insertCompressionLikeNode(referenceNode);
|
||||||
if(referenceInserted&&referenceNode?.parentElement&&preservedCompressionTaskMessages.length){
|
if(referenceInserted&&referenceNode?.parentElement&&referenceNodeOwnsTasks){
|
||||||
preservedCompressionTaskOwnerNode=referenceNode;
|
preservedCompressionTaskOwnerNode=referenceNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1079,7 +1079,7 @@ def test_context_anchor_reference_uses_session_summary_fallback():
|
|||||||
assert "const referenceText=(()=>{" in src
|
assert "const referenceText=(()=>{" in src
|
||||||
assert "if(!referenceMessage) return sessionCompressionSummary;" in src
|
assert "if(!referenceMessage) return sessionCompressionSummary;" in src
|
||||||
assert "return segment!==null?segment:raw;" in src
|
assert "return segment!==null?segment:raw;" in src
|
||||||
assert "loadedCompactionRawIdxs.length===0 && _shouldShowSettledCompressionReference(referenceText)" in src
|
assert "referenceMessageRawIdx<0 && _shouldShowSettledCompressionReference(referenceText)" in src
|
||||||
assert "!_isContextCompactionText(referenceText)" in src
|
assert "!_isContextCompactionText(referenceText)" in src
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,20 @@ assert.deepStrictEqual(
|
|||||||
_selectCompactionCardPlacements([], 50),
|
_selectCompactionCardPlacements([], 50),
|
||||||
{{preWindowMarkers: [], inlineMarkers: [], taskOwner: null}},
|
{{preWindowMarkers: [], inlineMarkers: [], taskOwner: null}},
|
||||||
);
|
);
|
||||||
|
// A current-summary fallback (no loaded marker matches the session summary)
|
||||||
|
// owns the preserved tasks even when stale markers are still loaded.
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
_selectCompactionCardPlacements([3, 11, 49, 88], 50, true),
|
||||||
|
{{
|
||||||
|
preWindowMarkers: [3, 11, 49],
|
||||||
|
inlineMarkers: [88],
|
||||||
|
taskOwner: {{kind: 'current-summary', rawIdx: -1}},
|
||||||
|
}},
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
_selectCompactionCardPlacements([], 50, true),
|
||||||
|
{{preWindowMarkers: [], inlineMarkers: [], taskOwner: {{kind: 'current-summary', rawIdx: -1}}}},
|
||||||
|
);
|
||||||
"""
|
"""
|
||||||
_run_node(script)
|
_run_node(script)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,528 @@
|
|||||||
|
"""Render-level regression: stale compaction markers must not hide the current summary.
|
||||||
|
|
||||||
|
A session can carry historical ``[CONTEXT COMPACTION]`` markers in its loaded
|
||||||
|
transcript while ``session.compression_anchor_summary`` already describes a
|
||||||
|
newer compaction that no loaded marker references. ``renderMessages()`` must
|
||||||
|
still render that current summary as its own settled card (as ``master`` did)
|
||||||
|
and the preserved task cards must be attached to exactly one owner.
|
||||||
|
|
||||||
|
The scenario drives the real ``renderMessages()`` through the same Node
|
||||||
|
harness used by ``tests/test_anchor_fallback_ownership.py``; only the DOM
|
||||||
|
shim gains a minimal ``innerHTML`` → ``firstElementChild`` parser so the
|
||||||
|
compaction card nodes built by ``renderMessages()`` are real elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from tests.test_anchor_fallback_ownership import (
|
||||||
|
_function_source,
|
||||||
|
_run_node_script,
|
||||||
|
_ui_js,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_REAL_FUNCTIONS = (
|
||||||
|
"_transparentStreamOrderedParts",
|
||||||
|
"_legacySettledFallbackHasToolMetadata",
|
||||||
|
"_isContextCompactionText",
|
||||||
|
"_isContextCompactionMessage",
|
||||||
|
"_compactionSummarySegment",
|
||||||
|
"_isPreservedCompressionTaskListMarkerText",
|
||||||
|
"_isPreservedCompressionTaskListMessage",
|
||||||
|
"_preservedCompressionTaskListPreview",
|
||||||
|
"_latestTodoToolItems",
|
||||||
|
"_hasActiveTodoItems",
|
||||||
|
"_latestPreservedCompressionTaskListMessages",
|
||||||
|
"_compressionStatusCardHtml",
|
||||||
|
"_preservedCompressionTaskListCardHtml",
|
||||||
|
"_preservedCompressionTaskListCardsHtml",
|
||||||
|
"_latestCompressionReferenceMessage",
|
||||||
|
"_shouldShowSettledCompressionReference",
|
||||||
|
"_compactionDigestText",
|
||||||
|
"_compactionCardPreview",
|
||||||
|
"_compressionReferenceCardHtml",
|
||||||
|
"_loadedCompactionMarkerRawIdxs",
|
||||||
|
"_selectCompactionCardPlacements",
|
||||||
|
"_insertCompactionCardNodes",
|
||||||
|
"_insertPreservedCompressionTaskFallback",
|
||||||
|
"_pinCompactionCardAtTop",
|
||||||
|
"_pinSettledCompressionReferenceAtTop",
|
||||||
|
"renderMessages",
|
||||||
|
)
|
||||||
|
|
||||||
|
_DOM_SHIM = r"""
|
||||||
|
class FakeClassList {
|
||||||
|
constructor(el) { this.el = el; }
|
||||||
|
_set() { return new Set(String(this.el.className || '').split(/\s+/).filter(Boolean)); }
|
||||||
|
contains(name) { return this._set().has(name); }
|
||||||
|
add(...names) {
|
||||||
|
const set = this._set();
|
||||||
|
names.forEach((name) => set.add(name));
|
||||||
|
this.el.className = Array.from(set).join(' ');
|
||||||
|
}
|
||||||
|
remove(...names) {
|
||||||
|
const set = this._set();
|
||||||
|
names.forEach((name) => set.delete(name));
|
||||||
|
this.el.className = Array.from(set).join(' ');
|
||||||
|
}
|
||||||
|
toggle(name) {
|
||||||
|
const set = this._set();
|
||||||
|
if (set.has(name)) set.delete(name); else set.add(name);
|
||||||
|
this.el.className = Array.from(set).join(' ');
|
||||||
|
return set.has(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function unescapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>').replace(/&/g, '&');
|
||||||
|
}
|
||||||
|
// Minimal HTML → FakeElement tree parser. Only used when renderMessages()
|
||||||
|
// reads `firstElementChild` after assigning innerHTML (the compaction /
|
||||||
|
// reference / preserved-task card nodes). Text nodes are dropped; the
|
||||||
|
// assertions below rely on data-* attributes only.
|
||||||
|
function parseHtmlInto(parent, html) {
|
||||||
|
const tagRe = /<(\/)?([A-Za-z][\w-]*)((?:\s+[\w:-]+(?:="[^"]*")?)*)\s*(\/)?>/g;
|
||||||
|
const stack = [parent];
|
||||||
|
let match;
|
||||||
|
while ((match = tagRe.exec(html))) {
|
||||||
|
if (match[1]) {
|
||||||
|
if (stack.length > 1) stack.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const el = new FakeElement(match[2]);
|
||||||
|
const attrRe = /([\w:-]+)(?:="([^"]*)")?/g;
|
||||||
|
let attr;
|
||||||
|
while ((attr = attrRe.exec(match[3] || ''))) {
|
||||||
|
el.setAttribute(attr[1], attr[2] === undefined ? '' : unescapeHtml(attr[2]));
|
||||||
|
}
|
||||||
|
stack[stack.length - 1].appendChild(el);
|
||||||
|
if (!match[4]) stack.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FakeElement {
|
||||||
|
constructor(tag = 'div') {
|
||||||
|
this.tagName = tag.toUpperCase();
|
||||||
|
this.children = [];
|
||||||
|
this.parentElement = null;
|
||||||
|
this.dataset = {};
|
||||||
|
this.attributes = {};
|
||||||
|
this.className = '';
|
||||||
|
this.id = '';
|
||||||
|
this.hidden = false;
|
||||||
|
this._innerHTML = '';
|
||||||
|
this._pendingHtml = null;
|
||||||
|
this.style = {};
|
||||||
|
this.classList = new FakeClassList(this);
|
||||||
|
}
|
||||||
|
set innerHTML(value) {
|
||||||
|
this._innerHTML = String(value);
|
||||||
|
for (const child of this.children) child.parentElement = null;
|
||||||
|
this.children = [];
|
||||||
|
this._pendingHtml = this._innerHTML ? this._innerHTML : null;
|
||||||
|
}
|
||||||
|
get innerHTML() { return this._innerHTML; }
|
||||||
|
get firstElementChild() {
|
||||||
|
if (this._pendingHtml !== null) {
|
||||||
|
const html = this._pendingHtml;
|
||||||
|
this._pendingHtml = null;
|
||||||
|
parseHtmlInto(this, html);
|
||||||
|
}
|
||||||
|
return this.children[0] || null;
|
||||||
|
}
|
||||||
|
appendChild(child) {
|
||||||
|
if (child.parentElement) child.remove();
|
||||||
|
child.parentElement = this;
|
||||||
|
this.children.push(child);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
insertBefore(child, ref) {
|
||||||
|
if (child.parentElement) child.remove();
|
||||||
|
child.parentElement = this;
|
||||||
|
const idx = this.children.indexOf(ref);
|
||||||
|
if (idx < 0) this.children.push(child);
|
||||||
|
else this.children.splice(idx, 0, child);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
if (!this.parentElement) return;
|
||||||
|
const idx = this.parentElement.children.indexOf(this);
|
||||||
|
if (idx >= 0) this.parentElement.children.splice(idx, 1);
|
||||||
|
this.parentElement = null;
|
||||||
|
}
|
||||||
|
setAttribute(name, value) {
|
||||||
|
this.attributes[name] = String(value);
|
||||||
|
if (name === 'id') this.id = String(value);
|
||||||
|
if (name === 'class') this.className = String(value);
|
||||||
|
if (name.startsWith('data-')) this.dataset[dataKey(name)] = String(value);
|
||||||
|
}
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'id') return this.id || null;
|
||||||
|
if (name === 'class') return this.className || null;
|
||||||
|
if (name.startsWith('data-')) {
|
||||||
|
const value = this.dataset[dataKey(name)];
|
||||||
|
return value === undefined ? null : String(value);
|
||||||
|
}
|
||||||
|
return this.attributes[name] === undefined ? null : this.attributes[name];
|
||||||
|
}
|
||||||
|
removeAttribute(name) {
|
||||||
|
delete this.attributes[name];
|
||||||
|
if (name.startsWith('data-')) delete this.dataset[dataKey(name)];
|
||||||
|
}
|
||||||
|
matches(selector) { return matchesSelector(this, selector); }
|
||||||
|
closest(selector) {
|
||||||
|
let node = this;
|
||||||
|
while (node) {
|
||||||
|
if (matchesSelector(node, selector)) return node;
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
const found = [];
|
||||||
|
const visit = (node) => {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (matchesSelector(child, selector)) found.push(child);
|
||||||
|
visit(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(this);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
querySelector(selector) {
|
||||||
|
return this.querySelectorAll(selector)[0] || null;
|
||||||
|
}
|
||||||
|
insertAdjacentHTML() {}
|
||||||
|
}
|
||||||
|
function dataKey(name) {
|
||||||
|
return String(name).slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
function matchesSelector(el, selector) {
|
||||||
|
return String(selector || '').split(',').some((part) => matchesSimple(el, part.trim()));
|
||||||
|
}
|
||||||
|
function matchesSimple(el, selector) {
|
||||||
|
if (!selector) return false;
|
||||||
|
const negated = [];
|
||||||
|
const baseSelector = selector.replace(/:not\(([^()]*)\)/g, (_, inner) => {
|
||||||
|
negated.push(String(inner || '').trim());
|
||||||
|
return '';
|
||||||
|
}).trim();
|
||||||
|
if (negated.some((inner) => inner && matchesSimple(el, inner))) return false;
|
||||||
|
if (!baseSelector) return true;
|
||||||
|
const classMatches = [...baseSelector.matchAll(/\.([A-Za-z0-9_-]+)/g)].map((m) => m[1]);
|
||||||
|
if (classMatches.some((name) => !el.classList.contains(name))) return false;
|
||||||
|
const attrMatches = [...baseSelector.matchAll(/\[([^=\]]+)(?:=["']?([^"'\]]+)["']?)?\]/g)];
|
||||||
|
for (const match of attrMatches) {
|
||||||
|
const value = el.getAttribute(match[1]);
|
||||||
|
if (value === null) return false;
|
||||||
|
if (match[2] !== undefined && String(value) !== String(match[2])) return false;
|
||||||
|
}
|
||||||
|
const idMatch = baseSelector.match(/#([A-Za-z0-9_-]+)/);
|
||||||
|
if (idMatch && el.id !== idMatch[1]) return false;
|
||||||
|
const tagMatch = baseSelector.match(/^[A-Za-z][A-Za-z0-9_-]*/);
|
||||||
|
if (tagMatch && el.tagName.toLowerCase() !== tagMatch[0].toLowerCase()) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
msgInner: new FakeElement('div'),
|
||||||
|
emptyState: new FakeElement('div'),
|
||||||
|
};
|
||||||
|
global.window = {};
|
||||||
|
global.document = {
|
||||||
|
createElement: (tag) => new FakeElement(tag),
|
||||||
|
getElementById: (id) => elements[id] || null,
|
||||||
|
};
|
||||||
|
global.performance = { now: () => 1 };
|
||||||
|
global.requestAnimationFrame = (fn) => fn();
|
||||||
|
global.setTimeout = (fn) => fn();
|
||||||
|
function $(id) { return elements[id] || null; }
|
||||||
|
function isTransparentStream() { return false; }
|
||||||
|
function isCompactWorklogMode() { return true; }
|
||||||
|
function isSimplifiedToolCalling() { return true; }
|
||||||
|
function t(key) { return key; }
|
||||||
|
function li() { return ''; }
|
||||||
|
function esc(value) {
|
||||||
|
return String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
function msgContent(message) {
|
||||||
|
if (Array.isArray(message.content)) {
|
||||||
|
return message.content.filter((p) => p && p.type === 'text').map((p) => p.text || p.content || '').join('\n');
|
||||||
|
}
|
||||||
|
return String(message.content || '');
|
||||||
|
}
|
||||||
|
let S;
|
||||||
|
const INFLIGHT = {};
|
||||||
|
let _loadingSessionId = null;
|
||||||
|
let _messageRenderWindowSid = null;
|
||||||
|
let _messageUserUnpinned = false;
|
||||||
|
let _programmaticScroll = false;
|
||||||
|
let _programmaticScrollSetAt = 0;
|
||||||
|
let _sessionHtmlCacheSid = null;
|
||||||
|
let _messagesTruncated = false;
|
||||||
|
let _oldestIdx = 0;
|
||||||
|
let virtualStart = 0;
|
||||||
|
const _sessionHtmlCache = new Map();
|
||||||
|
const _recycleStash = new Map();
|
||||||
|
const _msgNodeRecycleEnabled = false;
|
||||||
|
const _recycleResetAttrs = [];
|
||||||
|
const _ERR_MSG_RE = /__never__/;
|
||||||
|
|
||||||
|
function _captureMessageScrollSnapshot() { return null; }
|
||||||
|
function _resetMessageRenderWindow(sid) { _messageRenderWindowSid = sid; }
|
||||||
|
function _getVisibleMessagesWithIdx() {
|
||||||
|
return S.messages.map((m, rawIdx) => (
|
||||||
|
m && m.role !== 'tool' && !_isContextCompactionMessage(m) && !_isPreservedCompressionTaskListMessage(m)
|
||||||
|
) ? { m, rawIdx } : null).filter(Boolean);
|
||||||
|
}
|
||||||
|
function _messageVirtualKeepTailCount() { return 20; }
|
||||||
|
function _currentMessageVirtualWindow(vis) {
|
||||||
|
const start = Math.min(virtualStart, vis.length);
|
||||||
|
return {
|
||||||
|
virtualized: start > 0,
|
||||||
|
start,
|
||||||
|
end: vis.length,
|
||||||
|
topPad: start > 0 ? 120 * start : 0,
|
||||||
|
bottomPad: 0,
|
||||||
|
total: vis.length,
|
||||||
|
tailStart: vis.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function _messageVirtualWindowKeyFor() { return 'all'; }
|
||||||
|
function _messageRenderCacheSignature() { return 'sig'; }
|
||||||
|
function _compressionStateForCurrentSession() { return null; }
|
||||||
|
function clearCompressionUi() {}
|
||||||
|
function _handoffStateForCurrentSession() { return null; }
|
||||||
|
function _captureWorklogDetailDisclosureState() { return null; }
|
||||||
|
function _applySessionNavigationPrefs() {}
|
||||||
|
function _messageVirtualSpacer() { return new FakeElement('div'); }
|
||||||
|
function _wireMessageWindowLoadEarlierButton() {}
|
||||||
|
function _compressionAnchorIndex() { return null; }
|
||||||
|
function _assistantTurnFinalVisibleContentMap() { return new Map(); }
|
||||||
|
function _assistantTurnVisibleContentMap() { return new Map(); }
|
||||||
|
function _engineAwareCompressionCopy() {
|
||||||
|
return { label: 'context_compaction_label', preview: 'reference_only_label' };
|
||||||
|
}
|
||||||
|
function _createAssistantTurn() {
|
||||||
|
const turn = new FakeElement('div');
|
||||||
|
turn.className = 'assistant-turn';
|
||||||
|
const blocks = new FakeElement('div');
|
||||||
|
blocks.className = 'assistant-turn-blocks';
|
||||||
|
turn.appendChild(blocks);
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
function _assistantTurnBlocks(turn) { return turn ? turn.querySelector('.assistant-turn-blocks') : null; }
|
||||||
|
function _setLatestAssistantTurnLandmark() {}
|
||||||
|
function _assistantRoleHtml() { return ''; }
|
||||||
|
function _userMessageDomId(rawIdx) { return `user-${rawIdx}`; }
|
||||||
|
function _messageSessionIndexForRawIdx(rawIdx) { return rawIdx; }
|
||||||
|
function _messageViewportAnchorKeyForMessage() { return 'k'; }
|
||||||
|
function _stripAttachedFilesMarkerForDisplay(value) { return String(value || ''); }
|
||||||
|
function _stripWorkspaceDisplayPrefix(value) { return String(value || ''); }
|
||||||
|
function _stripLeadingAssistantThinkingMarkup(value) { return String(value || ''); }
|
||||||
|
function _getCachedRender(value) { return String(value || ''); }
|
||||||
|
function _formatInServerTz() { return ''; }
|
||||||
|
function _formatMessageFooterTimestamp() { return ''; }
|
||||||
|
function _questionJumpButtonHtml() { return ''; }
|
||||||
|
function _formatTurnTps() { return ''; }
|
||||||
|
function isTpsDisplayEnabled() { return false; }
|
||||||
|
function _renderAttachmentHtml() { return ''; }
|
||||||
|
function _isMarkerOnlyAssistantCompressionMessage() { return false; }
|
||||||
|
function _isAssistantEmptyPlaceholderContent() { return false; }
|
||||||
|
function _assistantTurnAnchorSettledFinalAnswer() { return null; }
|
||||||
|
function _worklogReasoningTextFromMessage() { return ''; }
|
||||||
|
function _assistantMessageBelongsInWorklog() { return false; }
|
||||||
|
function _assistantThinkingBelongsInWorklog() { return false; }
|
||||||
|
function _assistantReasoningPayloadText() { return ''; }
|
||||||
|
function _statusCardHtml() { return ''; }
|
||||||
|
function _collectHandoffSummaryStates() { return []; }
|
||||||
|
function _handoffCardsNode() { return null; }
|
||||||
|
function renderCompressionUi() {}
|
||||||
|
function _assistantToolAnchorIdxForMessage(messages, rawIdx) { return rawIdx; }
|
||||||
|
function _cliToolResultSnippet(value) { return String(value || ''); }
|
||||||
|
function _cliPatchSnippetFromArgs() { return ''; }
|
||||||
|
function _cliToolCardSnippet(value) { return String(value || ''); }
|
||||||
|
function _cliToolCardHasDiffSnippet() { return false; }
|
||||||
|
function _toolArgsSnapshot(args) { return args || {}; }
|
||||||
|
function _worklogReasonHtmlFromAnchor() { return ''; }
|
||||||
|
function _normalizeThinkingEchoCompare(value) { return String(value || ''); }
|
||||||
|
function _toolWorklogListEl(group) { return group; }
|
||||||
|
function ensureActivityGroup(parent) {
|
||||||
|
const group = new FakeElement('div');
|
||||||
|
if (parent) parent.appendChild(group);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
function _appendWorklogStep() {}
|
||||||
|
function _syncToolCallGroupSummary() {}
|
||||||
|
function _restoreWorklogDetailDisclosureState() {}
|
||||||
|
function _scrollAfterMessageRender() {}
|
||||||
|
function _maybeRecoverVirtualizedBlankViewport() { return false; }
|
||||||
|
function _updateMessageVirtualMeasurements() {}
|
||||||
|
function postProcessRenderedMessages() {}
|
||||||
|
function _postProcessWithAnchorSuppression() {}
|
||||||
|
function _formatGatewayModelLabel() { return ''; }
|
||||||
|
function _gatewayRoutingFailoverText() { return ''; }
|
||||||
|
function _gatewayModelWarningText() { return ''; }
|
||||||
|
function _usedModelTurnChipLabel() { return ''; }
|
||||||
|
function _formatTurnDuration() { return ''; }
|
||||||
|
function _renderSettledAnchorSceneForMessage() { return false; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SCENARIO = r"""
|
||||||
|
const TOTAL = 220;
|
||||||
|
const STALE_MARKER_IDXS = [40, 100, 160];
|
||||||
|
const TASK_LIST_IDX = 161;
|
||||||
|
const CURRENT_TOKEN = 'CURRENT-SUMMARY-TOKEN-7124';
|
||||||
|
const TASK_MARKER = '[Your active task list was preserved across context compression]';
|
||||||
|
|
||||||
|
function staleMarker(rawIdx) {
|
||||||
|
return '[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below.\n'
|
||||||
|
+ '## Historical Task Snapshot\nUser asked: "stale request ' + rawIdx + '"\n'
|
||||||
|
+ '## Goal\nSTALE-MARKER-' + rawIdx + ' historical state\n--- END OF CONTEXT SUMMARY ---';
|
||||||
|
}
|
||||||
|
function buildMessages() {
|
||||||
|
const messages = [];
|
||||||
|
for (let i = 0; i < TOTAL; i++) {
|
||||||
|
if (STALE_MARKER_IDXS.includes(i)) {
|
||||||
|
messages.push({ role: 'assistant', content: staleMarker(i) });
|
||||||
|
} else if (i === TASK_LIST_IDX) {
|
||||||
|
messages.push({ role: 'user', content: TASK_MARKER + '\n- [ ] finish the compaction fix' });
|
||||||
|
} else if (i % 2 === 0) {
|
||||||
|
messages.push({ role: 'user', content: 'question ' + i });
|
||||||
|
} else {
|
||||||
|
messages.push({ role: 'assistant', content: 'answer ' + i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
function render(opts) {
|
||||||
|
elements.msgInner = new FakeElement('div');
|
||||||
|
_sessionHtmlCache.clear();
|
||||||
|
_sessionHtmlCacheSid = null;
|
||||||
|
virtualStart = opts.virtualStart || 0;
|
||||||
|
_messagesTruncated = !!opts.truncated;
|
||||||
|
_oldestIdx = opts.truncated ? 50 : 0;
|
||||||
|
S = {
|
||||||
|
session: {
|
||||||
|
session_id: opts.sid,
|
||||||
|
compression_anchor_summary: opts.summary,
|
||||||
|
},
|
||||||
|
messages: buildMessages(),
|
||||||
|
toolCalls: [],
|
||||||
|
busy: false,
|
||||||
|
};
|
||||||
|
renderMessages();
|
||||||
|
const inner = elements.msgInner;
|
||||||
|
const cards = inner.querySelectorAll('[data-compression-card]');
|
||||||
|
const rawText = (node) => String(node.getAttribute('data-raw-text') || '');
|
||||||
|
const summaryCards = cards.filter((node) => rawText(node).includes(CURRENT_TOKEN));
|
||||||
|
const taskCards = cards.filter((node) => rawText(node).startsWith(TASK_MARKER));
|
||||||
|
const owners = inner.querySelectorAll('[data-compaction-task-owner]');
|
||||||
|
const ownerHasSummary = owners.length === 1
|
||||||
|
&& owners[0].querySelectorAll('[data-compression-card]').some((node) => rawText(node).includes(CURRENT_TOKEN));
|
||||||
|
const ownerHasTask = owners.length === 1
|
||||||
|
&& owners[0].querySelectorAll('[data-compression-card]').some((node) => rawText(node).startsWith(TASK_MARKER));
|
||||||
|
const placements = inner.querySelectorAll('[data-compaction-placement]');
|
||||||
|
const summaryTopIdx = summaryCards.length
|
||||||
|
? inner.children.findIndex((child) => child.querySelectorAll('[data-compression-card]').some((node) => rawText(node).includes(CURRENT_TOKEN)))
|
||||||
|
: -1;
|
||||||
|
const loadOlderIdx = inner.children.findIndex((child) => child.id === 'loadOlderIndicator');
|
||||||
|
return {
|
||||||
|
renderedRows: inner.querySelectorAll('[data-msg-idx]').length,
|
||||||
|
currentSummaryVisible: summaryCards.length > 0,
|
||||||
|
currentSummaryCards: summaryCards.length,
|
||||||
|
staleCards: placements.map((node) => node.getAttribute('data-compaction-placement')),
|
||||||
|
staleCardTexts: placements.map((node) => {
|
||||||
|
const card = node.querySelector('[data-compression-card]');
|
||||||
|
return card ? (/STALE-MARKER-(\d+)/.exec(rawText(card))?.[1] || null) : null;
|
||||||
|
}),
|
||||||
|
taskCards: taskCards.length,
|
||||||
|
taskOwners: owners.length,
|
||||||
|
ownerHasSummary,
|
||||||
|
ownerHasTask,
|
||||||
|
standaloneFallbacks: inner.querySelectorAll('[data-compaction-task-fallback]').length,
|
||||||
|
summaryTopIdx,
|
||||||
|
loadOlderIdx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEWER_SUMMARY = 'Compacted context: ' + CURRENT_TOKEN + ' — newest state after the last compaction';
|
||||||
|
const fullWindow = render({ sid: 'full', summary: NEWER_SUMMARY });
|
||||||
|
// Visible index 170 maps to raw idx >= 174 (three markers and the task-list
|
||||||
|
// message are not renderable rows), so every stale marker is pre-window.
|
||||||
|
const virtualWindow = render({ sid: 'virtual', summary: NEWER_SUMMARY, virtualStart: 170 });
|
||||||
|
const truncatedTail = render({ sid: 'truncated', summary: NEWER_SUMMARY, truncated: true });
|
||||||
|
// Control: the current summary IS referenced by the latest loaded marker, so
|
||||||
|
// the marker card renders it and no second (fallback) card may appear.
|
||||||
|
const matchingSummary = render({ sid: 'matching', summary: 'STALE-MARKER-160 historical state' });
|
||||||
|
const matchingCards = elements.msgInner.querySelectorAll('[data-compression-card]')
|
||||||
|
.filter((node) => String(node.getAttribute('data-raw-text') || '').includes('STALE-MARKER-160'));
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
fullWindow,
|
||||||
|
virtualWindow,
|
||||||
|
truncatedTail,
|
||||||
|
matchingSummary: {
|
||||||
|
...matchingSummary,
|
||||||
|
latestMarkerCards: matchingCards.length,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _scenario_script() -> str:
|
||||||
|
js = _ui_js()
|
||||||
|
evals = "\n".join(
|
||||||
|
f"eval({json.dumps(_function_source(js, name))});" for name in _REAL_FUNCTIONS
|
||||||
|
)
|
||||||
|
return textwrap.dedent(_DOM_SHIM) + "\n" + evals + "\n" + textwrap.dedent(_SCENARIO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stale_markers_do_not_suppress_current_summary_and_task_owner_is_unique():
|
||||||
|
result = json.loads(_run_node_script(_scenario_script()))
|
||||||
|
|
||||||
|
full = result["fullWindow"]
|
||||||
|
assert full["renderedRows"] > 200
|
||||||
|
assert full["currentSummaryVisible"] is True
|
||||||
|
assert full["currentSummaryCards"] == 1
|
||||||
|
assert full["staleCards"] == ["inline", "inline", "inline"]
|
||||||
|
assert full["staleCardTexts"] == ["40", "100", "160"]
|
||||||
|
assert full["taskCards"] == 1
|
||||||
|
assert full["taskOwners"] == 1
|
||||||
|
assert full["ownerHasSummary"] is True
|
||||||
|
assert full["ownerHasTask"] is True
|
||||||
|
assert full["standaloneFallbacks"] == 0
|
||||||
|
|
||||||
|
virtual = result["virtualWindow"]
|
||||||
|
assert virtual["currentSummaryVisible"] is True
|
||||||
|
assert virtual["currentSummaryCards"] == 1
|
||||||
|
assert virtual["staleCards"] == ["pre-window", "pre-window", "pre-window"]
|
||||||
|
assert virtual["staleCardTexts"] == ["40", "100", "160"]
|
||||||
|
assert virtual["taskCards"] == 1
|
||||||
|
assert virtual["taskOwners"] == 1
|
||||||
|
assert virtual["ownerHasSummary"] is True
|
||||||
|
assert virtual["standaloneFallbacks"] == 0
|
||||||
|
|
||||||
|
truncated = result["truncatedTail"]
|
||||||
|
assert truncated["currentSummaryVisible"] is True
|
||||||
|
assert truncated["currentSummaryCards"] == 1
|
||||||
|
assert truncated["loadOlderIdx"] >= 0
|
||||||
|
# The settled current-summary card is pinned directly under the
|
||||||
|
# load-earlier affordance, before the stale marker cards and rows.
|
||||||
|
assert truncated["summaryTopIdx"] == truncated["loadOlderIdx"] + 1
|
||||||
|
assert truncated["taskCards"] == 1
|
||||||
|
assert truncated["taskOwners"] == 1
|
||||||
|
assert truncated["ownerHasSummary"] is True
|
||||||
|
assert truncated["standaloneFallbacks"] == 0
|
||||||
|
|
||||||
|
matching = result["matchingSummary"]
|
||||||
|
assert matching["currentSummaryVisible"] is False
|
||||||
|
assert matching["latestMarkerCards"] == 1
|
||||||
|
assert matching["staleCards"] == ["inline", "inline", "inline"]
|
||||||
|
assert matching["taskCards"] == 1
|
||||||
|
assert matching["taskOwners"] == 1
|
||||||
|
assert matching["ownerHasTask"] is True
|
||||||
|
assert matching["standaloneFallbacks"] == 0
|
||||||
Reference in new issue
Block a user