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
|
||||
// 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.
|
||||
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 inlineMarkers=[];
|
||||
const boundary=Number.isFinite(firstRenderedRawIdx)?firstRenderedRawIdx:-1;
|
||||
@@ -15383,11 +15387,13 @@ function _selectCompactionCardPlacements(markerRawIdxs,firstRenderedRawIdx){
|
||||
if(rawIdx<boundary) preWindowMarkers.push(rawIdx);
|
||||
else inlineMarkers.push(rawIdx);
|
||||
}
|
||||
const taskOwner=inlineMarkers.length
|
||||
?{kind:'inline',rawIdx:inlineMarkers[inlineMarkers.length-1]}
|
||||
:!preWindowMarkers.length
|
||||
?null
|
||||
:{kind:'pre-window',rawIdx:preWindowMarkers[preWindowMarkers.length-1]};
|
||||
const taskOwner=currentSummaryFallback
|
||||
?{kind:'current-summary',rawIdx:-1}
|
||||
:inlineMarkers.length
|
||||
?{kind:'inline',rawIdx:inlineMarkers[inlineMarkers.length-1]}
|
||||
:!preWindowMarkers.length
|
||||
?null
|
||||
:{kind:'pre-window',rawIdx:preWindowMarkers[preWindowMarkers.length-1]};
|
||||
return {preWindowMarkers,inlineMarkers,taskOwner};
|
||||
}
|
||||
function _insertCompactionCardNodes(entries,taskOwner,insertNode){
|
||||
@@ -17072,7 +17078,17 @@ function renderMessages(options){
|
||||
// order; markers inside the window stay inline.
|
||||
const firstRenderedRawIdx=renderVisWithIdx.length?renderVisWithIdx[0].rawIdx:Infinity;
|
||||
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 markerMsg=S.messages[markerRawIdx];
|
||||
let raw='';
|
||||
@@ -17098,8 +17114,8 @@ function renderMessages(options){
|
||||
};
|
||||
const preWindowCompactionCards=compactionPlacements.preWindowMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'pre-window'));
|
||||
const compactionCardNodes=compactionPlacements.inlineMarkers.map(markerRawIdx=>_compactionCardEntry(markerRawIdx,'inline'));
|
||||
const referenceNode=(!compressionState && loadedCompactionRawIdxs.length===0 && _shouldShowSettledCompressionReference(referenceText) && (sessionCompressionAnchor!==null || sessionCompressionAnchorKey || sessionCompressionSummary))
|
||||
? (()=>{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 referenceNode=showCurrentSummaryFallback
|
||||
? (()=>{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;
|
||||
let referenceNodePinnedAtTop=false;
|
||||
let preservedCompressionTaskOwnerNode=null;
|
||||
@@ -17142,7 +17158,7 @@ function renderMessages(options){
|
||||
// Keep the settled compacted-context card immediately visible in a long,
|
||||
// tail-loaded conversation. Put it in flow (not inside an old tool turn).
|
||||
referenceNodePinnedAtTop=_pinSettledCompressionReferenceAtTop(inner,referenceNode,referenceMessageRawIdx);
|
||||
if(referenceNodePinnedAtTop&&referenceNode?.parentElement&&preservedCompressionTaskMessages.length){
|
||||
if(referenceNodePinnedAtTop&&referenceNode?.parentElement&&referenceNodeOwnsTasks){
|
||||
preservedCompressionTaskOwnerNode=referenceNode;
|
||||
}
|
||||
}
|
||||
@@ -17780,7 +17796,7 @@ function renderMessages(options){
|
||||
const referenceInserted=referenceNode&&referenceMessageRawIdx>=0
|
||||
?_insertCompressionLikeNodeByRawIdx(referenceNode,referenceMessageRawIdx)
|
||||
:_insertCompressionLikeNode(referenceNode);
|
||||
if(referenceInserted&&referenceNode?.parentElement&&preservedCompressionTaskMessages.length){
|
||||
if(referenceInserted&&referenceNode?.parentElement&&referenceNodeOwnsTasks){
|
||||
preservedCompressionTaskOwnerNode=referenceNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,7 @@ def test_context_anchor_reference_uses_session_summary_fallback():
|
||||
assert "const referenceText=(()=>{" in src
|
||||
assert "if(!referenceMessage) return sessionCompressionSummary;" 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
|
||||
|
||||
|
||||
|
||||
@@ -136,6 +136,20 @@ assert.deepStrictEqual(
|
||||
_selectCompactionCardPlacements([], 50),
|
||||
{{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)
|
||||
|
||||
|
||||
@@ -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