diff --git a/static/style.css b/static/style.css index c5d0e6e3..dd212efe 100644 --- a/static/style.css +++ b/static/style.css @@ -4109,6 +4109,15 @@ body.resizing .sidebar{transition:none!important;} font-size:11.5px; order:3; } +.transparent-event-time{ + flex-shrink:0; + order:4; + color:var(--muted); + font-variant-numeric:tabular-nums; + opacity:.55; + white-space:nowrap; + font-size:10px; +} .transparent-event-row .thinking-card-label{ display:inline; flex:0 0 auto; @@ -4133,7 +4142,7 @@ body.resizing .sidebar{transition:none!important;} } .transparent-event-status{ flex-shrink:0; - order:4; + order:5; color:var(--muted); opacity:.64; font-size:10px; @@ -4143,6 +4152,13 @@ body.resizing .sidebar{transition:none!important;} border-radius:6px; background:transparent; } +.transparent-event-row .thinking-card-btn-row{ + order:10; + flex-shrink:0; + display:inline-flex; + align-items:center; + gap:6px; +} /* Failed / running / interrupted must be legible — a failure in a long trace was previously invisible (only a muted .64 badge), a regression vs the old card's accent. (Trifecta finding V2.) */ diff --git a/static/ui.js b/static/ui.js index 3a42d2d7..b34c0f43 100644 --- a/static/ui.js +++ b/static/ui.js @@ -5568,6 +5568,108 @@ function _activityClockLabel(ts){ if(!Number.isFinite(stamp)||stamp<=0)return''; try{return new Date(stamp*1000).toLocaleTimeString([], {hour:'numeric',minute:'2-digit'});}catch(_){return'';} } +function _timestampSeconds(value){ + if(value===undefined||value===null||value==='') return null; + if(value instanceof Date){ + const stamp=value.getTime()/1000; + return Number.isFinite(stamp)&&stamp>0?stamp:null; + } + const numeric=Number(value); + if(Number.isFinite(numeric)&&numeric>0){ + const stamp=numeric>1e12?numeric/1000:numeric; + return Number.isFinite(stamp)&&stamp>0?stamp:null; + } + if(typeof value==='string'){ + const text=value.trim(); + if(!text||/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(text)) return null; + const parsed=Date.parse(text); + if(Number.isFinite(parsed)&&parsed>0) return parsed/1000; + } + return null; +} +function _firstValidTimestampSeconds(...values){ + for(const value of values){ + const stamp=_timestampSeconds(value); + if(stamp) return stamp; + } + return null; +} +function _transparentEventTimestampSeconds(row, opts){ + opts=opts||{}; + for(const key of ['ts','timestamp','created_at']){ + const stamp=_timestampSeconds(opts[key]); + if(stamp) return stamp; + } + const toolCall=opts.toolCall||row&&row._tcData||null; + if(toolCall&&typeof toolCall==='object'){ + for(const key of ['ts','timestamp','created_at','started_at','completed_at']){ + const stamp=_timestampSeconds(toolCall[key]); + if(stamp) return stamp; + } + } + if(row&&typeof row.getAttribute==='function'){ + for(const key of ['data-event-at','data-activity-at']){ + const stamp=_timestampSeconds(row.getAttribute(key)); + if(stamp) return stamp; + } + } + if(opts.live===true) return _activityNowSeconds(); + return null; +} +function _syncTransparentEventTimestamp(row, header, opts){ + if(!row||!header) return null; + opts=opts||{}; + const live=opts.live===true||row.getAttribute&&( + row.getAttribute('data-live-tid')==='1'|| + row.getAttribute('data-live-thinking')==='1'|| + row.getAttribute('data-live-assistant')==='1'|| + row.getAttribute('data-live-stream-owned')==='1' + ); + const explicitTs=_firstValidTimestampSeconds(opts.ts, opts.timestamp, opts.created_at); + const toolCall=opts.toolCall||row&&row._tcData||null; + const toolTs=toolCall&&typeof toolCall==='object' + ? _firstValidTimestampSeconds( + toolCall.ts, + toolCall.timestamp, + toolCall.created_at, + toolCall.started_at, + toolCall.completed_at + ) + : null; + const attrTs=row&&typeof row.getAttribute==='function' + ? _firstValidTimestampSeconds( + row.getAttribute('data-event-at'), + row.getAttribute('data-activity-at') + ) + : null; + const ts=explicitTs||toolTs||attrTs||(live?_activityNowSeconds():null); + const label=ts?_activityClockLabel(ts):''; + let timeEl=header.querySelector('.transparent-event-time'); + if(!label){ + if(timeEl) timeEl.remove(); + row.removeAttribute('data-event-at'); + row.removeAttribute('data-event-at-source'); + return null; + } + const source=explicitTs||toolTs||attrTs?'event':'live'; + if(!timeEl){ + timeEl=document.createElement('span'); + timeEl.className='transparent-event-time'; + } + timeEl.textContent=label; + timeEl.setAttribute('data-event-at',String(ts)); + timeEl.setAttribute('data-event-at-source',source); + row.setAttribute('data-event-at',String(ts)); + row.setAttribute('data-event-at-source',source); + const anchor=header.querySelector('.transparent-event-status,.thinking-card-btn-row,.tool-card-toggle,.thinking-card-toggle'); + if(timeEl.parentNode!==header){ + if(anchor&&anchor.parentNode===header) header.insertBefore(timeEl,anchor); + else header.appendChild(timeEl); + }else if(anchor&&timeEl.nextSibling!==anchor){ + header.insertBefore(timeEl,anchor); + } + return timeEl; +} function _activityStatusNode({kind='info',label='',detail='',status='done',ts=null,id=''}){ const row=document.createElement('div'); row.className=`agent-activity-status agent-activity-status-${kind} agent-activity-status-${status}`; @@ -10651,6 +10753,7 @@ function _decorateTransparentEventRow(row, opts){ else detail.appendChild(modes); detail.setAttribute('data-transparent-detail-mode','full'); } + if(typeof _syncTransparentEventTimestamp==='function') _syncTransparentEventTimestamp(row, header, {toolCall:tc, ts:opts.ts, live:opts.live===true}); _wireTransparentHeaderToggle(header); _attachCopyButton(header); } @@ -10691,6 +10794,7 @@ function _decorateTransparentEventRow(row, opts){ }else if(preview){ preview.remove(); } + if(typeof _syncTransparentEventTimestamp==='function') _syncTransparentEventTimestamp(row, header, {ts:opts.ts, live:opts.live===true}); _wireTransparentHeaderToggle(header); _attachCopyButton(header); } @@ -11277,6 +11381,22 @@ function _anchorSceneIsSettledSuccessfulCompression(row, settled){ function _anchorSceneToolCallFromRow(row, opts){ const tool=(row&&row.tool&&typeof row.tool==='object')?row.tool:{}; const payload=(row&&row.payload&&typeof row.payload==='object')?row.payload:{}; + const timestampSeconds=typeof _timestampSeconds==='function'?_timestampSeconds:function(value){ + const stamp=Number(value); + return Number.isFinite(stamp)&&stamp>0?(stamp>1e12?stamp/1000:stamp):null; + }; + const firstValidTimestampSeconds=typeof _firstValidTimestampSeconds==='function' + ? _firstValidTimestampSeconds + : function(...values){ + for(const value of values){ + const stamp=timestampSeconds(value); + if(stamp) return stamp; + } + return null; + }; + const rowTs=typeof _anchorSceneRowTimestampSeconds==='function' + ? _anchorSceneRowTimestampSeconds(row) + : firstValidTimestampSeconds(row&&row.created_at, row&&row.timestamp, row&&row.ts, row&&row.started_at, row&&row.completed_at); const id=tool.id||row.tool_call_id||payload.tid||payload.id||payload.tool_call_id||payload.tool_use_id||payload.call_id||''; const settled=!!(opts&&opts.settled); return { @@ -11291,11 +11411,34 @@ function _anchorSceneToolCallFromRow(row, opts){ done:settled?true:(tool.done!==null&&tool.done!==undefined?tool.done:(row.status!=='running'&&row.status!=='pending')), is_error:!!(tool.is_error||payload.is_error||row.status==='error'||row.status==='failed'), duration:tool.duration||payload.duration||payload.duration_seconds, - started_at:tool.started_at||payload.started_at, + started_at:firstValidTimestampSeconds(tool.started_at, payload.started_at, rowTs), + created_at:firstValidTimestampSeconds(tool.created_at, payload.created_at, rowTs), + timestamp:firstValidTimestampSeconds(tool.timestamp, payload.timestamp, rowTs), + ts:firstValidTimestampSeconds( + tool.ts, + payload.ts, + tool.timestamp, + payload.timestamp, + tool.created_at, + payload.created_at, + rowTs + ), tid:id, id, }; } +function _anchorSceneRowTimestampSeconds(row){ + if(!row) return null; + const timestampSeconds=typeof _timestampSeconds==='function'?_timestampSeconds:function(value){ + const stamp=Number(value); + return Number.isFinite(stamp)&&stamp>0?(stamp>1e12?stamp/1000:stamp):null; + }; + for(const key of ['created_at','timestamp','ts','started_at','completed_at']){ + const stamp=timestampSeconds(row[key]); + if(stamp) return stamp; + } + return null; +} function _anchorSceneNodeForRow(row, opts){ const settled=!!(opts&&opts.settled); if(!row) return null; @@ -11374,6 +11517,7 @@ function _anchorSceneTransparentNodeForRow(row, opts){ const live=!!(opts&&opts.live); if(!row) return null; let node=null; + const eventTs=typeof _anchorSceneRowTimestampSeconds==='function'?_anchorSceneRowTimestampSeconds(row):null; const meta={ segmentSeq:row.segment_seq||row.segmentSeq||'', burstId:row.activity_burst_id||row.burst_id||row.burstId||'', @@ -11403,7 +11547,9 @@ function _anchorSceneTransparentNodeForRow(row, opts){ type:'thinking', text, preview:text, + ts:eventTs, ...meta, + live, }); }else if(row.role==='tool'){ const toolCall=_anchorSceneToolCallFromRow(row,{settled}); @@ -11412,13 +11558,16 @@ function _anchorSceneTransparentNodeForRow(row, opts){ name:toolCall&&toolCall.name, status:_transparentToolStatus(toolCall,settled), toolCall, + ts:eventTs, ...meta, + live, }); }else{ node=_anchorSceneNodeForRow(row,{settled}); node=_decorateTransparentEventRow(node,{ type:String(row.role||'activity'), ...meta, + live, }); } if(!node) return null; @@ -11733,6 +11882,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){ const liveFooter=blocks.querySelector('#liveRunStatus'); const renderedRows=[]; for(const row of rows){ + const rowEventTs=typeof _anchorSceneRowTimestampSeconds==='function'?_anchorSceneRowTimestampSeconds(row):null; const node=_anchorSceneTransparentNodeForRow(row,{ live:true, settled:false, @@ -11743,7 +11893,9 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){ const key = _transparentLiveRowKey(node, activeStreamId); const existing = key ? preserveByKey.get(key) : null; const renderedNode = existing && _transparentLiveRowsCompatible(existing, node) - ? _refreshTransparentLiveRow(existing, node) + ? _refreshTransparentLiveRow(existing, node, { + preserveEventAt:!rowEventTs&&existing.getAttribute?existing.getAttribute('data-event-at'):null, + }) : node; if(existing) preserveByKey.delete(key); if(!renderedNode) continue; @@ -11866,7 +12018,19 @@ function _refreshTransparentThinkingLiveRow(existing, node){ const nodePreview = node.querySelector('.transparent-event-thinking-preview'); const previewText = nodePreview ? String(nodePreview.textContent || '') : nextText; if(typeof _decorateTransparentEventRow === 'function'){ - _decorateTransparentEventRow(existing,{type:'thinking',text:nextText,preview:previewText}); + const nodeStampSource = node.getAttribute ? String(node.getAttribute('data-event-at-source') || '') : ''; + const existingStamp = existing.getAttribute ? existing.getAttribute('data-event-at') : null; + const nodeStamp = node.getAttribute ? node.getAttribute('data-event-at') : null; + const nextStamp = nodeStampSource === 'event' + ? (nodeStamp || existingStamp) + : (existingStamp || nodeStamp); + _decorateTransparentEventRow(existing,{ + type:'thinking', + text:nextText, + preview:previewText, + ts:nextStamp||undefined, + live:true, + }); } return true; } @@ -11945,7 +12109,8 @@ function _refreshTransparentFadeProseRow(existing, node, preservedState){ return existing; } -function _refreshTransparentLiveRow(existing, node){ +function _refreshTransparentLiveRow(existing, node, opts){ + opts=opts||{}; if(!existing || !node || !existing.getAttribute) return node; if(existing===node) return existing; const preservedState = _transparentLiveRowInteractiveState(existing); @@ -11977,6 +12142,10 @@ function _refreshTransparentLiveRow(existing, node){ const htmlChanged = existing.innerHTML !== newHtml; if(htmlChanged) existing.innerHTML = newHtml; _rehydrateTransparentLiveRow(existing, node, preservedState); + if(opts.preserveEventAt){ + const header = existing.querySelector ? existing.querySelector('.tool-card-header,.thinking-card-header') : null; + if(header) _syncTransparentEventTimestamp(existing, header, {ts:opts.preserveEventAt, live:false}); + } return existing; } function _renderLiveAnchorActivitySceneForStream(streamId, sessionId, opts){ @@ -13607,6 +13776,9 @@ function _transparentStreamOrderedParts(message){ if(!message||message.role!=='assistant'||message._live||!Array.isArray(message.content)) return null; if(message._anchor_activity_scene) return null; const ordered=[]; + const messageTs=typeof _firstValidTimestampSeconds==='function' + ? _firstValidTimestampSeconds(message._ts, message.timestamp, message.created_at) + : (message._ts||message.timestamp||message.created_at); let hasText=false; let hasTool=false; for(const part of message.content){ @@ -13626,6 +13798,10 @@ function _transparentStreamOrderedParts(message){ toolUseId, name:part.name||'tool', input:(part.input&&typeof part.input==='object')?part.input:{}, + ts:part.ts, + timestamp:part.timestamp, + created_at:part.created_at, + message_ts:messageTs, }); hasTool=true; } @@ -13671,11 +13847,29 @@ function _collectToolResultSnippetsByTid(messages){ } return resultsByTid; } -function _transparentOrderedToolCall(part, rawIdx, toolCallsByTid, resultsByTid, persistedByTid){ +function _transparentOrderedToolCall(part, rawIdx, toolCallsByTid, resultsByTid, persistedByTid, messageTs){ const tid=String(part&&part.toolUseId||'').trim(); + const firstValidTimestampSeconds=typeof _firstValidTimestampSeconds==='function' + ? _firstValidTimestampSeconds + : function(...values){ + for(const value of values){ + const stamp=Number(value); + if(Number.isFinite(stamp)&&stamp>0) return stamp>1e12?stamp/1000:stamp; + } + return null; + }; + const messageStamp=firstValidTimestampSeconds(messageTs, part&&part.message_ts); + const partStamp=firstValidTimestampSeconds(part&&part.ts, part&&part.timestamp, part&&part.created_at); const liveTool=tid&&toolCallsByTid&&toolCallsByTid.get(tid); if(liveTool){ const next={...liveTool}; + const hasEventStamp=firstValidTimestampSeconds(next.ts, next.timestamp, next.created_at, next.started_at, next.completed_at); + const fallbackStamp=partStamp||messageStamp; + if(!hasEventStamp&&fallbackStamp){ + next.ts=fallbackStamp; + next.timestamp=fallbackStamp; + next.created_at=fallbackStamp; + } const liveSnip=(resultsByTid&&resultsByTid[tid])||(persistedByTid&&persistedByTid[tid])||''; if(liveSnip){ const patchSnippet=_cliPatchSnippetFromArgs(next.name||part.name||'tool', next.args||part.input||{}); @@ -13689,6 +13883,8 @@ function _transparentOrderedToolCall(part, rawIdx, toolCallsByTid, resultsByTid, const args=(part&&part.input&&typeof part.input==='object')?part.input:{}; const patchSnippet=_cliPatchSnippetFromArgs(name,args); const resultSnippet=(resultsByTid&&tid&&resultsByTid[tid])||(persistedByTid&&tid&&persistedByTid[tid])||''; + const fallbackStamp=partStamp||messageStamp; + const primaryStamp=firstValidTimestampSeconds(part&&part.ts, part&&part.timestamp, part&&part.created_at, fallbackStamp); return { name, tid, @@ -13698,6 +13894,9 @@ function _transparentOrderedToolCall(part, rawIdx, toolCallsByTid, resultsByTid, snippet:_cliToolCardSnippet(resultSnippet,patchSnippet), is_diff:_cliToolCardHasDiffSnippet(resultSnippet,patchSnippet), done:true, + ts:primaryStamp||undefined, + timestamp:primaryStamp||undefined, + created_at:primaryStamp||undefined, }; } function _assistantTurnAnchorSettledFinalAnswer(message, content, context){ @@ -14386,7 +14585,7 @@ function renderMessages(options){ orderedTransparentParts.forEach((part, partIdx)=>{ if(!part) return; if(part.kind==='tool'){ - const toolCall=_transparentOrderedToolCall(part, rawIdx, transparentOrderedToolCallsByTid, transparentToolResultsByTid, transparentPersistedSnippetByTid); + const toolCall=_transparentOrderedToolCall(part, rawIdx, transparentOrderedToolCallsByTid, transparentToolResultsByTid, transparentPersistedSnippetByTid); const toolRow=_decorateTransparentEventRow(buildToolCard(toolCall),{ type:'tool', name:toolCall&&toolCall.name, @@ -14926,11 +15125,13 @@ function renderMessages(options){ // position). Keyed by the assistant turn element. (Trifecta finding O-Bug1.) const transparentSeenThinking=new Map(); for(const entry of activityOrder){ + const {aIdx,segmentSeq,burstId,cards,thinkingIdx,includeAnchorReason}=entry; + const sourceMsg=aIdx>=0?S.messages[aIdx]:null; const event={ ...entry, - thinkingText:entry.thinkingIdx!==null?assistantThinking.get(entry.thinkingIdx):'', + ts:sourceMsg&&((sourceMsg._ts!==undefined&&sourceMsg._ts!==null)?sourceMsg._ts:sourceMsg.timestamp), + thinkingText:thinkingIdx!==null?assistantThinking.get(thinkingIdx):'', }; - const {aIdx,segmentSeq,burstId,cards}=event; if(aIdx 0 && i < src.length){ + const ch = src[i]; + if(ch === '{') depth += 1; + else if(ch === '}') depth -= 1; + i += 1; + } + return src.slice(start, i); +} +class FakeElement { + constructor(tag = 'div'){ + this.tagName = String(tag).toUpperCase(); + this.children = []; + this.parentNode = null; + this.attributes = Object.create(null); + this.dataset = Object.create(null); + this.style = Object.create(null); + this.hidden = false; + this.id = ''; + this._textContent = ''; + this._innerHTML = ''; + this._classes = new Set(); + const self = this; + this.classList = { + add(...names){ names.forEach(name => self._classes.add(name)); }, + remove(...names){ names.forEach(name => self._classes.delete(name)); }, + contains(name){ return self._classes.has(name); }, + toggle(name, force){ + if(force === true){ self._classes.add(name); return true; } + if(force === false){ self._classes.delete(name); return false; } + if(self._classes.has(name)){ self._classes.delete(name); return false; } + self._classes.add(name); + return true; + }, + }; + } + get parentElement(){ return this.parentNode; } + get firstChild(){ return this.children[0] || null; } + get nextSibling(){ + if(!this.parentNode) return null; + const siblings = this.parentNode.children; + const idx = siblings.indexOf(this); + return idx >= 0 ? (siblings[idx + 1] || null) : null; + } + get className(){ return Array.from(this._classes).join(' '); } + set className(value){ + this._classes = new Set(String(value).trim().split(/\s+/).filter(Boolean)); + } + get textContent(){ return this._textContent; } + set textContent(value){ + this._textContent = String(value ?? ''); + this._innerHTML = this._textContent; + this.children = []; + } + get innerHTML(){ return this._innerHTML; } + set innerHTML(value){ + this._innerHTML = String(value ?? ''); + this._textContent = this._innerHTML; + this.children = []; + } + setAttribute(name, value){ + const key = String(name); + const val = String(value); + this.attributes[key] = val; + if(key === 'id') this.id = val; + if(key.startsWith('data-')){ + const dataKey = key.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + this.dataset[dataKey] = val; + } + if(key === 'class') this.className = val; + } + getAttribute(name){ + return Object.prototype.hasOwnProperty.call(this.attributes, name) ? this.attributes[name] : null; + } + getAttributeNames(){ + return Object.keys(this.attributes); + } + removeAttribute(name){ + delete this.attributes[name]; + if(name === 'id') this.id = ''; + if(name.startsWith('data-')){ + const dataKey = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + delete this.dataset[dataKey]; + } + if(name === 'class') this._classes = new Set(); + } + appendChild(child){ + if(child && child.parentNode){ + child.remove(); + } + if(!child) return null; + child.parentNode = this; + this.children.push(child); + return child; + } + insertBefore(child, refNode){ + if(child && child.parentNode){ + child.remove(); + } + if(!child) return null; + const idx = this.children.indexOf(refNode); + child.parentNode = this; + if(idx < 0) this.children.push(child); + else this.children.splice(idx, 0, child); + return child; + } + replaceWith(...nodes){ + if(!this.parentNode) return; + const parent = this.parentNode; + const siblings = parent.children; + const idx = siblings.indexOf(this); + if(idx < 0) return; + siblings.splice(idx, 1); + this.parentNode = null; + let insertAt = idx; + for(const node of nodes){ + if(!node) continue; + if(node.parentNode) node.remove(); + node.parentNode = parent; + siblings.splice(insertAt, 0, node); + insertAt += 1; + } + } + remove(){ + if(!this.parentNode) return; + const siblings = this.parentNode.children; + const idx = siblings.indexOf(this); + if(idx >= 0) siblings.splice(idx, 1); + this.parentNode = null; + } + matches(selector){ + return matchesSelector(this, selector); + } + querySelector(selector){ + return this.querySelectorAll(selector)[0] || null; + } + querySelectorAll(selector){ + const out = []; + const walk = (node) => { + for(const child of node.children){ + if(matchesSelector(child, selector)) out.push(child); + walk(child); + } + }; + walk(this); + return out; + } + closest(selector){ + let node = this; + while(node){ + if(matchesSelector(node, selector)) return node; + node = node.parentNode; + } + return null; + } +} +function matchesSelector(el, selector){ + if(!selector) return false; + return selector.split(',').map(part => part.trim()).filter(Boolean).some(part => matchesSimple(el, part)); +} +function matchesSimple(el, selector){ + selector = selector.trim(); + if(!selector || selector.includes(' ') || selector.includes('>')) return false; + const idMatch = selector.match(/#([A-Za-z0-9_-]+)/); + if(idMatch && el.id !== idMatch[1]) return false; + const clsMatches = selector.match(/\.([A-Za-z0-9_-]+)/g) || []; + for(const cls of clsMatches){ + if(!el.classList.contains(cls.slice(1))) return false; + } + const attrMatches = selector.match(/\[([^=\]]+)(?:="([^"]*)")?\]/g) || []; + for(const attrMatch of attrMatches){ + const parsed = attrMatch.match(/\[([^=\]]+)(?:="([^"]*)")?\]/); + const name = parsed[1]; + const expected = parsed[2]; + const value = el.getAttribute(name); + if(value === null) return false; + if(expected !== undefined && String(value) !== String(expected)) return false; + } + const stripped = selector.replace(/#[A-Za-z0-9_-]+/g, '').replace(/\.[A-Za-z0-9_-]+/g, '').replace(/\[[^\]]+\]/g, '').trim(); + if(stripped && stripped !== '*' && stripped.toLowerCase() !== String(el.tagName || '').toLowerCase()) return false; + return !!(idMatch || clsMatches.length || attrMatches.length || stripped); +} +global.window = {}; +global.document = { + createElement:(tag)=>new FakeElement(tag), + createTextNode:(text)=>{ + const node = new FakeElement('#text'); + node.textContent = text; + return node; + }, + createDocumentFragment:()=>new FakeElement('#fragment'), +}; +global.CSS = { escape:(value)=>String(value) }; +global.requestAnimationFrame = (fn)=>fn(); +global._toolShortName = (name)=>String(name || ''); +global._transparentToolSummary = ()=>'summary'; +global._transparentEventPreview = (text)=>String(text || ''); +global._wireTransparentHeaderToggle = () => {}; +global._attachCopyButton = () => {}; +global._attachProgressBar = () => {}; +global._transparentToolStatus = () => 'Running'; +global._activityNowSeconds = () => 1700000000; +global._syncToolCallGroupSummary = () => {}; +global._syncTransparentEventControls = () => {}; +global._moveLiveRunStatusToTurnEnd = () => {}; +global._removeEmptyLiveWorklogShells = () => {}; +global.scrollIfPinned = () => {}; +global.isLiveAnchorActivitySceneOwner = () => false; +global._findLiveAssistantAnchorForSegment = () => null; +global._findLatestVisibleLiveAssistantByBurst = () => null; +global._findLatestVisibleLiveAssistant = () => null; +global._toolWorklogListEl = () => null; +global._activityKeyForLiveTurn = () => 'activity-key'; +global._assistantTurnBlocks = () => null; +eval(extractFunc('_activityClockLabel')); +eval(extractFunc('_timestampSeconds')); +eval(extractFunc('_firstValidTimestampSeconds')); +eval(extractFunc('_transparentEventTimestampSeconds')); +eval(extractFunc('_syncTransparentEventTimestamp')); +eval(extractFunc('_decorateTransparentEventRow')); +eval(extractFunc('_transparentStreamOrderedParts')); +eval(extractFunc('_transparentOrderedToolCall')); +eval(extractFunc('_transparentLiveRowAttributePairs')); +eval(extractFunc('_transparentLiveRowInteractiveState')); +eval(extractFunc('_rehydrateTransparentLiveRow')); +eval(extractFunc('_refreshTransparentThinkingLiveRow')); +eval(extractFunc('_refreshTransparentLiveRow')); +eval(extractFunc('_anchorSceneRowTimestampSeconds')); +eval(extractFunc('_anchorSceneToolCallFromRow')); +eval(extractFunc('_anchorSceneTransparentNodeForRow')); +eval(extractFunc('appendLiveToolCard')); +function makeToolRow(ts = null, live = false){ + const row = new FakeElement('div'); + row.className = 'tool-card-row'; + if(live) row.setAttribute('data-live-tid', '1'); + row._tcData = { name: 'search' }; + if(ts !== null && ts !== undefined) row._tcData.ts = ts; + const card = new FakeElement('div'); + card.className = 'tool-card'; + const header = new FakeElement('div'); + header.className = 'tool-card-header'; + const icon = new FakeElement('span'); + icon.className = 'tool-card-icon'; + const name = new FakeElement('span'); + name.className = 'tool-card-name'; + name.textContent = 'search'; + const preview = new FakeElement('span'); + preview.className = 'tool-card-preview'; + preview.textContent = 'preview'; + const status = new FakeElement('span'); + status.className = 'transparent-event-status'; + status.textContent = 'Running'; + const toggle = new FakeElement('span'); + toggle.className = 'tool-card-toggle'; + toggle.textContent = '>'; + const detail = new FakeElement('div'); + detail.className = 'tool-card-detail'; + header.appendChild(icon); + header.appendChild(name); + header.appendChild(preview); + header.appendChild(status); + header.appendChild(toggle); + card.appendChild(header); + card.appendChild(detail); + row.appendChild(card); + return row; +} +function makeThinkingRow(live = false){ + const row = new FakeElement('div'); + row.className = 'agent-activity-thinking'; + if(live){ + row.setAttribute('data-live-thinking', '1'); + row.setAttribute('data-live-thinking-key', 'turn'); + } + const card = new FakeElement('div'); + card.className = 'thinking-card'; + const header = new FakeElement('div'); + header.className = 'thinking-card-header'; + const icon = new FakeElement('span'); + icon.className = 'thinking-card-icon'; + const label = new FakeElement('span'); + label.className = 'thinking-card-label'; + label.textContent = 'Thinking'; + const preview = new FakeElement('span'); + preview.className = 'transparent-event-preview transparent-event-thinking-preview'; + preview.textContent = 'quiet trace'; + const btnRow = new FakeElement('span'); + btnRow.className = 'thinking-card-btn-row'; + const copy = new FakeElement('span'); + copy.className = 'transparent-event-copy'; + const toggle = new FakeElement('span'); + toggle.className = 'thinking-card-toggle'; + btnRow.appendChild(copy); + btnRow.appendChild(toggle); + header.appendChild(icon); + header.appendChild(label); + header.appendChild(preview); + header.appendChild(btnRow); + card.appendChild(header); + row.appendChild(card); + return row; +} +""" + + +def test_settled_tool_row_with_timestamp_sets_one_timestamp_node_and_updates_in_place(): + data = _run_node( + """ +const ts1 = 1700000000; +const ts2 = 1700000300; +const row = makeToolRow(null, false); +_decorateTransparentEventRow(row, { type: 'tool', toolCall: row._tcData, ts: ts1, status: 'Running' }); +const header = row.querySelector('.tool-card-header'); +const timeBefore = header.querySelector('.transparent-event-time'); +_decorateTransparentEventRow(row, { type: 'tool', toolCall: row._tcData, ts: ts2, status: 'Completed' }); +const timeAfter = header.querySelector('.transparent-event-time'); +const statusNode = header.querySelector('.transparent-event-status'); +const toggleNode = header.querySelector('.tool-card-toggle'); +process.stdout.write(JSON.stringify({ + count: header.querySelectorAll('.transparent-event-time').length, + timeBeforeSameAsAfter: timeBefore === timeAfter, + timeText: timeAfter && timeAfter.textContent, + rowEventAt: row.getAttribute('data-event-at'), + timeEventAt: timeAfter && timeAfter.getAttribute('data-event-at'), + timeBeforeIndex: header.children.indexOf(timeAfter), + statusIndex: header.children.indexOf(statusNode), + toggleIndex: header.children.indexOf(toggleNode), +})); +""" + ) + + assert data["count"] == 1 + assert data["timeBeforeSameAsAfter"] is True + assert data["timeText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000300)}));")["_clock"] + assert data["rowEventAt"] == "1700000300" + assert data["timeEventAt"] == "1700000300" + assert data["timeBeforeIndex"] < data["statusIndex"] < data["toggleIndex"] + + +def test_live_anchor_scene_thinking_row_uses_live_timestamp_before_stream_owner_flag(): + data = _run_node( + """ +const liveStamp = 1700000777; +global._activityNowSeconds = () => liveStamp; +global._thinkingActivityNode = (text) => makeThinkingRow(false); +const node = _anchorSceneTransparentNodeForRow({ role: 'thinking', row_id: 'live-think', text: 'quiet trace' }, { + live: true, + settled: false, + streamId: 'stream-1', + sessionId: 'session-1', +}); +const header = node && node.querySelector('.thinking-card-header'); +const timeNode = header && header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + hasNode: !!node, + timeText: timeNode && timeNode.textContent, + rowEventAt: node && node.getAttribute('data-event-at'), + liveOwned: node && node.getAttribute('data-live-stream-owned'), +})); +""" + ) + + assert data["hasNode"] is True + assert data["timeText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000777)}));")["_clock"] + assert data["rowEventAt"] == "1700000777" + assert data["liveOwned"] == "1" + + +def test_anchor_scene_row_timestamp_seconds_accepts_seconds_milliseconds_and_iso_strings(): + data = _run_node( + """ +const iso = '2026-06-11T00:00:01Z'; +const isoStamp = Date.parse(iso) / 1000; +process.stdout.write(JSON.stringify({ + seconds: _anchorSceneRowTimestampSeconds({ created_at: 1700000901 }), + millis: _anchorSceneRowTimestampSeconds({ created_at: 1700000901000 }), + iso: _anchorSceneRowTimestampSeconds({ created_at: iso }), + invalid: _anchorSceneRowTimestampSeconds({ created_at: 'not-a-date' }), + zero: _anchorSceneRowTimestampSeconds({ created_at: 0 }), + negative: _anchorSceneRowTimestampSeconds({ created_at: -1 }), + nil: _anchorSceneRowTimestampSeconds({}), + isoStamp, +})); +""" + ) + + assert data["seconds"] == 1700000901 + assert data["millis"] == 1700000901 + assert data["iso"] == data["isoStamp"] + assert data["invalid"] is None + assert data["zero"] is None + assert data["negative"] is None + assert data["nil"] is None + + +def test_settled_anchor_scene_rows_use_row_created_at_timestamps(): + data = _run_node( + """ +const toolTs = 1700000901; +const thinkTs = 1700000902; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + const tid = tc && (tc.tid || tc.id || tc.tool_call_id || tc.tool_use_id || tc.call_id || ''); + if(tid) row.setAttribute('data-live-tid', tid); + return row; +}; +global._thinkingActivityNode = (text) => makeThinkingRow(false); +const toolNode = _anchorSceneTransparentNodeForRow({ + role: 'tool', + row_id: 'scene-tool', + created_at: toolTs, + tool: { name: 'lookup' }, +}, { settled: true, finalAnswer: '' }); +const toolHeader = toolNode && toolNode.querySelector('.tool-card-header'); +const toolTime = toolHeader && toolHeader.querySelector('.transparent-event-time'); +const thinkNode = _anchorSceneTransparentNodeForRow({ + role: 'thinking', + row_id: 'scene-think', + created_at: thinkTs, + text: 'quiet trace', +}, { settled: true, finalAnswer: '' }); +const thinkHeader = thinkNode && thinkNode.querySelector('.thinking-card-header'); +const thinkTime = thinkHeader && thinkHeader.querySelector('.transparent-event-time'); +const missingNode = _anchorSceneTransparentNodeForRow({ + role: 'thinking', + row_id: 'scene-missing', + text: 'quiet trace', +}, { settled: true, finalAnswer: '' }); +const missingHeader = missingNode && missingNode.querySelector('.thinking-card-header'); +process.stdout.write(JSON.stringify({ + toolCount: toolHeader && toolHeader.querySelectorAll('.transparent-event-time').length, + toolText: toolTime && toolTime.textContent, + toolEventAt: toolNode && toolNode.getAttribute('data-event-at'), + thinkCount: thinkHeader && thinkHeader.querySelectorAll('.transparent-event-time').length, + thinkText: thinkTime && thinkTime.textContent, + thinkEventAt: thinkNode && thinkNode.getAttribute('data-event-at'), + missingCount: missingHeader && missingHeader.querySelectorAll('.transparent-event-time').length, + missingEventAt: missingNode && missingNode.getAttribute('data-event-at'), +})); +""" + ) + + assert data["toolCount"] == 1 + assert data["toolText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000901)}));")["_clock"] + assert data["toolEventAt"] == "1700000901" + assert data["thinkCount"] == 1 + assert data["thinkText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000902)}));")["_clock"] + assert data["thinkEventAt"] == "1700000902" + assert data["missingCount"] == 0 + assert data["missingEventAt"] is None + + +def test_settled_anchor_scene_rows_use_iso_created_at_timestamps(): + data = _run_node( + """ +const toolIso = '2026-06-11T00:00:01Z'; +const thinkIso = '2026-06-11T00:00:02Z'; +const toolStamp = Date.parse(toolIso) / 1000; +const thinkStamp = Date.parse(thinkIso) / 1000; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + const tid = tc && (tc.tid || tc.id || tc.tool_call_id || tc.tool_use_id || tc.call_id || ''); + if(tid) row.setAttribute('data-live-tid', tid); + return row; +}; +global._thinkingActivityNode = (text) => makeThinkingRow(false); +const toolNode = _anchorSceneTransparentNodeForRow({ + role: 'tool', + row_id: 'scene-tool', + created_at: toolIso, + tool: { name: 'lookup' }, +}, { settled: true, finalAnswer: '' }); +const toolHeader = toolNode && toolNode.querySelector('.tool-card-header'); +const toolTime = toolHeader && toolHeader.querySelector('.transparent-event-time'); +const thinkNode = _anchorSceneTransparentNodeForRow({ + role: 'thinking', + row_id: 'scene-think', + created_at: thinkIso, + text: 'quiet trace', +}, { settled: true, finalAnswer: '' }); +const thinkHeader = thinkNode && thinkNode.querySelector('.thinking-card-header'); +const thinkTime = thinkHeader && thinkHeader.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + toolCount: toolHeader && toolHeader.querySelectorAll('.transparent-event-time').length, + toolText: toolTime && toolTime.textContent, + toolEventAt: toolNode && toolNode.getAttribute('data-event-at'), + toolStamp, + thinkCount: thinkHeader && thinkHeader.querySelectorAll('.transparent-event-time').length, + thinkText: thinkTime && thinkTime.textContent, + thinkEventAt: thinkNode && thinkNode.getAttribute('data-event-at'), + thinkStamp, +})); +""" + ) + + assert data["toolCount"] == 1 + assert data["toolText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(Date.parse('2026-06-11T00:00:01Z')/1000)}));")["_clock"] + assert data["toolEventAt"] == str(int(data["toolStamp"])) + assert data["thinkCount"] == 1 + assert data["thinkText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(Date.parse('2026-06-11T00:00:02Z')/1000)}));")["_clock"] + assert data["thinkEventAt"] == str(int(data["thinkStamp"])) + + +def test_settled_thinking_row_without_timestamp_omits_timestamp_node(): + data = _run_node( + """ +const row = makeThinkingRow(); +_decorateTransparentEventRow(row, { type: 'thinking', text: 'quiet trace', preview: 'quiet trace' }); +const header = row.querySelector('.thinking-card-header'); +process.stdout.write(JSON.stringify({ + count: header.querySelectorAll('.transparent-event-time').length, + rowEventAt: row.getAttribute('data-event-at'), + hasPreview: !!header.querySelector('.transparent-event-thinking-preview'), +})); +""" + ) + + assert data["count"] == 0 + assert data["rowEventAt"] is None + assert data["hasPreview"] is True + + +def test_settled_thinking_row_with_timestamp_sets_one_timestamp_node(): + data = _run_node( + """ +const ts = 1700000066; +const row = makeThinkingRow(); +_decorateTransparentEventRow(row, { type: 'thinking', text: 'quiet trace', preview: 'quiet trace', ts }); +const header = row.querySelector('.thinking-card-header'); +const timeNode = header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + count: header.querySelectorAll('.transparent-event-time').length, + timeText: timeNode && timeNode.textContent, + rowEventAt: row.getAttribute('data-event-at'), + timeEventAt: timeNode && timeNode.getAttribute('data-event-at'), +})); +""" + ) + + assert data["count"] == 1 + assert data["timeText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000066)}));")["_clock"] + assert data["rowEventAt"] == "1700000066" + assert data["timeEventAt"] == "1700000066" + + +def test_live_tool_row_without_timestamp_uses_observed_live_time(): + data = _run_node( + """ +const liveStamp = 1700000123; +global._activityNowSeconds = () => liveStamp; +const row = makeToolRow(null, true); +_decorateTransparentEventRow(row, { type: 'tool', toolCall: row._tcData, status: 'Running', live: true }); +const header = row.querySelector('.tool-card-header'); +const timeBefore = header.querySelector('.transparent-event-time'); +const expected = _activityClockLabel(liveStamp); +global._activityNowSeconds = () => 1700000999; +_decorateTransparentEventRow(row, { type: 'tool', toolCall: row._tcData, status: 'Running', live: true }); +const timeAfter = header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + count: header.querySelectorAll('.transparent-event-time').length, + timeSameNode: timeBefore === timeAfter, + timeText: timeAfter && timeAfter.textContent, + expected, + rowEventAt: row.getAttribute('data-event-at'), + timeEventAt: timeAfter && timeAfter.getAttribute('data-event-at'), +})); +""" + ) + + assert data["count"] == 1 + assert data["timeSameNode"] is True + assert data["timeText"] == data["expected"] + assert data["rowEventAt"] == "1700000123" + assert data["timeEventAt"] == "1700000123" + + +def test_live_tool_row_replacement_preserves_original_fallback_timestamp(): + data = _run_node( + """ +const firstLiveStamp = 1700000123; +const replacementLiveStamp = 1700000999; +global._activityNowSeconds = () => firstLiveStamp; +global.isTransparentStream = () => true; +global.S = { session: { session_id: 'session-1' }, activeStreamId: 'stream-1' }; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + const tid = tc && (tc.tid || tc.id || tc.tool_call_id || tc.tool_use_id || tc.call_id || ''); + if(tid) row.setAttribute('data-live-tid', tid); + return row; +}; +const turn = new FakeElement('div'); +turn.id = 'liveAssistantTurn'; +const inner = new FakeElement('div'); +turn.appendChild(inner); +global.$ = (id) => id === 'liveAssistantTurn' ? turn : null; +global._assistantTurnBlocks = () => inner; +global._createAssistantTurn = () => turn; +global._syncTransparentEventControls = () => {}; +global._toolWorklogListEl = () => null; +appendLiveToolCard({ name: 'lookup', tid: 'tool-1', done: false }); +const initialRow = inner.querySelector('.transparent-event-row[data-live-tid="tool-1"]'); +const initialTime = initialRow && initialRow.querySelector('.transparent-event-time'); +global._activityNowSeconds = () => replacementLiveStamp; +appendLiveToolCard({ name: 'lookup', tid: 'tool-1', done: true }); +const replacementRow = inner.querySelector('.transparent-event-row[data-live-tid="tool-1"]'); +const replacementTime = replacementRow && replacementRow.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + rowCount: inner.querySelectorAll('.transparent-event-row').length, + initialEventAt: initialRow && initialRow.getAttribute('data-event-at'), + replacementEventAt: replacementRow && replacementRow.getAttribute('data-event-at'), + initialTimeText: initialTime && initialTime.textContent, + replacementTimeText: replacementTime && replacementTime.textContent, + sameText: initialTime && replacementTime && initialTime.textContent === replacementTime.textContent, +})); +""" + ) + + expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000123)}));")["_clock"] + assert data["rowCount"] == 1 + assert data["initialEventAt"] == "1700000123" + assert data["replacementEventAt"] == "1700000123" + assert data["initialTimeText"] == expected + assert data["replacementTimeText"] == expected + assert data["sameText"] is True + + +def test_ordered_tool_use_row_without_s_toolcalls_uses_assistant_timestamp(): + data = _run_node( + """ +const messageStamp = 1700000801; +const partStamp = 1700000802; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + return row; +}; +global._toolArgsSnapshot = (args) => args; +global._cliPatchSnippetFromArgs = () => ''; +global._cliToolCardSnippet = () => ''; +global._cliToolCardHasDiffSnippet = () => false; +const ordered = _transparentStreamOrderedParts({ + role: 'assistant', + content: [ + { type: 'text', text: 'working' }, + { type: 'tool_use', id: 'tool-a', name: 'lookup', input: { q: 'x' } }, + { type: 'tool_use', id: 'tool-b', name: 'lookup', input: { q: 'y' }, ts: partStamp }, + { type: 'tool_use', id: 'tool-c', name: 'lookup', input: { q: 'z' } }, + ], +}); +const normalizedWithMessageTs = ordered.find(part => part.toolUseId === 'tool-a'); +const normalizedWithPartTs = ordered.find(part => part.toolUseId === 'tool-b'); +const normalizedWithoutTs = ordered.find(part => part.toolUseId === 'tool-c'); +const fromMessage = _transparentOrderedToolCall(normalizedWithMessageTs, 7, new Map(), {}, {}, messageStamp); +const messageRow = _decorateTransparentEventRow(buildToolCard(fromMessage), { + type: 'tool', + toolCall: fromMessage, + status: 'Completed', +}); +const messageHeader = messageRow.querySelector('.tool-card-header'); +const messageTime = messageHeader && messageHeader.querySelector('.transparent-event-time'); +const fromPart = _transparentOrderedToolCall(normalizedWithPartTs, 8, new Map(), {}, {}, messageStamp); +const partRow = _decorateTransparentEventRow(buildToolCard(fromPart), { + type: 'tool', + toolCall: fromPart, + status: 'Completed', +}); +const partHeader = partRow.querySelector('.tool-card-header'); +const partTime = partHeader && partHeader.querySelector('.transparent-event-time'); +const omitted = _transparentOrderedToolCall(normalizedWithoutTs, 9, new Map(), {}, {}, null); +const omittedRow = _decorateTransparentEventRow(buildToolCard(omitted), { + type: 'tool', + toolCall: omitted, + status: 'Completed', +}); +const omittedHeader = omittedRow.querySelector('.tool-card-header'); +process.stdout.write(JSON.stringify({ + messageCount: messageHeader && messageHeader.querySelectorAll('.transparent-event-time').length, + messageEventAt: messageRow && messageRow.getAttribute('data-event-at'), + messageText: messageTime && messageTime.textContent, + partCount: partHeader && partHeader.querySelectorAll('.transparent-event-time').length, + partEventAt: partRow && partRow.getAttribute('data-event-at'), + partText: partTime && partTime.textContent, + omittedCount: omittedHeader && omittedHeader.querySelectorAll('.transparent-event-time').length, + omittedEventAt: omittedRow && omittedRow.getAttribute('data-event-at'), +})); +""" + ) + + message_expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000801)}));")["_clock"] + part_expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000802)}));")["_clock"] + assert data["messageCount"] == 1 + assert data["messageEventAt"] == "1700000801" + assert data["messageText"] == message_expected + assert data["partCount"] == 1 + assert data["partEventAt"] == "1700000802" + assert data["partText"] == part_expected + assert data["omittedCount"] == 0 + assert data["omittedEventAt"] is None + + +def test_ordered_tool_use_row_prefers_later_valid_timestamp_field(): + data = _run_node( + """ +const ordered = _transparentStreamOrderedParts({ + role: 'assistant', + content: [ + { type: 'text', text: 'working' }, + { type: 'tool_use', id: 'tool-a', name: 'lookup', input: { q: 'x' }, ts: 'bad', timestamp: 1700000802 }, + { type: 'tool_use', id: 'tool-b', name: 'lookup', input: { q: 'y' }, ts: 'bad', timestamp: 'still-bad', created_at: 1700000803 }, + ], +}); +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + return row; +}; +global._toolArgsSnapshot = (args) => args; +global._cliPatchSnippetFromArgs = () => ''; +global._cliToolCardSnippet = () => ''; +global._cliToolCardHasDiffSnippet = () => false; +const partA = ordered.find(part => part.toolUseId === 'tool-a'); +const partB = ordered.find(part => part.toolUseId === 'tool-b'); +const callA = _transparentOrderedToolCall(partA, 7, new Map(), {}, {}, 1700000801); +const callB = _transparentOrderedToolCall(partB, 8, new Map(), {}, {}, 1700000801); +const rowA = _decorateTransparentEventRow(buildToolCard(callA), { type: 'tool', toolCall: callA, status: 'Completed' }); +const rowB = _decorateTransparentEventRow(buildToolCard(callB), { type: 'tool', toolCall: callB, status: 'Completed' }); +const headerA = rowA.querySelector('.tool-card-header'); +const headerB = rowB.querySelector('.tool-card-header'); +const timeA = headerA && headerA.querySelector('.transparent-event-time'); +const timeB = headerB && headerB.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + eventAtA: rowA.getAttribute('data-event-at'), + timeTextA: timeA && timeA.textContent, + eventAtB: rowB.getAttribute('data-event-at'), + timeTextB: timeB && timeB.textContent, + countA: headerA && headerA.querySelectorAll('.transparent-event-time').length, + countB: headerB && headerB.querySelectorAll('.transparent-event-time').length, +})); +""" + ) + + assert data["eventAtA"] == "1700000802" + assert data["timeTextA"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000802)}));")["_clock"] + assert data["eventAtB"] == "1700000803" + assert data["timeTextB"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000803)}));")["_clock"] + assert data["countA"] == 1 + assert data["countB"] == 1 + + +def test_settled_anchor_scene_tool_row_prefers_valid_payload_timestamp_over_invalid_tool_timestamp(): + data = _run_node( + """ +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + return row; +}; +const row = { + role: 'tool', + row_id: 'anchor-tool', + tool: { name: 'lookup', ts: 'bad' }, + payload: { name: 'lookup', timestamp: 1700000904, created_at: 1700000904 }, +}; +const toolCall = _anchorSceneToolCallFromRow(row, { settled: true }); +const node = _anchorSceneTransparentNodeForRow(row, { settled: true, finalAnswer: '' }); +const header = node && node.querySelector('.tool-card-header'); +const timeNode = header && header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + toolTs: toolCall.ts, + toolTimestamp: toolCall.timestamp, + toolCreatedAt: toolCall.created_at, + eventAt: node && node.getAttribute('data-event-at'), + timeText: timeNode && timeNode.textContent, + count: header && header.querySelectorAll('.transparent-event-time').length, +})); +""" + ) + + expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000904)}));")["_clock"] + assert data["toolTs"] == 1700000904 + assert data["toolTimestamp"] == 1700000904 + assert data["toolCreatedAt"] == 1700000904 + assert data["eventAt"] == "1700000904" + assert data["timeText"] == expected + assert data["count"] == 1 + + +def test_live_anchor_scene_refresh_preserves_existing_fallback_timestamp(): + data = _run_node( + """ +const firstLiveStamp = 1700000100; +const refreshLiveStamp = 1700000999; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + return row; +}; +global._activityNowSeconds = () => firstLiveStamp; +const existing = _anchorSceneTransparentNodeForRow({ + role: 'tool', + row_id: 'scene-live-tool', + tool: { name: 'lookup' }, +}, { live: true, settled: false, streamId: 'stream-1', sessionId: 'session-1' }); +const existingHeader = existing.querySelector('.tool-card-header'); +const before = existingHeader && existingHeader.querySelector('.transparent-event-time'); +global._activityNowSeconds = () => refreshLiveStamp; +const candidate = _anchorSceneTransparentNodeForRow({ + role: 'tool', + row_id: 'scene-live-tool', + tool: { name: 'lookup' }, +}, { live: true, settled: false, streamId: 'stream-1', sessionId: 'session-1' }); +const refreshed = _refreshTransparentLiveRow(existing, candidate, { + preserveEventAt: existing.getAttribute('data-event-at'), +}); +const refreshedHeader = refreshed.querySelector('.tool-card-header'); +const after = refreshedHeader && refreshedHeader.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + beforeText: before && before.textContent, + afterText: after && after.textContent, + beforeEventAt: existing.getAttribute('data-event-at'), + afterEventAt: refreshed.getAttribute('data-event-at'), + count: refreshedHeader && refreshedHeader.querySelectorAll('.transparent-event-time').length, +})); +""" + ) + + expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000100)}));")["_clock"] + assert data["beforeText"] == expected + assert data["afterText"] == expected + assert data["beforeEventAt"] == "1700000100" + assert data["afterEventAt"] == "1700000100" + assert data["count"] == 1 + + +def test_live_anchor_scene_thinking_refresh_preserves_existing_fallback_timestamp(): + data = _run_node( + """ +const firstLiveStamp = 1700000111; +const refreshLiveStamp = 1700000998; +global._activityNowSeconds = () => firstLiveStamp; +global._thinkingActivityNode = (text) => makeThinkingRow(true); +const existing = _anchorSceneTransparentNodeForRow({ + role: 'thinking', + row_id: 'scene-live-thinking', + text: 'quiet trace', +}, { live: true, settled: false, streamId: 'stream-1', sessionId: 'session-1' }); +const existingHeader = existing.querySelector('.thinking-card-header'); +const before = existingHeader && existingHeader.querySelector('.transparent-event-time'); +global._activityNowSeconds = () => refreshLiveStamp; +const candidate = _anchorSceneTransparentNodeForRow({ + role: 'thinking', + row_id: 'scene-live-thinking', + text: 'quiet trace', +}, { live: true, settled: false, streamId: 'stream-1', sessionId: 'session-1' }); +const refreshed = _refreshTransparentLiveRow(existing, candidate, { + preserveEventAt: existing.getAttribute('data-event-at'), +}); +const refreshedHeader = refreshed.querySelector('.thinking-card-header'); +const after = refreshedHeader && refreshedHeader.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + beforeText: before && before.textContent, + afterText: after && after.textContent, + beforeEventAt: existing.getAttribute('data-event-at'), + afterEventAt: refreshed.getAttribute('data-event-at'), + count: refreshedHeader && refreshedHeader.querySelectorAll('.transparent-event-time').length, +})); +""" + ) + + expected = _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000111)}));")["_clock"] + assert data["beforeText"] == expected + assert data["afterText"] == expected + assert data["beforeEventAt"] == "1700000111" + assert data["afterEventAt"] == "1700000111" + assert data["count"] == 1 + + +def test_live_tool_row_prefers_later_valid_timestamp_field(): + data = _run_node( + """ +const liveStamp = 1700000999; +const liveTool = { name: 'lookup', tid: 'tool-live', ts: 'bad', timestamp: 1700000124, created_at: 'also-bad' }; +global.buildToolCard = (tc) => { + const row = makeToolRow(null, false); + row._tcData = tc; + return row; +}; +global._toolArgsSnapshot = (args) => args; +global._cliPatchSnippetFromArgs = () => ''; +global._cliToolCardSnippet = () => ''; +global._cliToolCardHasDiffSnippet = () => false; +const call = _transparentOrderedToolCall( + { kind: 'tool', toolUseId: 'tool-live', name: 'lookup', input: { q: 'x' } }, + 11, + new Map([['tool-live', liveTool]]), + {}, + {}, + 1700000100, +); +const row = _decorateTransparentEventRow(buildToolCard(call), { + type: 'tool', + toolCall: call, + status: 'Running', +}); +const header = row.querySelector('.tool-card-header'); +const timeNode = header && header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + eventAt: row.getAttribute('data-event-at'), + timeText: timeNode && timeNode.textContent, + count: header && header.querySelectorAll('.transparent-event-time').length, + liveStamp, +})); +""" + ) + + assert data["eventAt"] == "1700000124" + assert data["timeText"] == _run_node("process.stdout.write(JSON.stringify({_clock:_activityClockLabel(1700000124)}));")["_clock"] + assert data["count"] == 1 + + +def test_live_thinking_row_without_timestamp_uses_observed_live_time(): + data = _run_node( + """ +const liveStamp = 1700000456; +global._activityNowSeconds = () => liveStamp; +const row = makeThinkingRow(true); +_decorateTransparentEventRow(row, { type: 'thinking', text: 'quiet trace', preview: 'quiet trace', live: true }); +const header = row.querySelector('.thinking-card-header'); +const timeBefore = header.querySelector('.transparent-event-time'); +const expected = _activityClockLabel(liveStamp); +global._activityNowSeconds = () => 1700000999; +_decorateTransparentEventRow(row, { type: 'thinking', text: 'quiet trace', preview: 'quiet trace', live: true }); +const timeAfter = header.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + count: header.querySelectorAll('.transparent-event-time').length, + timeSameNode: timeBefore === timeAfter, + timeText: timeAfter && timeAfter.textContent, + expected, + rowEventAt: row.getAttribute('data-event-at'), + timeEventAt: timeAfter && timeAfter.getAttribute('data-event-at'), +})); +""" + ) + + assert data["count"] == 1 + assert data["timeSameNode"] is True + assert data["timeText"] == data["expected"] + assert data["rowEventAt"] == "1700000456" + assert data["timeEventAt"] == "1700000456" + + +def test_live_thinking_row_existing_update_promotes_late_timestamp(): + data = _run_node( + """ +eval(extractFunc('appendThinking')); +global.isSimplifiedToolCalling = () => true; +global.isTransparentStream = () => true; +global.S = { session: { session_id: 'session-1' }, activeStreamId: 'stream-1' }; +global._showThinking = true; +global._sanitizeThinkingDisplayText = (text) => String(text || ''); +global._thinkingActivityNode = () => makeThinkingRow(true); +global._renderThinkingInto = () => {}; +global._syncTransparentEventControls = () => {}; +global._moveLiveRunStatusToTurnEnd = () => {}; +global._removeEmptyLiveWorklogShells = () => {}; +global._toolWorklogListEl = () => null; +global.ensureLiveWorklogContainer = () => null; +global._syncToolCallGroupSummary = () => {}; +global._findLiveAssistantAnchorForSegment = () => null; +global._findLatestVisibleLiveAssistantByBurst = () => null; +global._findLatestVisibleLiveAssistant = () => null; +const turn = new FakeElement('div'); +turn.id = 'liveAssistantTurn'; +const blocks = new FakeElement('div'); +turn.appendChild(blocks); +global.$ = (id) => id === 'liveAssistantTurn' ? turn : null; +global._assistantTurnBlocks = () => blocks; +global._activityNowSeconds = () => 1700000111; +appendThinking('quiet trace', { thinkingKey: 'thinking-key', live: true }); +const row = blocks.querySelector('.agent-activity-thinking[data-live-thinking="1"][data-live-thinking-key="thinking-key"]'); +const before = row && row.querySelector('.transparent-event-time'); +const beforeEventAt = row && row.getAttribute('data-event-at'); +const beforeText = before && before.textContent; +global._activityNowSeconds = () => 1700000998; +appendThinking('quiet trace', { thinkingKey: 'thinking-key', live: true, ts: 1700000998 }); +const afterRow = blocks.querySelector('.agent-activity-thinking[data-live-thinking="1"][data-live-thinking-key="thinking-key"]'); +const after = afterRow && afterRow.querySelector('.transparent-event-time'); +process.stdout.write(JSON.stringify({ + beforeEventAt, + afterEventAt: afterRow && afterRow.getAttribute('data-event-at'), + beforeText, + afterText: after && after.textContent, + count: afterRow && afterRow.querySelectorAll('.transparent-event-time').length, +})); +""" + ) + + assert data["beforeEventAt"] == "1700000111" + assert data["afterEventAt"] == "1700000998" + assert data["beforeText"] == _run_node("process.stdout.write(JSON.stringify({clock:_activityClockLabel(1700000111)}));")["clock"] + assert data["afterText"] == _run_node("process.stdout.write(JSON.stringify({clock:_activityClockLabel(1700000998)}));")["clock"] + assert data["count"] == 1 + + +def test_compact_worklog_path_stays_off_transparent_decorator(): + data = _run_node( + """ +let decorated = 0; +let built = 0; +const turn = new FakeElement('div'); +turn.id = 'liveAssistantTurn'; +const inner = new FakeElement('div'); +const group = new FakeElement('div'); +const list = new FakeElement('div'); +global.S = { session: { session_id: 'session-1' }, activeStreamId: 'stream-1' }; +global.$ = (id) => id === 'liveAssistantTurn' ? turn : null; +global._assistantTurnBlocks = () => inner; +global.isTransparentStream = () => false; +global._decorateTransparentEventRow = () => { decorated += 1; }; +global.ensureLiveWorklogContainer = () => group; +global._liveToolStepEl = () => list; +global._toolWorklogListEl = () => list; +global.buildToolCard = (tc) => { + built += 1; + const row = new FakeElement('div'); + row.className = 'tool-card-row'; + row._tcData = tc; + return row; +}; +appendLiveToolCard({ name: 'lookup', done: false }); +process.stdout.write(JSON.stringify({ + decorated, + built, + rows: list.children.length, +})); +""" + ) + + assert data["decorated"] == 0 + assert data["built"] == 1 + assert data["rows"] == 1