Release: model-picker Selected badge + Transparent Stream dupe-prefix suppression (#5757, #5758) (#5850)

Two visible render PRs, gate-passed (deep-review round), Nathan-approved to ship.

- #5757 (rodboev): mark active model with a Selected badge + open its provider group (#5748).
  Screenshot-verified (badge clear + correctly placed on active row).
- #5758 (rodboev): suppress the live-token prose row that is a strict prefix of the final
  answer (#5749). Provenance-gated (live-prose:* token rows after last tool row only) so it
  can't eat legitimate narration; reproduction test proves the dupe is gone.

Codex SAFE + full regression green (1576 render tests). 18 own tests pass together.

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
nesquena-hermes 2026-07-09 12:51:18 -07:00 committed by GitHub
parent 2b457f1646
commit 8cc2d6f881
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 921 additions and 43 deletions

View file

@ -11,6 +11,10 @@
### Fixed
- **The model picker now marks the active model with a "Selected" badge.** The currently-selected conversation model shows a distinct **Selected** pill (separate from the "Configured" state), and the picker opens with that model's provider group expanded and scrolled into view — so it's obvious at a glance which model is in use. Thanks @rodboev. (#5757, #5748)
- **Transparent Stream no longer briefly renders a duplicate of the answer.** A live-token prose snapshot that was a strict prefix of the final answer escaped the existing dedupe guards (which only fired on near-complete matches) and rendered as a visible duplicate. A new provenance-gated guard suppresses only the ephemeral live-token accumulator row (role `prose` / kind `process_prose` / `token` source / `live-prose:*` id) when it's a strict shorter prefix of the settled answer and sits after the last tool row — so genuine intermediate narration is never suppressed. Thanks @rodboev. (#5758, #5749)
- **A dropped SSE connection can no longer grow the offline replay buffer without bound.** When a stream disconnected without being cancelled, the per-`StreamChannel` offline replay buffer grew for the whole turn (a per-abandoned-turn memory risk). It's now a bounded `deque` (drop-oldest); the reconnect tail is intact and any dropped frames are recovered through the run-journal `last_event_id` replay path (the client requests journal replay on reconnect; the server enforces journal gap coverage before draining the tail). Thanks @ai-ag2026. (#5788, #4633)
- **The embedded terminal no longer leaks a file descriptor, reader thread, and shell process on exit.** When the terminal client disconnected or the shell exited, the PTY fd, its reader thread, and the shell process were left dangling — accumulating over a long uptime (part of the #4633 long-uptime-crash family). Teardown now closes the fd once, stops the reader thread, and reaps the shell on both the client-exit and shell-exit paths. Thanks @ai-ag2026. (#5835, #4633)

View file

@ -1,3 +1,4 @@
[pytest]
markers =
integration: tests that hit the live test server or external integration surface
reproduction: tests that execute a direct issue reproduction fixture

View file

@ -212,6 +212,7 @@ const LOCALES = {
model_search_placeholder: 'Search models…',
model_search_no_results: 'No models found',
model_group_configured: 'Configured',
model_badge_selected: 'Selected',
ws_search_placeholder: 'Search workspaces…',
ws_no_results: 'No workspaces found',
workspace_switcher_aria: 'Switch workspace. Current workspace: {0}.',
@ -1914,6 +1915,7 @@ const LOCALES = {
model_search_placeholder: 'Cerca modelli…',
model_search_no_results: 'Nessun modello trovato',
model_group_configured: 'Configurato',
model_badge_selected: 'Selezionato',
ws_search_placeholder: 'Cerca workspace…',
ws_no_results: 'Nessun workspace trovato',
workspace_switcher_aria: 'Cambia workspace. Workspace corrente: {0}.',
@ -3607,6 +3609,7 @@ const LOCALES = {
model_search_placeholder: 'モデルを検索…',
model_search_no_results: 'モデルが見つかりません',
model_group_configured: '設定済み',
model_badge_selected: '選択中',
ws_search_placeholder: 'ワークスペースを検索…',
ws_no_results: 'ワークスペースが見つかりません',
workspace_switcher_aria: 'ワークスペースを切り替えます。現在のワークスペース: {0}。',
@ -5395,6 +5398,7 @@ const LOCALES = {
focus_label: 'Фокус',
model_search_no_results: 'Модели не найдены',
model_group_configured: 'Настроенные',
model_badge_selected: 'Выбрано',
ws_search_placeholder: 'Поиск рабочих пространств…',
ws_no_results: 'Рабочие пространства не найдены',
workspace_switcher_aria: 'Переключить рабочее пространство. Текущее рабочее пространство: {0}.',
@ -6914,6 +6918,7 @@ const LOCALES = {
model_search_placeholder: 'Buscar modelos…',
model_search_no_results: 'No se encontraron modelos',
model_group_configured: 'Configurados',
model_badge_selected: 'Seleccionado',
ws_search_placeholder: 'Buscar espacios de trabajo…',
ws_no_results: 'No se encontraron espacios de trabajo',
workspace_switcher_aria: 'Cambiar espacio de trabajo. Espacio de trabajo actual: {0}.',
@ -9539,6 +9544,7 @@ const LOCALES = {
model_search_placeholder: 'Modelle suchen…',
model_search_no_results: 'Keine Modelle gefunden',
model_group_configured: 'Konfiguriert',
model_badge_selected: 'Ausgewählt',
ws_search_placeholder: 'Arbeitsbereiche suchen…',
ws_no_results: 'Keine Arbeitsbereiche gefunden',
workspace_switcher_aria: 'Workspace wechseln. Aktueller Workspace: {0}.',
@ -10176,6 +10182,7 @@ const LOCALES = {
model_search_placeholder: '搜索模型…',
model_search_no_results: '未找到模型',
model_group_configured: '已配置',
model_badge_selected: '已选择',
ws_search_placeholder: '搜索工作区…',
ws_no_results: '未找到工作区',
workspace_switcher_aria: '切换工作区。当前工作区:{0}。',
@ -11829,6 +11836,7 @@ const LOCALES = {
model_search_placeholder: '搜尋模型…',
model_search_no_results: '找不到模型',
model_group_configured: '已設定',
model_badge_selected: '已選取',
ws_search_placeholder: '搜尋工作區…',
ws_no_results: '找不到工作區',
workspace_switcher_aria: '切換工作區。目前工作區:{0}。',
@ -13458,6 +13466,7 @@ const LOCALES = {
session_toolsets_failed: 'Falha ao atualizar os conjuntos de ferramentas: ',
model_search_no_results: 'Nenhum modelo encontrado',
model_group_configured: 'Configurados',
model_badge_selected: 'Selecionado',
ws_search_placeholder: 'Buscar espaços de trabalho…',
ws_no_results: 'Nenhum espaço de trabalho encontrado',
workspace_switcher_aria: 'Alternar espaço de trabalho. Espaço de trabalho atual: {0}.',
@ -15027,6 +15036,7 @@ const LOCALES = {
session_toolsets_failed: '도구 세트를 업데이트하지 못했습니다: ',
model_search_no_results: 'No models found',
model_group_configured: '구성됨',
model_badge_selected: '선택됨',
ws_search_placeholder: '워크스페이스 검색…',
ws_no_results: '워크스페이스를 찾을 수 없습니다',
workspace_switcher_aria: '워크스페이스 전환. 현재 워크스페이스: {0}.',
@ -16713,6 +16723,7 @@ const LOCALES = {
model_search_placeholder: 'Rechercher des modèles\u2026',
model_search_no_results: 'Aucun modèle trouvé',
model_group_configured: 'Configuré',
model_badge_selected: 'Sélectionné',
ws_search_placeholder: 'Rechercher des espaces de travail\u2026',
ws_no_results: 'Aucun espace de travail trouvé',
workspace_switcher_aria: 'Changer d’espace de travail. Espace de travail actuel : {0}.',
@ -19582,6 +19593,7 @@ const LOCALES = {
model_custom_placeholder: 'např. openai/gpt-5.4',
model_did_you_mean: (m) => ` — měli jste na mysli „${m}“?`,
model_group_configured: 'Nastavené',
model_badge_selected: 'Vybráno',
model_not_found_label: 'Model nenalezen',
model_scope_advisory: 'Vztahuje se na tuto konverzaci z vaší další zprávy.',
model_scope_toast: 'Vztahuje se na tuto konverzaci z vaší další zprávy.',
@ -20047,6 +20059,7 @@ const LOCALES = {
session_toolsets_failed: 'Araç setleri güncellenemedi: ',
model_search_no_results: 'Hiçbir model bulunamadı',
model_group_configured: 'Yapılandırılmış',
model_badge_selected: 'Seçili',
ws_search_placeholder: 'Çalışma alanlarını arayın\u2026',
ws_no_results: 'Çalışma alanı bulunamadı',
workspace_switcher_aria: 'Çalışma alanını değiştir. Geçerli çalışma alanı: {0}.',
@ -21726,6 +21739,7 @@ const LOCALES = {
model_search_placeholder: 'Szukaj modeli…',
model_search_no_results: 'Nie znaleziono żadnych modeli',
model_group_configured: 'Skonfigurowane',
model_badge_selected: 'Zaznaczony',
ws_search_placeholder: 'Szukaj obszarów roboczych…',
ws_no_results: 'Nie znaleziono żadnych obszarów roboczych',
workspace_switcher_aria: 'Przełącz obszar roboczy. Bieżący obszar roboczy: {0}.',
@ -23391,6 +23405,7 @@ const LOCALES = {
model_search_placeholder: 'Tìm model…',
model_search_no_results: 'Không tìm thấy model nào',
model_group_configured: 'Đã cấu hình',
model_badge_selected: 'Đã chọn',
ws_search_placeholder: 'Tìm workspace…',
ws_no_results: 'Không tìm thấy workspace nào',
workspace_switcher_aria: 'Chuyển workspace. Workspace hiện tại: {0}.',

View file

@ -3495,11 +3495,27 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
const rows=[];
const seen=new Set();
const seenTextKeys=[];
const pushRow=(row)=>{
const projectedRows=Array.isArray(base.activity_rows)?base.activity_rows:[];
const orderedRows=[];
for(const row of projectedRows){
if(row&&row.role==='terminal') continue;
orderedRows.push(row);
}
for(let idx=turnStart+1;idx<=lastAsstIndex;idx+=1){
const bucket=messageRows.get(idx)||[];
for(const row of bucket) orderedRows.push(row);
}
for(const row of projectedRows){
if(row&&row.role==='terminal') orderedRows.push(row);
}
const lastNonTerminalWorkRowIndex=orderedRows.reduce((last,row,idx)=>(row&&row.role==='tool')?idx:last,-1);
const rowIsLiveTokenFinalPrefix=(row,textKey,finalSegmentEligible)=>finalSegmentEligible&&row&&row.role==='prose'&&row.kind==='process_prose'&&String(row.source_event_type||'')==='token'&&String(row.local_id||'').startsWith('live-prose:')&&textKey&&finalKey&&textKey.length<finalKey.length&&finalKey.startsWith(textKey);
const pushRow=(row,rowIndex)=>{
if(!row||typeof row!=='object') return;
row=_anchorSceneSettleLiveRunningRow(row,hasSettledThinking);
if(!row||typeof row!=='object') return;
const textKey=_anchorSceneTextKey(row.text);
if(rowIsLiveTokenFinalPrefix(row,textKey,rowIndex>lastNonTerminalWorkRowIndex)) return;
const isTextual=row.role==='prose'||row.role==='thinking';
if(isTextual&&_anchorSceneRowLooksLikeFinalAnswer(textKey,finalKey)) return;
if(isTextual&&_anchorSceneRowTextOverlapsExisting(textKey,seenTextKeys)) return;
@ -3514,18 +3530,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
seq:rows.length,
});
};
const projectedRows=Array.isArray(base.activity_rows)?base.activity_rows:[];
for(const row of projectedRows){
if(row&&row.role==='terminal') continue;
pushRow(row);
}
for(let idx=turnStart+1;idx<=lastAsstIndex;idx+=1){
const bucket=messageRows.get(idx)||[];
for(const row of bucket) pushRow(row);
}
for(const row of projectedRows){
if(row&&row.role==='terminal') pushRow(row);
}
orderedRows.forEach((row,idx)=>pushRow(row,idx));
const scene={
...base,
version:'activity_scene_v1',

View file

@ -3237,7 +3237,7 @@
.ws-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;right:0;min-width:200px;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;overflow:hidden;max-height:320px;overflow-y:auto;}
.ws-dropdown.open{display:block;}
.ws-dropdown-footer{left:0;right:auto;bottom:calc(100% + 4px);min-width:280px;max-width:min(420px,calc(100vw - 32px));}
.model-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:280px;max-width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;overflow:hidden;max-height:320px;overflow-y:auto;}
.model-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;min-width:280px;max-width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:200;overflow:hidden;max-height:min(70vh,640px);overflow-y:auto;}
.model-dropdown.open{display:block;}
.model-scope-note{position:sticky;top:0;z-index:2;padding:9px 14px;border-bottom:1px solid var(--border);color:var(--text);font-size:11px;font-weight:650;line-height:1.4;background:color-mix(in srgb,var(--surface) 82%,var(--accent-bg));box-shadow:0 1px 0 rgba(0,0,0,.12);}
.model-group{padding:8px 14px 4px;font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted);text-transform:uppercase;border-top:1px solid var(--border2);margin-top:2px;}.model-group.collapsible{cursor:pointer;user-select:none}.model-group.collapsible:before{content:'\25b6';display:inline-block;font-size:8px;margin-right:5px;transition:transform .15s ease}.model-group.collapsible.open:before{transform:rotate(90deg)}.model-group-body:empty{display:none}
@ -3252,6 +3252,7 @@
.model-opt-badge{display:inline-flex;align-items:center;justify-content:center;padding:2px 7px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;border:1px solid transparent;}
.model-opt-badge--primary{background:rgba(50,184,198,.16);border-color:rgba(50,184,198,.32);color:#8fe7ef;}
.model-opt-badge--fallback{background:rgba(255,184,77,.14);border-color:rgba(255,184,77,.28);color:#ffd18a;}
.model-opt-badge--selected{background:rgba(99,179,255,.16);border-color:rgba(99,179,255,.34);color:#cfe8ff;}
.model-opt-id{display:block;font-size:10px;color:var(--muted);line-height:1.3;opacity:.72;word-break:break-word;}
.model-provider-hint{margin:2px 14px 6px;padding:7px 9px;border:1px solid var(--border2);border-radius:7px;background:color-mix(in srgb,var(--surface) 90%,var(--warning));color:var(--muted);font-size:11px;line-height:1.35;}
.model-opt-provider{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:9px;font-weight:600;letter-spacing:.03em;color:var(--muted);background:rgba(255,255,255,.05);border:1px solid var(--border2);margin-left:auto;white-space:nowrap;flex-shrink:0;}

View file

@ -3771,6 +3771,15 @@ function renderModelDropdown(){
}
return 500;
};
const _selectedModelState=(typeof _modelStateForSelect==='function')?_modelStateForSelect(sel,sel.value):{model:sel&&sel.value||'',model_provider:null};
const _modelProviderForSelectedBadge=(m)=>{
const _provider=String((m&&m.providerId)||(m&&m.badge&&m.badge.provider)||((typeof _providerFromModelValue==='function')?_providerFromModelValue(m&&m.value):'')||'').trim();
return (_provider&&_provider!=='default')?_provider:null;
};
const _isSelectedModelRow=(m)=>String((m&&m.value)||'')===String((_selectedModelState&&_selectedModelState.model)||(sel&&sel.value)||'')&&String(_modelProviderForSelectedBadge(m)||'')===String((_selectedModelState&&_selectedModelState.model_provider)||'');
const _selectedModelBadge=(m)=>_isSelectedModelRow(m)
?`<span class="model-opt-badge model-opt-badge--selected">${esc(t('model_badge_selected')||'Selected')}</span>`
:'';
const _renderProviderEndpointHint=(entry,parent)=>{
if(!entry||!entry.label||!entry.modelsEndpointError) return;
const hint=document.createElement('div');
@ -3780,13 +3789,13 @@ function renderModelDropdown(){
};
// Build a single model-option row (mirrors the main render loop's row markup),
// used both by the main render and by the in-place overflow reveal below.
const _buildModelRow=(m,sel,withProviderChip)=>{
const _buildModelRow=(m,withProviderChip)=>{
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
row.className='model-opt'+(_isSelectedModelRow(m)?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
const _plainGroup=m.group?String(m.group).replace(/\s*\(\d+\s+of\s+\d+\)\s*$/,''):'';
const providerChip=(_plainGroup&&withProviderChip)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${_selectedModelBadge(m)}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.onclick=()=>selectFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
return row;
};
@ -3840,7 +3849,7 @@ function renderModelDropdown(){
for(const m of extraModels){
if(!m||!m.id) continue;
if(_alreadyShown.has(esc(m.id))) continue;
const row=_buildModelRow({value:m.id,name:m.label||m.id,id:m.id,group:_plainLabel,groupKey,providerId:(og.dataset&&og.dataset.provider)||''},sel,false);
const row=_buildModelRow({value:m.id,name:m.label||m.id,id:m.id,group:_plainLabel,groupKey,providerId:(og.dataset&&og.dataset.provider)||''},false);
wrap.insertBefore(row,moreEl);
if(!firstNewRow) firstNewRow=row;
}
@ -3895,17 +3904,17 @@ function renderModelDropdown(){
const _selectedGroupKey=(()=>{
const _selVal=String((sel&&sel.value)||'');
if(!_selVal) return null;
const _hit=_modelData.find(m=>m&&!m.endpointErrorOnly&&String(m.value||'')===_selVal);
const _hit=_modelData.find(m=>m&&!m.endpointErrorOnly&&_isSelectedModelRow(m)) || _modelData.find(m=>m&&!m.endpointErrorOnly&&String(m.value||'')===_selVal);
return _hit?_hit.groupKey:null;
})();
const _makeModelRow=(m,sel,shouldRenderHeading)=>{
const _makeModelRow=(m,shouldRenderHeading)=>{
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
row.className='model-opt'+(_isSelectedModelRow(m)?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
const _plainGroup=m.group?String(m.group).replace(/\s*\(\d+\s+of\s+\d+\)\s*$/,''):'';
const _underOwnHeading=shouldRenderHeading&&!!(m.groupKey&&_groupWrappers[m.groupKey]);
const providerChip=(_plainGroup&&!_underOwnHeading)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${_selectedModelBadge(m)}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.onclick=()=>selectFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
return row;
};
@ -3990,7 +3999,7 @@ function renderModelDropdown(){
}
for(const m of configuredModels){
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
row.className='model-opt'+(_isSelectedModelRow(m)?' active':'');
let badgeLabel = '';
let modelName = m.name;
if (m.badge) {
@ -4004,7 +4013,7 @@ function renderModelDropdown(){
}
}
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(badgeLabel)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(modelName)}</span>${badgeHtml}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(modelName)}</span>${badgeHtml}${_selectedModelBadge(m)}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.onclick=()=>selectFromDropdown(m.value,(m.badge&&m.badge.provider)||m.providerId||null);
dd.appendChild(row);
}
@ -4105,16 +4114,16 @@ function renderModelDropdown(){
});
wrapper.appendChild(subHeading);
wrapper.appendChild(subWrapper);
for(const m of pfxRows) subWrapper.appendChild(_makeModelRow(m,sel,shouldRenderHeading));
for(const m of pfxRows) subWrapper.appendChild(_makeModelRow(m,shouldRenderHeading));
} else {
for(const m of pfxRows) wrapper.appendChild(_makeModelRow(m,sel,shouldRenderHeading));
for(const m of pfxRows) wrapper.appendChild(_makeModelRow(m,shouldRenderHeading));
}
}
} else {
for(const m of groupRows) wrapper.appendChild(_makeModelRow(m,sel,shouldRenderHeading));
for(const m of groupRows) wrapper.appendChild(_makeModelRow(m,shouldRenderHeading));
}
} else {
for(const m of groupRows) dd.appendChild(_makeModelRow(m,sel,shouldRenderHeading));
for(const m of groupRows) dd.appendChild(_makeModelRow(m,shouldRenderHeading));
}
if(!term&&hiddenCount){
const showAll=document.createElement('div');
@ -4243,6 +4252,8 @@ async function toggleModelDropdown(){
renderModelDropdown();
dd.classList.add('open');
_positionModelDropdown();
const activeRow=dd.querySelector('.model-opt.active');
if(activeRow&&typeof activeRow.scrollIntoView==='function') activeRow.scrollIntoView({block:'nearest'});
chip.classList.add('active');
const mobileAction=$('composerMobileModelAction');
if(mobileAction) mobileAction.classList.add('active');
@ -11513,6 +11524,7 @@ function _anchorSceneTransparentNodeForRow(row, opts){
const text=String(row.text||'').trim();
if(!text) return null;
const finalAnswer=String((opts&&opts.finalAnswer)||'').trim();
if(opts&&opts.liveTokenFinalPrefixEligible&&_anchorSceneLiveTokenFinalPrefix(row,text,finalAnswer)) return null;
if(finalAnswer&&_anchorSceneProseMatchesFinalAnswer(text,finalAnswer)) return null;
node=_anchorSceneNodeForRow(row,{settled});
if(!node) return null;
@ -11555,6 +11567,18 @@ function _anchorSceneTransparentNodeForRow(row, opts){
if(live) node.setAttribute('data-live-stream-owned','1');
return node;
}
function _anchorSceneLiveTokenFinalPrefix(row, proseText, finalAnswer){
if(!row||row.role!=='prose'||row.kind!=='process_prose') return false;
if(String(row.source_event_type||'')!=='token') return false;
if(!String(row.local_id||'').startsWith('live-prose:')) return false;
const norm=(s)=>String(s||'').replace(/\s+/g,' ').trim().toLowerCase();
const rowKey=norm(proseText), finalKey=norm(finalAnswer);
return !!(rowKey&&finalKey&&rowKey.length<finalKey.length&&finalKey.startsWith(rowKey));
}
function _anchorSceneLastNonTerminalWorkRowIndex(rows){
if(!Array.isArray(rows)) return -1;
return rows.reduce((last,row,idx)=>(row&&row.role==='tool')?idx:last,-1);
}
// Whitespace-insensitive compare so a scene prose row that IS the final answer
// (possibly re-wrapped) is recognized and not duplicated against the segment.
// Codex #4568: the prefix tolerance must NOT be able to suppress a DISTINCT
@ -12152,6 +12176,7 @@ function _renderSettledAnchorSceneTransparentForMessage(message, segment, rawIdx
const scene=message._anchor_activity_scene;
const rows=_anchorSceneRowsForRendering(scene,{settled:true});
if(!rows.length) return false;
const lastNonTerminalWorkRowIndex=_anchorSceneLastNonTerminalWorkRowIndex(rows);
// The assistant segment owns the final answer; pass it so intermediate prose
// rows render but the final-answer-duplicate prose row is suppressed.
const finalAnswer=String(
@ -12170,8 +12195,9 @@ function _renderSettledAnchorSceneTransparentForMessage(message, segment, rawIdx
}
});
let wrote=false;
for(const row of rows){
const node=_anchorSceneTransparentNodeForRow(row,{settled:true,finalAnswer});
for(let idx=0;idx<rows.length;idx+=1){
const row=rows[idx];
const node=_anchorSceneTransparentNodeForRow(row,{settled:true,finalAnswer,liveTokenFinalPrefixEligible:idx>lastNonTerminalWorkRowIndex});
if(!node) continue;
if(segment.parentElement===blocks) blocks.insertBefore(node,segment);
else blocks.appendChild(node);

View file

@ -0,0 +1,36 @@
{
"source": "Issue #5749 captured session JSON shape, redacted in the public issue body.",
"messages": [
{
"id": "assistant-5749",
"role": "assistant",
"content": "While reviewing the Transparent Stream replay, I found that the settled anchor scene kept a live token prose row from the final answer stream. That row was saved as progress even though it was only the beginning of the final response. On reload the renderer treated it as ordinary activity, so the reader saw the opening paragraph once above the answer and again inside the assistant message."
}
],
"anchor_activity_scenes": {
"assistant-5749": {
"scene": {
"mode": "transparent_stream",
"final_answer": "While reviewing the Transparent Stream replay, I found that the settled anchor scene kept a live token prose row from the final answer stream. That row was saved as progress even though it was only the beginning of the final response. On reload the renderer treated it as ordinary activity, so the reader saw the opening paragraph once above the answer and again inside the assistant message.",
"lifecycle": {
"terminal_state": "done"
},
"identity": {
"source_message_refs": [
"assistant-5749"
]
},
"activity_rows": [
{
"role": "prose",
"kind": "process_prose",
"source_event_type": "token",
"local_id": "live-prose:issue5749:1",
"text": "While reviewing the Transparent Stream replay, I found that the settled anchor scene kept a live token prose row from the final answer stream. That row was saved as progress",
"status": "running"
}
]
}
}
}
}

View file

@ -0,0 +1,41 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
def _function_body(src: str, name: str) -> str:
marker = f"function {name}"
start = src.index(marker)
brace = src.index("{", start)
depth = 0
for idx in range(brace, len(src)):
if src[idx] == "{":
depth += 1
elif src[idx] == "}":
depth -= 1
if depth == 0:
return src[brace + 1 : idx]
raise AssertionError(f"{name} body not found")
def test_settled_scene_keys_live_token_prefix_dedupe_to_final_answer_identity():
settle_body = _function_body(MESSAGES_JS, "_completeSettledAnchorSceneForTurn")
final_overlap_body = _function_body(MESSAGES_JS, "_anchorSceneRowLooksLikeFinalAnswer")
assert "lastNonTerminalWorkRowIndex" in settle_body
assert "rowIsLiveTokenFinalPrefix(row,textKey,rowIndex>lastNonTerminalWorkRowIndex)" in settle_body
assert "rowHasNonLiveDuplicate" not in settle_body
assert "_anchorSceneRowLooksLikeFinalAnswer(textKey,finalKey)" in settle_body
assert "(shorter/longer)>=0.9" in final_overlap_body
def test_render_scene_passes_final_segment_eligibility_to_live_prefix_guard():
ui_js = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
render_body = _function_body(ui_js, "_renderSettledAnchorSceneTransparentForMessage")
row_body = _function_body(ui_js, "_anchorSceneTransparentNodeForRow")
assert "const lastNonTerminalWorkRowIndex=_anchorSceneLastNonTerminalWorkRowIndex(rows);" in render_body
assert "liveTokenFinalPrefixEligible:idx>lastNonTerminalWorkRowIndex" in render_body
assert "opts&&opts.liveTokenFinalPrefixEligible&&_anchorSceneLiveTokenFinalPrefix" in row_body

View file

@ -11,6 +11,7 @@ from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
import textwrap
from pathlib import Path
@ -45,13 +46,19 @@ def _run_node_script(script: str) -> str:
if not node:
pytest.skip("node executable is required for JavaScript behavior checks")
try:
result = subprocess.run(
[node, "-e", script],
cwd=ROOT,
text=True,
capture_output=True,
timeout=10,
)
with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as handle:
handle.write(script)
script_path = handle.name
try:
result = subprocess.run(
[node, script_path],
cwd=ROOT,
text=True,
capture_output=True,
timeout=10,
)
finally:
Path(script_path).unlink(missing_ok=True)
except subprocess.TimeoutExpired as exc:
pytest.fail(
"node behavior check timed out"
@ -920,4 +927,5 @@ def test_anchor_settled_renderers_remain_the_primary_scene_path():
assert "if(!message||!message._anchor_activity_scene||!segment) return false;" in transparent
assert "_anchorSceneRowsForRendering(scene,{settled:true})" in transparent
assert "_anchorSceneTransparentNodeForRow(row,{settled:true,finalAnswer})" in transparent
assert "const lastNonTerminalWorkRowIndex=_anchorSceneLastNonTerminalWorkRowIndex(rows);" in transparent
assert "liveTokenFinalPrefixEligible:idx>lastNonTerminalWorkRowIndex" in transparent

View file

@ -299,6 +299,7 @@ global._createAssistantTurn = () => turn;
global._assistantTurnBlocks = () => turn;
global._anchorSceneTransparentNodeForRow = (row) => null;
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
eval(extractFunc('_transparentLiveRowKey'));
eval(extractFunc('_transparentLiveRowsCompatible'));

View file

@ -0,0 +1,62 @@
"""Regression coverage for #5748 model picker selected badge and scroll state."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
def _body_between(src: str, start: str, end: str) -> str:
start_idx = src.index(start)
end_idx = src.index(end, start_idx)
return src[start_idx:end_idx]
def test_model_dropdown_uses_viewport_height_cap():
assert "max-height:min(70vh,640px);overflow-y:auto;" in STYLE_CSS
def test_toggle_model_dropdown_scrolls_active_row_after_open():
body = _body_between(UI_JS, "async function toggleModelDropdown()", "function closeModelDropdown")
assert "_positionModelDropdown();" in body
assert "scrollIntoView({block:'nearest'})" in body
assert body.index("_positionModelDropdown();") < body.index("scrollIntoView({block:'nearest'})")
def test_model_picker_renders_selected_badge_without_replacing_configured_badge():
assert "const _selectedModelBadge=(m)=>" in UI_JS
assert "model-opt-badge--selected" in UI_JS
assert "t('model_badge_selected')||'Selected'" in UI_JS
assert "_getConfiguredModelBadge(modelId,badgeMap,providerId)" in UI_JS
def test_selected_badge_is_keyed_to_current_model_value():
assert "String((m&&m.value)||'')===String((_selectedModelState&&_selectedModelState.model)||(sel&&sel.value)||'')" in UI_JS
def test_selected_badge_is_keyed_to_current_model_provider():
assert "const _selectedModelState=(typeof _modelStateForSelect==='function')?_modelStateForSelect(sel,sel.value)" in UI_JS
assert "const _modelProviderForSelectedBadge=(m)=>" in UI_JS
assert "return (_provider&&_provider!=='default')?_provider:null;" in UI_JS
assert "String(_modelProviderForSelectedBadge(m)||'')===String((_selectedModelState&&_selectedModelState.model_provider)||'')" in UI_JS
assert "const _isSelectedModelRow=(m)=>" in UI_JS
assert "row.className='model-opt'+(_isSelectedModelRow(m)?' active':'');" in UI_JS
def test_selected_group_key_prefers_provider_matched_row_before_value_fallback():
assert "const _hit=_modelData.find(m=>m&&!m.endpointErrorOnly&&_isSelectedModelRow(m)) || _modelData.find(m=>m&&!m.endpointErrorOnly&&String(m.value||'')===_selVal);" in UI_JS
assert "_groupOpenState[groupKey]=(groupKey===_selectedGroupKey)" in UI_JS
def test_selected_badge_helper_does_not_accept_dead_select_parameter():
assert "_selectedModelBadge(m,sel)" not in UI_JS
assert "_selectedModelBadge(m)" in UI_JS
assert "_buildModelRow=(m,sel" not in UI_JS
assert "_makeModelRow(m,sel" not in UI_JS
def test_selected_badge_label_has_locale_entries():
assert I18N_JS.count("model_badge_selected:") >= 14

View file

@ -0,0 +1,674 @@
"""Regression tests for issue #5749 Transparent Stream prefix dedupe."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
ISSUE5749_CAPTURED_SESSION = json.loads(
(ROOT / "tests" / "fixtures" / "issue5749_captured_session_prefix.json").read_text(encoding="utf-8")
)
def _run_node(src, script, tmp_path):
assert NODE, "node is required for issue #5749 regression tests"
script_path = tmp_path / "issue5749_node_script.js"
script_path.write_text(script, encoding="utf-8")
result = subprocess.run([NODE, str(script_path)], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _normalized_text(text):
return " ".join(str(text or "").split()).lower()
def _issue5749_captured_scene():
scenes = ISSUE5749_CAPTURED_SESSION["anchor_activity_scenes"]
scene = next(iter(scenes.values()))["scene"]
rows = scene["activity_rows"]
assert len(rows) == 1
row = rows[0]
assert row["role"] == "prose"
assert row["kind"] == "process_prose"
assert row["source_event_type"] == "token"
assert row["local_id"].startswith("live-prose:")
assert not any(
other is not row and other.get("text") == row["text"] and not str(other.get("local_id", "")).startswith("live-prose:")
for other in rows
)
row_key = _normalized_text(row["text"])
final_key = _normalized_text(scene["final_answer"])
assert row_key and final_key.startswith(row_key)
assert 0.40 <= len(row_key) / len(final_key) <= 0.45
return scene, row
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_reproduction_fixture_matches_lone_live_prefix_shape():
scene, row = _issue5749_captured_scene()
final_key = _normalized_text(scene["final_answer"])
row_key = _normalized_text(row["text"])
assert row_key != final_key
assert final_key.startswith(row_key)
assert scene["final_answer"] == ISSUE5749_CAPTURED_SESSION["messages"][0]["content"]
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_settlement_preserves_pre_tool_live_token_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
status: 'running',
attachments: [{{ id: 'attachment-1' }}],
}},
{{
role: 'tool',
kind: 'tool_result',
source_event_type: 'tool',
local_id: 'tool-row-1',
text: 'Fetched docs',
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify({{
final_answer: scene.final_answer,
rows: scene.activity_rows.map(row => ({{
role: row.role,
kind: row.kind,
source_event_type: row.source_event_type,
local_id: row.local_id,
text: row.text,
status: row.status,
attachments: row.attachments || null,
}})),
}}));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data["final_answer"] == final_answer
assert [row["local_id"] for row in data["rows"]] == [fixture_row["local_id"], "tool-row-1"]
assert data["rows"][0]["text"] == prefix_text
assert data["rows"][0]["role"] == "prose"
assert data["rows"][1]["role"] == "tool"
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_distinct_live_token_prefix_rows_survive_settlement(tmp_path):
final_answer = "The solution is to preserve short legitimate live-token prefixes while still suppressing longer duplicated final-answer spans."
prefix_text = "Checking context"
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-short:1',
text: {json.dumps(prefix_text)},
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify(scene.activity_rows.map(row => row.text)));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data == [prefix_text]
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_long_live_progress_prefix_is_suppressed_without_settled_duplicate(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify(scene.activity_rows.map(row => ({{
local_id: row.local_id,
text: row.text,
status: row.status,
}}))));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data == []
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_suppresses_persisted_live_progress_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: true,
}});
process.stdout.write(JSON.stringify({{
liveResult: liveResult === null,
}}));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data["liveResult"] is True
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_suppresses_near_complete_live_prefix_rows(tmp_path):
final_answer = "I found the issue and I am fixing it by deduping live-token prefixes during settlement and render fallback now."
prefix_text = "I found the issue and I am fixing it by deduping live-token prefixes during settlement and render fallback"
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-1:near',
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: true,
}});
process.stdout.write(JSON.stringify(liveResult === null));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data is True
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_preserves_pre_tool_live_token_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: false,
}});
process.stdout.write(JSON.stringify({{
visible: !!liveResult,
rowId: liveResult && liveResult.attributes && liveResult.attributes['data-anchor-row-id'] || '',
}}));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data == {"visible": True, "rowId": fixture_row["local_id"]}
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_non_live_prefix_rows_survive_mode_and_attachment_variants(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const variants = [
{{
label: 'full_dom',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:3',
text: {json.dumps(prefix_text)},
attachments: [{{ id: 'attachment-1' }}],
}},
opts: {{ settled: true, finalAnswer: {json.dumps(final_answer)} }},
}},
{{
label: 'virtualized',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:4',
text: {json.dumps(prefix_text)},
}},
opts: {{ settled: false, finalAnswer: {json.dumps(final_answer)} }},
}},
{{
label: 'attachments',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:5',
text: {json.dumps(prefix_text)},
attachments: [{{ id: 'attachment-2' }}],
}},
opts: {{ settled: true, finalAnswer: {json.dumps(final_answer)} }},
}},
];
const rendered = variants.map(({{
label,
row,
opts,
}}) => {{
const node = _anchorSceneTransparentNodeForRow(row, opts);
return {{
label,
visible: !!node,
rowId: node && node.attributes && node.attributes['data-anchor-row-id'] || '',
attachmentCount: Array.isArray(row.attachments) ? row.attachments.length : 0,
}};
}});
process.stdout.write(JSON.stringify(rendered));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data == [
{"label": "full_dom", "visible": True, "rowId": "session-prose:stream-1:3", "attachmentCount": 1},
{"label": "virtualized", "visible": True, "rowId": "session-prose:stream-1:4", "attachmentCount": 0},
{"label": "attachments", "visible": True, "rowId": "session-prose:stream-1:5", "attachmentCount": 1},
]

View file

@ -924,10 +924,12 @@ def test_settled_anchor_scene_preserves_live_projected_order_before_backfill():
overlap = _function_body(MESSAGES_JS, "_anchorSceneRowTextOverlapsExisting")
projected_idx = complete.index("const projectedRows=Array.isArray(base.activity_rows)?base.activity_rows:[];")
projected_push_idx = complete.index("for(const row of projectedRows){")
backfill_idx = complete.index("for(let idx=turnStart+1;idx<=lastAsstIndex;idx+=1)")
terminal_idx = complete.index("if(row&&row.role==='terminal') pushRow(row);", backfill_idx)
assert projected_idx < projected_push_idx < backfill_idx < terminal_idx
ordered_idx = complete.index("const orderedRows=[];", projected_idx)
projected_push_idx = complete.index("orderedRows.push(row);", ordered_idx)
backfill_idx = complete.index("for(let idx=turnStart+1;idx<=lastAsstIndex;idx+=1)", projected_push_idx)
terminal_idx = complete.index("if(row&&row.role==='terminal') orderedRows.push(row);", backfill_idx)
replay_idx = complete.index("orderedRows.forEach((row,idx)=>pushRow(row,idx));", terminal_idx)
assert projected_idx < ordered_idx < projected_push_idx < backfill_idx < terminal_idx < replay_idx
assert "const seenTextKeys=[];" in complete
assert "_anchorSceneRowTextOverlapsExisting(textKey,seenTextKeys)" in complete
@ -990,7 +992,8 @@ def test_transparent_stream_renders_persisted_anchor_scene_after_reload():
assert 'blocks.querySelectorAll(\'[data-anchor-settled-scene-row="1"],.transparent-event-row[data-anchor-scene-row="1"]\')' in transparent
# combined fix: the final answer text is computed and threaded into the row
# renderer so intermediate prose survives while the final-answer duplicate is dropped.
assert "_anchorSceneTransparentNodeForRow(row,{settled:true,finalAnswer})" in transparent
assert "const lastNonTerminalWorkRowIndex=_anchorSceneLastNonTerminalWorkRowIndex(rows);" in transparent
assert "liveTokenFinalPrefixEligible:idx>lastNonTerminalWorkRowIndex" in transparent
assert "finalAnswer" in transparent
assert "blocks.insertBefore(node,segment)" in transparent
assert "_syncTransparentEventControls(turn)" in transparent
@ -1233,6 +1236,7 @@ global._renderAnchorSceneRowsIntoWorklog=(group, rows)=>{{
}};
global._syncToolCallGroupSummary=()=>{{}};
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
eval(extractFunc('renderLiveAnchorActivityScene'));
eval(extractFunc('_transparentLiveRowKey'));