diff --git a/CHANGELOG.md b/CHANGELOG.md
index 281d2bbf..a2884950 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/static/ui.js b/static/ui.js
index 3d4c0dad..566029a8 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -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){
diff --git a/tests/test_issue5367_transparent_live_row_reconcile.py b/tests/test_issue5367_transparent_live_row_reconcile.py
index 04b4f16d..c3669f7c 100644
--- a/tests/test_issue5367_transparent_live_row_reconcile.py
+++ b/tests/test_issue5367_transparent_live_row_reconcile.py
@@ -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 = '';
+ 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 = '';
+ 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)=>``;
+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 = '';
+ 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:'',
+ 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:'',
+ 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 = '';
+ 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:'',
+ 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:'',
+ 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 = '';
+ 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 = '';
+ 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": '',
+ "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": '',
+ "toolStyle": snapshot["snapshotToolNormalStyle"],
+ "toolTitle": "Snapshot tool title",
+ "toolAria": "Snapshot tool aria",
+ "thinkingHtml": '',
+ "thinkingStyle": snapshot["snapshotThinkingNormalStyle"],
+ "thinkingTitle": None,
+ "thinkingAria": None,
+ }
+ assert snapshot["afterSnapshotExpiry"] == {
+ "toolFeedback": False,
+ "thinkingFeedback": False,
+ "detachedWrites": [],
+ "threw": False,
+ "restoredStillNormal": True,
+ }