@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.
- package/package.json +34 -5
- package/public/js/homepage.min.js +35 -0
- package/public/js/session-detail.min.js +461 -0
- package/public/js/telemetry-browser.min.js +1 -0
- package/public/js/time-analyze.min.js +518 -0
- package/server.js +3 -0
- package/src/app.js +2 -1
- package/src/controllers/insightController.js +31 -0
- package/src/controllers/sessionController.js +56 -0
- package/src/controllers/tagController.js +8 -0
- package/src/controllers/uploadController.js +32 -1
- package/src/middleware/common.js +20 -1
- package/src/telemetry.js +152 -0
- package/views/index.ejs +9 -494
- package/views/session-vue.ejs +166 -1869
- package/views/telemetry-snippet.ejs +26 -0
- package/views/time-analyze.ejs +2 -2217
- package/.env.example +0 -14
- package/.nycrc +0 -29
- package/CHANGELOG.md +0 -290
- package/examples/parser-usage.js +0 -114
|
@@ -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} {{ 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")})();})();
|