fix(#4024): trifecta UX + correctness pass — rehydrate live-restore, thinking dedup, quiet thinking rows, legible failures, skin-proof reset, compact args, flat output, live-only fade/anim
Functional (Codex+Opus trifecta findings): - C1: restoreLiveTurnHtmlForSession now calls _rehydrateTransparentStreamDom (controls were dead after active-session live-turn restore). - O-Bug1: per-turn echoed-thinking dedup in the transparent settled path (mirrors the compact seenReasons), fixes duplicate out-of-order Thinking rows. - O-Bug2: tool completion preserves the row's open state + Full/Output tab. - O-Edge: settled/reloaded done===false tools show 'Interrupted' (not a permanent Running shimmer); progress bar clears for Interrupted too. Visual (trifecta CSS): - V1: reset .thinking-card to flat/transparent (was the heaviest object in the stream). - V2: failed=error color + left border, running/interrupted legible (was an invisible muted badge). - V3: scoped !important reset beats the 6 per-skin .tool-card re-card rules. - V4: inline key:value args (was a tall stacked form). - V5/V7: output pre flattened to a quiet left-rail, max-height:none (one expanded zone, no nested scroll). - V6: drop redundant tool/status pseudo-args from the detail body. - V8: row fading gated to the live turn only + floor raised for WCAG; settled history stays full-opacity. - V9: entrance animation scoped to #liveAssistantTurn (was replaying on every renderMessages). - V10: larger mobile header tap targets + active detail-tab underline. O-Bug3 (live thinking overwrite when no seq) intentionally deferred — documented follow-up; settled render is already correct and a live heuristic risks a streaming regression.
This commit is contained in:
@@ -3175,9 +3175,12 @@ body.resizing .sidebar{transition:none!important;}
|
||||
background:transparent;
|
||||
overflow:hidden;
|
||||
box-shadow:none;
|
||||
animation:transparent-event-enter .18s ease-out both;
|
||||
transition:background .18s ease,border-color .18s ease,opacity .22s ease;
|
||||
}
|
||||
/* Only animate entrance for rows in the LIVE turn — putting the animation on the
|
||||
base row class replayed the whole transcript's shimmer on every renderMessages
|
||||
rebuild. (Trifecta finding V9.) */
|
||||
#liveAssistantTurn .transparent-event-row{animation:transparent-event-enter .18s ease-out both;}
|
||||
.transparent-event-row[data-event-type="thinking"]{
|
||||
background:transparent;
|
||||
border-color:transparent;
|
||||
@@ -3193,10 +3196,29 @@ body.resizing .sidebar{transition:none!important;}
|
||||
border-color:var(--border-subtle);
|
||||
background:color-mix(in srgb,var(--surface-subtle) 48%,transparent);
|
||||
}
|
||||
.transparent-event-row .tool-card{
|
||||
border:0;
|
||||
border-radius:0;
|
||||
background:transparent;
|
||||
/* Transparent Stream: both tool AND thinking inner cards must reset to a flat,
|
||||
transparent row. The thinking-card previously kept its base accent-bg/border/
|
||||
radius/msg-rail chrome (it was the heaviest object in the stream — Frank's
|
||||
complaint, trifecta V1). The scoped !important also beats the per-skin
|
||||
`:root[data-skin] .tool-card{...!important}` rules (graphite/sienna/catppuccin/
|
||||
hepburn/neon/verdigris) that otherwise re-card every row (trifecta V3). */
|
||||
.transparent-event-row .tool-card,
|
||||
.transparent-event-row .tool-card:hover,
|
||||
.transparent-event-row .tool-card-running,
|
||||
.transparent-event-row .thinking-card,
|
||||
.transparent-event-row .thinking-card:hover,
|
||||
:root[data-skin] .transparent-event-row .tool-card,
|
||||
:root[data-skin] .transparent-event-row .tool-card:hover,
|
||||
:root[data-skin] .transparent-event-row .tool-card-running,
|
||||
:root[data-skin] .transparent-event-row .thinking-card,
|
||||
:root[data-skin] .transparent-event-row .thinking-card:hover{
|
||||
border:0!important;
|
||||
border-left:0!important;
|
||||
border-radius:0!important;
|
||||
background:transparent!important;
|
||||
box-shadow:none!important;
|
||||
margin:0!important;
|
||||
max-width:none!important;
|
||||
overflow:visible;
|
||||
}
|
||||
.transparent-event-row .tool-card-header,
|
||||
@@ -3285,6 +3307,19 @@ body.resizing .sidebar{transition:none!important;}
|
||||
border-radius:6px;
|
||||
background:transparent;
|
||||
}
|
||||
/* 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.) */
|
||||
.transparent-event-status[data-status="failed"]{color:var(--error,#e5484d);opacity:1;font-weight:600;}
|
||||
.transparent-event-status[data-status="running"]{color:var(--accent-text);opacity:.95;font-weight:600;}
|
||||
.transparent-event-status[data-status="interrupted"]{color:var(--warning,var(--muted));opacity:.95;font-weight:600;}
|
||||
.transparent-event-row[data-event-status="Failed"]{
|
||||
border-left-color:color-mix(in srgb,var(--error,#e5484d) 55%,transparent)!important;
|
||||
}
|
||||
.transparent-event-row[data-event-status="Failed"]:hover,
|
||||
.transparent-event-row[data-event-status="Failed"][data-expanded="1"]{
|
||||
border-left-color:color-mix(in srgb,var(--error,#e5484d) 70%,transparent)!important;
|
||||
}
|
||||
.transparent-event-copy{
|
||||
border:0;
|
||||
background:transparent;
|
||||
@@ -3360,8 +3395,8 @@ body.resizing .sidebar{transition:none!important;}
|
||||
color:var(--muted);
|
||||
font-size:10.5px;
|
||||
}
|
||||
.transparent-detail-mode{cursor:pointer;opacity:.72;transition:opacity .18s ease,color .18s ease;}
|
||||
.transparent-detail-mode.active{color:var(--text);opacity:1;font-weight:600;}
|
||||
.transparent-detail-mode{cursor:pointer;opacity:.72;padding:2px 4px 3px;transition:opacity .18s ease,color .18s ease,box-shadow .18s ease;}
|
||||
.transparent-detail-mode.active{color:var(--text);opacity:1;font-weight:600;box-shadow:inset 0 -1px 0 var(--accent);}
|
||||
.transparent-event-row .tool-card-detail,
|
||||
.transparent-event-row .thinking-card-body{
|
||||
display:block;
|
||||
@@ -3391,18 +3426,26 @@ body.resizing .sidebar{transition:none!important;}
|
||||
.transparent-event-row .tool-card-detail > *,
|
||||
.transparent-event-row .thinking-card-body > *{min-height:0;}
|
||||
.transparent-event-row .tool-card-args,
|
||||
.transparent-event-row .tool-card-result{padding:5px 7px;}
|
||||
.transparent-event-row .tool-card-result{padding:4px 7px 4px 9px;}
|
||||
.transparent-event-row .tool-card-detail[data-transparent-detail-mode="output"] .tool-card-args{display:none;}
|
||||
.transparent-event-row .tool-card-args{margin-bottom:0;}
|
||||
.transparent-event-row .tool-arg-pair{display:flex;flex-direction:column;gap:1px;padding:3px 0;border-bottom:1px solid color-mix(in srgb,var(--border-subtle) 72%,transparent);}
|
||||
.transparent-event-row .tool-arg-pair:last-child{border-bottom:0;}
|
||||
.transparent-event-row .tool-arg-key{color:var(--muted);font-size:10px;font-family:var(--font-mono);display:block;margin-bottom:0;letter-spacing:.01em;}
|
||||
.transparent-event-row .tool-arg-val{font-family:var(--font-mono);font-size:var(--message-code-font-size);color:var(--text);line-height:1.5;}
|
||||
/* Compact inline args: key beside value (not stacked), no per-pair rules — the
|
||||
stacked column layout made offset=735 consume two ruled lines and read like a
|
||||
form. Restores the old .tool-card-args density. (Trifecta V4/V5.) */
|
||||
.transparent-event-row .tool-arg-pair{display:flex;flex-direction:row;align-items:baseline;gap:8px;padding:1px 0;border-bottom:0;}
|
||||
.transparent-event-row .tool-arg-key{flex:0 0 auto;min-width:54px;color:var(--muted);font-size:10px;font-family:var(--font-mono);letter-spacing:.01em;opacity:.85;}
|
||||
.transparent-event-row .tool-arg-val{flex:1 1 auto;min-width:0;font-family:var(--font-mono);font-size:var(--message-code-font-size);color:var(--text);line-height:1.5;white-space:pre-wrap;word-break:break-word;}
|
||||
/* Flatten the output box into a quiet left-rail instead of a second bordered
|
||||
card, so args + output read as ONE expanded zone, not two disjoint cards.
|
||||
max-height:none kills the nested scrollbar — the detail's own scroll owns it. */
|
||||
.transparent-event-row .tool-card-result pre{
|
||||
background:var(--code-bg);
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:6px;
|
||||
padding:5px 7px;
|
||||
background:transparent;
|
||||
border:0;
|
||||
border-left:2px solid var(--border-subtle);
|
||||
border-radius:0;
|
||||
padding:2px 0 2px 9px;
|
||||
margin:0;
|
||||
max-height:none;
|
||||
color:var(--muted);
|
||||
line-height:1.5;
|
||||
}
|
||||
@@ -3418,6 +3461,11 @@ body.resizing .sidebar{transition:none!important;}
|
||||
@media(max-width:600px){
|
||||
.transparent-event-controls,
|
||||
.transparent-event-row{margin-left:0;width:100%;max-width:none;}
|
||||
/* Larger header tap targets on mobile (the 19px desktop header is a poor
|
||||
touch target). (Trifecta finding V10.) */
|
||||
.transparent-event-row .tool-card-header,
|
||||
.transparent-event-row .thinking-card-header{min-height:30px;padding:5px 8px;}
|
||||
.transparent-detail-mode{padding:4px 6px 5px;}
|
||||
}
|
||||
@keyframes transparent-event-enter{
|
||||
from{opacity:0;transform:translateY(2px);}
|
||||
@@ -3425,11 +3473,12 @@ body.resizing .sidebar{transition:none!important;}
|
||||
}
|
||||
|
||||
/* ── Old-event fading (transparent mode: medium → low) ─────────────── */
|
||||
.transparent-event-row[data-transparent-fade="1"]{opacity:.82;}
|
||||
.transparent-event-row[data-transparent-fade="2"]{opacity:.66;}
|
||||
.transparent-event-row[data-transparent-fade="3"]{opacity:.52;}
|
||||
.transparent-event-row[data-transparent-fade="4"]{opacity:.4;}
|
||||
.transparent-event-row[data-transparent-fade="5"]{opacity:.32;}
|
||||
/* ── Old-event fading (transparent mode, LIVE turn only — gated in JS) ───── */
|
||||
.transparent-event-row[data-transparent-fade="1"]{opacity:.86;}
|
||||
.transparent-event-row[data-transparent-fade="2"]{opacity:.76;}
|
||||
.transparent-event-row[data-transparent-fade="3"]{opacity:.68;}
|
||||
.transparent-event-row[data-transparent-fade="4"]{opacity:.6;}
|
||||
.transparent-event-row[data-transparent-fade="5"]{opacity:.54;}
|
||||
.transparent-event-row[data-transparent-fade]{transition:opacity .22s ease;}
|
||||
.transparent-event-row[data-transparent-fade]:hover{opacity:1;}
|
||||
|
||||
|
||||
84
static/ui.js
84
static/ui.js
@@ -5346,6 +5346,14 @@ function restoreLiveTurnHtmlForSession(sid){
|
||||
_mergeRestoredLiveAssistantSegment(restored, existing);
|
||||
if(existing) existing.replaceWith(restored);
|
||||
else inner.appendChild(restored);
|
||||
// Transparent Stream: liveTurnHtml is restored via template.innerHTML, which
|
||||
// drops the property-bound onclick/onkeydown handlers wired by
|
||||
// _wireTransparentHeaderToggle / _attachCopyButton / _syncTransparentEventControls /
|
||||
// _wireTransparentTurnToggle. The settled cache fast-path re-runs the rehydrate;
|
||||
// this active-session live-turn restore path must too, or row toggles, copy
|
||||
// buttons, expand/collapse, and the turn chevron silently stop working after a
|
||||
// session-switch/reconnect restore. (Codex trifecta finding C1.)
|
||||
if(typeof _rehydrateTransparentStreamDom==='function') _rehydrateTransparentStreamDom(restored);
|
||||
if(typeof normalizeLiveActivityGroupPlacement==='function') normalizeLiveActivityGroupPlacement(restored);
|
||||
const liveGroup=restored.querySelector('.tool-call-group[data-live-tool-call-group="1"]');
|
||||
if(liveGroup&&typeof _startActivityElapsedTimer==='function') _startActivityElapsedTimer(liveGroup);
|
||||
@@ -6619,9 +6627,9 @@ function _transparentEventPreview(text){
|
||||
if(!clean) return '';
|
||||
return clean.length>180?`${clean.slice(0,177)}...`:clean;
|
||||
}
|
||||
function _transparentToolStatus(tc){
|
||||
function _transparentToolStatus(tc, settled){
|
||||
if(tc&&tc.is_error) return 'Failed';
|
||||
if(tc&&tc.done===false) return 'Running';
|
||||
if(tc&&tc.done===false) return settled?'Interrupted':'Running';
|
||||
return 'Completed';
|
||||
}
|
||||
function _copyEventToClipboard(row){
|
||||
@@ -6763,10 +6771,10 @@ function _wireTransparentHeaderToggle(header){
|
||||
function _transparentToolDetailHtml(tc, status){
|
||||
const args=tc&&tc.args&&typeof tc.args==='object'?tc.args:{};
|
||||
const argEntries=Object.entries(args);
|
||||
const meta=[
|
||||
['tool', tc&&tc.name?tc.name:'tool'],
|
||||
['status', status||_transparentToolStatus(tc)],
|
||||
];
|
||||
// The tool name is already shown in the row header and the status is shown as
|
||||
// a badge, so don't repeat them as pseudo-args in the body. Only surface a
|
||||
// duration meta when present. (Trifecta finding V6 — reduce redundancy.)
|
||||
const meta=[];
|
||||
if(tc&&tc.duration!==undefined&&tc.duration!==null) meta.push(['duration', String(tc.duration)]);
|
||||
const preview=String((tc&&(tc.snippet||tc.preview||tc.result||tc.output))||'').trim();
|
||||
const argHtml=[...meta,...argEntries].map(([k,v])=>`<div class="tool-arg-pair"><span class="tool-arg-key">${esc(String(k))}</span><span class="tool-arg-val">${esc(typeof v==='string'?v:JSON.stringify(v,null,2))}</span></div>`).join('');
|
||||
@@ -6891,7 +6899,7 @@ function _decorateTransparentEventRow(row, opts){
|
||||
// Update the 3D progress bar to reflect the new status.
|
||||
const progress=card.querySelector('.transparent-event-progress');
|
||||
if(progress){
|
||||
if(status==='Completed'||status==='Failed'){
|
||||
if(status==='Completed'||status==='Failed'||status==='Interrupted'){
|
||||
progress.removeAttribute('data-progress-running');
|
||||
progress.setAttribute('data-progress-percent','100%');
|
||||
progress.style.setProperty('--transparent-progress-percent','100%');
|
||||
@@ -7061,9 +7069,19 @@ function _wireTransparentTurnToggle(turn){
|
||||
// at 0.32 so labels stay readable.
|
||||
function _applyTransparentRowFading(turn){
|
||||
if(!turn||!isTransparentStream()) return;
|
||||
// Recency-fading only makes sense on the LIVE turn (draw the eye to the most
|
||||
// recent activity). On settled/historical turns it permanently dims the trace
|
||||
// below readable contrast (floor .32) — the opposite of a transparent record.
|
||||
// So clear any fade on non-live turns and only fade the live turn.
|
||||
// (Trifecta finding V8.)
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return;
|
||||
const rows=Array.from(blocks.querySelectorAll(':scope > .transparent-event-row'));
|
||||
const isLive=turn.id==='liveAssistantTurn'||turn.getAttribute('data-live-assistant-turn')==='1';
|
||||
if(!isLive){
|
||||
rows.forEach(row=>row.removeAttribute('data-transparent-fade'));
|
||||
return;
|
||||
}
|
||||
const total=rows.length;
|
||||
for(let i=0;i<total;i++){
|
||||
const row=rows[i];
|
||||
@@ -9396,6 +9414,11 @@ function renderMessages(options){
|
||||
}else{
|
||||
// ── transparent_stream path: individual expandable event rows ──
|
||||
const transparentInsertCursors=new Map();
|
||||
// Per-turn dedup of echoed thinking text — mirrors the compact-worklog
|
||||
// path's `seenReasons` Set (the transparent branch previously had none,
|
||||
// so the same echoed reasoning rendered twice, once out of chronological
|
||||
// position). Keyed by the assistant turn element. (Trifecta finding O-Bug1.)
|
||||
const transparentSeenThinking=new Map();
|
||||
for(const entry of activityOrder){
|
||||
const event={
|
||||
...entry,
|
||||
@@ -9422,22 +9445,32 @@ function renderMessages(options){
|
||||
else blocks.appendChild(row);
|
||||
};
|
||||
if(event.thinkingText){
|
||||
const thinkingRow=_decorateTransparentEventRow(_thinkingActivityNode(event.thinkingText,false),{
|
||||
type:'thinking',
|
||||
text:event.thinkingText,
|
||||
preview:event.thinkingText,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
});
|
||||
if(!anchorIsWorklogSource) insertBeforeAnchor(thinkingRow);
|
||||
else insertAfterCursor(thinkingRow);
|
||||
const _thinkKey=typeof _normalizeThinkingEchoCompare==='function'
|
||||
? _normalizeThinkingEchoCompare(event.thinkingText)
|
||||
: String(event.thinkingText).trim();
|
||||
let _seen=transparentSeenThinking.get(anchorTurn);
|
||||
if(!_seen){_seen=new Set();transparentSeenThinking.set(anchorTurn,_seen);}
|
||||
if(_thinkKey&&_seen.has(_thinkKey)){
|
||||
// Echoed reasoning already rendered for this turn — skip the duplicate.
|
||||
}else{
|
||||
if(_thinkKey)_seen.add(_thinkKey);
|
||||
const thinkingRow=_decorateTransparentEventRow(_thinkingActivityNode(event.thinkingText,false),{
|
||||
type:'thinking',
|
||||
text:event.thinkingText,
|
||||
preview:event.thinkingText,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
});
|
||||
if(!anchorIsWorklogSource) insertBeforeAnchor(thinkingRow);
|
||||
else insertAfterCursor(thinkingRow);
|
||||
}
|
||||
}
|
||||
for(const toolCall of cards){
|
||||
event.toolCall=toolCall;
|
||||
const toolRow=_decorateTransparentEventRow(buildToolCard(event.toolCall),{
|
||||
type:'tool',
|
||||
name:event.toolCall&&event.toolCall.name,
|
||||
status:_transparentToolStatus(event.toolCall),
|
||||
status:_transparentToolStatus(event.toolCall,true),
|
||||
toolCall:event.toolCall,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
@@ -10359,6 +10392,23 @@ function appendLiveToolCard(tc){
|
||||
burstId,
|
||||
});
|
||||
replacement.dataset.liveTid=tid;
|
||||
// Preserve the user's expand state + detail tab across tool completion:
|
||||
// the running row is rebuilt fresh on toolComplete, which would otherwise
|
||||
// snap an expanded row shut and reset its Full/Output tab. (Trifecta O-Bug2.)
|
||||
try{
|
||||
const _oldCard=existing.querySelector('.tool-card,.thinking-card');
|
||||
const _newCard=replacement.querySelector('.tool-card,.thinking-card');
|
||||
if(_oldCard&&_newCard&&_oldCard.classList.contains('open')){
|
||||
_setTransparentCardOpen(_newCard,true);
|
||||
const _oldDetail=existing.querySelector('.tool-card-detail');
|
||||
const _newDetail=replacement.querySelector('.tool-card-detail');
|
||||
const _mode=_oldDetail&&_oldDetail.getAttribute('data-transparent-detail-mode');
|
||||
if(_newDetail&&_mode){
|
||||
const _tab=_newDetail.querySelector(`.transparent-detail-mode[data-mode="${_mode}"]`);
|
||||
if(_tab) _setTransparentDetailMode(_tab,_mode);
|
||||
}
|
||||
}
|
||||
}catch(_){ /* non-fatal: completion still renders, just collapsed */ }
|
||||
existing.replaceWith(replacement);
|
||||
_syncTransparentEventControls(turn);
|
||||
_moveLiveRunStatusToTurnEnd();
|
||||
|
||||
Reference in New Issue
Block a user