Merge pull request #6907 from nesquena/release/stage-6037-1786396633
Release exp-v0.52.196: Transparent Stream copy feedback (#6037, @Stacey2911)
This commit is contained in:
3 files changed
+979
-7
No files matched your search
@@ -5,6 +5,8 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- **Transparent Stream copy buttons now confirm success in place.** The tool-event and thinking copy controls flash a check for ~1.5s after a successful copy — the same feedback the normal chat copy buttons already give — then revert to the copy glyph. The confirmation is now clearly visible without hover (important on touch), stays correct across live-row reconciliation, cleans up if its row is torn down mid-feedback, and is never serialized into a restored session snapshot. Thanks @Stacey2911. (#6037)
|
||||
|
||||
- **Kanban boards can now set a default workspace path.** The board settings modal gains a "Default workspace path" field (a type-or-pick combobox sourced from your saved workspaces, so you can choose a known path or type a custom one — validated server-side). Tasks on the board without an explicit workspace path inherit this default. The board switcher chip is now always shown (even with a single board) and its menu item is now "Board settings…", enabled for the default board. Thanks @rodboev. (#6055, #5932)
|
||||
|
||||
- **The workspace file tree can now be sorted, and the "Show hidden files" toggle moved into a menu.** The workspace pane's ⋮ options menu gains a "Sort by" group — Name (A→Z), Name (Z→A), Date created, Date modified — and your choice persists across sessions. "Date created" is disabled with an inline note on platforms/servers that don't report creation time. The pre-existing "Show hidden files" toggle moves from a permanent inline row into that same menu, reclaiming vertical space on every workspace view; a small indicator on the heading still reflects when hidden files are shown. Thanks @rodboev. (#6091, #6066)
|
||||
|
||||
+166
-7
@@ -9306,7 +9306,33 @@ function snapshotLiveTurnHtmlForSession(sid){
|
||||
const turn=$('liveAssistantTurn');
|
||||
if(!turn) return;
|
||||
if(turn.dataset&&turn.dataset.sessionId&&turn.dataset.sessionId!==sid) return;
|
||||
INFLIGHT[sid].liveTurnHtml=turn.outerHTML;
|
||||
let snapshotTurn=turn;
|
||||
const sourceControls=turn.querySelectorAll
|
||||
? Array.from(turn.querySelectorAll('.transparent-event-copy,.thinking-copy-btn'))
|
||||
: [];
|
||||
if(sourceControls.some(control=>!!control._transparentCopiedFeedbackNormal)){
|
||||
// Copy-success feedback is transient, while its normal presentation lives
|
||||
// only on element properties. Sanitize an independent clone so switching
|
||||
// sessions cannot persist the check/Copied/accent presentation, without
|
||||
// mutating the visible control or disturbing its active feedback timer.
|
||||
snapshotTurn=turn.cloneNode(true);
|
||||
const clonedControls=Array.from(snapshotTurn.querySelectorAll('.transparent-event-copy,.thinking-copy-btn'));
|
||||
sourceControls.forEach((source,index)=>{
|
||||
const normal=source._transparentCopiedFeedbackNormal;
|
||||
const clone=clonedControls[index];
|
||||
if(!normal||!clone) return;
|
||||
clone.innerHTML=normal.innerHTML;
|
||||
if(clone.style){
|
||||
if(normal.styleCssText!==null) clone.style.cssText=normal.styleCssText;
|
||||
else clone.style.color=normal.color||'';
|
||||
}
|
||||
if(normal.titleAttr===null) clone.removeAttribute('title');
|
||||
else clone.setAttribute('title',normal.titleAttr);
|
||||
if(normal.ariaLabel===null) clone.removeAttribute('aria-label');
|
||||
else clone.setAttribute('aria-label',normal.ariaLabel);
|
||||
});
|
||||
}
|
||||
INFLIGHT[sid].liveTurnHtml=snapshotTurn.outerHTML;
|
||||
}
|
||||
|
||||
function _liveAssistantSegmentTextLength(seg){
|
||||
@@ -11216,7 +11242,124 @@ function _transparentToolSummary(tc){
|
||||
if(target) return target;
|
||||
return '';
|
||||
}
|
||||
function _copyEventToClipboard(row){
|
||||
function _showTransparentCopiedFeedback(control,row,opts){
|
||||
if(!control&&!row) return;
|
||||
opts=opts||{};
|
||||
// A live-row refresh may replace a header's copy control with new markup.
|
||||
// Keep the lifetime on the persistent row, then resolve the currently visible
|
||||
// control for every render/expiry rather than retaining a detached button.
|
||||
const feedbackRow=row||(control&&control.closest?control.closest('.transparent-event-row'):null);
|
||||
const owner=feedbackRow||control;
|
||||
if(!owner) return;
|
||||
const currentControl=()=>{
|
||||
if(feedbackRow&&feedbackRow.querySelector){
|
||||
return feedbackRow.querySelector('.transparent-event-copy,.thinking-copy-btn');
|
||||
}
|
||||
return control||null;
|
||||
};
|
||||
const connectedControl=()=>{
|
||||
const target=currentControl();
|
||||
if(!target||target.isConnected!==true) return null;
|
||||
if(feedbackRow&&feedbackRow.isConnected!==true) return null;
|
||||
return target;
|
||||
};
|
||||
const clearFeedbackState=(state)=>{
|
||||
if(state&&state.timer){
|
||||
clearTimeout(state.timer);
|
||||
state.timer=null;
|
||||
}
|
||||
if(owner._transparentCopiedFeedback===state) delete owner._transparentCopiedFeedback;
|
||||
};
|
||||
const restoreControl=(target)=>{
|
||||
if(!target) return;
|
||||
const normal=target._transparentCopiedFeedbackNormal;
|
||||
if(!normal) return;
|
||||
target.innerHTML=normal.innerHTML;
|
||||
if(target.style){
|
||||
if(normal.styleCssText!==null) target.style.cssText=normal.styleCssText;
|
||||
else target.style.color=normal.color||'';
|
||||
}
|
||||
if(normal.titleAttr===null){
|
||||
if(target.removeAttribute) target.removeAttribute('title');
|
||||
}else if(target.setAttribute){
|
||||
target.setAttribute('title',normal.titleAttr);
|
||||
}
|
||||
if(normal.ariaLabel===null){
|
||||
if(target.removeAttribute) target.removeAttribute('aria-label');
|
||||
}else if(target.setAttribute){
|
||||
target.setAttribute('aria-label',normal.ariaLabel);
|
||||
}
|
||||
delete target._transparentCopiedFeedbackNormal;
|
||||
};
|
||||
const renderCopiedControl=(target)=>{
|
||||
if(!target) return;
|
||||
if(!target._transparentCopiedFeedbackNormal){
|
||||
target._transparentCopiedFeedbackNormal={
|
||||
innerHTML:target.innerHTML,
|
||||
color:target.style?target.style.color:undefined,
|
||||
styleCssText:target.style&&typeof target.style.cssText==='string'?target.style.cssText:null,
|
||||
titleAttr:target.getAttribute?target.getAttribute('title'):null,
|
||||
ariaLabel:target.getAttribute?target.getAttribute('aria-label'):null,
|
||||
};
|
||||
}
|
||||
const copiedLabel=t('copied')||'Copied';
|
||||
target.innerHTML=typeof li==='function'?li('check',11):'✓';
|
||||
if(target.style){
|
||||
target.style.color='var(--accent)';
|
||||
target.style.opacity='1';
|
||||
}
|
||||
if(target.setAttribute) target.setAttribute('title',copiedLabel);
|
||||
else target.title=copiedLabel;
|
||||
if(target.setAttribute) target.setAttribute('aria-label',copiedLabel);
|
||||
};
|
||||
const now=Date.now();
|
||||
let state=owner._transparentCopiedFeedback;
|
||||
if(opts.rehydrate){
|
||||
if(!state) return;
|
||||
const target=connectedControl();
|
||||
if(!target){
|
||||
clearFeedbackState(state);
|
||||
return;
|
||||
}
|
||||
if(!state.expiresAt||state.expiresAt<=now){
|
||||
if(state.timer) clearTimeout(state.timer);
|
||||
state.timer=null;
|
||||
restoreControl(target);
|
||||
if(owner._transparentCopiedFeedback===state) delete owner._transparentCopiedFeedback;
|
||||
return;
|
||||
}
|
||||
renderCopiedControl(target);
|
||||
return;
|
||||
}
|
||||
const target=connectedControl();
|
||||
if(!target){
|
||||
if(state) clearFeedbackState(state);
|
||||
return;
|
||||
}
|
||||
if(!state){
|
||||
state={timer:null,generation:0,expiresAt:0};
|
||||
owner._transparentCopiedFeedback=state;
|
||||
}else if(state.timer){
|
||||
clearTimeout(state.timer);
|
||||
}
|
||||
state.generation+=1;
|
||||
const generation=state.generation;
|
||||
state.expiresAt=now+1500;
|
||||
renderCopiedControl(target);
|
||||
state.timer=setTimeout(()=>{
|
||||
if(owner._transparentCopiedFeedback!==state||state.generation!==generation) return;
|
||||
const expiryTarget=connectedControl();
|
||||
if(!expiryTarget){
|
||||
state.timer=null;
|
||||
delete owner._transparentCopiedFeedback;
|
||||
return;
|
||||
}
|
||||
state.timer=null;
|
||||
restoreControl(expiryTarget);
|
||||
delete owner._transparentCopiedFeedback;
|
||||
},1500);
|
||||
}
|
||||
function _copyEventToClipboard(row,control){
|
||||
if(!row) return;
|
||||
const type=row.getAttribute('data-event-type');
|
||||
let text='';
|
||||
@@ -11259,9 +11402,12 @@ function _copyEventToClipboard(row){
|
||||
}else{
|
||||
text=row.textContent||'';
|
||||
}
|
||||
if(!text) return;
|
||||
const copied=()=>_showTransparentCopiedFeedback(control,row);
|
||||
const fallback=()=>{
|
||||
let ta=null;
|
||||
try{
|
||||
const ta=document.createElement('textarea');
|
||||
ta=document.createElement('textarea');
|
||||
ta.value=text;
|
||||
ta.setAttribute('readonly','');
|
||||
ta.style.position='absolute';
|
||||
@@ -11269,14 +11415,17 @@ function _copyEventToClipboard(row){
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok=document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if(ok) copied();
|
||||
if(typeof showToast==='function') showToast(ok?(t('copied')||'Copied'):(t('copy_failed')||'Copy failed'),1600);
|
||||
}catch(_){
|
||||
if(typeof showToast==='function') showToast(t('copy_failed')||'Copy failed',2000,'error');
|
||||
}finally{
|
||||
if(ta&&ta.parentNode) ta.parentNode.removeChild(ta);
|
||||
}
|
||||
};
|
||||
if(navigator&&navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(text).then(()=>{
|
||||
copied();
|
||||
if(typeof showToast==='function') showToast(`${t('copied')||'Copied'} ${label}`,1600);
|
||||
}).catch(fallback);
|
||||
}else{
|
||||
@@ -11287,21 +11436,29 @@ function _attachCopyButton(header){
|
||||
if(!header) return null;
|
||||
const bindCopyButton=(btn)=>{
|
||||
if(!btn) return null;
|
||||
const row=header.closest?header.closest('.transparent-event-row'):null;
|
||||
const feedbackState=row&&row._transparentCopiedFeedback;
|
||||
const feedbackActive=!!(feedbackState&&Number(feedbackState.expiresAt)>Date.now());
|
||||
btn.classList.add('transparent-event-copy');
|
||||
btn.setAttribute('role','button');
|
||||
btn.setAttribute('tabindex','0');
|
||||
btn.setAttribute('aria-label',t('copy')||'Copy');
|
||||
btn.setAttribute('data-transparent-copy','1');
|
||||
btn.title=t('copy')||'Copy';
|
||||
if(!btn._transparentCopiedFeedback&&!feedbackActive){
|
||||
btn.setAttribute('aria-label',t('copy')||'Copy');
|
||||
btn.title=t('copy')||'Copy';
|
||||
}
|
||||
const handler=function(ev){
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
_copyEventToClipboard(header.closest('.transparent-event-row'));
|
||||
_copyEventToClipboard(header.closest('.transparent-event-row'),btn);
|
||||
};
|
||||
btn.onclick=handler;
|
||||
btn.onkeydown=function(ev){
|
||||
if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();handler(ev);}
|
||||
};
|
||||
if(btn.parentNode&&typeof _showTransparentCopiedFeedback==='function'){
|
||||
_showTransparentCopiedFeedback(btn,row,{rehydrate:true});
|
||||
}
|
||||
return btn;
|
||||
};
|
||||
// Reuse ANY existing copy button (handles both .transparent-event-copy
|
||||
@@ -11324,6 +11481,8 @@ function _attachCopyButton(header){
|
||||
const toggle=header.querySelector('.tool-card-toggle,.thinking-card-toggle');
|
||||
if(toggle&&toggle.parentNode===header) header.insertBefore(btn,toggle);
|
||||
else header.appendChild(btn);
|
||||
const row=header.closest?header.closest('.transparent-event-row'):null;
|
||||
if(typeof _showTransparentCopiedFeedback==='function') _showTransparentCopiedFeedback(btn,row,{rehydrate:true});
|
||||
return btn;
|
||||
}
|
||||
function _transparentEventCountLabel(toolCount){
|
||||
|
||||
@@ -728,6 +728,7 @@ global.document = { createElement:(tag)=>new FakeElement(tag), body:new FakeElem
|
||||
global.t = (key)=>key === 'copy' ? 'Copy' : key;
|
||||
global.showToast = ()=>{};
|
||||
|
||||
eval(extractFunc('_showTransparentCopiedFeedback'));
|
||||
eval(extractFunc('_copyEventToClipboard'));
|
||||
eval(extractFunc('_attachCopyButton'));
|
||||
eval(extractFunc('_setTransparentCardOpen'));
|
||||
@@ -1825,3 +1826,813 @@ process.stdout.write(JSON.stringify({
|
||||
assert data["scrollTopPreserved"] is True
|
||||
assert data["innerHTMLSetCount"] == 0
|
||||
assert data["cardOpen"] is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_transparent_rebound_copy_controls_report_only_confirmed_success():
|
||||
script = r"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync(process.env.UI_JS_PATH, 'utf8');
|
||||
function extractFunc(name){
|
||||
const marker = new RegExp('function\\s+' + name + '\\s*\\(');
|
||||
const start = src.search(marker);
|
||||
if(start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{', start) + 1;
|
||||
let depth = 1;
|
||||
while(depth > 0 && i < src.length){
|
||||
if(src[i] === '{') depth += 1;
|
||||
else if(src[i] === '}') depth -= 1;
|
||||
i += 1;
|
||||
}
|
||||
return src.slice(start, i);
|
||||
}
|
||||
const htmlRegistry = new Map();
|
||||
const snapshotRegistry = new Map();
|
||||
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._styleValues = Object.create(null);
|
||||
this.style = {};
|
||||
const self = this;
|
||||
Object.defineProperty(this.style, 'color', {
|
||||
get(){ return self._styleValues.color || ''; },
|
||||
set(value){ self._recordWrite('style'); self._styleValues.color=String(value ?? ''); },
|
||||
});
|
||||
Object.defineProperty(this.style, 'opacity', {
|
||||
get(){ return self._styleValues.opacity || ''; },
|
||||
set(value){ self._recordWrite('style'); self._styleValues.opacity=String(value ?? ''); },
|
||||
});
|
||||
Object.defineProperty(this.style, 'cssText', {
|
||||
get(){ return Object.entries(self._styleValues).filter(([,value])=>value !== '').map(([name,value])=>`${name}: ${value};`).join(' '); },
|
||||
set(value){
|
||||
self._recordWrite('style');
|
||||
self._styleValues = Object.create(null);
|
||||
String(value ?? '').split(';').forEach(part=>{
|
||||
const index=part.indexOf(':');
|
||||
if(index < 0) return;
|
||||
const name=part.slice(0,index).trim();
|
||||
const item=part.slice(index+1).trim();
|
||||
if(name) self._styleValues[name]=item;
|
||||
});
|
||||
},
|
||||
});
|
||||
this._title = '';
|
||||
this.onclick = null;
|
||||
this.onkeydown = null;
|
||||
this._innerHTML = '';
|
||||
this._textContent = '';
|
||||
this._classes = new Set();
|
||||
this.id = '';
|
||||
this.content = this.tagName === 'TEMPLATE' ? { firstElementChild:null } : null;
|
||||
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){
|
||||
const next = force === undefined ? !self._classes.has(name) : !!force;
|
||||
if(next) self._classes.add(name);
|
||||
else self._classes.delete(name);
|
||||
return next;
|
||||
},
|
||||
};
|
||||
}
|
||||
get parentElement(){ return this.parentNode; }
|
||||
cloneNode(deep){
|
||||
const clone=new FakeElement(this.tagName);
|
||||
clone.className=this.className;
|
||||
clone._innerHTML=this._innerHTML;
|
||||
clone._textContent=this._textContent;
|
||||
clone.style.cssText=this.style.cssText;
|
||||
Object.entries(this.attributes).forEach(([name,value])=>clone.setAttribute(name,value));
|
||||
if(deep) this.children.forEach(child=>clone.appendChild(child.cloneNode(true)));
|
||||
return clone;
|
||||
}
|
||||
get isConnected(){
|
||||
let node = this;
|
||||
while(node){
|
||||
if(global.document&&node===global.document.body) return true;
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
get title(){ return this._title; }
|
||||
set title(value){ this._recordWrite('title'); this._title=String(value ?? ''); }
|
||||
get className(){ return Array.from(this._classes).join(' '); }
|
||||
set className(value){ this._classes = new Set(String(value).split(/\s+/).filter(Boolean)); }
|
||||
get innerHTML(){
|
||||
if(this.tagName === 'TEMPLATE') return this._templateHtml || '';
|
||||
return this._innerHTML;
|
||||
}
|
||||
set innerHTML(value){
|
||||
if(this.tagName === 'TEMPLATE'){
|
||||
this._templateHtml=String(value ?? '');
|
||||
const stored=snapshotRegistry.get(this._templateHtml);
|
||||
this.content.firstElementChild=stored ? stored.cloneNode(true) : null;
|
||||
return;
|
||||
}
|
||||
this._recordWrite('innerHTML');
|
||||
this.children.forEach(child=>{ child.parentNode=null; });
|
||||
this._innerHTML = String(value ?? '');
|
||||
this._textContent = this._innerHTML;
|
||||
this.children = [];
|
||||
const factory = htmlRegistry.get(this._innerHTML);
|
||||
if(factory){
|
||||
this._textContent = '';
|
||||
factory().forEach(child=>this.appendChild(child));
|
||||
}
|
||||
}
|
||||
get textContent(){ return this.children.length ? this.children.map(child=>child.textContent).join('') : this._textContent; }
|
||||
set textContent(value){
|
||||
this._recordWrite('textContent');
|
||||
this.children.forEach(child=>{ child.parentNode=null; });
|
||||
this._textContent = String(value ?? '');
|
||||
this._innerHTML = this._textContent;
|
||||
this.children = [];
|
||||
}
|
||||
_recordWrite(kind){
|
||||
if(this._trackDetachedWrites&& !this.isConnected) this._trackDetachedWrites.push(kind);
|
||||
}
|
||||
setAttribute(name, value){
|
||||
this._recordWrite(name === 'aria-label' ? 'aria-label' : `attribute:${name}`);
|
||||
this.attributes[String(name)] = String(value);
|
||||
if(name === 'title') this.title = String(value);
|
||||
if(name === 'id') this.id = String(value);
|
||||
if(String(name).startsWith('data-')){
|
||||
const key=String(name).slice(5).replace(/-([a-z])/g,(_,char)=>char.toUpperCase());
|
||||
this.dataset[key]=String(value);
|
||||
}
|
||||
}
|
||||
getAttribute(name){ return Object.prototype.hasOwnProperty.call(this.attributes, name) ? this.attributes[name] : null; }
|
||||
getAttributeNames(){ return Object.keys(this.attributes); }
|
||||
removeAttribute(name){
|
||||
this._recordWrite(name === 'aria-label' ? 'aria-label' : `attribute:${name}`);
|
||||
delete this.attributes[name];
|
||||
if(name === 'title') this.title = '';
|
||||
if(name === 'id') this.id = '';
|
||||
if(String(name).startsWith('data-')){
|
||||
const key=String(name).slice(5).replace(/-([a-z])/g,(_,char)=>char.toUpperCase());
|
||||
delete this.dataset[key];
|
||||
}
|
||||
}
|
||||
appendChild(child){ if(child.parentNode) child.remove(); child.parentNode = this; this.children.push(child); return child; }
|
||||
insertBefore(child, ref){
|
||||
if(child.parentNode) child.remove();
|
||||
const index = this.children.indexOf(ref);
|
||||
child.parentNode = this;
|
||||
if(index < 0) this.children.push(child);
|
||||
else this.children.splice(index, 0, child);
|
||||
return child;
|
||||
}
|
||||
removeChild(child){ child.remove(); return child; }
|
||||
remove(){
|
||||
if(!this.parentNode) return;
|
||||
const siblings = this.parentNode.children;
|
||||
const index = siblings.indexOf(this);
|
||||
if(index >= 0) siblings.splice(index, 1);
|
||||
this.parentNode = null;
|
||||
}
|
||||
replaceWith(replacement){
|
||||
if(!this.parentNode) return;
|
||||
const siblings=this.parentNode.children;
|
||||
const index=siblings.indexOf(this);
|
||||
if(replacement.parentNode) replacement.remove();
|
||||
if(index >= 0){
|
||||
siblings[index]=replacement;
|
||||
replacement.parentNode=this.parentNode;
|
||||
this.parentNode=null;
|
||||
}
|
||||
}
|
||||
_serialize(){
|
||||
const attrs=Object.entries(this.attributes);
|
||||
if(this.id&&!Object.prototype.hasOwnProperty.call(this.attributes,'id')) attrs.push(['id',this.id]);
|
||||
if(this.className&&!Object.prototype.hasOwnProperty.call(this.attributes,'class')) attrs.push(['class',this.className]);
|
||||
if(this.style.cssText) attrs.push(['style',this.style.cssText]);
|
||||
const attrText=attrs.map(([name,value])=>` ${name}="${String(value)}"`).join('');
|
||||
const content=this.children.length ? this.children.map(child=>child._serialize()).join('') : this.innerHTML;
|
||||
return `<${this.tagName.toLowerCase()}${attrText}>${content}</${this.tagName.toLowerCase()}>`;
|
||||
}
|
||||
get outerHTML(){
|
||||
const html=this._serialize();
|
||||
snapshotRegistry.set(html,this.cloneNode(true));
|
||||
return html;
|
||||
}
|
||||
select(){}
|
||||
querySelector(selector){ return this.querySelectorAll(selector)[0] || null; }
|
||||
querySelectorAll(selector){
|
||||
const found = [];
|
||||
const walk = node=>{
|
||||
node.children.forEach(child=>{
|
||||
if(matches(child, selector)) found.push(child);
|
||||
walk(child);
|
||||
});
|
||||
};
|
||||
walk(this);
|
||||
return found;
|
||||
}
|
||||
closest(selector){
|
||||
let node = this;
|
||||
while(node){ if(matches(node, selector)) return node; node = node.parentNode; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function matches(node, selector){
|
||||
return String(selector||'').split(',').map(part=>part.trim()).filter(Boolean).some(part=>{
|
||||
const classes = part.match(/\.([A-Za-z0-9_-]+)/g) || [];
|
||||
if(classes.some(cls=>!node.classList.contains(cls.slice(1)))) return false;
|
||||
const attrs = part.match(/\[([^=\]]+)(?:="([^"]*)")?\]/g) || [];
|
||||
if(attrs.some(raw=>{
|
||||
const match = raw.match(/\[([^=\]]+)(?:="([^"]*)")?\]/);
|
||||
const value = node.getAttribute(match[1]);
|
||||
return value === null || (match[2] !== undefined && value !== match[2]);
|
||||
})) return false;
|
||||
return classes.length > 0 || attrs.length > 0;
|
||||
});
|
||||
}
|
||||
function element(tag, classes){
|
||||
const node = new FakeElement(tag);
|
||||
node.className = classes || '';
|
||||
return node;
|
||||
}
|
||||
function toolRow(){
|
||||
const row = element('div', 'transparent-event-row');
|
||||
row.setAttribute('data-event-type', 'tool');
|
||||
row.setAttribute('data-expanded', '0');
|
||||
row._tcData = { name:'shell', args:{ command:'echo secret-token' }, snippet:'tool output' };
|
||||
const card = element('div', 'tool-card');
|
||||
const header = element('div', 'tool-card-header');
|
||||
const copy = element('span', 'transparent-event-copy');
|
||||
copy.innerHTML = '<svg data-original="tool-copy"></svg>';
|
||||
const toggle = element('span', 'tool-card-toggle');
|
||||
header.appendChild(copy);
|
||||
header.appendChild(toggle);
|
||||
card.appendChild(header);
|
||||
row.appendChild(card);
|
||||
return { row, card, header, copy };
|
||||
}
|
||||
let reconcileTemplateId = 0;
|
||||
function reconcileToolRow(version, original){
|
||||
const token = `__reconcile_tool_${version}_${++reconcileTemplateId}__`;
|
||||
htmlRegistry.set(token, ()=>{
|
||||
const card = element('div', 'tool-card');
|
||||
const header = element('div', 'tool-card-header');
|
||||
const copy = element('span', 'transparent-event-copy');
|
||||
copy.innerHTML = original.html;
|
||||
copy.style.color = original.color;
|
||||
copy.style.opacity = original.opacity || '';
|
||||
copy.setAttribute('title', original.title);
|
||||
copy.setAttribute('aria-label', original.aria);
|
||||
const toggle = element('span', 'tool-card-toggle');
|
||||
header.appendChild(copy);
|
||||
header.appendChild(toggle);
|
||||
card.appendChild(header);
|
||||
return [card];
|
||||
});
|
||||
const row = element('div', 'transparent-event-row');
|
||||
row.setAttribute('data-event-type', 'tool');
|
||||
row.setAttribute('data-anchor-row-id', 'copy-race');
|
||||
row.setAttribute('data-anchor-row-role', 'tool');
|
||||
row.setAttribute('data-anchor-source-event-type', 'process_tool');
|
||||
row.setAttribute('data-expanded', '0');
|
||||
row._tcData = { name:'shell', args:{ command:`echo ${version}` }, snippet:`${version} output` };
|
||||
row.innerHTML = token;
|
||||
return {
|
||||
row,
|
||||
card:row.querySelector('.tool-card'),
|
||||
header:row.querySelector('.tool-card-header'),
|
||||
copy:row.querySelector('.transparent-event-copy'),
|
||||
};
|
||||
}
|
||||
function thinkingRow(text){
|
||||
const row = element('div', 'transparent-event-row transparent-thinking-event');
|
||||
row.setAttribute('data-event-type', 'thinking');
|
||||
row.setAttribute('data-expanded', '0');
|
||||
const card = element('div', 'thinking-card');
|
||||
const header = element('div', 'thinking-card-header');
|
||||
const copy = element('button', 'thinking-copy-btn');
|
||||
copy.innerHTML = '<svg data-original="thinking-copy"></svg>';
|
||||
const toggle = element('span', 'thinking-card-toggle');
|
||||
const body = element('div', 'thinking-card-body');
|
||||
const pre = element('pre', '');
|
||||
pre.textContent = text;
|
||||
header.appendChild(copy);
|
||||
header.appendChild(toggle);
|
||||
body.appendChild(pre);
|
||||
card.appendChild(header);
|
||||
card.appendChild(body);
|
||||
row.appendChild(card);
|
||||
return { row, card, header, copy };
|
||||
}
|
||||
|
||||
let nextTimer = 1;
|
||||
const timers = new Map();
|
||||
const clearedTimers = [];
|
||||
global.setTimeout = (fn, ms)=>{ const id=nextTimer++; timers.set(id,{ fn, ms }); return id; };
|
||||
global.clearTimeout = (id)=>{ clearedTimers.push(id); timers.delete(id); };
|
||||
function runTimer(id){ const timer=timers.get(id); timers.delete(id); timer.fn(); }
|
||||
let clipboardMode = 'success';
|
||||
let deferredClipboardResolve = null;
|
||||
let fallbackResult = true;
|
||||
const writes = [];
|
||||
const toasts = [];
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
configurable:true,
|
||||
value:{ clipboard:{ writeText:(text)=>{
|
||||
writes.push(text);
|
||||
if(clipboardMode === 'deferred'){
|
||||
return new Promise(resolve=>{ deferredClipboardResolve=resolve; });
|
||||
}
|
||||
return clipboardMode === 'success' ? Promise.resolve() : Promise.reject(new Error('denied'));
|
||||
} } },
|
||||
});
|
||||
global.document = {
|
||||
body:new FakeElement('body'),
|
||||
createElement:(tag)=>new FakeElement(tag),
|
||||
execCommand:()=>fallbackResult,
|
||||
};
|
||||
global.t = key=>({ copy:'Copy', copied:'Copied', copy_failed:'Copy failed' })[key] || key;
|
||||
global.li = (name, size)=>`<svg data-icon="${name}" data-size="${size}"></svg>`;
|
||||
global.showToast = (message, duration, type)=>toasts.push({ message, duration, type:type||'' });
|
||||
global._redactToolTargetLabel = value=>String(value).replace(/secret-token/g, '[REDACTED]');
|
||||
global._wireTransparentHeaderToggle = ()=>{};
|
||||
global._setTransparentCardOpen = (card, open)=>card.classList.toggle('open', !!open);
|
||||
global.INFLIGHT = Object.create(null);
|
||||
global.S = { session:{ session_id:'snapshot-session' } };
|
||||
global.requestAnimationFrame = (fn)=>fn();
|
||||
global._rehydrateTransparentStreamDom = ()=>{};
|
||||
global.normalizeLiveActivityGroupPlacement = ()=>{};
|
||||
global._dedupeLiveProcessedWorklogAnchors = ()=>{};
|
||||
global.placeLiveToolCardsHost = ()=>{};
|
||||
global._postProcessWithAnchorSuppression = ()=>{};
|
||||
global.$ = (id)=>document.body.querySelector(`[id="${id}"]`);
|
||||
|
||||
eval(extractFunc('snapshotLiveTurnHtmlForSession'));
|
||||
eval(extractFunc('_liveAssistantSegmentTextLength'));
|
||||
eval(extractFunc('_mergeRestoredLiveAssistantSegment'));
|
||||
eval(extractFunc('restoreLiveTurnHtmlForSession'));
|
||||
eval(extractFunc('_showTransparentCopiedFeedback'));
|
||||
eval(extractFunc('_copyEventToClipboard'));
|
||||
eval(extractFunc('_attachCopyButton'));
|
||||
eval(extractFunc('_transparentLiveRowAttributePairs'));
|
||||
eval(extractFunc('_transparentLiveRowInteractiveState'));
|
||||
eval(extractFunc('_rehydrateTransparentLiveRow'));
|
||||
eval(extractFunc('_refreshTransparentThinkingLiveRow'));
|
||||
eval(extractFunc('_refreshTransparentLiveRow'));
|
||||
|
||||
function eventProbe(){
|
||||
return {
|
||||
stopped:false,
|
||||
prevented:false,
|
||||
stopPropagation(){ this.stopped=true; },
|
||||
preventDefault(){ this.prevented=true; },
|
||||
};
|
||||
}
|
||||
async function settle(){ await Promise.resolve(); await Promise.resolve(); }
|
||||
function replaceCopyControl(header, oldCopy, classes, original){
|
||||
const replacement = element('button', classes);
|
||||
replacement.innerHTML = original.html;
|
||||
replacement.style.color = original.color;
|
||||
replacement.style.opacity = original.opacity || '';
|
||||
replacement.setAttribute('title', original.title);
|
||||
replacement.setAttribute('aria-label', original.aria);
|
||||
oldCopy.remove();
|
||||
const toggle = header.querySelector('.tool-card-toggle,.thinking-card-toggle');
|
||||
if(toggle&&toggle.parentNode===header) header.insertBefore(replacement,toggle);
|
||||
else header.appendChild(replacement);
|
||||
_attachCopyButton(header);
|
||||
return replacement;
|
||||
}
|
||||
|
||||
(async()=>{
|
||||
const tool = toolRow();
|
||||
document.body.appendChild(tool.row);
|
||||
_attachCopyButton(tool.header);
|
||||
tool.copy.innerHTML = '<svg data-original="tool-copy"></svg>';
|
||||
tool.copy.style.color = 'rgb(1, 2, 3)';
|
||||
tool.copy.style.opacity = '0.28';
|
||||
tool.copy.setAttribute('title', 'Original tool title');
|
||||
tool.copy.setAttribute('aria-label', 'Original tool aria');
|
||||
const original = {
|
||||
html:tool.copy.innerHTML,
|
||||
color:tool.copy.style.color,
|
||||
opacity:tool.copy.style.opacity,
|
||||
styleCssText:tool.copy.style.cssText,
|
||||
title:tool.copy.title,
|
||||
titleAttr:tool.copy.getAttribute('title'),
|
||||
aria:tool.copy.getAttribute('aria-label'),
|
||||
};
|
||||
const firstEvent = eventProbe();
|
||||
tool.copy.onclick(firstEvent);
|
||||
await settle();
|
||||
const firstTimerId = Array.from(timers.keys())[0];
|
||||
const firstTimer = timers.get(firstTimerId);
|
||||
const firstState = {
|
||||
check:tool.copy.innerHTML.includes('data-icon="check"'),
|
||||
title:tool.copy.title,
|
||||
aria:tool.copy.getAttribute('aria-label'),
|
||||
opacity:tool.copy.style.opacity,
|
||||
duration:firstTimer.ms,
|
||||
stopped:firstEvent.stopped,
|
||||
prevented:firstEvent.prevented,
|
||||
rowExpanded:tool.row.getAttribute('data-expanded'),
|
||||
cardOpen:tool.card.classList.contains('open'),
|
||||
};
|
||||
const staleCallback = firstTimer.fn;
|
||||
tool.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const secondTimerId = Array.from(timers.keys())[0];
|
||||
staleCallback();
|
||||
const staleTimerIgnored = tool.copy.innerHTML.includes('data-icon="check"');
|
||||
runTimer(secondTimerId);
|
||||
const restored = {
|
||||
html:tool.copy.innerHTML,
|
||||
color:tool.copy.style.color,
|
||||
opacity:tool.copy.style.opacity,
|
||||
styleCssText:tool.copy.style.cssText,
|
||||
title:tool.copy.title,
|
||||
titleAttr:tool.copy.getAttribute('title'),
|
||||
aria:tool.copy.getAttribute('aria-label'),
|
||||
};
|
||||
|
||||
const thinking = thinkingRow('transparent reasoning');
|
||||
document.body.appendChild(thinking.row);
|
||||
const candidate = thinkingRow('candidate reasoning');
|
||||
_rehydrateTransparentLiveRow(thinking.row, candidate.row, { expanded:false });
|
||||
const thinkingControls = thinking.header.querySelectorAll('.transparent-event-copy,.thinking-copy-btn');
|
||||
thinking.copy.style.opacity = '0.4';
|
||||
const thinkingOriginalStyle = thinking.copy.style.cssText;
|
||||
thinking.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const thinkingState = {
|
||||
count:thinkingControls.length,
|
||||
bound:typeof thinking.copy.onclick === 'function' && typeof thinking.copy.onkeydown === 'function',
|
||||
check:thinking.copy.innerHTML.includes('data-icon="check"'),
|
||||
opacity:thinking.copy.style.opacity,
|
||||
copied:writes[writes.length - 1],
|
||||
};
|
||||
runTimer(Array.from(timers.keys())[0]);
|
||||
const thinkingRestoredStyle = thinking.copy.style.cssText;
|
||||
|
||||
clipboardMode = 'failure';
|
||||
fallbackResult = true;
|
||||
tool.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const fallbackSuccess = {
|
||||
check:tool.copy.innerHTML.includes('data-icon="check"'),
|
||||
toast:toasts[toasts.length - 1].message,
|
||||
};
|
||||
runTimer(Array.from(timers.keys())[0]);
|
||||
|
||||
fallbackResult = false;
|
||||
const toastCountBeforeFailure = toasts.length;
|
||||
tool.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const failure = {
|
||||
check:tool.copy.innerHTML.includes('data-icon="check"'),
|
||||
feedbackState:!!tool.row._transparentCopiedFeedback,
|
||||
toasts:toasts.slice(toastCountBeforeFailure).map(item=>item.message),
|
||||
};
|
||||
|
||||
const empty = thinkingRow('');
|
||||
document.body.appendChild(empty.row);
|
||||
_attachCopyButton(empty.header);
|
||||
const writesBeforeEmpty = writes.length;
|
||||
empty.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const emptyCopySkipped = writes.length === writesBeforeEmpty && !empty.row._transparentCopiedFeedback;
|
||||
|
||||
// Copy success can settle after the real live-row reconciler has replaced the
|
||||
// activated control. The stable row owns the feedback lifetime, so the current
|
||||
// visible replacement receives the check.
|
||||
clipboardMode = 'deferred';
|
||||
const race = reconcileToolRow('existing', {
|
||||
html:'<svg data-original="existing"></svg>',
|
||||
color:'rgb(3, 4, 5)',
|
||||
title:'Existing title',
|
||||
aria:'Existing aria',
|
||||
opacity:'0.28',
|
||||
});
|
||||
document.body.appendChild(race.row);
|
||||
_attachCopyButton(race.header);
|
||||
let headerToggleCount = 0;
|
||||
race.header.onclick = ()=>{ headerToggleCount += 1; };
|
||||
const beforeSuccess = eventProbe();
|
||||
const activatedCopy = race.copy;
|
||||
race.copy.onclick(beforeSuccess);
|
||||
const candidateBeforeSuccess = reconcileToolRow('replacement-before-success', {
|
||||
html:'<svg data-original="replacement-before-success"></svg>',
|
||||
color:'rgb(7, 8, 9)',
|
||||
title:'Replacement before success title',
|
||||
aria:'Replacement before success aria',
|
||||
opacity:'0.31',
|
||||
});
|
||||
_refreshTransparentLiveRow(race.row, candidateBeforeSuccess.row);
|
||||
race.card = race.row.querySelector('.tool-card');
|
||||
race.header = race.row.querySelector('.tool-card-header');
|
||||
race.copy = race.row.querySelector('.transparent-event-copy');
|
||||
// Binding supplies the normal Copy label. Set distinct presentation after
|
||||
// reconciliation so the feedback snapshot proves which control is current.
|
||||
race.copy.innerHTML = '<svg data-original="replacement-before-success"></svg>';
|
||||
race.copy.style.color = 'rgb(7, 8, 9)';
|
||||
race.copy.setAttribute('title', 'Replacement before success title');
|
||||
race.copy.setAttribute('aria-label', 'Replacement before success aria');
|
||||
const activatedControlDetached = !activatedCopy.isConnected;
|
||||
const beforeResolutionCheck = race.copy.innerHTML.includes('data-icon="check"');
|
||||
deferredClipboardResolve();
|
||||
await settle();
|
||||
const firstRaceTimerId = race.row._transparentCopiedFeedback.timer;
|
||||
const staleRaceTimer = timers.get(firstRaceTimerId).fn;
|
||||
const replacementAfterSuccess = {
|
||||
check:race.copy.innerHTML.includes('data-icon="check"'),
|
||||
title:race.copy.title,
|
||||
aria:race.copy.getAttribute('aria-label'),
|
||||
opacity:race.copy.style.opacity,
|
||||
};
|
||||
const candidateDuringFeedback = reconcileToolRow('replacement-during-feedback', {
|
||||
html:'<svg data-original="replacement-during-feedback"></svg>',
|
||||
color:'rgb(11, 12, 13)',
|
||||
title:'Replacement during feedback title',
|
||||
aria:'Replacement during feedback aria',
|
||||
opacity:'0.33',
|
||||
});
|
||||
_refreshTransparentLiveRow(race.row, candidateDuringFeedback.row);
|
||||
race.card = race.row.querySelector('.tool-card');
|
||||
race.header = race.row.querySelector('.tool-card-header');
|
||||
race.copy = race.row.querySelector('.transparent-event-copy');
|
||||
const inheritedDuringFeedback = {
|
||||
check:race.copy.innerHTML.includes('data-icon="check"'),
|
||||
title:race.copy.title,
|
||||
aria:race.copy.getAttribute('aria-label'),
|
||||
opacity:race.copy.style.opacity,
|
||||
};
|
||||
clipboardMode = 'success';
|
||||
race.copy.onclick(eventProbe());
|
||||
await settle();
|
||||
const latestRaceTimerId = race.row._transparentCopiedFeedback.timer;
|
||||
const latestRaceTimerDuration = timers.get(latestRaceTimerId).ms;
|
||||
staleRaceTimer();
|
||||
const staleTimerDidNotClearLatest = race.copy.innerHTML.includes('data-icon="check"');
|
||||
race.row.remove();
|
||||
const detachedWrites = [];
|
||||
race.row._trackDetachedWrites = detachedWrites;
|
||||
race.copy._trackDetachedWrites = detachedWrites;
|
||||
let expiryThrew = false;
|
||||
try{ runTimer(latestRaceTimerId); }catch(_){ expiryThrew = true; }
|
||||
const detachedExpiry = {
|
||||
rowConnected:race.row.isConnected,
|
||||
controlConnected:race.copy.isConnected,
|
||||
feedbackState:!!race.row._transparentCopiedFeedback,
|
||||
writes:detachedWrites,
|
||||
threw:expiryThrew,
|
||||
html:race.copy.innerHTML,
|
||||
color:race.copy.style.color,
|
||||
title:race.copy.title,
|
||||
aria:race.copy.getAttribute('aria-label'),
|
||||
};
|
||||
|
||||
clipboardMode = 'deferred';
|
||||
const thinkingRace = thinkingRow('rebound transparent reasoning');
|
||||
document.body.appendChild(thinkingRace.row);
|
||||
_attachCopyButton(thinkingRace.header);
|
||||
thinkingRace.copy.onclick(eventProbe());
|
||||
thinkingRace.copy = replaceCopyControl(thinkingRace.header, thinkingRace.copy, 'thinking-copy-btn', {
|
||||
html:'<svg data-original="thinking-replacement"></svg>',
|
||||
color:'rgb(14, 15, 16)',
|
||||
title:'Thinking replacement title',
|
||||
aria:'Thinking replacement aria',
|
||||
opacity:'0.36',
|
||||
});
|
||||
deferredClipboardResolve();
|
||||
await settle();
|
||||
const thinkingRaceCheck = {
|
||||
count:thinkingRace.header.querySelectorAll('.transparent-event-copy,.thinking-copy-btn').length,
|
||||
bound:typeof thinkingRace.copy.onclick === 'function' && typeof thinkingRace.copy.onkeydown === 'function',
|
||||
check:thinkingRace.copy.innerHTML.includes('data-icon="check"'),
|
||||
opacity:thinkingRace.copy.style.opacity,
|
||||
};
|
||||
runTimer(thinkingRace.row._transparentCopiedFeedback.timer);
|
||||
|
||||
// Real snapshot/restore lifecycle with two simultaneous feedback controls.
|
||||
const msgInner = element('div', 'msg-inner');
|
||||
msgInner.setAttribute('id', 'msgInner');
|
||||
const liveTurn = element('div', 'assistant-turn');
|
||||
liveTurn.setAttribute('id', 'liveAssistantTurn');
|
||||
liveTurn.setAttribute('data-session-id', 'snapshot-session');
|
||||
const snapshotTool = toolRow();
|
||||
const snapshotThinking = thinkingRow('snapshot reasoning');
|
||||
liveTurn.appendChild(snapshotTool.row);
|
||||
liveTurn.appendChild(snapshotThinking.row);
|
||||
msgInner.appendChild(liveTurn);
|
||||
document.body.appendChild(msgInner);
|
||||
_attachCopyButton(snapshotTool.header);
|
||||
_attachCopyButton(snapshotThinking.header);
|
||||
snapshotTool.copy.innerHTML = '<svg data-original="snapshot-tool-copy"></svg>';
|
||||
snapshotTool.copy.style.cssText = 'color: rgb(21, 22, 23); opacity: 0.28; border-width: 2px;';
|
||||
snapshotTool.copy.setAttribute('title', 'Snapshot tool title');
|
||||
snapshotTool.copy.setAttribute('aria-label', 'Snapshot tool aria');
|
||||
snapshotThinking.copy.innerHTML = '<svg data-original="snapshot-thinking-copy"></svg>';
|
||||
snapshotThinking.copy.style.cssText = 'opacity: 0.42; padding-left: 3px;';
|
||||
snapshotThinking.copy.removeAttribute('title');
|
||||
snapshotThinking.copy.removeAttribute('aria-label');
|
||||
const snapshotToolNormalStyle = snapshotTool.copy.style.cssText;
|
||||
const snapshotThinkingNormalStyle = snapshotThinking.copy.style.cssText;
|
||||
_showTransparentCopiedFeedback(snapshotTool.copy, snapshotTool.row);
|
||||
_showTransparentCopiedFeedback(snapshotThinking.copy, snapshotThinking.row);
|
||||
const snapshotToolTimer = snapshotTool.row._transparentCopiedFeedback.timer;
|
||||
const snapshotThinkingTimer = snapshotThinking.row._transparentCopiedFeedback.timer;
|
||||
INFLIGHT['snapshot-session'] = {};
|
||||
snapshotLiveTurnHtmlForSession('snapshot-session');
|
||||
const storedSnapshot = INFLIGHT['snapshot-session'].liveTurnHtml;
|
||||
const liveAfterSnapshot = {
|
||||
toolCheck:snapshotTool.copy.innerHTML.includes('data-icon="check"'),
|
||||
thinkingCheck:snapshotThinking.copy.innerHTML.includes('data-icon="check"'),
|
||||
toolTitle:snapshotTool.copy.getAttribute('title'),
|
||||
toolAria:snapshotTool.copy.getAttribute('aria-label'),
|
||||
toolOpacity:snapshotTool.copy.style.opacity,
|
||||
thinkingOpacity:snapshotThinking.copy.style.opacity,
|
||||
toolFeedback:!!snapshotTool.row._transparentCopiedFeedback,
|
||||
thinkingFeedback:!!snapshotThinking.row._transparentCopiedFeedback,
|
||||
toolNormalSaved:!!snapshotTool.copy._transparentCopiedFeedbackNormal,
|
||||
thinkingNormalSaved:!!snapshotThinking.copy._transparentCopiedFeedbackNormal,
|
||||
};
|
||||
const restoreResult = restoreLiveTurnHtmlForSession('snapshot-session');
|
||||
const restoredTurn = $('liveAssistantTurn');
|
||||
const restoredControls = restoredTurn.querySelectorAll('.transparent-event-copy,.thinking-copy-btn');
|
||||
const restoredSnapshotState = {
|
||||
count:restoredControls.length,
|
||||
toolHtml:restoredControls[0].innerHTML,
|
||||
toolStyle:restoredControls[0].style.cssText,
|
||||
toolTitle:restoredControls[0].getAttribute('title'),
|
||||
toolAria:restoredControls[0].getAttribute('aria-label'),
|
||||
thinkingHtml:restoredControls[1].innerHTML,
|
||||
thinkingStyle:restoredControls[1].style.cssText,
|
||||
thinkingTitle:restoredControls[1].getAttribute('title'),
|
||||
thinkingAria:restoredControls[1].getAttribute('aria-label'),
|
||||
};
|
||||
const expiryDetachedWrites = [];
|
||||
snapshotTool.copy._trackDetachedWrites = expiryDetachedWrites;
|
||||
snapshotThinking.copy._trackDetachedWrites = expiryDetachedWrites;
|
||||
let snapshotExpiryThrew = false;
|
||||
try{
|
||||
runTimer(snapshotToolTimer);
|
||||
runTimer(snapshotThinkingTimer);
|
||||
}catch(_){ snapshotExpiryThrew = true; }
|
||||
const afterSnapshotExpiry = {
|
||||
toolFeedback:!!snapshotTool.row._transparentCopiedFeedback,
|
||||
thinkingFeedback:!!snapshotThinking.row._transparentCopiedFeedback,
|
||||
detachedWrites:expiryDetachedWrites,
|
||||
threw:snapshotExpiryThrew,
|
||||
restoredStillNormal:restoredControls[0].innerHTML.includes('snapshot-tool-copy') && restoredControls[1].innerHTML.includes('snapshot-thinking-copy'),
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
firstState,
|
||||
original,
|
||||
restored,
|
||||
firstTimerReplaced:clearedTimers.includes(firstTimerId) && secondTimerId !== firstTimerId,
|
||||
staleTimerIgnored,
|
||||
payload:writes[0],
|
||||
thinkingState,
|
||||
thinkingOriginalStyle,
|
||||
thinkingRestoredStyle,
|
||||
fallbackSuccess,
|
||||
failure,
|
||||
emptyCopySkipped,
|
||||
replacementRace:{
|
||||
activatedControlDetached,
|
||||
beforeResolutionCheck,
|
||||
replacementAfterSuccess,
|
||||
inheritedDuringFeedback,
|
||||
latestRaceTimerDuration,
|
||||
staleTimerDidNotClearLatest,
|
||||
detachedExpiry,
|
||||
oneFunctionalControl:race.header.querySelectorAll('.transparent-event-copy,.thinking-copy-btn').length === 1 && typeof race.copy.onclick === 'function',
|
||||
activationDidNotToggle:beforeSuccess.stopped && beforeSuccess.prevented && headerToggleCount === 0,
|
||||
},
|
||||
thinkingReplacementRace:thinkingRaceCheck,
|
||||
snapshotLifecycle:{
|
||||
storedSnapshot,
|
||||
snapshotToolNormalStyle,
|
||||
snapshotThinkingNormalStyle,
|
||||
liveAfterSnapshot,
|
||||
restoreResult,
|
||||
restoredSnapshotState,
|
||||
afterSnapshotExpiry,
|
||||
},
|
||||
}));
|
||||
})().catch(error=>{ console.error(error); process.exit(1); });
|
||||
"""
|
||||
data = _run_node_script(script, str(ROOT / "static" / "ui.js"))
|
||||
assert data["firstState"] == {
|
||||
"check": True,
|
||||
"title": "Copied",
|
||||
"aria": "Copied",
|
||||
"opacity": "1",
|
||||
"duration": 1500,
|
||||
"stopped": True,
|
||||
"prevented": True,
|
||||
"rowExpanded": "0",
|
||||
"cardOpen": False,
|
||||
}
|
||||
assert data["firstTimerReplaced"] is True
|
||||
assert data["staleTimerIgnored"] is True
|
||||
assert data["restored"] == data["original"]
|
||||
assert "tool: shell" in data["payload"]
|
||||
assert "[REDACTED]" in data["payload"]
|
||||
assert "secret-token" not in data["payload"]
|
||||
assert "tool output" in data["payload"]
|
||||
assert data["thinkingState"] == {
|
||||
"count": 1,
|
||||
"bound": True,
|
||||
"check": True,
|
||||
"opacity": "1",
|
||||
"copied": "transparent reasoning",
|
||||
}
|
||||
assert data["thinkingRestoredStyle"] == data["thinkingOriginalStyle"]
|
||||
assert data["fallbackSuccess"] == {"check": True, "toast": "Copied"}
|
||||
assert data["failure"] == {
|
||||
"check": False,
|
||||
"feedbackState": False,
|
||||
"toasts": ["Copy failed"],
|
||||
}
|
||||
assert data["emptyCopySkipped"] is True
|
||||
assert data["replacementRace"] == {
|
||||
"activatedControlDetached": True,
|
||||
"beforeResolutionCheck": False,
|
||||
"replacementAfterSuccess": {
|
||||
"check": True,
|
||||
"title": "Copied",
|
||||
"aria": "Copied",
|
||||
"opacity": "1",
|
||||
},
|
||||
"inheritedDuringFeedback": {
|
||||
"check": True,
|
||||
"title": "Copied",
|
||||
"aria": "Copied",
|
||||
"opacity": "1",
|
||||
},
|
||||
"latestRaceTimerDuration": 1500,
|
||||
"staleTimerDidNotClearLatest": True,
|
||||
"detachedExpiry": {
|
||||
"rowConnected": False,
|
||||
"controlConnected": False,
|
||||
"feedbackState": False,
|
||||
"writes": [],
|
||||
"threw": False,
|
||||
"html": '<svg data-icon="check" data-size="11"></svg>',
|
||||
"color": "var(--accent)",
|
||||
"title": "Copied",
|
||||
"aria": "Copied",
|
||||
},
|
||||
"oneFunctionalControl": True,
|
||||
"activationDidNotToggle": True,
|
||||
}
|
||||
assert data["thinkingReplacementRace"] == {
|
||||
"count": 1,
|
||||
"bound": True,
|
||||
"check": True,
|
||||
"opacity": "1",
|
||||
}
|
||||
snapshot = data["snapshotLifecycle"]
|
||||
assert "snapshot-tool-copy" in snapshot["storedSnapshot"]
|
||||
assert "snapshot-thinking-copy" in snapshot["storedSnapshot"]
|
||||
assert 'title="Snapshot tool title"' in snapshot["storedSnapshot"]
|
||||
assert 'aria-label="Snapshot tool aria"' in snapshot["storedSnapshot"]
|
||||
assert "Copied" not in snapshot["storedSnapshot"]
|
||||
assert 'data-icon="check"' not in snapshot["storedSnapshot"]
|
||||
assert "var(--accent)" not in snapshot["storedSnapshot"]
|
||||
assert "opacity: 1" not in snapshot["storedSnapshot"]
|
||||
assert snapshot["liveAfterSnapshot"] == {
|
||||
"toolCheck": True,
|
||||
"thinkingCheck": True,
|
||||
"toolTitle": "Copied",
|
||||
"toolAria": "Copied",
|
||||
"toolOpacity": "1",
|
||||
"thinkingOpacity": "1",
|
||||
"toolFeedback": True,
|
||||
"thinkingFeedback": True,
|
||||
"toolNormalSaved": True,
|
||||
"thinkingNormalSaved": True,
|
||||
}
|
||||
assert snapshot["restoreResult"] is True
|
||||
assert snapshot["restoredSnapshotState"] == {
|
||||
"count": 2,
|
||||
"toolHtml": '<svg data-original="snapshot-tool-copy"></svg>',
|
||||
"toolStyle": snapshot["snapshotToolNormalStyle"],
|
||||
"toolTitle": "Snapshot tool title",
|
||||
"toolAria": "Snapshot tool aria",
|
||||
"thinkingHtml": '<svg data-original="snapshot-thinking-copy"></svg>',
|
||||
"thinkingStyle": snapshot["snapshotThinkingNormalStyle"],
|
||||
"thinkingTitle": None,
|
||||
"thinkingAria": None,
|
||||
}
|
||||
assert snapshot["afterSnapshotExpiry"] == {
|
||||
"toolFeedback": False,
|
||||
"thinkingFeedback": False,
|
||||
"detachedWrites": [],
|
||||
"threw": False,
|
||||
"restoredStillNormal": True,
|
||||
}
|
||||
Reference in new issue
Block a user