@qiaolei81/copilot-session-viewer 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,751 +0,0 @@
1
- (()=>{var $e=(u,g)=>()=>(g||u((g={exports:{}}).exports,g),g.exports);var qe=$e((Gt,ie)=>{function Vt(u){let g=new Map,f=new Map,x=0;for(let n of u)if(n.type==="subagent.started"){let l=n.data?.toolCallId;l&&f.set(l,{name:n.data?.agentDisplayName||n.data?.agentName||"SubAgent",colorIndex:x++,meta:{agentName:n.data?.agentName||"",agentDisplayName:n.data?.agentDisplayName||"",agentDescription:n.data?.agentDescription||""}})}for(let n of u){if(n.type!=="tool.execution_start")continue;let l=n.data?.toolCallId;if(!l||!f.has(l))continue;let v=f.get(l),p=n.data?.arguments;if(typeof p=="string")try{p=JSON.parse(p)}catch{continue}!p||typeof p!="object"||(p.description&&(v.meta.taskDescription=p.description),p.name&&(v.meta.taskName=p.name),p.agent_type&&(v.meta.agentType=p.agent_type),p.mode&&(v.meta.taskMode=p.mode),p.description&&v.name===(v.meta.agentDisplayName||v.meta.agentName)&&(v.name=p.description))}for(let n of u)if(n.type==="assistant.message"&&n.data?.subAgentName&&n.data?.subAgentId){let l=n.data.subAgentId;f.has(l)||f.set(l,{name:n.data.subAgentName,colorIndex:x++,meta:{agentName:n.data.subAgentName}}),g.set(n.stableId,l)}for(let n of u)if(n._subagent?.id){let l=n._subagent.id;f.has(l)||f.set(l,{name:n._subagent.name||"SubAgent",colorIndex:x++,meta:{agentName:n._subagent.name||""}}),g.set(n.stableId,l)}if(f.size===0)return{ownerMap:g,subagentInfo:f};let V=new Map;for(let n of u)n.id&&V.set(n.id,n);for(let n of u)if(n.type==="assistant.message"){let l=n.data?.parentToolCallId;l&&f.has(l)&&g.set(n.stableId,l)}for(let n of u){if(n.type!=="reasoning")continue;let l=n.parentId,v=0;for(;l&&v<10;){let p=V.get(l);if(!p)break;if(p.type==="assistant.message"){let C=p.data?.parentToolCallId;C&&f.has(C)&&g.set(n.stableId,C);break}l=p.parentId,v++}}let m=new Map;for(let n of u){if(n.type!=="tool.execution_start")continue;let l=n.parentId,v=0;for(;l&&v<10;){let p=V.get(l);if(!p)break;if(p.type==="assistant.message"){let C=p.data?.parentToolCallId;if(C&&f.has(C)){g.set(n.stableId,C);let X=n.data?.toolCallId;X&&m.set(X,C)}break}l=p.parentId,v++}}for(let n of u){if(n.type!=="tool.execution_complete")continue;let l=n.data?.toolCallId;l&&m.has(l)&&g.set(n.stableId,m.get(l))}for(let n of u){if(n.type!=="tool.invocation")continue;let l=n.data?.parentToolCallId;l&&f.has(l)&&g.set(n.stableId,l)}let b=null;for(let n of u)n.type==="subagent.started"&&n.data?.toolCallId&&f.has(n.data.toolCallId)?b=n.data.toolCallId:(n.type==="subagent.completed"||n.type==="subagent.failed")&&n.data?.toolCallId===b?b=null:b&&!g.has(n.stableId)&&g.set(n.stableId,b);for(let[n,l]of f)for(let v of u){if(!(g.get(v.stableId)===n||v._subagent?.id===n||v.data?.subAgentId===n))continue;let C=v.model||v.data?.model;if(C){l.meta.model=C;break}}return{ownerMap:g,subagentInfo:f}}function Pt(u,g,f){return g?u.filter(x=>(x.type==="subagent.started"||x.type==="subagent.completed"||x.type==="subagent.failed")&&x.data?.toolCallId===g||f.get(x.stableId)===g||x._subagent?.id===g||x.data?.subAgentId===g):u}typeof ie<"u"&&ie.exports&&(ie.exports={computeSubagentOwnership:Vt,filterBySubagent:Pt})});var Fe=$e((Kt,re)=>{function Ue(u){if(!u||typeof u!="object")return 0;let g=Number.isFinite(u.inputTokens)?u.inputTokens:0,f=Number.isFinite(u.cacheReadTokens)?u.cacheReadTokens:0,x=Number.isFinite(u.cacheWriteTokens)?u.cacheWriteTokens:0;return Math.max(g-f-x,0)}function jt(u){if(!u||typeof u!="object")return null;let g=Number.isFinite(u.cacheReadTokens)?u.cacheReadTokens:0,f=Ue(u)+g;return g===0||f===0?null:Math.round(g/f*100)}typeof re<"u"&&re.exports&&(re.exports={getDisplayInputTokens:Ue,getCacheHitRatio:jt})});(function(){let{computeSubagentOwnership:u,filterBySubagent:g}=qe(),{getDisplayInputTokens:f,getCacheHitRatio:x}=Fe();if(typeof Vue>"u"){console.error("Vue is not loaded");return}if(typeof window.VueVirtualScroller>"u"){console.error("VueVirtualScroller is not loaded");return}console.log("Initializing Vue app...");let{createApp:V,ref:m,computed:b,onMounted:n,onBeforeUnmount:l,watch:v,nextTick:p}=Vue,{DynamicScroller:C,DynamicScrollerItem:X}=window.VueVirtualScroller,ve=V({components:{DynamicScroller:C,DynamicScrollerItem:X},setup(){let h=m(window.__PAGE_DATA.sessionId),w=m(window.__PAGE_DATA.metadata),le=m(!1),fe=()=>window.innerWidth<=640,Y=m(fe()?!0:localStorage.getItem("sidebarCollapsed")==="true");v(Y,e=>{fe()||localStorage.setItem("sidebarCollapsed",e.toString())});let D=m({}),L=m({}),Q=50,be=()=>{let e=Object.keys(D.value);e.length>Q&&e.slice(0,e.length-Q).forEach(s=>delete D.value[s]);let t=Object.keys(L.value);t.length>Q&&t.slice(0,t.length-Q).forEach(s=>delete L.value[s])},R=m("all"),P=m(""),U=m(""),he=m(0),k=m(null),Te=m({start:0,end:0}),A=m(null),Z=m(!1),de=m(""),ye=m(null),ce=m(!1),Be=b(()=>{let e=0;return R.value!=="all"&&e++,A.value&&e++,P.value.trim()&&e++,e}),ze=()=>{R.value="all",A.value=null,P.value="",U.value="",ce.value=!1},j=null,ee=null;v(P,e=>{clearTimeout(j),j=setTimeout(()=>{U.value=e,e.trim()&&window.trackClick&&window.trackClick("SearchUsed",{query:e.substring(0,50),resultCount:B.value.length,sessionId:h.value})},300)}),v(R,()=>{be()}),v(U,()=>{be()}),v(Z,e=>{e&&p(()=>{ye.value?.focus()})});let O=m([]),ke=m(!0),xe=m(null),S=b(()=>O.value.filter(t=>t.type!=="assistant.turn_end"&&t.type!=="assistant.turn_complete").sort((t,a)=>{let s=t.timestamp?new Date(t.timestamp).getTime():0,o=a.timestamp?new Date(a.timestamp).getTime():0;return s!==o?s-o:(t._fileIndex??0)-(a._fileIndex??0)}).map((t,a)=>({...t,virtualIndex:a,stableId:t.id||`${t.timestamp}-${t.type}-${a}`}))),Ve=e=>{if(!U.value.trim())return!0;let t=U.value.toLowerCase();return[e.data?.message,e.data?.text,e.data?.content,e.data?.reason,e.data?.reasoningText,e.data?.errorType,e.data?.previousModel,e.data?.newModel].filter(Boolean).join(" ").toLowerCase().includes(t)},B=b(()=>{let e=a=>{let s=a.type||"";return s!=="tool.execution_start"&&s!=="tool.execution_complete"},t=S.value.filter(e);return U.value.trim()&&(t=t.filter(Ve)),t}),H=b(()=>{let e=B.value;if(A.value){let{ownerMap:s}=G.value;e=g(e,A.value,s)}R.value!=="all"&&(e=e.filter(s=>s.type===R.value));let t=["assistant.turn_start","subagent.started","subagent.completed","subagent.failed"],a=e.length;return e.map((s,o)=>{let i=e[o+1],r=o===a-1,d=i&&t.includes(i.type);return{...s,filteredIndex:o,filteredTotal:a,isLastEvent:r||d}})}),Pe=b(()=>{let e={};return B.value.forEach(t=>{t.type&&(e[t.type]=(e[t.type]||0)+1)}),e}),je=b(()=>{if(!U.value.trim())return null;let e=B.value.length;return e>0?`${e} result${e!==1?"s":""}`:"No matches"}),He=b(()=>{let e=Object.keys(D.value).filter(a=>D.value[a]).length,t=Object.keys(L.value).filter(a=>L.value[a]).length;return e+t}),we=b(()=>{let e=B.value.length,t=[{type:"all",label:`All (${e})`,count:e}],a={};B.value.forEach(o=>{o.type&&(a[o.type]=(a[o.type]||0)+1)});let s=Object.entries(a).sort((o,i)=>i[1]-o[1]).map(([o,i])=>({type:o,label:`${o} (${i})`,count:i,disabled:!1}));return[...t,...s]}),W=b(()=>{let e=S.value.filter(a=>a.type==="assistant.turn_start"),t=S.value.filter(a=>a.type==="user.message");return e.map((a,s)=>{let o=s,i=new Date(a.timestamp).getTime(),r,d=e.indexOf(a)+1;d<e.length?r=new Date(e[d].timestamp).getTime():r=Date.now();let c=r-i,T=Math.floor(c/1e3),y=Math.floor(T/60),$=T%60,q=y>0?`${y}m ${$}s`:`${$}s`,z=S.value.slice(0,S.value.indexOf(a)).reverse().find(I=>I.type==="user.message"),ae=z?t.indexOf(z)+1:0;return{id:o,index:a.virtualIndex,originalTurnId:a.data?.turnId,timestamp:a.timestamp,duration:q,message:z?.data?.content||z?.data?.transformedContent||"",userReqNumber:ae}})}),We=b(()=>{let e=[],t=new Map;return W.value.forEach(a=>{let s=a.userReqNumber||0;if(!t.has(s)){let o={reqNumber:s,message:a.message,turns:[]};t.set(s,o),e.push(o)}t.get(s).turns.push(a)}),e}),Ge=(e,t)=>e?e.length<=t?e:e.substring(0,t)+"\u2026":"",G=b(()=>u(S.value)),ue=b(()=>{let{subagentInfo:e}=G.value;if(e.size===0)return[];let t=[];for(let[a,s]of e)t.push({toolCallId:a,name:s.name,colorIndex:s.colorIndex,meta:s.meta||{}});return t}),Ke=b(()=>{let e=de.value.toLowerCase().trim();return e?ue.value.filter(t=>{let a=t.meta||{};return[t.name,a.taskName,a.taskDescription,a.agentName,a.agentType,a.agentDescription,a.model].filter(Boolean).join(" ").toLowerCase().includes(e)}):ue.value}),Je=b(()=>{if(!A.value)return null;let{ownerMap:e,subagentInfo:t}=G.value,a=A.value;if(!t.has(a))return null;let s=0,o=null,i=null;for(let d of S.value){let c=(d.type==="subagent.started"||d.type==="subagent.completed"||d.type==="subagent.failed")&&d.data?.toolCallId===a,T=e.get(d.stableId)===a,y=d._subagent?.id===a,$=d.data?.subAgentId===a;if((c||T||y||$)&&(s++,d.timestamp!==null&&d.timestamp!==void 0)){let q=new Date(d.timestamp).getTime();(o===null||q<o)&&(o=q),(i===null||q>i)&&(i=q)}}let r=o===null||i===null?0:i-o;return{eventCount:s,durationMs:r}}),Xe=e=>{if(!e)return"";let t=new Date(e),a=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),o=String(t.getSeconds()).padStart(2,"0");return`${a}:${s}:${o}`},Ye=e=>{if(!e)return"";let t=new Date(e),a=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),o=String(t.getSeconds()).padStart(2,"0"),i=String(t.getMilliseconds()).padStart(3,"0");return`${a}:${s}:${o}.${i}`},E=new Map,Ce=200,Qe=e=>{if(!e)return"";if(E.has(e))return E.get(e);try{let t=e.replace(/\\r\\n/g,`
2
- `).replace(/\\n/g,`
3
- `).replace(/\\t/g," ").replace(/\\"/g,'"').replace(/\\\\/g,"\\"),a={ALLOWED_TAGS:["p","br","strong","em","code","pre","a","ul","ol","li","h1","h2","h3","h4","h5","h6","blockquote","table","thead","tbody","tr","th","td","hr","del","span","div","mark"],ALLOWED_ATTR:["href","style","class"],ALLOW_DATA_ATTR:!1,ALLOWED_URI_REGEXP:/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i},s=t.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);if(s){let r=s[1],d=s[2],c=r.split(`
4
- `),T=[],y=0;for(;y<c.length;){let I=c[y];if(!I.trim()||!I.includes(":")){y++;continue}let se=I.indexOf(":"),ne=I.substring(0,se).trim(),oe=I.substring(se+1).trim();if(oe==="|"||oe===">"){let _e=[];for(y++;y<c.length&&(c[y].startsWith(" ")||c[y].startsWith(" ")||c[y].trim()==="");)_e.push(c[y].trim()),y++;let Bt=oe===">"?" ":`
5
- `;T.push({key:ne,value:_e.filter(zt=>zt).join(Bt)})}else T.push({key:ne,value:oe}),y++}let $='<table style="margin-bottom: 16px; border-collapse: collapse; width: 100%;"><tbody>';T.forEach(I=>{let se=DOMPurify.sanitize(I.key,{ALLOWED_TAGS:[]}),ne=DOMPurify.sanitize(I.value,{ALLOWED_TAGS:[]});$+=`<tr><td style="padding: 4px 12px; border: 1px solid #30363d; font-weight: 600; color: #7d8590;">${se}</td><td style="padding: 4px 12px; border: 1px solid #30363d;">${ne}</td></tr>`}),$+="</tbody></table>";let q=marked.parse(d),z=DOMPurify.sanitize(q,a),ae=$+z;if(E.size>=Ce){let I=E.keys().next().value;E.delete(I)}return E.set(e,ae),ae}let o=marked.parse(t),i=DOMPurify.sanitize(o,a);if(E.size>=Ce){let r=E.keys().next().value;E.delete(r)}return E.set(e,i),i}catch{return e}},Ze=e=>{let t={...D.value},a=!!t[e];t[e]?delete t[e]:t[e]=!0,D.value=t,window.trackClick&&window.trackClick("EventExpanded",{eventType:"tool",action:a?"collapse":"expand",sessionId:h.value})},et=(e,t)=>{if(!t||!t.trim()||!e)return e;let a=t.trim(),s=Me(a).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),o=document.createElement("div");o.innerHTML=e;let i=r=>{if(r.nodeType===Node.TEXT_NODE){let d=r.textContent,c=new RegExp(`(${s})`,"gi");if(c.test(d)){let T=d.replace(c,'<mark class="search-highlight">$1</mark>'),y=document.createElement("span");y.innerHTML=T,r.parentNode.replaceChild(y,r)}}else r.nodeType===Node.ELEMENT_NODE&&r.tagName!=="SCRIPT"&&r.tagName!=="STYLE"&&Array.from(r.childNodes).forEach(i)};return Array.from(o.childNodes).forEach(i),o.innerHTML},tt=e=>{let t={...L.value},a=!!t[e];t[e]?delete t[e]:t[e]=!0,L.value=t,window.trackClick&&window.trackClick("EventExpanded",{eventType:"content",action:a?"collapse":"expand",sessionId:h.value})},at=e=>e?e.split(`
6
- `).length>20||e.length>2e3:!1,st=e=>{let t=e.split(`
7
- `);return t.length<=20?e:t.slice(0,20).join(`
8
- `)+`
9
-
10
- ...`},nt=(e,t)=>{if(t?.data?.badgeLabel&&t?.data?.badgeClass)return{label:t.data.badgeLabel,class:t.data.badgeClass};if(e==="message"&&t?.data?.role==="toolResult")return{label:"TOOL RESULT",class:"badge-tool"};if(e==="session.model_change")return{label:"MODEL CHANGE",class:"badge-session"};if(e==="session.truncation")return{label:"TRUNCATION",class:"badge-truncation"};if(e==="session.compaction_start"||e==="session.compaction_complete")return{label:"COMPACTION",class:"badge-compaction"};if(e==="system.notification")return{label:"SYSTEM",class:"badge-system"};let s=(e||"").split(".")[0]||"unknown";return{user:{label:"USER",class:"badge-user"},assistant:{label:"ASSISTANT",class:"badge-assistant"},reasoning:{label:"REASONING",class:"badge-reasoning"},turn:{label:"TURN",class:"badge-turn"},tool:{label:"TOOL",class:"badge-tool"},subagent:{label:"SUBAGENT",class:"badge-subagent"},skill:{label:"SKILL",class:"badge-skill"},session:{label:"SESSION",class:"badge-session"},error:{label:"ERROR",class:"badge-error"},abort:{label:"ABORT",class:"badge-error"}}[s]||{label:s.toUpperCase(),class:"badge-info"}},ot=e=>{if(!e.complete)return{icon:"\u23F3",color:"tool-status-running",text:""};let t=e.complete.data||{};return t.error||t.isError?{icon:"\u274C",color:"tool-status-error",text:""}:{icon:"\u2713",color:"tool-status-success",text:""}},it=e=>{if(!e.complete?.data?.error)return"";let t=e.complete.data.error;if(typeof t=="object"&&t.message)return t.message;if(typeof t=="string"){try{let a=JSON.parse(t);if(a.message)return a.message}catch{}return t}return String(t)},rt=e=>{if(!e.complete)return"";let t=new Date(e.start.timestamp).getTime(),s=new Date(e.complete.timestamp).getTime()-t;return s>=100?`${parseFloat((s/1e3).toPrecision(3))}s`:""},Ht=e=>{let t={};if(e.start?.timestamp&&(t.startTime=e.start.timestamp),e.complete?.timestamp&&(t.endTime=e.complete.timestamp),t.startTime&&t.endTime){let a=new Date(t.endTime).getTime()-new Date(t.startTime).getTime();a>=0&&(t.duration=`${parseFloat((a/1e3).toPrecision(3))}s (${a}ms)`)}return t},lt=e=>{if(!e.start)return"";let t=e.start.data?.arguments||{},a=e.start.data?.toolName||e.tool||"",s="";if(a==="bash"||a==="exec")s=t.command||t.description||"";else if(a==="ask_user")s=t.question||t.message||"";else if(a==="read"||a==="write"||a==="edit")s=t.file_path||t.path||"";else if(a==="view")s=t.path||t.file||"";else if(a==="create")s=t.path||t.name||"";else if(a==="report_intent")s=t.intent||t.message||"";else if(a==="web_search")s=t.query||"";else if(a==="web_fetch")s=t.url||"";else if(a==="browser"){let o=t.action||"",i=t.targetUrl||t.url||"";s=i?`${o} ${i}`:o}else s=t.description||t.command||t.message||t.path||t.file_path||t.query||"";return s&&s.length>200&&(s=s.substring(0,200)+"..."),s},dt=e=>e.data?.tools&&e.data.tools.length>0,ct=e=>e.data?.tools&&Array.isArray(e.data.tools)?e.data.tools.filter(t=>t&&typeof t=="object"&&t.name).map(t=>{let a=t.result!==void 0||t.status==="completed"||t.status==="error",s={};if(t.startTime&&(s.startTime=t.startTime),t.endTime&&(s.endTime=t.endTime),s.startTime&&s.endTime){let o=new Date(s.endTime).getTime()-new Date(s.startTime).getTime();o>=0&&(s.duration=`${parseFloat((o/1e3).toPrecision(3))}s (${o}ms)`)}return{tool:t.name,timing:s,start:{timestamp:t.startTime,data:{toolName:t.name,arguments:t.input||t.arguments||{}}},complete:a?{timestamp:t.endTime,data:{result:t.result,error:t.status==="error"?t.error:null}}:null}}):[],me=["#58a6ff","#f0883e","#a371f7","#3fb950","#f778ba","#79c0ff","#d29922","#56d4dd"],ut=e=>{let t=0;for(let a=0;a<e.length;a++){let s=e.charCodeAt(a);t=(t<<5)-t+s,t=t&t}return t},Se=e=>{let{ownerMap:t,subagentInfo:a}=G.value;if(e.type==="subagent.started"||e.type==="subagent.completed"||e.type==="subagent.failed"){let i=e.data?.toolCallId;if(i&&a.has(i)){let r=a.get(i);return{name:r.name,toolCallId:i,colorIndex:r.colorIndex}}return null}if(e._subagent){let i=e._subagent.id,r=e._subagent.name;if(a.has(i)){let d=a.get(i);return{name:d.name,toolCallId:i,colorIndex:d.colorIndex}}return{name:r,toolCallId:i,colorIndex:Math.abs(ut(i))}}if(e.data?.subAgentId){let i=e.data.subAgentId,r=a.get(i);if(r)return{name:r.name,toolCallId:i,colorIndex:r.colorIndex}}let s=t.get(e.stableId);if(!s)return null;let o=a.get(s);return o?{name:o.name,toolCallId:s,colorIndex:o.colorIndex}:null},mt=e=>{let t=Se(e);return t?me[t.colorIndex%me.length]:null},gt=e=>{if(window.trackClick){let t=we.value.find(a=>a.type===e);window.trackClick("EventFilterClicked",{filterType:e,filterLabel:t?t.label:e,sessionId:h.value})}R.value=e},pt=e=>{A.value=e,Z.value=!1,de.value="",e&&(R.value="all"),window.trackClick&&window.trackClick("SubagentSelected",{subagent:e,sessionId:h.value})},Ie=e=>{P.value="",R.value="all",A.value=null,he.value=e.id,Vue.nextTick(()=>{if(k.value){let t=H.value.findIndex(a=>a.virtualIndex===e.index);if(t>=0){let a=s=>{s<=0||!k.value||(k.value.scrollToItem(t),setTimeout(()=>a(s-1),100))};setTimeout(()=>a(3),50)}}})},vt=()=>{if(!k.value)return;let e=t=>{t<=0||!k.value||(k.value.scrollToItem(0),setTimeout(()=>e(t-1),100))};e(3)},ft=()=>{if(!k.value)return;let e=H.value.length-1,t=a=>{a<=0||!k.value||(k.value.scrollToItem(e),setTimeout(()=>t(a-1),100))};t(5)},Ee=e=>{window.trackClick&&window.trackClick("TurnClicked",{turnNumber:e,sessionId:h.value});let t=W.value.find(a=>a.id===e);if(t){let a=`UserReq${t.userReqNumber}_Turn${t.id}`,s=`${window.location.pathname}?eventType=assistant.turn_start&eventName=${a}`;window.history.pushState({},"",s),Ie(t)}},bt=e=>{if(!e)return"";let t=e.replace(/\/$/,"").split("/");return t[t.length-1]||e},ht=e=>{let t=W.value.find(s=>s.index===e);if(!t)return"?";let a=t.originalTurnId??t.id;return t.userReqNumber>0?`${t.userReqNumber} - Turn ${a}`:`Turn ${a}`},Tt=e=>W.value.find(a=>a.index===e)?.duration||null,Me=e=>{let t=document.createElement("div");return t.textContent=e,t.innerHTML},yt=e=>e?new Date(e).toLocaleString():"N/A",kt=async()=>{console.log("[Export] exportSession called"),window.trackClick&&window.trackClick("ExportClicked",{sessionId:h.value}),le.value=!0;try{console.log("[Export] Fetching:",`/session/${h.value}/export`);let e=await fetch(`/session/${h.value}/export`);if(console.log("[Export] Response received:",e.status,e.ok),console.log("[Export] Response received:",e.status,e.ok),!e.ok)throw new Error("Share failed");console.log("[Export] Creating blob...");let t=await e.blob();console.log("[Export] Blob size:",t.size,"type:",t.type);let a=window.URL.createObjectURL(t);console.log("[Export] Creating download link...");let s=document.createElement("a");s.href=a,s.download=`session-${h.value}.zip`,document.body.appendChild(s),s.click(),console.log("[Export] Download triggered"),window.URL.revokeObjectURL(a),document.body.removeChild(s),console.log("[Export] Showing success feedback...");let o="\u{1F4E4} Share Session",i="\u2713 Downloaded!",r=document.querySelector(".export-btn");r&&(r.textContent=i,r.style.background="#238636",console.log("[Export] Button text updated to:",r.textContent),setTimeout(()=>{r.textContent=o,r.style.background="",console.log("[Export] Button text restored")},2e3))}catch(e){console.error("[Export] Share session error:",e),alert("Failed to share session: "+e.message)}finally{le.value=!1,console.log("[Export] Export complete")}},Ne=e=>{let t=document.querySelector(".filter-type-wrapper");t&&!t.contains(e.target)&&(ce.value=!1)},Re=e=>{e.ctrlKey&&e.key==="b"&&(e.preventDefault(),Y.value=!Y.value)};l(()=>{document.removeEventListener("click",Ne),window.removeEventListener("keydown",Re),j&&(clearTimeout(j),j=null),ee&&(ee(),ee=null),D.value={},L.value={},E.clear()}),n(async()=>{document.addEventListener("click",Ne),document.addEventListener("click",()=>{Z.value=!1});try{console.log("[Navigation] Starting event loading...");let t=await fetch(`/api/sessions/${h.value}/events`);if(!t.ok)throw new Error(`Failed to load events: ${t.statusText}`);let a=await t.json();if(Array.isArray(a))O.value=a;else if(a.events&&Array.isArray(a.events))O.value=a.events,console.log("[Navigation] Pagination:",a.pagination);else throw new Error("Invalid response format");if(console.log("[Navigation] Events loaded:",O.value.length),O.value.length>0){let d=O.value[O.value.length-1],c=d.timestamp||d.time||d.data?.timestamp;c&&(w.value.updated=new Date(c))}let s=new URLSearchParams(window.location.search),o=s.get("eventType"),i=s.get("eventName"),r=s.get("eventTimestamp");console.log("[Navigation] URL params:",o,i,r),o&&i&&(console.log("[Navigation] Waiting for Vue to render..."),Vue.nextTick(()=>{console.log("[Navigation] nextTick - flatEvents count:",S.value?.length);let d=null;if(o==="assistant.turn_start"){let c=i.match(/UserReq(\d+)_Turn(\d+)/);if(c){let T=parseInt(c[2],10);if(!isNaN(T)){console.log("[Navigation] Jumping to turn:",T),Ee(T);return}}}else o==="subagent.started"?(console.log("[Navigation] Searching for subagent:",i,"timestamp:",r),r&&(d=S.value.find(c=>c.type==="subagent.started"&&c.timestamp===r)),d||(d=S.value.find(c=>c.type==="subagent.started"&&(c.data?.agentDisplayName===i||c.data?.agentName===i||c.data?.label===i))),console.log("[Navigation] Target event found:",d?"YES":"NO","virtualIndex:",d?.virtualIndex)):d=S.value.find(c=>c.type===o);if(d){let c=H.value.findIndex(T=>T.virtualIndex===d.virtualIndex);if(console.log("[Navigation] Target in filteredEvents at index:",c),c>=0&&k.value){console.log("[Navigation] Scrolling to index:",c);let T=y=>{y<=0||!k.value||(k.value.scrollToItem(c),setTimeout(()=>T(y-1),100))};setTimeout(()=>T(3),50)}else console.log("[Navigation] Failed - targetIndex:",c,"scrollerRef:",!!k.value)}else console.log("[Navigation] Target event not found")}))}catch(t){console.error("Error loading events:",t),xe.value=t.message}finally{ke.value=!1}window.addEventListener("keydown",Re),window.marked&&marked.setOptions({breaks:!0,gfm:!0});let e=()=>{if(!k.value)return;let t=null;if(k.value.$el&&typeof k.value.$el.querySelector=="function"?t=k.value.$el.querySelector(".vue-recycle-scroller"):k.value.querySelector&&typeof k.value.querySelector=="function"&&(t=k.value.querySelector(".vue-recycle-scroller")),t||(t=document.querySelector(".vue-recycle-scroller")),t){let a=t.scrollTop,s=t.clientHeight,o=80,i=Math.floor(a/o),r=Math.ceil(s/o),d=Math.min(i+r,H.value.length),c=Math.max(1,i+1),T=Math.max(1,d);Te.value={start:Math.min(c,T),end:T}}};setTimeout(()=>{e();let t=document.querySelector(".vue-recycle-scroller");t&&(t.addEventListener("scroll",e),ee=()=>{t.removeEventListener("scroll",e)})},500)});let K=m([]),ge=m([]),J=m(!1),M=m([]),_=m(""),pe=m(null),N=m(""),F=m(!1),te=m([]),Ae=m(0),xt=e=>!e||e===0?"0":e<1e3?e.toString():Math.floor(e/1e3)+"K",wt=e=>{if(!e||e===0)return"0s";let t=Math.floor(e/1e3);if(t<60)return(e/1e3).toFixed(1)+"s";let a=Math.floor(t/60),s=t%60;return`${a}m ${s}s`},Ct=b(()=>{if(!w.value.usage||!w.value.usage.modelMetrics)return 0;let e=0;for(let t in w.value.usage.modelMetrics){let a=w.value.usage.modelMetrics[t].usage;a&&(e+=(a.inputTokens||0)+(a.outputTokens||0))}return e}),St=b(()=>{if(!w.value.usage||!w.value.usage.modelMetrics)return 0;let e=0;for(let t in w.value.usage.modelMetrics)e+=w.value.usage.modelMetrics[t].requests?.count||0;return e}),It=b(()=>!w.value.usage||!w.value.usage.modelMetrics?0:Object.keys(w.value.usage.modelMetrics).length),Et=e=>{let t=w.value.usage?.modelMetrics[e];return!t||!t.usage?null:x(t.usage)},Mt=e=>{let t=w.value.usage?.modelMetrics[e];return!t||!t.usage?0:f(t.usage)},Nt=b(()=>{let e=new Map;for(let t of S.value)if(t.data?.tools&&Array.isArray(t.data.tools))for(let a of t.data.tools)a&&a.name&&e.set(a.name,(e.get(a.name)||0)+1);return Array.from(e,([t,a])=>({name:t,count:a})).sort((t,a)=>a.count-t.count)}),Rt=e=>e==null?"":e+" premium",De=["#3b82f6","#10b981","#f59e0b","#ef4444","#8b5cf6","#ec4899","#06b6d4","#f97316"],At=e=>{let t=0;for(let a=0;a<e.length;a++)t=e.charCodeAt(a)+((t<<5)-t);return De[Math.abs(t)%De.length]},Dt=async()=>{try{let e=await fetch(`/api/sessions/${h.value}/tags`);if(e.ok){let t=await e.json();K.value=t.tags||[]}}catch(e){console.error("Error loading tags:",e)}},Le=async()=>{try{let e=await fetch("/api/tags");if(e.ok){let t=await e.json();ge.value=t.tags||[]}}catch(e){console.error("Error loading all tags:",e)}},Lt=async e=>{try{window.trackClick&&e.filter(s=>!K.value.includes(s)).forEach(s=>{window.trackClick("TagAdded",{sessionId:h.value,tag:s})});let t=await fetch(`/api/sessions/${h.value}/tags`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({tags:e})});if(t.ok){let a=await t.json();return K.value=a.tags||[],N.value="",!0}else{let a=await t.json();return N.value=a.error||"Failed to save tags",!1}}catch(t){return console.error("Error saving tags:",t),N.value="Network error",!1}},Ot=()=>{M.value=[...K.value],J.value=!0,N.value="",setTimeout(()=>{pe.value&&pe.value.focus()},10)},_t=()=>{J.value=!1,M.value=[],_.value="",F.value=!1,N.value=""},Oe=()=>{let e=_.value.trim().toLowerCase();if(e){if(e.length>30){N.value="Tag must be 30 characters or less";return}if(M.value.length>=10){N.value="Maximum 10 tags per session";return}if(M.value.includes(e)){N.value="Tag already added",_.value="";return}M.value.push(e),_.value="",F.value=!1,N.value=""}},$t=e=>{M.value=M.value.filter(t=>t!==e),N.value=""},qt=()=>{let e=_.value.trim().toLowerCase();if(!e){F.value=!1,te.value=[];return}let t=ge.value.filter(a=>a.toLowerCase().includes(e)&&!M.value.includes(a)).slice(0,5);t.length>0?(F.value=!0,te.value=t,Ae.value=0):(F.value=!1,te.value=[])},Ut=e=>{_.value=e,Oe()},Ft=async()=>{setTimeout(async()=>{if(!J.value)return;await Lt(M.value)&&(J.value=!1,M.value=[],_.value="",F.value=!1,await Le())},200)};return n(async()=>{await Dt(),await Le()}),{sessionId:h,metadata:w,exporting:le,sidebarCollapsed:Y,expandedTools:D,expandedContent:L,expansionCount:He,currentFilter:R,searchText:P,currentTurnIndex:he,scrollerRef:k,visibleRange:Te,loadedEvents:O,eventsLoading:ke,eventsError:xe,flatEvents:S,filteredEvents:H,eventCounts:Pe,filters:we,turns:W,userReqs:We,truncateText:Ge,formatTime:Xe,formatToolTime:Ye,formatDateTime:yt,renderMarkdown:Qe,highlightSearchText:et,toggleTool:Ze,toggleContent:tt,isContentTooLong:at,truncateContent:st,getBadgeInfo:nt,getToolStatus:ot,getToolErrorMessage:it,getToolDuration:rt,getToolCommand:lt,hasTools:dt,getToolGroups:ct,getSubagentInfo:Se,getSubagentColor:mt,subagentOwnership:G,setFilter:gt,selectSubagent:pt,selectedSubagent:A,subagentList:ue,filteredSubagentList:Ke,subagentDropdownOpen:Z,subagentSearchQuery:de,subagentSearchRef:ye,subagentTokenUsage:Je,SUBAGENT_COLORS:me,typeFilterOpen:ce,activeFilterCount:Be,clearAllFilters:ze,scrollToTurn:Ie,scrollToTop:vt,scrollToBottom:ft,jumpToTurn:Ee,getTurnNumber:ht,getTurnDuration:Tt,repoBasename:bt,escapeHtml:Me,exportSession:kt,searchResultCount:je,sessionTags:K,allTags:ge,tagsEditing:J,editingTags:M,tagInputValue:_,tagInputRef:pe,tagsError:N,showAutocomplete:F,autocompleteOptions:te,autocompleteSelectedIndex:Ae,getTagColor:At,startEditTags:Ot,cancelEditTags:_t,addTag:Oe,removeTagFromEdit:$t,updateAutocomplete:qt,selectAutocompleteOption:Ut,saveTagsOnBlur:Ft,formatTokens:xt,formatDuration:wt,formatCost:Rt,totalTokens:Ct,totalRequests:St,totalModels:It,getDisplayUsageInputTokens:Mt,getModelCacheHitRatio:Et,toolCallingSummary:Nt}},template:`
11
- <div class="container">
12
- <div class="header">
13
- <a href="/" class="home-btn">\u2190 Back to Home</a>
14
- <h1>\u{1F4CB} Session: {{ sessionId }}
15
- <span v-if="metadata.sessionStatus === 'wip'" style="font-size: 12px; padding: 2px 8px; border-radius: 3px; background: rgba(210, 153, 34, 0.2); color: #d29922; border: 1px solid rgba(210, 153, 34, 0.4); vertical-align: middle; margin-left: 8px;">\u{1F504} WIP</span>
16
- </h1>
17
- <div style="display: flex; gap: 10px;">
18
- <a :href="'/session/' + sessionId + '/time-analyze'" class="time-analyze-btn" @click="trackClick && trackClick('TimeAnalyzeClicked', { sessionId: sessionId })">\u23F1 Analysis</a>
19
- <button @click="exportSession" class="export-btn" :disabled="exporting" v-if="!metadata.source || !['vscode', 'modernize'].includes(metadata.source)">
20
- {{ exporting ? '\u23F3 Sharing...' : '\u{1F4E4} Share Session' }}
21
- </button>
22
- </div>
23
- </div>
24
-
25
- <div class="main-layout">
26
- <!-- Mobile overlay backdrop -->
27
- <div
28
- v-if="!sidebarCollapsed"
29
- @click="sidebarCollapsed = true"
30
- class="sidebar-backdrop"
31
- ></div>
32
- <div :class="['sidebar', { collapsed: sidebarCollapsed }]">
33
- <div class="sidebar-section">
34
- <div class="sidebar-section-title">Session Info</div>
35
- <div class="session-info">
36
- <table class="session-info-table">
37
- <tbody>
38
- <tr v-if="metadata.source">
39
- <td>Source</td>
40
- <td>
41
- <!-- Use backend-provided source metadata (Violation #3 fix) -->
42
- <span :class="['source-badge', metadata.sourceBadgeClass || 'source-copilot']">
43
- {{ metadata.sourceName || 'GitHub Copilot' }}
44
- </span>
45
- </td>
46
- </tr>
47
- <tr v-if="metadata.modernizeVersion">
48
- <td>Version</td>
49
- <td>{{ metadata.modernizeVersion }}</td>
50
- </tr>
51
- <tr v-if="metadata.source === 'modernize' && metadata.copilotVersion">
52
- <td>Copilot SDK</td>
53
- <td>{{ metadata.copilotVersion }}</td>
54
- </tr>
55
- <tr v-if="metadata.copilotVersion && metadata.source !== 'modernize'">
56
- <td>Version</td>
57
- <td>{{ metadata.copilotVersion }}</td>
58
- </tr>
59
- <tr v-if="metadata.model">
60
- <td>Model</td>
61
- <td>{{ metadata.model }}</td>
62
- </tr>
63
- <tr v-if="metadata.agentName">
64
- <td>Agent</td>
65
- <td>\u{1F916} {{ metadata.agentName }}</td>
66
- </tr>
67
- <tr v-if="metadata.repo">
68
- <td>Repo</td>
69
- <td>{{ metadata.repo }}</td>
70
- </tr>
71
- <tr v-if="metadata.branch">
72
- <td>Branch</td>
73
- <td>{{ metadata.branch }}</td>
74
- </tr>
75
- <tr v-if="metadata.cwd && !metadata.repo">
76
- <td>Repo</td>
77
- <td>{{ metadata.cwd }}</td>
78
- </tr>
79
- <tr v-if="metadata.created">
80
- <td>Created</td>
81
- <td>{{ formatDateTime(metadata.created) }}</td>
82
- </tr>
83
- <tr v-if="metadata.updated">
84
- <td>Updated</td>
85
- <td>{{ formatDateTime(metadata.updated) }}</td>
86
- </tr>
87
- </tbody>
88
- </table>
89
- </div>
90
- </div>
91
-
92
- <!-- Usage Section -->
93
- <div v-if="metadata.usage" class="sidebar-section">
94
- <div class="sidebar-section-title">Token Usage</div>
95
- <div class="usage-container">
96
- <div class="usage-summary">
97
- <div class="usage-summary-eyebrow">Overview</div>
98
- <div class="usage-summary-total">
99
- {{ formatTokens(totalTokens) }} <span class="usage-summary-total-unit">tokens</span>
100
- </div>
101
- <div class="usage-summary-caption">
102
- Usage captured across {{ totalModels }} model{{ totalModels === 1 ? '' : 's' }}
103
- </div>
104
- <div class="usage-summary-metrics">
105
- <div class="usage-metric-card usage-metric-card-summary">
106
- <span class="usage-metric-label">Requests</span>
107
- <span class="usage-metric-value">{{ totalRequests }} reqs</span>
108
- </div>
109
- <div class="usage-metric-card usage-metric-card-summary">
110
- <span class="usage-metric-label">Models</span>
111
- <span class="usage-metric-value">{{ totalModels }}</span>
112
- </div>
113
- <div class="usage-metric-card usage-metric-card-summary">
114
- <span class="usage-metric-label">API Time</span>
115
- <span class="usage-metric-value">{{ formatDuration(metadata.usage.totalApiDurationMs) }}</span>
116
- </div>
117
- </div>
118
- </div>
119
-
120
- <div class="usage-expanded">
121
- <!-- Model breakdown -->
122
- <div v-if="Object.keys(metadata.usage.modelMetrics).length > 0" class="usage-section">
123
- <div class="usage-section-header">
124
- <div class="usage-section-title">Models</div>
125
- <div class="usage-section-badge">{{ totalModels }}</div>
126
- </div>
127
- <div class="usage-model-list">
128
- <div v-for="(metrics, model) in metadata.usage.modelMetrics" :key="model" class="usage-model">
129
- <div class="usage-model-header">
130
- <div class="usage-model-name" :title="model">{{ model }}</div>
131
- <div class="usage-model-meta">
132
- <span class="usage-meta-pill">{{ metrics.requests?.count || 0 }} reqs</span>
133
- <span v-if="metrics.requests?.cost" class="usage-meta-pill usage-meta-pill-premium">{{ formatCost(metrics.requests.cost) }}</span>
134
- <span v-if="getModelCacheHitRatio(model) !== null" class="usage-meta-pill usage-meta-pill-cache">{{ getModelCacheHitRatio(model) }}% cache</span>
135
- </div>
136
- </div>
137
- <div v-if="metrics.usage" class="usage-metric-grid">
138
- <div class="usage-metric-card">
139
- <span class="usage-metric-label">Input</span>
140
- <span class="usage-metric-value">{{ formatTokens(getDisplayUsageInputTokens(model)) }}</span>
141
- </div>
142
- <div class="usage-metric-card">
143
- <span class="usage-metric-label">Output</span>
144
- <span class="usage-metric-value">{{ formatTokens(metrics.usage.outputTokens || 0) }}</span>
145
- </div>
146
- <div v-if="metrics.usage?.cacheReadTokens" class="usage-metric-card">
147
- <span class="usage-metric-label">Cache Read</span>
148
- <span class="usage-metric-value">{{ formatTokens(metrics.usage.cacheReadTokens) }}</span>
149
- </div>
150
- <div v-if="metrics.usage?.cacheWriteTokens" class="usage-metric-card">
151
- <span class="usage-metric-label">Cache Write</span>
152
- <span class="usage-metric-value">{{ formatTokens(metrics.usage.cacheWriteTokens) }}</span>
153
- </div>
154
- </div>
155
- </div>
156
- </div>
157
- </div>
158
-
159
- <!-- Context window breakdown -->
160
- <div v-if="metadata.usage.currentTokens || metadata.usage.systemTokens || metadata.usage.conversationTokens || metadata.usage.toolDefinitionsTokens" class="usage-section">
161
- <div class="usage-section-header">
162
- <div class="usage-section-title">Context Window</div>
163
- </div>
164
- <div class="usage-metric-grid">
165
- <div v-if="metadata.usage.currentTokens" class="usage-metric-card">
166
- <span class="usage-metric-label">Current</span>
167
- <span class="usage-metric-value">{{ formatTokens(metadata.usage.currentTokens) }}</span>
168
- </div>
169
- <div v-if="metadata.usage.systemTokens" class="usage-metric-card">
170
- <span class="usage-metric-label">System</span>
171
- <span class="usage-metric-value">{{ formatTokens(metadata.usage.systemTokens) }}</span>
172
- </div>
173
- <div v-if="metadata.usage.conversationTokens" class="usage-metric-card">
174
- <span class="usage-metric-label">Conversation</span>
175
- <span class="usage-metric-value">{{ formatTokens(metadata.usage.conversationTokens) }}</span>
176
- </div>
177
- <div v-if="metadata.usage.toolDefinitionsTokens" class="usage-metric-card">
178
- <span class="usage-metric-label">Tools</span>
179
- <span class="usage-metric-value">{{ formatTokens(metadata.usage.toolDefinitionsTokens) }}</span>
180
- </div>
181
- </div>
182
- </div>
183
-
184
- <!-- Code changes -->
185
- <div v-if="metadata.usage.codeChanges && (metadata.usage.codeChanges.linesAdded > 0 || metadata.usage.codeChanges.linesRemoved > 0)" class="usage-section">
186
- <div class="usage-section-header">
187
- <div class="usage-section-title">Code Changes</div>
188
- </div>
189
- <div class="usage-metric-grid usage-metric-grid-compact">
190
- <div class="usage-metric-card">
191
- <span class="usage-metric-label">Added</span>
192
- <span class="usage-metric-value usage-metric-value-added">+{{ metadata.usage.codeChanges.linesAdded }}</span>
193
- </div>
194
- <div class="usage-metric-card">
195
- <span class="usage-metric-label">Removed</span>
196
- <span class="usage-metric-value usage-metric-value-removed">-{{ metadata.usage.codeChanges.linesRemoved }}</span>
197
- </div>
198
- <div class="usage-metric-card">
199
- <span class="usage-metric-label">Files</span>
200
- <span class="usage-metric-value">{{ metadata.usage.codeChanges.filesModified?.length || 0 }}</span>
201
- </div>
202
- </div>
203
- </div>
204
- </div>
205
- </div>
206
- </div>
207
-
208
- <!-- Tool Calling Summary -->
209
- <div v-if="toolCallingSummary.length" class="sidebar-section">
210
- <div class="sidebar-section-title">Tool Calls</div>
211
- <div class="tool-summary-list">
212
- <div v-for="item in toolCallingSummary" :key="item.name" class="tool-summary-item">
213
- <div class="tool-summary-bar" :style="{ width: (item.count / toolCallingSummary[0].count * 100) + '%' }"></div>
214
- <span class="tool-summary-name" :title="item.name">{{ item.name }}</span>
215
- <span class="tool-summary-count">{{ item.count }}</span>
216
- </div>
217
- </div>
218
- </div>
219
-
220
- <!-- Session Tags -->
221
- <div class="sidebar-section session-tags-container">
222
- <div class="sidebar-section-title">Tags</div>
223
- <div v-if="!tagsEditing" class="tags-display">
224
- <span
225
- v-for="tag in sessionTags"
226
- :key="tag"
227
- class="tag-label"
228
- :style="{ backgroundColor: getTagColor(tag) }"
229
- >
230
- {{ tag }}
231
- </span>
232
- <button class="tags-edit-btn" @click="startEditTags" title="Edit tags">
233
- \u270F\uFE0F
234
- </button>
235
- </div>
236
- <div v-else class="tags-dropdown">
237
- <div class="tags-input-container">
238
- <span
239
- v-for="tag in editingTags"
240
- :key="tag"
241
- class="tag-input-chip"
242
- :style="{ backgroundColor: getTagColor(tag) }"
243
- >
244
- {{ tag }}
245
- <button @click="removeTagFromEdit(tag)" title="Remove tag">\xD7</button>
246
- </span>
247
- <input
248
- ref="tagInputRef"
249
- v-model="tagInputValue"
250
- @keydown.enter.prevent="addTag"
251
- @keydown.escape="cancelEditTags"
252
- @blur="saveTagsOnBlur"
253
- @input="updateAutocomplete"
254
- class="tags-text-input"
255
- placeholder="Type tag name..."
256
- maxlength="30"
257
- />
258
- </div>
259
- <div v-if="showAutocomplete && autocompleteOptions.length > 0" class="tags-autocomplete">
260
- <div
261
- v-for="(option, index) in autocompleteOptions"
262
- :key="option"
263
- :class="['tags-autocomplete-item', { selected: index === autocompleteSelectedIndex }]"
264
- @click="selectAutocompleteOption(option)"
265
- @mouseenter="autocompleteSelectedIndex = index"
266
- >
267
- {{ option }}
268
- </div>
269
- </div>
270
- <div v-if="tagsError" class="tags-error">{{ tagsError }}</div>
271
- </div>
272
- </div>
273
- </div>
274
-
275
- <div class="content">
276
- <div class="unified-filter-bar">
277
- <div class="filter-bar-row">
278
- <button
279
- class="sidebar-toggle"
280
- @click="() => { sidebarCollapsed = !sidebarCollapsed; trackClick && trackClick('SidebarToggled', { state: sidebarCollapsed ? 'open' : 'collapsed', sessionId: sessionId }); }"
281
- :title="sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
282
- >
283
- \u2630
284
- </button>
285
-
286
- <div class="filter-bar-search">
287
- <input
288
- v-model="searchText"
289
- type="text"
290
- placeholder="\u{1F50D} Search events..."
291
- class="search-input"
292
- />
293
- <span v-if="searchResultCount" class="search-result-count">
294
- {{ searchResultCount }}
295
- </span>
296
- </div>
297
-
298
- <div class="filter-bar-divider"></div>
299
-
300
- <!-- Turn dropdown with optgroup -->
301
- <select
302
- v-if="turns.length > 0"
303
- v-model="currentTurnIndex"
304
- @change="jumpToTurn(currentTurnIndex)"
305
- class="turn-dropdown"
306
- >
307
- <optgroup
308
- v-for="req in userReqs"
309
- :key="req.reqNumber"
310
- :label="req.reqNumber > 0 ? 'UserReq ' + req.reqNumber + ': ' + truncateText(req.message, 40) : 'Setup'"
311
- >
312
- <option v-for="turn in req.turns" :key="turn.id" :value="turn.id">
313
- Turn {{ turn.originalTurnId ?? turn.id }} ({{ turn.duration }})
314
- </option>
315
- </optgroup>
316
- </select>
317
-
318
- <div class="filter-bar-divider"></div>
319
-
320
- <!-- Subagent selector (rich dropdown with search) -->
321
- <div v-if="subagentList.length > 0" class="subagent-selector" style="position:relative">
322
- <button
323
- class="subagent-dropdown-trigger"
324
- @click.stop="subagentDropdownOpen = !subagentDropdownOpen"
325
- >
326
- <span class="subagent-trigger-icon">\u{1F916}</span>
327
- <span class="subagent-trigger-label">{{ selectedSubagent ? (subagentList.find(s => s.toolCallId === selectedSubagent)?.name || 'Agent') : 'All Agents' }}</span>
328
- <span class="subagent-trigger-arrow">\u25BE</span>
329
- </button>
330
- <div v-if="subagentDropdownOpen" class="subagent-dropdown-panel" @click.stop>
331
- <input
332
- class="subagent-search-input"
333
- v-model="subagentSearchQuery"
334
- placeholder="Search agents..."
335
- ref="subagentSearchRef"
336
- @keydown.escape="subagentDropdownOpen = false"
337
- />
338
- <div class="subagent-dropdown-list">
339
- <div
340
- class="subagent-dropdown-item"
341
- :class="{ active: !selectedSubagent }"
342
- @click="selectSubagent(null)"
343
- >
344
- <div class="subagent-item-name">\u{1F916} All Agents</div>
345
- </div>
346
- <div
347
- v-for="sa in filteredSubagentList"
348
- :key="sa.toolCallId"
349
- class="subagent-dropdown-item"
350
- :class="{ active: selectedSubagent === sa.toolCallId }"
351
- @click="selectSubagent(sa.toolCallId)"
352
- >
353
- <div class="subagent-item-color" :style="{ background: SUBAGENT_COLORS[sa.colorIndex % SUBAGENT_COLORS.length] }"></div>
354
- <div class="subagent-item-body">
355
- <div class="subagent-item-name">{{ sa.name }}</div>
356
- <div v-if="sa.meta.taskName || sa.meta.agentType || sa.meta.model" class="subagent-item-meta">
357
- <span v-if="sa.meta.taskName" class="subagent-meta-tag">{{ sa.meta.taskName }}</span>
358
- <span v-if="sa.meta.agentType" class="subagent-meta-tag dim">{{ sa.meta.agentType }}</span>
359
- <span v-if="sa.meta.model" class="subagent-meta-tag dim">{{ sa.meta.model }}</span>
360
- </div>
361
- <div v-if="sa.meta.agentDescription" class="subagent-item-desc">{{ sa.meta.agentDescription }}</div>
362
- </div>
363
- </div>
364
- <div v-if="filteredSubagentList.length === 0" class="subagent-dropdown-empty">No matches</div>
365
- </div>
366
- </div>
367
- <span v-if="subagentTokenUsage" class="subagent-usage-badge">
368
- {{ subagentTokenUsage.eventCount }} events \xB7 {{ formatDuration(subagentTokenUsage.durationMs) }}
369
- </span>
370
- </div>
371
-
372
- <div class="filter-bar-divider"></div>
373
-
374
- <!-- Event type dropdown -->
375
- <div class="filter-type-wrapper">
376
- <button
377
- class="filter-type-toggle"
378
- :class="{ active: currentFilter !== 'all' }"
379
- @click.stop="typeFilterOpen = !typeFilterOpen"
380
- >
381
- \u26A1 {{ currentFilter === 'all' ? 'All Types' : currentFilter }} \u25BE
382
- </button>
383
- <div v-if="typeFilterOpen" class="filter-type-menu">
384
- <div class="filter-type-menu-header">Event Types</div>
385
- <div class="filter-type-menu-options">
386
- <div
387
- v-for="filter in filters"
388
- :key="filter.type"
389
- :class="['filter-type-menu-item', { active: currentFilter === filter.type }]"
390
- @click="setFilter(filter.type); typeFilterOpen = false"
391
- >
392
- <span class="filter-type-menu-label">{{ filter.type === 'all' ? 'All' : filter.type }}</span>
393
- <span class="filter-type-menu-count">{{ filter.count }}</span>
394
- </div>
395
- </div>
396
- </div>
397
- </div>
398
- </div>
399
-
400
- <!-- Active filter chips -->
401
- <div v-if="activeFilterCount > 0" class="active-filters-bar">
402
- <span v-if="currentFilter !== 'all'" class="filter-chip">
403
- Type: {{ currentFilter }}
404
- <button @click="setFilter('all')" class="filter-chip-remove" title="Remove filter">\xD7</button>
405
- </span>
406
- <span v-if="selectedSubagent" class="filter-chip">
407
- Agent: {{ subagentList.find(s => s.toolCallId === selectedSubagent)?.name || selectedSubagent }}
408
- <button @click="selectSubagent(null)" class="filter-chip-remove" title="Remove filter">\xD7</button>
409
- </span>
410
- <span v-if="searchText.trim()" class="filter-chip">
411
- Search: "{{ searchText.length > 20 ? searchText.substring(0, 20) + '\u2026' : searchText }}"
412
- <button @click="searchText = ''" class="filter-chip-remove" title="Remove filter">\xD7</button>
413
- </span>
414
- <button class="clear-all-filters-btn" @click="clearAllFilters">Clear all</button>
415
- </div>
416
- </div>
417
-
418
- <!-- Loading state -->
419
- <div v-if="eventsLoading" class="loading-message">
420
- <div style="text-align: center; padding: 40px; color: #c9d1d9;">
421
- \u23F3 Loading events...
422
- </div>
423
- </div>
424
-
425
- <!-- Error state -->
426
- <div v-else-if="eventsError" class="error-message">
427
- <div style="text-align: center; padding: 40px; color: #f85149;">
428
- \u274C Error loading events: {{ eventsError }}
429
- </div>
430
- </div>
431
-
432
- <!-- Events list -->
433
- <DynamicScroller
434
- v-else
435
- ref="scrollerRef"
436
- :items="filteredEvents"
437
- :min-item-size="80"
438
- :prerender="10"
439
- key-field="stableId"
440
- class="scroller"
441
- >
442
- <template #default="{ item, index, active }">
443
- <DynamicScrollerItem
444
- :item="item"
445
- :active="active"
446
- :size-dependencies="[expansionCount]"
447
- :data-index="index"
448
- >
449
- <!-- Turn Start Divider -->
450
- <div
451
- v-if="item.type === 'assistant.turn_start'"
452
- :data-type="item.type"
453
- :data-index="item.virtualIndex"
454
- class="turn-divider"
455
- >
456
- <div class="turn-divider-line-left"></div>
457
- <span class="turn-divider-text">
458
- UserReq {{ getTurnNumber(item.virtualIndex) }}
459
- <template v-if="metadata.source === 'vscode'">
460
- <span class="turn-time">{{ formatTime(item.timestamp) }}</span>
461
- <span v-if="getTurnDuration(item.virtualIndex)" class="turn-duration">{{ getTurnDuration(item.virtualIndex) }}</span>
462
- </template>
463
- <template v-else>Start</template>
464
- </span>
465
- <div class="turn-divider-line-right"></div>
466
- <div class="divider-separator"></div>
467
- </div>
468
-
469
- <!-- Subagent Divider -->
470
- <div
471
- v-else-if="item.type === 'subagent.started' || item.type === 'subagent.completed' || item.type === 'subagent.failed'"
472
- :data-type="item.type"
473
- :data-index="item.virtualIndex"
474
- :class="['subagent-divider', item.type.split('.')[1]]"
475
- :style="{
476
- '--sa-color': getSubagentColor(item) || '#58a6ff'
477
- }"
478
- >
479
- <div class="subagent-divider-line-left" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
480
- <span class="subagent-divider-text" :style="{ color: getSubagentColor(item) || '#58a6ff', borderColor: getSubagentColor(item) || '#58a6ff', background: (getSubagentColor(item) || '#58a6ff') + '1a' }">
481
- \u{1F916} {{ item.data?.agentDisplayName || item.data?.agentName || 'SubAgent' }}
482
- <span v-if="subagentOwnership.subagentInfo.get(item.data?.toolCallId)?.meta?.model" class="subagent-divider-model">\xB7 {{ subagentOwnership.subagentInfo.get(item.data?.toolCallId).meta.model }}</span>
483
- {{ item.type === 'subagent.started' ? 'Start \u25B6' : item.type === 'subagent.completed' ? 'Complete \u2713' : 'Failed \u2717' }}
484
- </span>
485
- <div class="subagent-divider-line-right" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
486
- <div class="divider-separator"></div>
487
- </div>
488
-
489
- <!-- Regular Event -->
490
- <div
491
- v-else
492
- :class="['event', getSubagentInfo(item) ? 'event-in-subagent' : '']"
493
- :data-type="item.type"
494
- :data-index="item.virtualIndex"
495
- :style="getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}"
496
- >
497
- <div class="event-header">
498
- <span :class="['event-badge', getBadgeInfo(item.type, item).class]">
499
- {{ getBadgeInfo(item.type, item).label }}
500
- </span>
501
- <span
502
- v-if="getSubagentInfo(item)"
503
- class="subagent-owner-tag"
504
- :style="{ '--subagent-color': getSubagentColor(item) || '#58a6ff', '--subagent-hover-bg': ((getSubagentColor(item) || '#58a6ff') + '26') }"
505
- :title="'Filter to ' + getSubagentInfo(item).name"
506
- @click.stop="selectSubagent(getSubagentInfo(item).toolCallId)"
507
- >\u{1F916} {{ getSubagentInfo(item).name }}</span>
508
- <span class="event-timestamp">{{ formatTime(item.timestamp) }}</span>
509
- </div>
510
-
511
- <!-- Abort event: show reason -->
512
- <div v-if="item.type === 'abort' && item.data?.reason" class="event-content">
513
- <strong>Reason:</strong> {{ item.data.reason }}
514
- </div>
515
-
516
- <!-- Session start: show type and selectedModel -->
517
- <div v-else-if="item.type === 'session.start'" class="event-content">
518
- <div v-if="item.data?.type"><strong>Type:</strong> {{ item.data.type }}</div>
519
- <div v-if="item.data?.selectedModel"><strong>Model:</strong> {{ item.data.selectedModel }}</div>
520
- <div v-if="item.data?.producer"><strong>Producer:</strong> {{ item.data.producer }}</div>
521
- </div>
522
-
523
- <!-- Session resume: show resumeTime, eventCount, context -->
524
- <div v-else-if="item.type === 'session.resume'" class="event-content">
525
- <div v-if="item.data?.resumeTime"><strong>Resume Time:</strong> {{ formatDateTime(item.data.resumeTime) }}</div>
526
- <div v-if="item.data?.eventCount"><strong>Event Count:</strong> {{ item.data.eventCount }}</div>
527
- <div v-if="item.data?.context?.branch"><strong>Branch:</strong> {{ item.data.context.branch }}</div>
528
- <div v-if="item.data?.context?.repository"><strong>Repository:</strong> {{ item.data.context.repository }}</div>
529
- <div v-if="item.data?.context?.cwd"><strong>Working Directory:</strong> {{ item.data.context.cwd }}</div>
530
- </div>
531
-
532
- <!-- Session error: show errorType + message -->
533
- <div v-else-if="item.type === 'session.error' && (item.data?.errorType || item.data?.message)" class="event-content">
534
- <div v-if="item.data?.errorType"><strong>Error Type:</strong> {{ item.data.errorType }}</div>
535
- <div v-if="item.data?.message"><strong>Message:</strong> {{ item.data.message }}</div>
536
- </div>
537
-
538
- <!-- Model change: show previousModel \u2192 newModel -->
539
- <div v-else-if="item.type === 'session.model_change'" class="event-content model-change-content">
540
- <div v-if="item.data?.previousModel && item.data?.newModel" class="model-change-text">
541
- <span class="model-name">{{ item.data.previousModel }}</span>
542
- <span class="model-arrow">\u2192</span>
543
- <span class="model-name">{{ item.data.newModel }}</span>
544
- </div>
545
- <div v-else-if="item.data?.newModel" class="model-change-text">
546
- Switched to <span class="model-name">{{ item.data.newModel }}</span>
547
- </div>
548
- <div v-else-if="item.data?.model" class="model-change-text">
549
- Switched to <span class="model-name">{{ item.data.model }}</span>
550
- </div>
551
- <div v-else class="model-change-text">
552
- Model changed
553
- </div>
554
- </div>
555
-
556
- <!-- Session truncation: show token/message removal info -->
557
- <div v-else-if="item.type === 'system.notification'" class="event-content" style="opacity:0.7">
558
- <span>{{ item.data?.message }}</span>
559
- </div>
560
-
561
- <div v-else-if="item.type === 'session.truncation'" class="event-content">
562
- <div v-if="item.data?.messagesRemovedDuringTruncation"><strong>Messages removed:</strong> {{ item.data.messagesRemovedDuringTruncation }}</div>
563
- <div v-if="item.data?.tokensRemovedDuringTruncation"><strong>Tokens removed:</strong> {{ item.data.tokensRemovedDuringTruncation.toLocaleString() }}</div>
564
- <div v-if="item.data?.preTruncationTokensInMessages"><strong>Pre-truncation tokens:</strong> {{ item.data.preTruncationTokensInMessages.toLocaleString() }}</div>
565
- <div v-if="item.data?.postTruncationMessagesLength"><strong>Post-truncation messages:</strong> {{ item.data.postTruncationMessagesLength }}</div>
566
- <div v-if="item.data?.performedBy"><strong>Performed by:</strong> {{ item.data.performedBy }}</div>
567
- </div>
568
-
569
- <!-- Session compaction start -->
570
- <div v-else-if="item.type === 'session.compaction_start'" class="event-content">
571
- Context compaction started
572
- </div>
573
-
574
- <!-- Session compaction complete: show results -->
575
- <div v-else-if="item.type === 'session.compaction_complete'" class="event-content">
576
- <div v-if="item.data?.success != null"><strong>Success:</strong> {{ item.data.success ? '\u2713' : '\u2717' }}</div>
577
- <div v-if="item.data?.compactionTokensUsed">
578
- <strong>Tokens used:</strong>
579
- input {{ item.data.compactionTokensUsed.input?.toLocaleString() || 0 }},
580
- output {{ item.data.compactionTokensUsed.output?.toLocaleString() || 0 }}
581
- <span v-if="item.data.compactionTokensUsed.cachedInput">, cached {{ item.data.compactionTokensUsed.cachedInput.toLocaleString() }}</span>
582
- </div>
583
- <div v-if="item.data?.preCompactionMessagesLength"><strong>Pre-compaction messages:</strong> {{ item.data.preCompactionMessagesLength }}</div>
584
- <div v-if="item.data?.preCompactionTokens"><strong>Pre-compaction tokens:</strong> {{ item.data.preCompactionTokens.toLocaleString() }}</div>
585
- <div v-if="item.data?.summaryContent" style="margin-top: 8px;">
586
- <button
587
- @click="toggleContent('compaction-' + item.stableId)"
588
- style="background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;"
589
- >
590
- {{ expandedContent['compaction-' + item.stableId] ? 'Hide summary \u25B2' : 'Show summary \u25BC' }}
591
- </button>
592
- <div v-if="expandedContent['compaction-' + item.stableId]" class="event-content" style="margin-top: 8px;" v-html="renderMarkdown(item.data.summaryContent)"></div>
593
- </div>
594
- </div>
595
-
596
- <!-- Hook event: compact summary with collapsible args/result -->
597
- <div v-else-if="item.data?.hookType" class="hook-content">
598
- <div class="hook-summary">
599
- <span style="color: #8b949e;">{{ item.data.hookType }}</span>
600
- <span v-if="item.data.hookToolName" style="color: #8b949e;"> \u2192 </span>
601
- <span v-if="item.data.hookToolName" style="color: #c9d1d9;">{{ item.data.hookToolName }}</span>
602
- <span v-if="item.data.hookDurationMs != null" style="color: #7d8590; margin-left: 8px;">{{ item.data.hookDurationMs }}ms</span>
603
- <span v-if="item.data.hookSuccess === true" style="color: #3fb950; margin-left: 4px;">\u2713</span>
604
- <span v-if="item.data.hookSuccess === false" style="color: #ff7b72; margin-left: 4px;">\u2717</span>
605
- </div>
606
- <div v-if="item.data.hookArgs && Object.keys(item.data.hookArgs).length > 0" class="hook-section">
607
- <div class="hook-section-header" @click="toggleContent('hook-args-' + item.stableId)">
608
- <span class="tool-expand-icon">{{ expandedContent['hook-args-' + item.stableId] ? '\u25BC' : '\u25B6' }}</span>
609
- <span style="color: #8b949e;">Arguments</span>
610
- </div>
611
- <div v-if="expandedContent['hook-args-' + item.stableId]" class="hook-section-body">
612
- <pre>{{ JSON.stringify(item.data.hookArgs, null, 2) }}</pre>
613
- </div>
614
- </div>
615
- <div v-if="item.data.hookResult" class="hook-section">
616
- <div class="hook-section-header" @click="toggleContent('hook-result-' + item.stableId)">
617
- <span class="tool-expand-icon">{{ expandedContent['hook-result-' + item.stableId] ? '\u25BC' : '\u25B6' }}</span>
618
- <span style="color: #8b949e;">Result</span>
619
- </div>
620
- <div v-if="expandedContent['hook-result-' + item.stableId]" class="hook-section-body">
621
- <pre>{{ item.data.hookResult }}</pre>
622
- </div>
623
- </div>
624
- <div v-if="item.data.hookError" style="color: #ff7b72; margin-top: 4px;">
625
- Error: {{ item.data.hookError }}
626
- </div>
627
- </div>
628
-
629
- <!-- Regular content (unified format from server) -->
630
- <div v-else-if="item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent">
631
- <div
632
- class="event-content"
633
- v-html="highlightSearchText(
634
- renderMarkdown(
635
- (expandedContent[item.stableId] || !isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent))
636
- ? (item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)
637
- : truncateContent(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)
638
- ),
639
- searchText
640
- )"
641
- ></div>
642
- <div
643
- v-if="isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)"
644
- style="margin-top: 8px;"
645
- >
646
- <button
647
- @click="toggleContent(item.stableId)"
648
- :data-content-id="item.stableId"
649
- style="background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;"
650
- >
651
- {{ expandedContent[item.stableId] ? 'Show less \u25B2' : 'Show more \u25BC' }}
652
- </button>
653
- </div>
654
- </div>
655
-
656
- <!-- No content at all (no message and no tools) -->
657
- <div v-else-if="!hasTools(item) && !item.data?.reasoningText" class="event-content" style="color: #7d8590; font-style: italic;">
658
- No available message
659
- </div>
660
-
661
- <!-- Reasoning text (shown after main content, before tool calls) -->
662
- <div v-if="item.data?.reasoningText" class="event-content reasoning-text-content">
663
- <div
664
- v-html="highlightSearchText(
665
- renderMarkdown(
666
- (expandedContent[item.stableId + '-reasoning'] || !isContentTooLong(item.data.reasoningText))
667
- ? item.data.reasoningText
668
- : truncateContent(item.data.reasoningText)
669
- ),
670
- searchText
671
- )"
672
- ></div>
673
- <div v-if="isContentTooLong(item.data.reasoningText)" style="margin-top: 8px;">
674
- <button
675
- @click="toggleContent(item.stableId + '-reasoning')"
676
- style="background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;"
677
- >
678
- {{ expandedContent[item.stableId + '-reasoning'] ? 'Show less \u25B2' : 'Show more \u25BC' }}
679
- </button>
680
- </div>
681
- </div>
682
-
683
- <!-- Tool calls section (independent of message content, but don't need "No available message" if tools exist) -->
684
- <div v-if="hasTools(item)" class="tool-list">
685
- <div
686
- v-for="(group, idx) in getToolGroups(item)"
687
- :key="idx"
688
- class="tool-item"
689
- >
690
- <div
691
- class="tool-header-line"
692
- @click="toggleTool(item.stableId + '-' + idx)"
693
- >
694
- <span class="tool-connector">{{ idx === getToolGroups(item).length - 1 ? '\u2514\u2500' : '\u251C\u2500' }}</span>
695
- <span class="tool-expand-icon">{{ expandedTools[item.stableId + '-' + idx] ? '\u25BC' : '\u25B6' }}</span>
696
- <span class="tool-name">\u{1F527}&nbsp;{{ group.start?.data?.toolName || group.tool || 'Tool' }}</span>
697
- <span :class="getToolStatus(group).color" style="margin-left: 4px;">({{ getToolStatus(group).icon }}{{ getToolDuration(group) ? ' ' + getToolDuration(group) : '' }})</span>
698
- <span v-if="getToolCommand(group)" style="color: #7d8590; margin-left: 8px;">{{ getToolCommand(group) }}</span>
699
- <span v-if="getToolErrorMessage(group)" style="color: #ff7b72; margin-left: 8px;">{{ getToolErrorMessage(group).length > 80 ? getToolErrorMessage(group).substring(0, 80) + '...' : getToolErrorMessage(group) }}</span>
700
- </div>
701
-
702
- <div v-if="expandedTools[item.stableId + '-' + idx]" class="tool-detail">
703
- <div v-if="group.timing.startTime || group.timing.endTime || group.timing.duration" class="tool-detail-section">
704
- <div class="tool-detail-content tool-timing-line">
705
- <span v-if="group.timing.startTime"><span class="tool-timing-label">Start</span> {{ formatToolTime(group.timing.startTime) }}</span>
706
- <span v-if="group.timing.endTime"><span class="tool-timing-label">Complete</span> {{ formatToolTime(group.timing.endTime) }}</span>
707
- <span v-if="group.timing.duration"><span class="tool-timing-label">Duration</span> {{ group.timing.duration }}</span>
708
- </div>
709
- </div>
710
- <div v-if="group.start?.data?.arguments" class="tool-detail-section">
711
- <div class="tool-detail-title">Arguments:</div>
712
- <div class="tool-detail-content">
713
- <pre>{{ JSON.stringify(group.start.data.arguments, null, 2) }}</pre>
714
- </div>
715
- </div>
716
- <div v-if="group.complete?.data?.result" class="tool-detail-section">
717
- <div class="tool-detail-title">Result:</div>
718
- <div class="tool-detail-content">
719
- <pre>{{ JSON.stringify(group.complete.data.result, null, 2) }}</pre>
720
- </div>
721
- </div>
722
- <div v-if="getToolErrorMessage(group)" class="tool-detail-section">
723
- <div class="tool-detail-title">Error:</div>
724
- <div class="tool-detail-content" style="color: #ff7b72;">
725
- {{ getToolErrorMessage(group) }}
726
- </div>
727
- </div>
728
- </div>
729
- </div>
730
- </div>
731
-
732
- <!-- Separator (inside event for proper height calculation) -->
733
- <div v-if="!item.isLastEvent" class="event-separator"></div>
734
- </div>
735
- </DynamicScrollerItem>
736
- </template>
737
- </DynamicScroller>
738
-
739
- <!-- Bottom spacer: ensures last item clears mobile browser nav bar -->
740
- <div class="scroller-bottom-spacer"></div>
741
-
742
- <!-- Floating scroll buttons -->
743
- <div class="scroll-float-btns">
744
- <button @click="scrollToTop" title="Scroll to top" class="scroll-edge-btn">\u25B2</button>
745
- <button @click="scrollToBottom" title="Scroll to bottom" class="scroll-edge-btn">\u25BC</button>
746
- </div>
747
- </div>
748
- </div>
749
-
750
- </div>
751
- `});console.log("Mounting Vue app to #app..."),console.log("App config:",ve.config),console.log("Target element:",document.getElementById("app"));try{let h=ve.mount("#app");console.log("Vue app mounted successfully!",h?"Instance created":"No instance"),console.log("VM type:",typeof h,"Has exportSession:",typeof h?.exportSession),console.log("VM keys:",h?Object.keys(h).slice(0,10):"NO_VM"),console.log("#app innerHTML length:",document.getElementById("app").innerHTML.length),console.log("#app first 100 chars:",document.getElementById("app").innerHTML.substring(0,100))}catch(h){console.error("Mount failed:",h),console.error("Error stack:",h.stack)}})();})();