|
786 | 786 | .compareSection h3{margin:0 0 10px;font-size:var(--fs-14)} |
787 | 787 | .compareSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px} |
788 | 788 | .compareSectionHead h3{margin:0} |
789 | | -.compareInsights{list-style:none;margin:0;padding:0;display:grid;gap:6px} |
790 | | -.compareInsights li{display:grid;grid-template-columns:10px minmax(0,1fr);gap:8px;align-items:baseline;font-size:var(--fs-13);line-height:1.5;color:var(--dim)} |
791 | | -.compareInsights li:before{content:"";width:8px;height:8px;border-radius:var(--radius-round);background:var(--accent);transform:translateY(-1px)} |
792 | | -.compareInsights li.warn:before{background:var(--warn)} |
793 | | -.compareInsights li.good:before{background:var(--good)} |
794 | | -.compareInsights li.caveat{color:var(--faint);font-size:var(--fs-12)} |
795 | | -.compareInsights li.caveat:before{background:var(--faint)} |
| 789 | +.compareWinners{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px} |
| 790 | +.compareWinners strong{display:flex;align-items:center;gap:8px} |
| 791 | +.compareWinners small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
796 | 792 | .compareTableWrap{overflow-x:auto} |
797 | 793 | .compareTable{width:100%;border-collapse:collapse;font-size:var(--fs-13)} |
798 | 794 | .compareTable th,.compareTable td{padding:6px 10px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap} |
@@ -3760,6 +3756,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
3760 | 3756 | return `<div class=h>${cursor?'Local cost estimate':'Cost calculation'}</div>${rows}<div class=total><span>Total</span><span>${money(s.total_cost||0)}${s.cost_approx?' est':''}</span></div><div class=note>${esc(note)}</div>`; |
3761 | 3757 | } |
3762 | 3758 | let LATEST=null, CURRENT=null, pinned=null, sessionRequest=0, selectedPollId='', RUNTIME_CATALOG={}; |
| 3759 | +const LIVE_XSESSION={state:{snapshot:null,revision:''},session:{snapshot:null,revision:''}}; |
3763 | 3760 | let AGENT_ACCESS=null, pendingAgentAccess=null, agentAccessLoading=false; |
3764 | 3761 | const CURRENT_SESSION_ORDER_KEY='tm_current_session_order_v1'; |
3765 | 3762 | const CURRENT_SESSION_ORDER_MIGRATION_KEY='tm_current_session_newest_first_v1'; |
@@ -3858,7 +3855,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
3858 | 3855 | function sessionScopeCopy(scope){ |
3859 | 3856 | return { |
3860 | 3857 | all:['Local session history.','Search, filter, and compare local session history.'], |
3861 | | - compare:['Selected sessions side by side.','Compare up to four selected sessions and review content-free insights.'], |
| 3858 | + compare:['Selected sessions side by side.','Compare up to four selected sessions and see which is cheapest and fastest.'], |
3862 | 3859 | subagents:['Investigate child-agent spend, completion, and role patterns.','Filter child agents, inspect incomplete work, and open their parent sessions.'], |
3863 | 3860 | }[scope]||['Live local traces · last 30 minutes.','Select a card to inspect it. Drag a card or press ⌥ and an arrow key to reorder.']; |
3864 | 3861 | } |
@@ -4358,7 +4355,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
4358 | 4355 | const NAV_COMMANDS=[ |
4359 | 4356 | {id:'sessions',label:'Sessions',description:'Choose a recently active local session',section:'Navigate',action:'sessions'}, |
4360 | 4357 | {id:'all-sessions',label:'All sessions',description:'Search, filter, and open recorded session history',aliases:'logs',section:'Navigate',route:'sessions-all',glyph:'All'}, |
4361 | | - {id:'compare-sessions',label:'Compare sessions',description:'Put selected sessions side by side with insights',aliases:'diff same prompt runs',section:'Navigate',route:'sessions-compare',glyph:'A|B'}, |
| 4358 | + {id:'compare-sessions',label:'Compare sessions',description:'Put selected sessions side by side',aliases:'diff same prompt runs',section:'Navigate',route:'sessions-compare',glyph:'A|B'}, |
4362 | 4359 | {id:'spend',label:'Spend',description:'Review spend across days and runtimes',section:'Navigate',route:'spend'}, |
4363 | 4360 | {id:'models',label:'Models',description:'Compare model spend, speed, and cache efficiency',section:'Navigate',route:'models'}, |
4364 | 4361 | {id:'subagents',label:'Subagents',description:'Track custom-agent spend and inspect child runs',section:'Navigate',route:'subagents'}, |
@@ -9997,12 +9994,18 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
9997 | 9994 | return `<article class="card compareCard" style="--compare-color:${compareColor(i)}"><div class=compareCardTop><span class=compareLetter>${s.letter}</span><span class="badge app ${appBadgeClass(s)}">${runtimeMarkSvg(s)}${esc(s.label||s.provider)}</span>${baseline&&sessions.length>1?'<span class=compareBaselineTag>Baseline</span>':''}</div><div class=compareCardTitle title="${esc(s.title)}">${esc(s.title)}</div><div class=compareCardMeta>${esc(s.primary_model||(s.models||[]).join(', ')||'model unknown')} · ${esc(s.start||'')}</div><div class="compareCardCost mono">${s.cost==null?costValueHtml('',false):esc(money(s.cost)+(s.cost_approx||s.usage_basis!=='reported'?' est':''))}</div><div class=compareCardMeta>${f(s.turns)} execs · ${s.duration_s==null?'time unavailable':esc(waitFmt(s.duration_s))+' active'} · ${s.tokens==null?'tokens unavailable':esc(compactNumber(s.tokens))+' tok'}</div><div class=compareCardActions><button type=button data-compare-open-session="${esc(s.open_id||s.id)}">Open</button>${sessions.length>1?`<button type=button data-compare-baseline="${esc(s.key)}"${baseline?' disabled':''}>${baseline?'Baseline':'Set as baseline'}</button>`:''}<button type=button data-compare-remove="${esc(s.key)}">Remove</button></div></article>`; |
9998 | 9995 | }).join('')}</div>`; |
9999 | 9996 | if(sessions.length<2){ |
10000 | | - root.innerHTML=`${notice}${cards}<section class="card compareEmpty emptyState"><h3>Add one more session</h3><p>Pick another session in All sessions, or add a matching run below, to see side-by-side metrics and insights.</p><button class=tbtn type=button data-compare-pick>Choose sessions</button></section>${compareMatchesHtml(compareData.matches,sessions)}`; |
| 9997 | + root.innerHTML=`${notice}${cards}<section class="card compareEmpty emptyState"><h3>Add one more session</h3><p>Pick another session in All sessions, or add a matching run below, to see side-by-side metrics.</p><button class=tbtn type=button data-compare-pick>Choose sessions</button></section>${compareMatchesHtml(compareData.matches,sessions)}`; |
10001 | 9998 | return; |
10002 | 9999 | } |
10003 | | - const insights=`<section class="card compareSection"><h3>Insights</h3><ul class=compareInsights>${(compareData.insights||[]).map(item=>`<li class="${esc(item.kind==='caveat'?'caveat':item.tone||'info')}">${esc(item.text)}</li>`).join('')}</ul></section>`; |
| 10000 | + const winner=(key,label,fmt)=>{ |
| 10001 | + const index=sessions.findIndex(s=>s.key===best[key]),s=sessions[index],known=sessions.filter(row=>row[key]!=null); |
| 10002 | + const value=s?`<span class=compareLetter style="--compare-color:${compareColor(index)}">${s.letter}</span>${esc(fmt(s))}`:known.length>=2?'Tied':'--'; |
| 10003 | + const note=s?esc(s.title):known.length>=2?`All at ${esc(fmt(known[0]))}`:'Not reported for enough sessions'; |
| 10004 | + return `<div class=metric><span>${label}</span><strong>${value}</strong><small${s?` title="${esc(s.title)}"`:''}>${note}</small></div>`; |
| 10005 | + }; |
| 10006 | + const winners=`<section class="card compareSection"><div class=compareWinners>${winner('cost','Cheapest',s=>money(s.cost)+(s.cost_approx||s.usage_basis!=='reported'?' est':''))}${winner('duration_s','Fastest',s=>waitFmt(s.duration_s)+' active')}</div></section>`; |
10004 | 10007 | const table=`<section class="card compareSection"><div class=compareSectionHead><h3>Side by side</h3><span class=allSessionsHint>Deltas are against baseline ${esc((sessions.find(s=>s.key===compareBaseline)||sessions[0]).letter)} · unavailable evidence shows --</span></div>${compareTableHtml(sessions,best)}</section>`; |
10005 | | - root.innerHTML=notice+cards+insights+table+compareChartHtml(sessions)+compareBreakdownHtml(sessions)+compareMatchesHtml(compareData.matches,sessions); |
| 10008 | + root.innerHTML=notice+cards+winners+table+compareChartHtml(sessions)+compareBreakdownHtml(sessions)+compareMatchesHtml(compareData.matches,sessions); |
10006 | 10009 | } |
10007 | 10010 | $('session-compare').onclick=event=>{ |
10008 | 10011 | const target=event.target.closest('button'); |
@@ -11373,14 +11376,31 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
11373 | 11376 | if(show)applyHashRoute(); |
11374 | 11377 | return refreshSelectedSession({show,notifyOnError:true}); |
11375 | 11378 | } |
| 11379 | +function adoptXsession(holder,state){ |
| 11380 | + if(state?.xsession_unchanged){ |
| 11381 | + if(holder.snapshot&&state.xsession_revision===holder.revision){state.xsession=holder.snapshot;return state;} |
| 11382 | + holder.snapshot=null;holder.revision=''; |
| 11383 | + return null; |
| 11384 | + } |
| 11385 | + holder.snapshot=state?.xsession||null; |
| 11386 | + holder.revision=holder.snapshot?String(state?.xsession_revision||''):''; |
| 11387 | + return state; |
| 11388 | +} |
| 11389 | +async function fetchLiveJson(url,holder,label){ |
| 11390 | + for(let attempt=0;attempt<2;attempt++){ |
| 11391 | + const response=await fetch(url+(url.includes('?')?'&':'?')+'xsession='+encodeURIComponent(holder.revision),{cache:'no-store'}); |
| 11392 | + if(!response.ok)throw new Error(`${label} request failed (${response.status}).`); |
| 11393 | + const state=adoptXsession(holder,await response.json()); |
| 11394 | + if(state)return state; |
| 11395 | + } |
| 11396 | + throw new Error(`${label} request failed.`); |
| 11397 | +} |
11376 | 11398 | async function refreshSelectedSession({show=true,notifyOnError=false}={}){ |
11377 | 11399 | const id=pinned,request=sessionRequest; |
11378 | 11400 | if(!id||selectedPollId===id)return null; |
11379 | 11401 | selectedPollId=id; |
11380 | 11402 | try{ |
11381 | | - const response=await fetch('/session?id='+encodeURIComponent(id)+'&live=1',{cache:'no-store'}); |
11382 | | - if(!response.ok)throw new Error(`Session request failed (${response.status}).`); |
11383 | | - const state=await response.json(); |
| 11403 | + const state=await fetchLiveJson('/session?id='+encodeURIComponent(id)+'&live=1',LIVE_XSESSION.session,'Session'); |
11384 | 11404 | if(request!==sessionRequest||pinned!==id)return null; |
11385 | 11405 | if(!state||!stateSessionId(state))throw new Error('Session was not found.'); |
11386 | 11406 | CURRENT=state; |
@@ -11461,9 +11481,7 @@ <h3 id=session-model-identity-title>Assign model to this session</h3> |
11461 | 11481 | if(statePollBusy)return; |
11462 | 11482 | statePollBusy=true; |
11463 | 11483 | try{ |
11464 | | - const response=await fetch('/state',{cache:'no-store'}); |
11465 | | - if(!response.ok)throw new Error(`State request failed (${response.status}).`); |
11466 | | - applyLiveState(await response.json()); |
| 11484 | + applyLiveState(await fetchLiveJson('/state',LIVE_XSESSION.state,'State')); |
11467 | 11485 | }catch(error){ |
11468 | 11486 | if($('view-capabilities').classList.contains('on')){ |
11469 | 11487 | if(capabilityLastGoodAt)setCapabilityAvailability('stale'); |
|
0 commit comments