@qiaolei81/copilot-session-viewer 0.3.2 → 0.3.4

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.
@@ -0,0 +1,461 @@
1
+ (()=>{(function(){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:ue,ref:u,computed:T,onMounted:F,onBeforeUnmount:me,watch:$}=Vue,{DynamicScroller:ge,DynamicScrollerItem:pe}=window.VueVirtualScroller,G=ue({components:{DynamicScroller:ge,DynamicScrollerItem:pe},setup(){let m=u(window.__PAGE_DATA.sessionId),W=u(window.__PAGE_DATA.metadata),z=u(!1),K=()=>window.innerWidth<=640,q=u(K()?!0:localStorage.getItem("sidebarCollapsed")==="true");$(q,e=>{K()||localStorage.setItem("sidebarCollapsed",e.toString())});let x=u({}),w=u({}),U=50,J=()=>{let e=Object.keys(x.value);e.length>U&&e.slice(0,e.length-U).forEach(s=>delete x.value[s]);let t=Object.keys(w.value);t.length>U&&t.slice(0,t.length-U).forEach(s=>delete w.value[s])},S=u("all"),V=u(""),E=u(""),X=u(0),g=u(null),Y=u({start:0,end:0}),L=null;$(V,e=>{clearTimeout(L),L=setTimeout(()=>{E.value=e,e.trim()&&window.trackClick&&window.trackClick("SearchUsed",{query:e.substring(0,50),resultCount:M.value.length,sessionId:m.value})},300)}),$(S,()=>{J()}),$(E,()=>{J()});let I=u([]),Z=u(!0),Q=u(null),v=T(()=>I.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,n=a.timestamp?new Date(a.timestamp).getTime():0;return s!==n?s-n:(t._fileIndex??0)-(a._fileIndex??0)}).map((t,a)=>({...t,virtualIndex:a,stableId:t.id||`${t.timestamp}-${t.type}-${a}`}))),ve=e=>{if(!E.value.trim())return!0;let t=E.value.toLowerCase();return[e.data?.message,e.data?.text,e.data?.content,e.data?.reason,e.data?.errorType,e.data?.previousModel,e.data?.newModel].filter(Boolean).join(" ").toLowerCase().includes(t)},M=T(()=>{let e=a=>{let s=a.type||"";return s!=="tool.execution_start"&&s!=="tool.execution_complete"},t=v.value.filter(e);return E.value.trim()&&(t=t.filter(ve)),t}),A=T(()=>{let e=M.value;S.value!=="all"&&(e=e.filter(s=>s.type===S.value));let t=["assistant.turn_start","subagent.started","subagent.completed","subagent.failed"],a=e.length;return e.map((s,n)=>{let r=e[n+1],o=n===a-1,i=r&&t.includes(r.type);return{...s,filteredIndex:n,filteredTotal:a,isLastEvent:o||i}})}),fe=T(()=>{let e={};return M.value.forEach(t=>{t.type&&(e[t.type]=(e[t.type]||0)+1)}),e}),be=T(()=>{if(!E.value.trim())return null;let e=M.value.length;return e>0?`${e} result${e!==1?"s":""}`:"No matches"}),he=T(()=>{let e=Object.keys(x.value).filter(a=>x.value[a]).length,t=Object.keys(w.value).filter(a=>w.value[a]).length;return e+t}),ee=T(()=>{let e=M.value.length,t=[{type:"all",label:`All (${e})`,count:e}],a={};M.value.forEach(n=>{n.type&&(a[n.type]=(a[n.type]||0)+1)});let s=Object.entries(a).sort((n,r)=>r[1]-n[1]).map(([n,r])=>({type:n,label:`${n} (${r})`,count:r,disabled:!1}));return[...t,...s]}),R=T(()=>{let e=v.value.filter(a=>a.type==="assistant.turn_start"),t=v.value.filter(a=>a.type==="user.message");return e.map((a,s)=>{let n=s,r=new Date(a.timestamp).getTime(),o,i=e.indexOf(a)+1;i<e.length?o=new Date(e[i].timestamp).getTime():o=Date.now();let c=o-r,l=Math.floor(c/1e3),d=Math.floor(l/60),y=l%60,P=d>0?`${d}m ${y}s`:`${y}s`,p=v.value.slice(0,v.value.indexOf(a)).reverse().find(_=>_.type==="user.message"),N=p?t.indexOf(p)+1:0;return{id:n,index:a.virtualIndex,originalTurnId:a.data?.turnId,timestamp:a.timestamp,duration:P,message:p?.data?.content||p?.data?.transformedContent||"",userReqNumber:N}})}),Te=T(()=>{let e=[],t=new Map;return R.value.forEach(a=>{let s=a.userReqNumber||0;if(!t.has(s)){let n={reqNumber:s,message:a.message,turns:[]};t.set(s,n),e.push(n)}t.get(s).turns.push(a)}),e}),ye=(e,t)=>e?e.length<=t?e:e.substring(0,t)+"\u2026":"",xe=T(()=>{let e=v.value,t=new Map,a=new Map,s=0;for(let o of e)if(o.type==="subagent.started"){let i=o.data?.toolCallId;i&&a.set(i,{name:o.data?.agentDisplayName||o.data?.agentName||"SubAgent",colorIndex:s++})}for(let o of e)if(o.type==="assistant.message"&&o.data?.subAgentName&&o.data?.subAgentId){let i=o.data.subAgentId;a.has(i)||a.set(i,{name:o.data.subAgentName,colorIndex:s++}),t.set(o.stableId,i)}if(a.size===0)return{ownerMap:t,subagentInfo:a};let n=new Map;for(let o of e)o.id&&n.set(o.id,o);for(let o of e)if(o.type==="assistant.message"){let i=o.data?.parentToolCallId;i&&a.has(i)&&t.set(o.stableId,i)}for(let o of e){if(o.type!=="reasoning")continue;let i=o.parentId,c=0;for(;i&&c<10;){let l=n.get(i);if(!l)break;if(l.type==="assistant.message"){let d=l.data?.parentToolCallId;d&&a.has(d)&&t.set(o.stableId,d);break}i=l.parentId,c++}}let r=new Map;for(let o of e){if(o.type!=="tool.execution_start")continue;let i=o.parentId,c=0;for(;i&&c<10;){let l=n.get(i);if(!l)break;if(l.type==="assistant.message"){let d=l.data?.parentToolCallId;if(d&&a.has(d)){t.set(o.stableId,d);let y=o.data?.toolCallId;y&&r.set(y,d)}break}i=l.parentId,c++}}for(let o of e){if(o.type!=="tool.execution_complete")continue;let i=o.data?.toolCallId;i&&r.has(i)&&t.set(o.stableId,r.get(i))}for(let o of e){if(o.type!=="tool.invocation")continue;let i=o.data?.parentToolCallId;i&&a.has(i)&&t.set(o.stableId,i)}return{ownerMap:t,subagentInfo:a}}),we=e=>{if(!e)return"";let t=new Date(e),a=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),n=String(t.getSeconds()).padStart(2,"0");return`${a}:${s}:${n}`},f=new Map,te=200,Ie=e=>{if(!e)return"";if(f.has(e))return f.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 o=s[1],i=s[2],c=o.split(`
4
+ `).filter(p=>p.trim()&&p.includes(":")).map(p=>{let N=p.indexOf(":"),_=p.substring(0,N).trim(),tt=p.substring(N+1).trim();return{key:_,value:tt}}),l='<table style="margin-bottom: 16px; border-collapse: collapse; width: 100%;"><tbody>';c.forEach(p=>{let N=DOMPurify.sanitize(p.key,{ALLOWED_TAGS:[]}),_=DOMPurify.sanitize(p.value,{ALLOWED_TAGS:[]});l+=`<tr><td style="padding: 4px 12px; border: 1px solid #30363d; font-weight: 600; color: #7d8590;">${N}</td><td style="padding: 4px 12px; border: 1px solid #30363d;">${_}</td></tr>`}),l+="</tbody></table>";let d=marked.parse(i),y=DOMPurify.sanitize(d,a),P=l+y;if(f.size>=te){let p=f.keys().next().value;f.delete(p)}return f.set(e,P),P}let n=marked.parse(t),r=DOMPurify.sanitize(n,a);if(f.size>=te){let o=f.keys().next().value;f.delete(o)}return f.set(e,r),r}catch{return e}},Ce=e=>{let t={...x.value},a=!!t[e];t[e]?delete t[e]:t[e]=!0,x.value=t,window.trackClick&&window.trackClick("EventExpanded",{eventType:"tool",action:a?"collapse":"expand",sessionId:m.value})},ke=(e,t)=>{if(!t||!t.trim()||!e)return e;let a=t.trim(),s=re(a).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n=document.createElement("div");n.innerHTML=e;let r=o=>{if(o.nodeType===Node.TEXT_NODE){let i=o.textContent,c=new RegExp(`(${s})`,"gi");if(c.test(i)){let l=i.replace(c,'<mark class="search-highlight">$1</mark>'),d=document.createElement("span");d.innerHTML=l,o.parentNode.replaceChild(d,o)}}else o.nodeType===Node.ELEMENT_NODE&&o.tagName!=="SCRIPT"&&o.tagName!=="STYLE"&&Array.from(o.childNodes).forEach(r)};return Array.from(n.childNodes).forEach(r),n.innerHTML},Se=e=>{let t={...w.value},a=!!t[e];t[e]?delete t[e]:t[e]=!0,w.value=t,window.trackClick&&window.trackClick("EventExpanded",{eventType:"content",action:a?"collapse":"expand",sessionId:m.value})},Ee=e=>e?e.split(`
5
+ `).length>20||e.length>2e3:!1,Me=e=>{let t=e.split(`
6
+ `);return t.length<=20?e:t.slice(0,20).join(`
7
+ `)+`
8
+
9
+ ...`},Ne=(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"}},Le=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:""}},Ae=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)},Re=e=>{if(!e.complete)return"";let t=new Date(e.start.timestamp).getTime(),s=new Date(e.complete.timestamp).getTime()-t;return s>=100?`${(s/1e3).toFixed(1)}s`:""},De=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 n=t.action||"",r=t.targetUrl||t.url||"";s=r?`${n} ${r}`:n}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},Oe=e=>e.data?.tools&&e.data.tools.length>0,_e=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";return{tool:t.name,start:{data:{toolName:t.name,arguments:t.input||t.arguments||{}}},complete:a?{data:{result:t.result,error:t.status==="error"?t.error:null}}:null}}):[],ae=["#58a6ff","#f0883e","#a371f7","#3fb950","#f778ba","#79c0ff","#d29922","#56d4dd"],$e=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}=xe.value;if(e.type==="subagent.started"||e.type==="subagent.completed"||e.type==="subagent.failed"){let r=e.data?.toolCallId;if(r&&a.has(r)){let o=a.get(r);return{name:o.name,toolCallId:r,colorIndex:o.colorIndex}}return null}if(e._subagent){let r=e._subagent.id,o=e._subagent.name;if(a.has(r)){let i=a.get(r);return{name:i.name,toolCallId:r,colorIndex:i.colorIndex}}return{name:o,toolCallId:r,colorIndex:Math.abs($e(r))}}if(e.data?.subAgentId){let r=e.data.subAgentId,o=a.get(r);if(o)return{name:o.name,toolCallId:r,colorIndex:o.colorIndex}}let s=t.get(e.stableId);if(!s)return null;let n=a.get(s);return n?{name:n.name,toolCallId:s,colorIndex:n.colorIndex}:null},qe=e=>{let t=se(e);return t?ae[t.colorIndex%ae.length]:null},Ue=e=>{if(window.trackClick){let t=ee.value.find(a=>a.type===e);window.trackClick("EventFilterClicked",{filterType:e,filterLabel:t?t.label:e,sessionId:m.value})}S.value=e},oe=e=>{V.value="",S.value="all",X.value=e.id,Vue.nextTick(()=>{if(g.value){let t=A.value.findIndex(a=>a.virtualIndex===e.index);if(t>=0){let a=s=>{s<=0||!g.value||(g.value.scrollToItem(t),setTimeout(()=>a(s-1),100))};setTimeout(()=>a(3),50)}}})},Be=()=>{if(!g.value)return;let e=t=>{t<=0||!g.value||(g.value.scrollToItem(0),setTimeout(()=>e(t-1),100))};e(3)},Pe=()=>{if(!g.value)return;let e=A.value.length-1,t=a=>{a<=0||!g.value||(g.value.scrollToItem(e),setTimeout(()=>t(a-1),100))};t(5)},ne=e=>{window.trackClick&&window.trackClick("TurnClicked",{turnNumber:e,sessionId:m.value});let t=R.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),oe(t)}},ze=e=>{if(!e)return"";let t=e.replace(/\/$/,"").split("/");return t[t.length-1]||e},Ve=e=>{let t=R.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}`},He=e=>R.value.find(a=>a.index===e)?.duration||null,re=e=>{let t=document.createElement("div");return t.textContent=e,t.innerHTML},je=e=>e?new Date(e).toLocaleString():"N/A",Fe=async()=>{console.log("[Export] exportSession called"),window.trackClick&&window.trackClick("ExportClicked",{sessionId:m.value}),z.value=!0;try{console.log("[Export] Fetching:",`/session/${m.value}/export`);let e=await fetch(`/session/${m.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-${m.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 n="\u{1F4E4} Share Session",r="\u2713 Downloaded!",o=document.querySelector(".export-btn");o&&(o.textContent=r,o.style.background="#238636",console.log("[Export] Button text updated to:",o.textContent),setTimeout(()=>{o.textContent=n,o.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{z.value=!1,console.log("[Export] Export complete")}};F(async()=>{try{console.log("[Navigation] Starting event loading...");let a=await fetch(`/api/sessions/${m.value}/events`);if(!a.ok)throw new Error(`Failed to load events: ${a.statusText}`);let s=await a.json();if(Array.isArray(s))I.value=s;else if(s.events&&Array.isArray(s.events))I.value=s.events,console.log("[Navigation] Pagination:",s.pagination);else throw new Error("Invalid response format");if(console.log("[Navigation] Events loaded:",I.value.length),I.value.length>0){let c=I.value[I.value.length-1],l=c.timestamp||c.time||c.data?.timestamp;l&&(W.value.updated=new Date(l))}let n=new URLSearchParams(window.location.search),r=n.get("eventType"),o=n.get("eventName"),i=n.get("eventTimestamp");console.log("[Navigation] URL params:",r,o,i),r&&o&&(console.log("[Navigation] Waiting for Vue to render..."),Vue.nextTick(()=>{console.log("[Navigation] nextTick - flatEvents count:",v.value?.length);let c=null;if(r==="assistant.turn_start"){let l=o.match(/UserReq(\d+)_Turn(\d+)/);if(l){let d=parseInt(l[2],10);if(!isNaN(d)){console.log("[Navigation] Jumping to turn:",d),ne(d);return}}}else r==="subagent.started"?(console.log("[Navigation] Searching for subagent:",o,"timestamp:",i),i&&(c=v.value.find(l=>l.type==="subagent.started"&&l.timestamp===i)),c||(c=v.value.find(l=>l.type==="subagent.started"&&(l.data?.agentDisplayName===o||l.data?.agentName===o||l.data?.label===o))),console.log("[Navigation] Target event found:",c?"YES":"NO","virtualIndex:",c?.virtualIndex)):c=v.value.find(l=>l.type===r);if(c){let l=A.value.findIndex(d=>d.virtualIndex===c.virtualIndex);if(console.log("[Navigation] Target in filteredEvents at index:",l),l>=0&&g.value){console.log("[Navigation] Scrolling to index:",l);let d=y=>{y<=0||!g.value||(g.value.scrollToItem(l),setTimeout(()=>d(y-1),100))};setTimeout(()=>d(3),50)}else console.log("[Navigation] Failed - targetIndex:",l,"scrollerRef:",!!g.value)}else console.log("[Navigation] Target event not found")}))}catch(a){console.error("Error loading events:",a),Q.value=a.message}finally{Z.value=!1}window.addEventListener("keydown",a=>{a.ctrlKey&&a.key==="b"&&(a.preventDefault(),q.value=!q.value)}),window.marked&&marked.setOptions({breaks:!0,gfm:!0});let e=()=>{if(!g.value)return;let a=null;if(g.value.$el&&typeof g.value.$el.querySelector=="function"?a=g.value.$el.querySelector(".vue-recycle-scroller"):g.value.querySelector&&typeof g.value.querySelector=="function"&&(a=g.value.querySelector(".vue-recycle-scroller")),a||(a=document.querySelector(".vue-recycle-scroller")),a){let s=a.scrollTop,n=a.clientHeight,r=80,o=Math.floor(s/r),i=Math.ceil(n/r),c=Math.min(o+i,A.value.length),l=Math.max(1,o+1),d=Math.max(1,c);Y.value={start:Math.min(l,d),end:d}}},t=null;setTimeout(()=>{e();let a=document.querySelector(".vue-recycle-scroller");a&&(a.addEventListener("scroll",e),t=()=>{a.removeEventListener("scroll",e)})},500),me(()=>{L&&(clearTimeout(L),L=null),t&&t(),x.value={},w.value={},f.clear()})});let D=u([]),H=u([]),O=u(!1),b=u([]),C=u(""),j=u(null),h=u(""),k=u(!1),B=u([]),ie=u(0),le=["#3b82f6","#10b981","#f59e0b","#ef4444","#8b5cf6","#ec4899","#06b6d4","#f97316"],Ge=e=>{let t=0;for(let a=0;a<e.length;a++)t=e.charCodeAt(a)+((t<<5)-t);return le[Math.abs(t)%le.length]},We=async()=>{try{let e=await fetch(`/api/sessions/${m.value}/tags`);if(e.ok){let t=await e.json();D.value=t.tags||[]}}catch(e){console.error("Error loading tags:",e)}},de=async()=>{try{let e=await fetch("/api/tags");if(e.ok){let t=await e.json();H.value=t.tags||[]}}catch(e){console.error("Error loading all tags:",e)}},Ke=async e=>{try{window.trackClick&&e.filter(s=>!D.value.includes(s)).forEach(s=>{window.trackClick("TagAdded",{sessionId:m.value,tag:s})});let t=await fetch(`/api/sessions/${m.value}/tags`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({tags:e})});if(t.ok){let a=await t.json();return D.value=a.tags||[],h.value="",!0}else{let a=await t.json();return h.value=a.error||"Failed to save tags",!1}}catch(t){return console.error("Error saving tags:",t),h.value="Network error",!1}},Je=()=>{b.value=[...D.value],O.value=!0,h.value="",setTimeout(()=>{j.value&&j.value.focus()},10)},Xe=()=>{O.value=!1,b.value=[],C.value="",k.value=!1,h.value=""},ce=()=>{let e=C.value.trim().toLowerCase();if(e){if(e.length>30){h.value="Tag must be 30 characters or less";return}if(b.value.length>=10){h.value="Maximum 10 tags per session";return}if(b.value.includes(e)){h.value="Tag already added",C.value="";return}b.value.push(e),C.value="",k.value=!1,h.value=""}},Ye=e=>{b.value=b.value.filter(t=>t!==e),h.value=""},Ze=()=>{let e=C.value.trim().toLowerCase();if(!e){k.value=!1,B.value=[];return}let t=H.value.filter(a=>a.toLowerCase().includes(e)&&!b.value.includes(a)).slice(0,5);t.length>0?(k.value=!0,B.value=t,ie.value=0):(k.value=!1,B.value=[])},Qe=e=>{C.value=e,ce()},et=async()=>{setTimeout(async()=>{if(!O.value)return;await Ke(b.value)&&(O.value=!1,b.value=[],C.value="",k.value=!1,await de())},200)};return F(async()=>{await We(),await de()}),{sessionId:m,metadata:W,exporting:z,sidebarCollapsed:q,expandedTools:x,expandedContent:w,expansionCount:he,currentFilter:S,searchText:V,currentTurnIndex:X,scrollerRef:g,visibleRange:Y,loadedEvents:I,eventsLoading:Z,eventsError:Q,flatEvents:v,filteredEvents:A,eventCounts:fe,filters:ee,turns:R,userReqs:Te,truncateText:ye,formatTime:we,formatDateTime:je,renderMarkdown:Ie,highlightSearchText:ke,toggleTool:Ce,toggleContent:Se,isContentTooLong:Ee,truncateContent:Me,getBadgeInfo:Ne,getToolStatus:Le,getToolErrorMessage:Ae,getToolDuration:Re,getToolCommand:De,hasTools:Oe,getToolGroups:_e,getSubagentInfo:se,getSubagentColor:qe,setFilter:Ue,scrollToTurn:oe,scrollToTop:Be,scrollToBottom:Pe,jumpToTurn:ne,getTurnNumber:Ve,getTurnDuration:He,repoBasename:ze,escapeHtml:re,exportSession:Fe,searchResultCount:be,sessionTags:D,allTags:H,tagsEditing:O,editingTags:b,tagInputValue:C,tagInputRef:j,tagsError:h,showAutocomplete:k,autocompleteOptions:B,autocompleteSelectedIndex:ie,getTagColor:Ge,startEditTags:Je,cancelEditTags:Xe,addTag:ce,removeTagFromEdit:Ye,updateAutocomplete:Ze,selectAutocompleteOption:Qe,saveTagsOnBlur:et}},template:`
10
+ <div class="container">
11
+ <div class="header">
12
+ <a href="/" class="home-btn">\u2190 Back to Home</a>
13
+ <h1>\u{1F4CB} Session: {{ sessionId }}
14
+ <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>
15
+ </h1>
16
+ <div style="display: flex; gap: 10px;">
17
+ <a :href="'/session/' + sessionId + '/time-analyze'" class="time-analyze-btn" @click="trackClick && trackClick('TimeAnalyzeClicked', { sessionId: sessionId })">\u23F1 Analysis</a>
18
+ <button @click="exportSession" class="export-btn" :disabled="exporting">
19
+ {{ exporting ? '\u23F3 Sharing...' : '\u{1F4E4} Share Session' }}
20
+ </button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="main-layout">
25
+ <!-- Mobile overlay backdrop -->
26
+ <div
27
+ v-if="!sidebarCollapsed"
28
+ @click="sidebarCollapsed = true"
29
+ class="sidebar-backdrop"
30
+ ></div>
31
+ <div :class="['sidebar', { collapsed: sidebarCollapsed }]">
32
+ <div class="sidebar-section">
33
+ <div class="sidebar-section-title">Session Info</div>
34
+ <div class="session-info">
35
+ <div v-if="metadata.summary" class="session-summary-block">{{ metadata.summary }}</div>
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.copilotVersion">
48
+ <td>Version</td>
49
+ <td>{{ metadata.copilotVersion }}</td>
50
+ </tr>
51
+ <tr v-if="metadata.model">
52
+ <td>Model</td>
53
+ <td>{{ metadata.model }}</td>
54
+ </tr>
55
+ <tr v-if="metadata.repo">
56
+ <td>Repo</td>
57
+ <td>{{ metadata.repo }}</td>
58
+ </tr>
59
+ <tr v-if="metadata.branch">
60
+ <td>Branch</td>
61
+ <td>{{ metadata.branch }}</td>
62
+ </tr>
63
+ <tr v-if="metadata.cwd && !metadata.repo">
64
+ <td>Repo</td>
65
+ <td>{{ metadata.cwd }}</td>
66
+ </tr>
67
+ <tr v-if="metadata.created">
68
+ <td>Created</td>
69
+ <td>{{ formatDateTime(metadata.created) }}</td>
70
+ </tr>
71
+ <tr v-if="metadata.updated">
72
+ <td>Updated</td>
73
+ <td>{{ formatDateTime(metadata.updated) }}</td>
74
+ </tr>
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="sidebar-section">
81
+ <div class="sidebar-section-title">Event Filters</div>
82
+ <div class="event-filters">
83
+ <button
84
+ v-for="filter in filters"
85
+ :key="filter.type"
86
+ :class="['filter-btn', { active: currentFilter === filter.type }]"
87
+ :disabled="filter.disabled"
88
+ @click="setFilter(filter.type)"
89
+ >
90
+ {{ filter.label }}
91
+ </button>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Session Tags -->
96
+ <div class="sidebar-section session-tags-container">
97
+ <div class="sidebar-section-title">Tags</div>
98
+ <div v-if="!tagsEditing" class="tags-display">
99
+ <span
100
+ v-for="tag in sessionTags"
101
+ :key="tag"
102
+ class="tag-label"
103
+ :style="{ backgroundColor: getTagColor(tag) }"
104
+ >
105
+ {{ tag }}
106
+ </span>
107
+ <button class="tags-edit-btn" @click="startEditTags" title="Edit tags">
108
+ \u270F\uFE0F
109
+ </button>
110
+ </div>
111
+ <div v-else class="tags-dropdown">
112
+ <div class="tags-input-container">
113
+ <span
114
+ v-for="tag in editingTags"
115
+ :key="tag"
116
+ class="tag-input-chip"
117
+ :style="{ backgroundColor: getTagColor(tag) }"
118
+ >
119
+ {{ tag }}
120
+ <button @click="removeTagFromEdit(tag)" title="Remove tag">\xD7</button>
121
+ </span>
122
+ <input
123
+ ref="tagInputRef"
124
+ v-model="tagInputValue"
125
+ @keydown.enter.prevent="addTag"
126
+ @keydown.escape="cancelEditTags"
127
+ @blur="saveTagsOnBlur"
128
+ @input="updateAutocomplete"
129
+ class="tags-text-input"
130
+ placeholder="Type tag name..."
131
+ maxlength="30"
132
+ />
133
+ </div>
134
+ <div v-if="showAutocomplete && autocompleteOptions.length > 0" class="tags-autocomplete">
135
+ <div
136
+ v-for="(option, index) in autocompleteOptions"
137
+ :key="option"
138
+ :class="['tags-autocomplete-item', { selected: index === autocompleteSelectedIndex }]"
139
+ @click="selectAutocompleteOption(option)"
140
+ @mouseenter="autocompleteSelectedIndex = index"
141
+ >
142
+ {{ option }}
143
+ </div>
144
+ </div>
145
+ <div v-if="tagsError" class="tags-error">{{ tagsError }}</div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="content">
151
+ <div class="scroll-indicator">
152
+ <div class="content-toolbar-left">
153
+ <button
154
+ class="sidebar-toggle"
155
+ @click="() => { sidebarCollapsed = !sidebarCollapsed; trackClick && trackClick('SidebarToggled', { state: sidebarCollapsed ? 'open' : 'collapsed', sessionId: sessionId }); }"
156
+ :title="sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
157
+ >
158
+ \u2630
159
+ </button>
160
+
161
+ <!-- Turn dropdown with optgroup -->
162
+ <select
163
+ v-if="turns.length > 0"
164
+ v-model="currentTurnIndex"
165
+ @change="jumpToTurn(currentTurnIndex)"
166
+ class="turn-dropdown"
167
+ >
168
+ <optgroup
169
+ v-for="req in userReqs"
170
+ :key="req.reqNumber"
171
+ :label="req.reqNumber > 0 ? 'UserReq ' + req.reqNumber + ': ' + truncateText(req.message, 40) : 'Setup'"
172
+ >
173
+ <option v-for="turn in req.turns" :key="turn.id" :value="turn.id">
174
+ Turn {{ turn.originalTurnId ?? turn.id }} ({{ turn.duration }})
175
+ </option>
176
+ </optgroup>
177
+ </select>
178
+ </div>
179
+ <div class="content-toolbar-center">
180
+ </div>
181
+ <div class="content-toolbar-right">
182
+ <input
183
+ v-model="searchText"
184
+ type="text"
185
+ placeholder="\u{1F50D} Search events..."
186
+ class="search-input"
187
+ />
188
+ <span v-if="searchResultCount" class="search-result-count">
189
+ {{ searchResultCount }}
190
+ </span>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Loading state -->
195
+ <div v-if="eventsLoading" class="loading-message">
196
+ <div style="text-align: center; padding: 40px; color: #c9d1d9;">
197
+ \u23F3 Loading events...
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Error state -->
202
+ <div v-else-if="eventsError" class="error-message">
203
+ <div style="text-align: center; padding: 40px; color: #f85149;">
204
+ \u274C Error loading events: {{ eventsError }}
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Events list -->
209
+ <DynamicScroller
210
+ v-else
211
+ ref="scrollerRef"
212
+ :items="filteredEvents"
213
+ :min-item-size="80"
214
+ key-field="stableId"
215
+ class="scroller"
216
+ >
217
+ <template #default="{ item, index, active }">
218
+ <DynamicScrollerItem
219
+ :item="item"
220
+ :active="active"
221
+ :size-dependencies="[expansionCount]"
222
+ :data-index="index"
223
+ >
224
+ <!-- Turn Start Divider -->
225
+ <div
226
+ v-if="item.type === 'assistant.turn_start'"
227
+ :data-type="item.type"
228
+ :data-index="item.virtualIndex"
229
+ class="turn-divider"
230
+ >
231
+ <div class="turn-divider-line-left"></div>
232
+ <span class="turn-divider-text">
233
+ UserReq {{ getTurnNumber(item.virtualIndex) }}
234
+ <template v-if="metadata.source === 'vscode'">
235
+ <span class="turn-time">{{ formatTime(item.timestamp) }}</span>
236
+ <span v-if="getTurnDuration(item.virtualIndex)" class="turn-duration">{{ getTurnDuration(item.virtualIndex) }}</span>
237
+ </template>
238
+ <template v-else>Start</template>
239
+ </span>
240
+ <div class="turn-divider-line-right"></div>
241
+ <div class="divider-separator"></div>
242
+ </div>
243
+
244
+ <!-- Subagent Divider -->
245
+ <div
246
+ v-else-if="item.type === 'subagent.started' || item.type === 'subagent.completed' || item.type === 'subagent.failed'"
247
+ :data-type="item.type"
248
+ :data-index="item.virtualIndex"
249
+ :class="['subagent-divider', item.type.split('.')[1]]"
250
+ :style="{
251
+ '--sa-color': getSubagentColor(item) || '#58a6ff'
252
+ }"
253
+ >
254
+ <div class="subagent-divider-line-left" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
255
+ <span class="subagent-divider-text" :style="{ color: getSubagentColor(item) || '#58a6ff', borderColor: getSubagentColor(item) || '#58a6ff', background: (getSubagentColor(item) || '#58a6ff') + '1a' }">
256
+ \u{1F916} {{ item.data?.agentDisplayName || item.data?.agentName || 'SubAgent' }}
257
+ {{ item.type === 'subagent.started' ? 'Start \u25B6' : item.type === 'subagent.completed' ? 'Complete \u2713' : 'Failed \u2717' }}
258
+ </span>
259
+ <div class="subagent-divider-line-right" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
260
+ <div class="divider-separator"></div>
261
+ </div>
262
+
263
+ <!-- Regular Event -->
264
+ <div
265
+ v-else
266
+ :class="['event', getSubagentInfo(item) ? 'event-in-subagent' : '']"
267
+ :data-type="item.type"
268
+ :data-index="item.virtualIndex"
269
+ :style="getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}"
270
+ >
271
+ <div class="event-header">
272
+ <span :class="['event-badge', getBadgeInfo(item.type, item).class]">
273
+ {{ getBadgeInfo(item.type, item).label }}
274
+ </span>
275
+ <span
276
+ v-if="getSubagentInfo(item)"
277
+ class="subagent-owner-tag"
278
+ :style="{ color: getSubagentColor(item), borderColor: getSubagentColor(item) }"
279
+ >{{ getSubagentInfo(item).name }}</span>
280
+ <span v-if="metadata.source !== 'vscode'" class="event-timestamp">{{ formatTime(item.timestamp) }}</span>
281
+ </div>
282
+
283
+ <!-- Abort event: show reason -->
284
+ <div v-if="item.type === 'abort' && item.data?.reason" class="event-content">
285
+ <strong>Reason:</strong> {{ item.data.reason }}
286
+ </div>
287
+
288
+ <!-- Session start: show type and selectedModel -->
289
+ <div v-else-if="item.type === 'session.start'" class="event-content">
290
+ <div v-if="item.data?.type"><strong>Type:</strong> {{ item.data.type }}</div>
291
+ <div v-if="item.data?.selectedModel"><strong>Model:</strong> {{ item.data.selectedModel }}</div>
292
+ <div v-if="item.data?.producer"><strong>Producer:</strong> {{ item.data.producer }}</div>
293
+ </div>
294
+
295
+ <!-- Session resume: show resumeTime, eventCount, context -->
296
+ <div v-else-if="item.type === 'session.resume'" class="event-content">
297
+ <div v-if="item.data?.resumeTime"><strong>Resume Time:</strong> {{ formatDateTime(item.data.resumeTime) }}</div>
298
+ <div v-if="item.data?.eventCount"><strong>Event Count:</strong> {{ item.data.eventCount }}</div>
299
+ <div v-if="item.data?.context?.branch"><strong>Branch:</strong> {{ item.data.context.branch }}</div>
300
+ <div v-if="item.data?.context?.repository"><strong>Repository:</strong> {{ item.data.context.repository }}</div>
301
+ <div v-if="item.data?.context?.cwd"><strong>Working Directory:</strong> {{ item.data.context.cwd }}</div>
302
+ </div>
303
+
304
+ <!-- Session error: show errorType + message -->
305
+ <div v-else-if="item.type === 'session.error' && (item.data?.errorType || item.data?.message)" class="event-content">
306
+ <div v-if="item.data?.errorType"><strong>Error Type:</strong> {{ item.data.errorType }}</div>
307
+ <div v-if="item.data?.message"><strong>Message:</strong> {{ item.data.message }}</div>
308
+ </div>
309
+
310
+ <!-- Model change: show previousModel \u2192 newModel -->
311
+ <div v-else-if="item.type === 'session.model_change'" class="event-content model-change-content">
312
+ <div v-if="item.data?.previousModel && item.data?.newModel" class="model-change-text">
313
+ <span class="model-name">{{ item.data.previousModel }}</span>
314
+ <span class="model-arrow">\u2192</span>
315
+ <span class="model-name">{{ item.data.newModel }}</span>
316
+ </div>
317
+ <div v-else-if="item.data?.newModel" class="model-change-text">
318
+ Switched to <span class="model-name">{{ item.data.newModel }}</span>
319
+ </div>
320
+ <div v-else-if="item.data?.model" class="model-change-text">
321
+ Switched to <span class="model-name">{{ item.data.model }}</span>
322
+ </div>
323
+ <div v-else class="model-change-text">
324
+ Model changed
325
+ </div>
326
+ </div>
327
+
328
+ <!-- Session truncation: show token/message removal info -->
329
+ <div v-else-if="item.type === 'system.notification'" class="event-content" style="opacity:0.7">
330
+ <span>{{ item.data?.message }}</span>
331
+ </div>
332
+
333
+ <div v-else-if="item.type === 'session.truncation'" class="event-content">
334
+ <div v-if="item.data?.messagesRemovedDuringTruncation"><strong>Messages removed:</strong> {{ item.data.messagesRemovedDuringTruncation }}</div>
335
+ <div v-if="item.data?.tokensRemovedDuringTruncation"><strong>Tokens removed:</strong> {{ item.data.tokensRemovedDuringTruncation.toLocaleString() }}</div>
336
+ <div v-if="item.data?.preTruncationTokensInMessages"><strong>Pre-truncation tokens:</strong> {{ item.data.preTruncationTokensInMessages.toLocaleString() }}</div>
337
+ <div v-if="item.data?.postTruncationMessagesLength"><strong>Post-truncation messages:</strong> {{ item.data.postTruncationMessagesLength }}</div>
338
+ <div v-if="item.data?.performedBy"><strong>Performed by:</strong> {{ item.data.performedBy }}</div>
339
+ </div>
340
+
341
+ <!-- Session compaction start -->
342
+ <div v-else-if="item.type === 'session.compaction_start'" class="event-content">
343
+ Context compaction started
344
+ </div>
345
+
346
+ <!-- Session compaction complete: show results -->
347
+ <div v-else-if="item.type === 'session.compaction_complete'" class="event-content">
348
+ <div v-if="item.data?.success != null"><strong>Success:</strong> {{ item.data.success ? '\u2713' : '\u2717' }}</div>
349
+ <div v-if="item.data?.compactionTokensUsed">
350
+ <strong>Tokens used:</strong>
351
+ input {{ item.data.compactionTokensUsed.input?.toLocaleString() || 0 }},
352
+ output {{ item.data.compactionTokensUsed.output?.toLocaleString() || 0 }}
353
+ <span v-if="item.data.compactionTokensUsed.cachedInput">, cached {{ item.data.compactionTokensUsed.cachedInput.toLocaleString() }}</span>
354
+ </div>
355
+ <div v-if="item.data?.preCompactionMessagesLength"><strong>Pre-compaction messages:</strong> {{ item.data.preCompactionMessagesLength }}</div>
356
+ <div v-if="item.data?.preCompactionTokens"><strong>Pre-compaction tokens:</strong> {{ item.data.preCompactionTokens.toLocaleString() }}</div>
357
+ <div v-if="item.data?.summaryContent" style="margin-top: 8px;">
358
+ <button
359
+ @click="toggleContent('compaction-' + item.stableId)"
360
+ style="background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;"
361
+ >
362
+ {{ expandedContent['compaction-' + item.stableId] ? 'Hide summary \u25B2' : 'Show summary \u25BC' }}
363
+ </button>
364
+ <div v-if="expandedContent['compaction-' + item.stableId]" class="event-content" style="margin-top: 8px;" v-html="renderMarkdown(item.data.summaryContent)"></div>
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Regular content (unified format from server) -->
369
+ <div v-else-if="item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent">
370
+ <div
371
+ class="event-content"
372
+ v-html="highlightSearchText(
373
+ renderMarkdown(
374
+ (expandedContent[item.stableId] || !isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent))
375
+ ? (item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)
376
+ : truncateContent(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)
377
+ ),
378
+ searchText
379
+ )"
380
+ ></div>
381
+ <div
382
+ v-if="isContentTooLong(item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent)"
383
+ style="margin-top: 8px;"
384
+ >
385
+ <button
386
+ @click="toggleContent(item.stableId)"
387
+ :data-content-id="item.stableId"
388
+ style="background: none; border: 1px solid #30363d; color: #58a6ff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;"
389
+ >
390
+ {{ expandedContent[item.stableId] ? 'Show less \u25B2' : 'Show more \u25BC' }}
391
+ </button>
392
+ </div>
393
+ </div>
394
+
395
+ <!-- No content at all (no message and no tools) -->
396
+ <div v-else-if="!hasTools(item)" class="event-content" style="color: #7d8590; font-style: italic;">
397
+ No available message
398
+ </div>
399
+
400
+ <!-- Tool calls section (independent of message content, but don't need "No available message" if tools exist) -->
401
+ <div v-if="hasTools(item)" class="tool-list">
402
+ <div
403
+ v-for="(group, idx) in getToolGroups(item)"
404
+ :key="idx"
405
+ class="tool-item"
406
+ >
407
+ <div
408
+ class="tool-header-line"
409
+ @click="toggleTool(item.stableId + '-' + idx)"
410
+ >
411
+ <span class="tool-connector">{{ idx === getToolGroups(item).length - 1 ? '\u2514\u2500' : '\u251C\u2500' }}</span>
412
+ <span class="tool-expand-icon">{{ expandedTools[item.stableId + '-' + idx] ? '\u25BC' : '\u25B6' }}</span>
413
+ <span class="tool-name">\u{1F527}&nbsp;{{ group.start?.data?.toolName || group.tool || 'Tool' }}</span>
414
+ <span :class="getToolStatus(group).color" style="margin-left: 4px;">({{ getToolStatus(group).icon }}{{ getToolDuration(group) ? ' ' + getToolDuration(group) : '' }})</span>
415
+ <span v-if="getToolCommand(group)" style="color: #7d8590; margin-left: 8px;">{{ getToolCommand(group) }}</span>
416
+ <span v-if="getToolErrorMessage(group)" style="color: #ff7b72; margin-left: 8px;">{{ getToolErrorMessage(group).length > 80 ? getToolErrorMessage(group).substring(0, 80) + '...' : getToolErrorMessage(group) }}</span>
417
+ </div>
418
+
419
+ <div v-if="expandedTools[item.stableId + '-' + idx]" class="tool-detail">
420
+ <div v-if="group.start?.data?.arguments" class="tool-detail-section">
421
+ <div class="tool-detail-title">Arguments:</div>
422
+ <div class="tool-detail-content">
423
+ <pre>{{ JSON.stringify(group.start.data.arguments, null, 2) }}</pre>
424
+ </div>
425
+ </div>
426
+ <div v-if="group.complete?.data?.result" class="tool-detail-section">
427
+ <div class="tool-detail-title">Result:</div>
428
+ <div class="tool-detail-content">
429
+ <pre>{{ JSON.stringify(group.complete.data.result, null, 2) }}</pre>
430
+ </div>
431
+ </div>
432
+ <div v-if="getToolErrorMessage(group)" class="tool-detail-section">
433
+ <div class="tool-detail-title">Error:</div>
434
+ <div class="tool-detail-content" style="color: #ff7b72;">
435
+ {{ getToolErrorMessage(group) }}
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+
442
+ <!-- Separator (inside event for proper height calculation) -->
443
+ <div v-if="!item.isLastEvent" class="event-separator"></div>
444
+ </div>
445
+ </DynamicScrollerItem>
446
+ </template>
447
+ </DynamicScroller>
448
+
449
+ <!-- Bottom spacer: ensures last item clears mobile browser nav bar -->
450
+ <div class="scroller-bottom-spacer"></div>
451
+
452
+ <!-- Floating scroll buttons -->
453
+ <div class="scroll-float-btns">
454
+ <button @click="scrollToTop" title="Scroll to top" class="scroll-edge-btn">\u25B2</button>
455
+ <button @click="scrollToBottom" title="Scroll to bottom" class="scroll-edge-btn">\u25BC</button>
456
+ </div>
457
+ </div>
458
+ </div>
459
+
460
+ </div>
461
+ `});console.log("Mounting Vue app to #app..."),console.log("App config:",G.config),console.log("Target element:",document.getElementById("app"));try{let m=G.mount("#app");console.log("Vue app mounted successfully!",m?"Instance created":"No instance"),console.log("VM type:",typeof m,"Has exportSession:",typeof m?.exportSession),console.log("VM keys:",m?Object.keys(m).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(m){console.error("Mount failed:",m),console.error("Error stack:",m.stack)}})();})();
@@ -0,0 +1 @@
1
+ (()=>{(function(){"use strict";let n=window.__DISABLE_TELEMETRY===!0;window.trackClick=function(e,t={}){if(!n)try{if(typeof window.appInsights>"u"||!window.appInsights){console.debug("Application Insights not loaded, skipping telemetry event:",e);return}window.appInsights.trackEvent({name:e,properties:{...t,timestamp:new Date().toISOString(),userAgent:navigator.userAgent,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight}}),console.debug("Telemetry event tracked:",e,t)}catch(i){console.error("Failed to track telemetry event:",e,i)}},window.trackPageView=function(e,t={}){if(!n)try{if(typeof window.appInsights>"u"||!window.appInsights){console.debug("Application Insights not loaded, skipping page view:",e);return}window.appInsights.trackPageView({name:e,properties:{...t,timestamp:new Date().toISOString()}}),console.debug("Page view tracked:",e,t)}catch(i){console.error("Failed to track page view:",e,i)}},window.trackMetric=function(e,t,i={}){if(!n)try{if(typeof window.appInsights>"u"||!window.appInsights){console.debug("Application Insights not loaded, skipping metric:",e);return}window.appInsights.trackMetric({name:e,average:t,properties:{...i,timestamp:new Date().toISOString()}}),console.debug("Metric tracked:",e,t,i)}catch(o){console.error("Failed to track metric:",e,o)}},console.log(n?"\u{1F4CA} Browser telemetry disabled (window.__DISABLE_TELEMETRY = true)":"\u{1F4CA} Browser telemetry wrapper initialized")})();})();