@qiaolei81/copilot-session-viewer 0.3.1 → 0.3.3

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.
@@ -773,2226 +773,11 @@
773
773
  <script src="https://cdn.jsdelivr.net/npm/vue@3.5.28/dist/vue.global.prod.js" integrity="sha384-EyKhbIJoP1t1fKIFRNEfYKy4uy8qxs7UNS4Cab53xyXqCTUB1PCoxeFsD0G/NX9W" crossorigin="anonymous"></script>
774
774
 
775
775
  <script>
776
- window.sessionData = {
776
+ window.__PAGE_DATA = {
777
777
  sessionId: '<%= sessionId %>',
778
778
  metadata: <%- JSON.stringify(metadata).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029') %>
779
779
  };
780
780
  </script>
781
-
782
- <script>
783
- const { createApp, ref, computed, onMounted, onUnmounted, reactive } = Vue;
784
-
785
- const app = createApp({
786
- setup() {
787
- const sessionId = ref(window.sessionData.sessionId);
788
- const metadata = ref(window.sessionData.metadata);
789
- const events = ref([]);
790
- const loading = ref(true);
791
- const error = ref(null);
792
- const activeTab = ref('timeline');
793
- const sortField = ref('timestamp');
794
- const sortDir = ref('asc');
795
- const insightReport = ref(null);
796
- const insightLog = ref(null);
797
- const insightLoading = ref(false);
798
- const insightError = ref(null);
799
- const insightGeneratedAt = ref(null);
800
- const showMarkerLegend = ref(false);
801
- const copyLabel = ref('📊 Copy as Mermaid Gantt');
802
-
803
- // Helper: normalize message to string (handle arrays/objects from Copilot)
804
- const normalizeMessage = (msg) => {
805
- if (!msg) return '';
806
- if (typeof msg === 'string') return msg;
807
- if (Array.isArray(msg)) {
808
- // Handle content array (Copilot format)
809
- return msg.map(c => c.text || c.content || '').join(' ');
810
- }
811
- if (typeof msg === 'object' && msg.text) return msg.text;
812
- return String(msg);
813
- };
814
-
815
- // Gantt crosshair
816
- const ganttCrosshairX = ref(null); // px from left of gantt-container
817
- const ganttCrosshairTime = ref('');
818
- const onGanttMouseMove = (e) => {
819
- const container = e.currentTarget;
820
- // Find the first gantt-bar-area to get the bar column bounds
821
- const barArea = container.querySelector('.gantt-bar-area');
822
- if (!barArea) return;
823
- const barRect = barArea.getBoundingClientRect();
824
- const containerRect = container.getBoundingClientRect();
825
- const barLeft = barRect.left - containerRect.left;
826
- const barRight = barLeft + barRect.width;
827
- const mouseX = e.clientX - containerRect.left;
828
-
829
- if (mouseX >= barLeft && mouseX <= barRight) {
830
- ganttCrosshairX.value = mouseX;
831
- // Compute timestamp from position
832
- const pct = (mouseX - barLeft) / barRect.width;
833
- const ts = sessionStart.value + pct * totalDuration.value;
834
- const d = new Date(ts);
835
- const h = String(d.getHours()).padStart(2, '0');
836
- const m = String(d.getMinutes()).padStart(2, '0');
837
- const s = String(d.getSeconds()).padStart(2, '0');
838
- ganttCrosshairTime.value = h + ':' + m + ':' + s;
839
- } else {
840
- ganttCrosshairX.value = null;
841
- }
842
- };
843
- const onGanttMouseLeave = () => {
844
- ganttCrosshairX.value = null;
845
- };
846
-
847
- const copyTimelineMarkdown = async () => {
848
- const items = unifiedTimelineItems.value;
849
- if (!items.length) return;
850
-
851
- // Helper: convert timestamp to Unix epoch milliseconds
852
- const toEpochMs = (ts) => {
853
- if (!ts) return 0;
854
- return new Date(ts).getTime();
855
- };
856
-
857
- // Sanitize label for Mermaid: strip chars that break syntax or could escape the code block
858
- const sanitize = (str) => (str || '').replace(/[`\n\r]/g, '').replace(/[:;#]/g, '-').replace(/\s+/g, ' ').trim().substring(0, 100);
859
- const normalizeMessage = (msg) => {
860
- if (!msg) return '';
861
- if (typeof msg === 'string') return msg;
862
- if (Array.isArray(msg)) {
863
- // Handle content array (Copilot format)
864
- return msg.map(c => c.text || c.content || '').join(' ');
865
- }
866
- if (typeof msg === 'object' && msg.text) return msg.text;
867
- return String(msg);
868
- };
869
-
870
- // Deduplicate task IDs within the mermaid block
871
- const usedIds = {};
872
- const uniqueId = (base) => {
873
- const clean = base.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
874
- if (!usedIds[clean]) { usedIds[clean] = 1; return clean; }
875
- usedIds[clean]++;
876
- return clean + '_' + usedIds[clean];
877
- };
878
-
879
- const lines = [];
880
- lines.push('```mermaid');
881
- lines.push('gantt');
882
- lines.push(' title Session Timeline – ' + sanitize(sessionId.value));
883
- lines.push(' dateFormat x');
884
- lines.push(' axisFormat %H:%M:%S');
885
- lines.push('');
886
-
887
- for (const item of items) {
888
- if (item.rowType === 'user-req') {
889
- const msg = sanitize(normalizeMessage(item.message) || 'No message').substring(0, 40);
890
- const label = 'UserReq ' + item.userReqNumber + ' – ' + msg + ' (' + formatDuration(item.duration) + ')';
891
- const id = uniqueId('userreq_' + item.userReqNumber);
892
- const start = toEpochMs(item.startTime);
893
- const end = toEpochMs(item.endTime);
894
- lines.push(' ' + label + ' :milestone, ' + id + ', ' + start + ', ' + end);
895
- } else if (item.rowType === 'subagent') {
896
- const start = toEpochMs(item.startTime);
897
- const end = toEpochMs(item.endTime);
898
- const toolInfo = (item.toolCalls != null ? item.toolCalls : 0) + ' tools';
899
- const label = sanitize(item.name) + ' – ' + formatDuration(item.duration) + ' (' + toolInfo + ')';
900
- const id = uniqueId(item.name);
901
- const tag = item.status === 'failed' ? 'crit, '
902
- : item.status === 'incomplete' ? 'active, ' : '';
903
- lines.push(' ' + label + ' :' + tag + id + ', ' + start + ', ' + end);
904
- } else if (item.rowType === 'main-agent') {
905
- const start = toEpochMs(item.startTime);
906
- const end = toEpochMs(item.endTime);
907
- const detail = sanitize(item.summary || 'idle');
908
- const label = 'Main Agent – ' + formatDuration(item.duration) + ' (' + detail + ')';
909
- const id = uniqueId('main_agent');
910
- lines.push(' ' + label + ' :' + id + ', ' + start + ', ' + end);
911
- }
912
- }
913
-
914
- lines.push('```');
915
- lines.push('');
916
-
917
- const md = lines.join('\n');
918
-
919
- try {
920
- await navigator.clipboard.writeText(md);
921
- copyLabel.value = '✅ Copied!';
922
- } catch (err) {
923
- // Fallback for non-secure contexts
924
- const textarea = document.createElement('textarea');
925
- textarea.value = md;
926
- textarea.style.position = 'fixed';
927
- textarea.style.opacity = '0';
928
- document.body.appendChild(textarea);
929
- textarea.select();
930
- document.execCommand('copy');
931
- document.body.removeChild(textarea);
932
- copyLabel.value = '✅ Copied!';
933
- }
934
- setTimeout(() => { copyLabel.value = '📊 Copy as Mermaid Gantt'; }, 2000);
935
- };
936
-
937
- // 事件标记类别定义
938
- const EVENT_MARKER_CATEGORIES = {
939
- 'tool.execution_start': { color: '#d29922', shape: 'diamond', label: 'Tool Start' },
940
- 'tool.execution_complete': { color: '#e3b341', shape: 'diamond', label: 'Tool Complete' },
941
- 'assistant.message': { color: '#8b949e', shape: 'circle', label: 'Message' },
942
- 'user.message': { color: '#79c0ff', shape: 'square', label: 'User Message' },
943
- 'session.start': { color: '#56d364', shape: 'square', label: 'Session Start' },
944
- 'session.resume': { color: '#56d364', shape: 'square', label: 'Session Resume' },
945
- 'session.error': { color: '#f85149', shape: 'triangle', label: 'Error' },
946
- 'session.truncation': { color: '#f0883e', shape: 'triangle', label: 'Truncation' },
947
- 'session.compaction_start': { color: '#a371f7', shape: 'square', label: 'Compaction Start' },
948
- 'session.compaction_complete': { color: '#bc8cff', shape: 'square', label: 'Compaction End' },
949
- 'session.model_change': { color: '#f778ba', shape: 'square', label: 'Model Change' },
950
- 'abort': { color: '#ff7b72', shape: 'triangle', label: 'Abort' },
951
- };
952
- const TRACKABLE_EVENT_TYPES = new Set(Object.keys(EVENT_MARKER_CATEGORIES));
953
-
954
- // ── Helpers ──
955
- const formatDuration = (ms) => {
956
- if (ms == null || ms < 0) return '—';
957
- if (ms < 1000) return Math.round(ms) + 'ms';
958
- const s = ms / 1000;
959
- if (s < 60) {
960
- const rounded = Math.round(s * 10) / 10; // 四舍五入到一位小数
961
- return (rounded % 1 === 0 ? Math.round(rounded) : rounded.toFixed(1)) + 's';
962
- }
963
- const m = Math.floor(s / 60);
964
- const remainder = Math.floor(s % 60);
965
- if (m < 60) return m + 'm ' + remainder + 's';
966
- const h = Math.floor(m / 60);
967
- return h + 'h ' + (m % 60) + 'm';
968
- };
969
-
970
- const formatTime = (ts) => {
971
- if (!ts) return '';
972
- const d = new Date(ts);
973
- return String(d.getHours()).padStart(2, '0') + ':' +
974
- String(d.getMinutes()).padStart(2, '0') + ':' +
975
- String(d.getSeconds()).padStart(2, '0');
976
- };
977
-
978
- const formatDateTime = (ts) => {
979
- if (!ts) return '';
980
- return new Date(ts).toLocaleString();
981
- };
982
-
983
- // ── Session timeline ──
984
- const sessionStart = computed(() => {
985
- if (!events.value.length) return null;
986
- // Find first event with valid timestamp
987
- for (const ev of events.value) {
988
- const ts = ev.timestamp || ev.snapshot?.timestamp;
989
- if (ts) return new Date(ts).getTime();
990
- }
991
- return null;
992
- });
993
-
994
- const sessionEnd = computed(() => {
995
- if (!events.value.length) return null;
996
- // Find last event with valid timestamp
997
- for (let i = events.value.length - 1; i >= 0; i--) {
998
- const ev = events.value[i];
999
- const ts = ev.timestamp || ev.snapshot?.timestamp;
1000
- if (ts) return new Date(ts).getTime();
1001
- }
1002
- return null;
1003
- });
1004
-
1005
- const totalDuration = computed(() => {
1006
- if (!sessionStart.value || !sessionEnd.value) return 0;
1007
- return sessionEnd.value - sessionStart.value;
1008
- });
1009
-
1010
- // ── Shared sorted events (computed once, reused everywhere) ──
1011
- // Stable sort: use _fileIndex (set by backend) as tiebreaker for identical timestamps
1012
- const sortedEvents = computed(() => {
1013
- return [...events.value].sort((a, b) => {
1014
- const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
1015
- const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
1016
- if (timeA !== timeB) return timeA - timeB;
1017
- return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
1018
- });
1019
- });
1020
-
1021
- // ── Map tool.execution_start events to owning subagent via parentToolCallId ──
1022
- const subagentToolMap = computed(() => {
1023
- const sorted = sortedEvents.value;
1024
-
1025
- // 1. Collect all subagent toolCallIds
1026
- const subagentToolCallIds = new Set();
1027
- for (const ev of sorted) {
1028
- if (ev.type === 'subagent.started') {
1029
- const tcid = ev.data?.toolCallId;
1030
- if (tcid) subagentToolCallIds.add(tcid);
1031
- }
1032
- }
1033
-
1034
- // 2. Build id → event lookup
1035
- const idMap = new Map();
1036
- for (const ev of sorted) {
1037
- if (ev.id) idMap.set(ev.id, ev);
1038
- }
1039
-
1040
- // 3. For each tool.execution_start, walk parentId to find assistant.message,
1041
- // then read data.parentToolCallId
1042
- const toolToSubagent = new Map(); // tool event id → subagent toolCallId
1043
- const startIdByToolCallId = new Map(); // data.toolCallId → subagent toolCallId (for matching complete events)
1044
- for (const ev of sorted) {
1045
- if (ev.type !== 'tool.execution_start') continue;
1046
- let current = ev.parentId;
1047
- let depth = 0;
1048
- while (current && depth < 10) {
1049
- const parent = idMap.get(current);
1050
- if (!parent) break;
1051
- if (parent.type === 'assistant.message') {
1052
- const ptcid = parent.data?.parentToolCallId;
1053
- if (ptcid && subagentToolCallIds.has(ptcid)) {
1054
- toolToSubagent.set(ev.id, ptcid);
1055
- const tcid = ev.data?.toolCallId;
1056
- if (tcid) startIdByToolCallId.set(tcid, ptcid);
1057
- }
1058
- break;
1059
- }
1060
- current = parent.parentId;
1061
- depth++;
1062
- }
1063
- }
1064
-
1065
- // 4. Map tool.execution_complete events via their toolCallId
1066
- for (const ev of sorted) {
1067
- if (ev.type !== 'tool.execution_complete') continue;
1068
- const tcid = ev.data?.toolCallId;
1069
- if (tcid && startIdByToolCallId.has(tcid)) {
1070
- toolToSubagent.set(ev.id, startIdByToolCallId.get(tcid));
1071
- }
1072
- }
1073
-
1074
- return toolToSubagent;
1075
- });
1076
-
1077
- // ── Sub-agent analysis ──
1078
- const subagentAnalysis = computed(() => {
1079
- const sorted = sortedEvents.value;
1080
- const results = [];
1081
- const startStack = [];
1082
-
1083
- for (const ev of sorted) {
1084
- if (ev.type === 'subagent.started') {
1085
- startStack.push(ev);
1086
- } else if (ev.type === 'subagent.completed' || ev.type === 'subagent.failed') {
1087
- // Find matching start by toolCallId
1088
- const tcid = ev.data?.toolCallId;
1089
- let startIdx = -1;
1090
- if (tcid) {
1091
- for (let i = startStack.length - 1; i >= 0; i--) {
1092
- if (startStack[i].data?.toolCallId === tcid) {
1093
- startIdx = i;
1094
- break;
1095
- }
1096
- }
1097
- }
1098
- // Fallback to LIFO if no toolCallId match (pop last started)
1099
- if (startIdx < 0 && startStack.length > 0) {
1100
- startIdx = startStack.length - 1;
1101
- }
1102
- const startEv = startIdx >= 0 ? startStack.splice(startIdx, 1)[0] : null;
1103
- // Extract name from matched start event (completed event doesn't have name)
1104
- const name = startEv?.data?.agentDisplayName || startEv?.data?.agentName || 'SubAgent';
1105
- const startTime = startEv ? new Date(startEv.timestamp).getTime() : null;
1106
- const endTime = new Date(ev.timestamp).getTime();
1107
- const duration = startTime ? endTime - startTime : null;
1108
-
1109
- // Count tool calls using parentToolCallId attribution (not time-window)
1110
- const subagentTcid = startEv?.data?.toolCallId;
1111
- let toolCalls = 0;
1112
- const innerEvents = [];
1113
- if (startEv) {
1114
- for (const e of sorted) {
1115
- if (e.type === 'tool.execution_start') {
1116
- if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
1117
- toolCalls++;
1118
- }
1119
- }
1120
- // Keep time-window for innerEvents/markers (visual only)
1121
- const t = new Date(e.timestamp).getTime();
1122
- if (t >= startTime && t <= endTime) {
1123
- if (TRACKABLE_EVENT_TYPES.has(e.type)) {
1124
- if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
1125
- // Non-tool trackable events: use time-window
1126
- innerEvents.push({ type: e.type, timestamp: t, data: e.data });
1127
- } else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
1128
- // Tool events: only include if attributed to this subagent
1129
- innerEvents.push({ type: e.type, timestamp: t, data: e.data });
1130
- }
1131
- }
1132
- }
1133
- }
1134
- }
1135
-
1136
- // Build event markers with clustering
1137
- const innerEventMarkers = buildEventMarkers(innerEvents, startTime, duration);
1138
-
1139
- results.push({
1140
- name,
1141
- status: ev.type === 'subagent.completed' ? 'completed' : 'failed',
1142
- startTime: startEv?.timestamp || null,
1143
- endTime: ev.timestamp,
1144
- duration,
1145
- toolCalls,
1146
- innerEventMarkers
1147
- });
1148
- }
1149
- }
1150
-
1151
- // Handle incomplete sub-agents (started but never completed/failed)
1152
- const sessionEndTime = sorted.length > 0 ? new Date(sorted[sorted.length - 1].timestamp).getTime() : Date.now();
1153
- for (const startEv of startStack) {
1154
- const name = startEv.data?.agentDisplayName || startEv.data?.agentName || 'SubAgent';
1155
- const startTime = new Date(startEv.timestamp).getTime();
1156
- const duration = sessionEndTime - startTime;
1157
-
1158
- // Count tool calls using parentToolCallId attribution (not time-window)
1159
- const subagentTcid = startEv.data?.toolCallId;
1160
- let toolCalls = 0;
1161
- const innerEvents = [];
1162
- for (const e of sorted) {
1163
- if (e.type === 'tool.execution_start') {
1164
- if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
1165
- toolCalls++;
1166
- }
1167
- }
1168
- // Keep time-window for innerEvents/markers (visual only)
1169
- const t = new Date(e.timestamp).getTime();
1170
- if (t >= startTime && t <= sessionEndTime) {
1171
- if (TRACKABLE_EVENT_TYPES.has(e.type)) {
1172
- if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
1173
- innerEvents.push({ type: e.type, timestamp: t, data: e.data });
1174
- } else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
1175
- innerEvents.push({ type: e.type, timestamp: t, data: e.data });
1176
- }
1177
- }
1178
- }
1179
- }
1180
-
1181
- const innerEventMarkers = buildEventMarkers(innerEvents, startTime, duration);
1182
-
1183
- results.push({
1184
- name,
1185
- status: 'incomplete',
1186
- startTime: startEv.timestamp,
1187
- endTime: sorted[sorted.length - 1]?.timestamp || startEv.timestamp,
1188
- duration,
1189
- toolCalls,
1190
- innerEventMarkers
1191
- });
1192
- }
1193
-
1194
- return results.sort((a, b) => {
1195
- const tA = a.startTime ? new Date(a.startTime).getTime() : 0;
1196
- const tB = b.startTime ? new Date(b.startTime).getTime() : 0;
1197
- return tA - tB;
1198
- });
1199
- });
1200
-
1201
- const maxSubagentDuration = computed(() => {
1202
- return Math.max(...subagentAnalysis.value.map(s => s.duration || 0), 1);
1203
- });
1204
-
1205
- const subagentStats = computed(() => {
1206
- const agents = subagentAnalysis.value;
1207
- const completed = agents.filter(a => a.status === 'completed').length;
1208
- const failed = agents.filter(a => a.status === 'failed').length;
1209
- const incomplete = agents.filter(a => a.status === 'incomplete').length;
1210
- const successRate = agents.length ? ((completed / agents.length) * 100).toFixed(0) : 100;
1211
-
1212
- // Merge overlapping intervals to get actual wall-clock time in subagents
1213
- const intervals = agents
1214
- .filter(a => a.startTime && a.endTime)
1215
- .map(a => [new Date(a.startTime).getTime(), new Date(a.endTime).getTime()])
1216
- .sort((a, b) => a[0] - b[0]);
1217
- const mergedIntervals = [];
1218
- for (const [s, e] of intervals) {
1219
- if (!mergedIntervals.length || s >= mergedIntervals[mergedIntervals.length - 1].e) {
1220
- mergedIntervals.push({ s, e });
1221
- } else if (e > mergedIntervals[mergedIntervals.length - 1].e) {
1222
- mergedIntervals[mergedIntervals.length - 1].e = e;
1223
- }
1224
- }
1225
- const totalTime = mergedIntervals.reduce((sum, iv) => sum + (iv.e - iv.s), 0);
1226
-
1227
- // Sum per-subagent tool counts (already correctly attributed via parentToolCallId)
1228
- const totalTools = agents.reduce((sum, a) => sum + (a.toolCalls || 0), 0);
1229
-
1230
- return { completed, failed, incomplete, totalTime, totalTools, successRate };
1231
- });
1232
-
1233
- // ── Event marker builder (shared by subagent + agent-op) ──
1234
- const buildEventMarkers = (innerEvents, startTime, duration) => {
1235
- if (!innerEvents.length || !duration) return [];
1236
-
1237
- // Separate high-priority events (always shown individually) from tool events (clustered)
1238
- const HIGH_PRIORITY_TYPES = new Set([
1239
- 'session.start', 'session.resume', 'session.error',
1240
- 'session.truncation', 'session.compaction_start', 'session.compaction_complete',
1241
- 'session.model_change', 'abort', 'user.message',
1242
- ]);
1243
-
1244
- const hiPriEvents = [];
1245
- const toolEvents = [];
1246
- for (const ev of innerEvents) {
1247
- if (HIGH_PRIORITY_TYPES.has(ev.type)) {
1248
- hiPriEvents.push(ev);
1249
- } else {
1250
- toolEvents.push(ev);
1251
- }
1252
- }
1253
-
1254
- // Build individual markers for high-priority events (never clustered)
1255
- const hiPriMarkers = hiPriEvents.map(ev => {
1256
- const relPos = ((ev.timestamp - startTime) / duration) * 100;
1257
- const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
1258
- return {
1259
- type: ev.type,
1260
- position: Math.max(0, Math.min(100, relPos)),
1261
- color: cat.color || '#8b949e',
1262
- shape: cat.shape || 'circle',
1263
- label: cat.label || ev.type,
1264
- timestamp: ev.timestamp,
1265
- toolName: ev.data?.toolName || null,
1266
- };
1267
- });
1268
-
1269
- // Cluster tool events into fixed time buckets
1270
- // Adaptive: aim for ~20 buckets max, with a minimum of 5 minutes per bucket
1271
- const MIN_BUCKET_MS = 5 * 60 * 1000; // 5 minutes
1272
- const bucketSize = Math.max(MIN_BUCKET_MS, duration / 20);
1273
-
1274
- const buckets = new Map(); // bucketIndex -> events[]
1275
- for (const ev of toolEvents) {
1276
- const bucketIdx = Math.floor((ev.timestamp - startTime) / bucketSize);
1277
- if (!buckets.has(bucketIdx)) buckets.set(bucketIdx, []);
1278
- buckets.get(bucketIdx).push(ev);
1279
- }
1280
-
1281
- // Helper: interpolate color from tool yellow to error red based on error ratio
1282
- const toolErrorColor = (errorRatio) => {
1283
- // 0 = #d29922 (yellow), 1 = #f85149 (red)
1284
- const r = Math.round(210 + (248 - 210) * errorRatio);
1285
- const g = Math.round(153 + (81 - 153) * errorRatio);
1286
- const b = Math.round(34 + (73 - 34) * errorRatio);
1287
- return 'rgb(' + r + ',' + g + ',' + b + ')';
1288
- };
1289
-
1290
- const isToolError = (ev) => {
1291
- return ev.type === 'tool.execution_complete' && (ev.data?.isError || !!ev.data?.error);
1292
- };
1293
-
1294
- const toolMarkers = [];
1295
- for (const [bucketIdx, group] of buckets) {
1296
- const bucketMid = startTime + (bucketIdx + 0.5) * bucketSize;
1297
- const relPos = ((bucketMid - startTime) / duration) * 100;
1298
- const pos = Math.max(0, Math.min(100, relPos));
1299
-
1300
- // Count errors in this bucket
1301
- const errorCount = group.filter(isToolError).length;
1302
- const completeCount = group.filter(ev => ev.type === 'tool.execution_complete').length;
1303
- const errorRatio = completeCount > 0 ? errorCount / completeCount : 0;
1304
-
1305
- if (group.length === 1) {
1306
- const ev = group[0];
1307
- const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
1308
- const color = isToolError(ev) ? '#f85149' : cat.color || '#8b949e';
1309
- toolMarkers.push({
1310
- type: ev.type,
1311
- position: pos,
1312
- color,
1313
- shape: cat.shape || 'circle',
1314
- label: isToolError(ev) ? (cat.label || ev.type) + ' (error)' : (cat.label || ev.type),
1315
- timestamp: ev.timestamp,
1316
- toolName: ev.data?.toolName || null,
1317
- });
1318
- } else {
1319
- // Summarize the cluster
1320
- const typeCounts = {};
1321
- group.forEach(ev => {
1322
- const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
1323
- const lbl = cat.label || ev.type;
1324
- typeCounts[lbl] = (typeCounts[lbl] || 0) + 1;
1325
- });
1326
- if (errorCount > 0) typeCounts['Errors'] = errorCount;
1327
- const summaryParts = Object.entries(typeCounts).map(([l, c]) => c + ' ' + l);
1328
- const clusterColor = errorRatio > 0 ? toolErrorColor(errorRatio) : ((() => {
1329
- const dominantType = group.reduce((best, ev) => {
1330
- const cnt = group.filter(e => e.type === ev.type).length;
1331
- return cnt > best.cnt ? { type: ev.type, cnt } : best;
1332
- }, { type: group[0].type, cnt: 0 }).type;
1333
- return (EVENT_MARKER_CATEGORIES[dominantType] || {}).color || '#8b949e';
1334
- })());
1335
-
1336
- toolMarkers.push({
1337
- type: 'cluster',
1338
- position: pos,
1339
- color: clusterColor,
1340
- shape: 'cluster',
1341
- label: summaryParts.join(', '),
1342
- count: group.length,
1343
- items: group,
1344
- });
1345
- }
1346
- }
1347
-
1348
- // Merge and sort by position
1349
- return [...hiPriMarkers, ...toolMarkers].sort((a, b) => a.position - b.position);
1350
- };
1351
-
1352
- // ── Build Agent Operation item for gaps ──
1353
- const buildAgentOpItem = (sorted, gapStart, gapEnd) => {
1354
- const duration = gapEnd - gapStart;
1355
- const gapEvents = [];
1356
- const eventCounts = {};
1357
- let toolCalls = 0;
1358
- for (const e of sorted) {
1359
- const t = new Date(e.timestamp).getTime();
1360
- if (t >= gapStart && t <= gapEnd) {
1361
- // For tool events, only include those NOT attributed to a subagent
1362
- if (e.type.startsWith('tool.')) {
1363
- const isSubagentTool = e.id && subagentToolMap.value.has(e.id);
1364
- if (!isSubagentTool) {
1365
- if (TRACKABLE_EVENT_TYPES.has(e.type)) {
1366
- gapEvents.push({ type: e.type, timestamp: t, data: e.data });
1367
- }
1368
- if (e.type === 'tool.execution_start') {
1369
- toolCalls++;
1370
- }
1371
- eventCounts.tool = (eventCounts.tool || 0) + 1;
1372
- }
1373
- } else {
1374
- if (TRACKABLE_EVENT_TYPES.has(e.type)) {
1375
- gapEvents.push({ type: e.type, timestamp: t, data: e.data });
1376
- }
1377
- let cat = 'other';
1378
- if (e.type.startsWith('assistant.')) cat = 'message';
1379
- else if (e.type.startsWith('user.')) cat = 'user';
1380
- else if (e.type.startsWith('session.')) cat = 'session';
1381
- eventCounts[cat] = (eventCounts[cat] || 0) + 1;
1382
- }
1383
- }
1384
- }
1385
-
1386
- // Build summary string
1387
- const parts = [];
1388
- if (toolCalls) parts.push(toolCalls + ' tool' + (toolCalls > 1 ? 's' : ''));
1389
- if (eventCounts.message) parts.push(eventCounts.message + ' message' + (eventCounts.message > 1 ? 's' : ''));
1390
- if (eventCounts.user) parts.push(eventCounts.user + ' user msg');
1391
- if (eventCounts.session) parts.push(eventCounts.session + ' session event' + (eventCounts.session > 1 ? 's' : ''));
1392
- if (eventCounts.other) parts.push(eventCounts.other + ' other');
1393
- const summary = parts.length ? parts.join(', ') : 'idle';
1394
-
1395
- const innerEventMarkers = buildEventMarkers(gapEvents, gapStart, duration);
1396
-
1397
- return {
1398
- itemType: 'agent-op',
1399
- name: 'Main Agent',
1400
- summary,
1401
- toolCalls,
1402
- startTime: new Date(gapStart).toISOString(),
1403
- endTime: new Date(gapEnd).toISOString(),
1404
- duration,
1405
- eventCounts,
1406
- innerEventMarkers,
1407
- };
1408
- };
1409
-
1410
- // ── Subagent timeline items (subagent bars + agent-op gaps) ──
1411
- const subagentTimelineItems = computed(() => {
1412
- const agents = subagentAnalysis.value;
1413
- if (!agents.length) return [];
1414
-
1415
- const sorted = sortedEvents.value;
1416
- const items = [];
1417
-
1418
- for (let i = 0; i < agents.length; i++) {
1419
- const sa = agents[i];
1420
-
1421
- // Before first subagent: check gap from session start
1422
- if (i === 0 && sa.startTime) {
1423
- const gapStart = sessionStart.value;
1424
- const gapEnd = new Date(sa.startTime).getTime();
1425
- if (gapEnd - gapStart > 500) {
1426
- items.push(buildAgentOpItem(sorted, gapStart, gapEnd));
1427
- }
1428
- }
1429
-
1430
- // Add subagent itself
1431
- items.push({ ...sa, itemType: 'subagent' });
1432
-
1433
- // Gap between this and next subagent (or session end)
1434
- const nextSa = agents[i + 1];
1435
- const gapStart = new Date(sa.endTime).getTime();
1436
- const gapEnd = nextSa
1437
- ? new Date(nextSa.startTime).getTime()
1438
- : sessionEnd.value;
1439
-
1440
- if (gapEnd - gapStart > 500) {
1441
- items.push(buildAgentOpItem(sorted, gapStart, gapEnd));
1442
- }
1443
- }
1444
-
1445
- return items;
1446
- });
1447
-
1448
- // ── Turn analysis ──
1449
- const turnAnalysis = computed(() => {
1450
- try {
1451
- const sorted = sortedEvents.value;
1452
- const assistantMessages = sorted.filter(e => e.type === 'assistant.message');
1453
- const allUserMessages = sorted.filter(e => e.type === 'user.message');
1454
-
1455
- return assistantMessages.map((msg, idx) => {
1456
- const ts = msg.timestamp;
1457
- if (!ts) {
1458
- console.warn('[turnAnalysis] Message without timestamp:', msg);
1459
- return null;
1460
- }
1461
- const startTime = new Date(ts).getTime();
1462
- if (isNaN(startTime)) {
1463
- console.warn('[turnAnalysis] Invalid timestamp:', ts, msg);
1464
- return null;
1465
- }
1466
-
1467
- const nextMsg = assistantMessages[idx + 1];
1468
- const endTime = nextMsg
1469
- ? new Date(nextMsg.timestamp).getTime()
1470
- : sessionEnd.value || startTime;
1471
- const duration = endTime - startTime;
1472
-
1473
- // Find user message before this assistant message
1474
- const msgIndex = sorted.indexOf(msg);
1475
- const userMessage = sorted
1476
- .slice(0, msgIndex)
1477
- .reverse()
1478
- .find(e => e.type === 'user.message');
1479
-
1480
- const userReqNumber = userMessage
1481
- ? allUserMessages.indexOf(userMessage) + 1
1482
- : 0;
1483
-
1484
- // Extract display text
1485
- let displayText = '';
1486
- const hasText = msg.data?.message && msg.data.message.trim() !== '';
1487
-
1488
- if (hasText) {
1489
- displayText = normalizeMessage(msg.data.message);
1490
- } else if (msg.data?.tools && msg.data.tools.length > 0) {
1491
- // Only tool calls, show tool names
1492
- const toolNames = msg.data.tools.map(t => t.name || 'unknown').join(', ');
1493
- displayText = `Tool calls: ${toolNames}`;
1494
- } else {
1495
- displayText = '(empty assistant message)';
1496
- }
1497
-
1498
- // Count tool calls
1499
- const toolCalls = msg.data?.tools?.length || 0;
1500
-
1501
- return {
1502
- turnId: msg.id ?? `msg-${idx}`,
1503
- userReqNumber,
1504
- message: normalizeMessage(userMessage?.data?.message || userMessage?.data?.content || userMessage?.data?.transformedContent || ''),
1505
- displayText,
1506
- hasText,
1507
- startTime: msg.timestamp,
1508
- endTime: nextMsg?.timestamp || events.value[events.value.length - 1]?.timestamp,
1509
- duration,
1510
- toolCalls
1511
- };
1512
- }).filter(t => t !== null);
1513
- } catch (err) {
1514
- console.error('[turnAnalysis] Error:', err);
1515
- error.value = 'Error analyzing turns: ' + err.message;
1516
- return [];
1517
- }
1518
- });
1519
-
1520
- const maxTurnDuration = computed(() => {
1521
- return Math.max(...turnAnalysis.value.map(t => t.duration || 0), 1);
1522
- });
1523
-
1524
- // ── Grouped turns by UserReq ──
1525
- const groupedTurns = computed(() => {
1526
- const groups = new Map();
1527
-
1528
- for (const turn of turnAnalysis.value) {
1529
- const reqNum = turn.userReqNumber || 0;
1530
- if (!groups.has(reqNum)) {
1531
- groups.set(reqNum, {
1532
- userReqNumber: reqNum,
1533
- message: turn.message,
1534
- turns: []
1535
- });
1536
- }
1537
- groups.get(reqNum).turns.push(turn);
1538
- }
1539
-
1540
- return Array.from(groups.values()).sort((a, b) => a.userReqNumber - b.userReqNumber);
1541
- });
1542
-
1543
- // ── Tool operations analysis ──
1544
- const toolAnalysis = computed(() => {
1545
- const sorted = sortedEvents.value;
1546
- const toolGroups = new Map();
1547
-
1548
- for (const ev of sorted) {
1549
- if (ev.type === 'tool.execution_start') {
1550
- const toolId = ev.data?.toolCallId;
1551
- if (toolId) {
1552
- toolGroups.set(toolId, { start: ev });
1553
- }
1554
- } else if (ev.type === 'tool.execution_complete') {
1555
- const toolId = ev.data?.toolCallId;
1556
- if (toolId && toolGroups.has(toolId)) {
1557
- toolGroups.get(toolId).complete = ev;
1558
- }
1559
- }
1560
- }
1561
-
1562
- const results = [];
1563
- toolGroups.forEach((group, toolId) => {
1564
- const startTime = new Date(group.start.timestamp).getTime();
1565
- const endTime = group.complete
1566
- ? new Date(group.complete.timestamp).getTime()
1567
- : null;
1568
- const duration = endTime ? endTime - startTime : null;
1569
- const toolName = group.start.data?.toolName || group.start.data?.tool || 'unknown';
1570
- const args = group.start.data?.arguments || {};
1571
- const isError = group.complete?.data?.isError || !!group.complete?.data?.error;
1572
-
1573
- // Extract file path or command
1574
- let description = '';
1575
- if (toolName === 'Bash' || toolName === 'bash' || toolName === 'exec') {
1576
- description = args.command || args.description || '';
1577
- } else if (['Read', 'read', 'Write', 'write', 'Edit', 'edit'].includes(toolName)) {
1578
- description = args.file_path || args.path || '';
1579
- } else if (['Glob', 'glob'].includes(toolName)) {
1580
- description = args.pattern || '';
1581
- } else if (['Grep', 'grep'].includes(toolName)) {
1582
- description = args.pattern || '';
1583
- } else if (['Task', 'task'].includes(toolName)) {
1584
- description = args.description || args.prompt?.substring(0, 80) || '';
1585
- } else {
1586
- description = args.description || args.command || args.file_path ||
1587
- args.path || args.query || args.url || '';
1588
- }
1589
- if (description.length > 120) {
1590
- description = description.substring(0, 120) + '...';
1591
- }
1592
-
1593
- results.push({
1594
- toolId,
1595
- toolName,
1596
- description,
1597
- startTime: group.start.timestamp,
1598
- endTime: group.complete?.timestamp || null,
1599
- duration,
1600
- isError,
1601
- isRunning: !group.complete
1602
- });
1603
- });
1604
-
1605
- return results;
1606
- });
1607
-
1608
- const sortedToolAnalysis = computed(() => {
1609
- const items = [...toolAnalysis.value];
1610
- items.sort((a, b) => {
1611
- if (sortField.value === 'duration') {
1612
- return sortDir.value === 'asc'
1613
- ? (a.duration || 0) - (b.duration || 0)
1614
- : (b.duration || 0) - (a.duration || 0);
1615
- }
1616
- if (sortField.value === 'toolName') {
1617
- const cmp = (a.toolName || '').localeCompare(b.toolName || '');
1618
- return sortDir.value === 'asc' ? cmp : -cmp;
1619
- }
1620
- // default: timestamp
1621
- const tA = new Date(a.startTime).getTime();
1622
- const tB = new Date(b.startTime).getTime();
1623
- return sortDir.value === 'asc' ? tA - tB : tB - tA;
1624
- });
1625
- return items;
1626
- });
1627
-
1628
- const maxToolDuration = computed(() => {
1629
- return Math.max(...toolAnalysis.value.map(t => t.duration || 0), 1);
1630
- });
1631
-
1632
- // ── File operations ──
1633
- const fileOperations = computed(() => {
1634
- const fileTools = ['view', 'read', 'write', 'edit', 'create', 'glob', 'grep', 'notebookedit'];
1635
- // VSCode Copilot Chat tool name mappings
1636
- const vsCodeFileToolMap = {
1637
- 'copilot_readfile': 'read',
1638
- 'copilot_createfile': 'write',
1639
- 'copilot_createdirectory': 'write',
1640
- 'copilot_findfiles': 'search',
1641
- 'copilot_findtextinfiles': 'search',
1642
- 'copilot_listdirectory': 'read',
1643
- 'textedit': 'edit',
1644
- 'copilot_replacestring': 'edit',
1645
- 'copilot_multireplacestring': 'edit',
1646
- };
1647
- const ops = [];
1648
-
1649
- for (const ev of events.value) {
1650
- if (ev.type === 'tool.execution_start') {
1651
- const toolName = ev.data?.toolName?.toLowerCase() || '';
1652
- const args = ev.data?.arguments || {};
1653
- const path = args.path || args.file || args.directory || args.pattern || '';
1654
-
1655
- // Check standard file tools
1656
- if (fileTools.includes(toolName)) {
1657
- if (path) {
1658
- let opType = 'other';
1659
- if (toolName === 'view' || toolName === 'read') opType = 'read';
1660
- else if (toolName === 'write' || toolName === 'notebookedit' || toolName === 'create') opType = 'write';
1661
- else if (toolName === 'edit') opType = 'edit';
1662
- else if (toolName === 'glob' || toolName === 'grep') opType = 'search';
1663
-
1664
- ops.push({
1665
- toolName: ev.data?.toolName || toolName,
1666
- opType,
1667
- filePath: path,
1668
- timestamp: ev.timestamp,
1669
- startTime: ev.timestamp
1670
- });
1671
- }
1672
- }
1673
- // Check VSCode file tools
1674
- else if (vsCodeFileToolMap[toolName]) {
1675
- const opType = vsCodeFileToolMap[toolName];
1676
- ops.push({
1677
- toolName: ev.data?.toolName || toolName,
1678
- opType,
1679
- filePath: path || '(implicit)',
1680
- timestamp: ev.timestamp,
1681
- startTime: ev.timestamp
1682
- });
1683
- }
1684
- }
1685
- }
1686
-
1687
- return ops.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
1688
- });
1689
-
1690
- const fileStats = computed(() => {
1691
- const ops = fileOperations.value;
1692
- const uniqueFiles = new Set(ops.map(o => o.filePath));
1693
- return {
1694
- uniqueCount: uniqueFiles.size,
1695
- totalOps: ops.length,
1696
- reads: ops.filter(o => o.opType === 'read').length,
1697
- writes: ops.filter(o => o.opType === 'write').length,
1698
- edits: ops.filter(o => o.opType === 'edit').length,
1699
- searches: ops.filter(o => o.opType === 'search').length
1700
- };
1701
- });
1702
-
1703
- // ── Tool time by category ──
1704
- const toolTimeByCategory = computed(() => {
1705
- const catMap = {};
1706
- toolAnalysis.value.forEach(t => {
1707
- const name = (t.toolName || 'unknown').toLowerCase();
1708
- let cat;
1709
- if (['bash', 'exec'].includes(name)) cat = 'Bash/Exec';
1710
- else if (['read'].includes(name)) cat = 'Read';
1711
- else if (['write'].includes(name)) cat = 'Write';
1712
- else if (['edit'].includes(name)) cat = 'Edit';
1713
- else if (['glob'].includes(name)) cat = 'Glob';
1714
- else if (['grep'].includes(name)) cat = 'Grep';
1715
- else if (['task'].includes(name)) cat = 'Task (SubAgent)';
1716
- else if (['web_search', 'websearch'].includes(name)) cat = 'Web Search';
1717
- else if (['web_fetch', 'webfetch'].includes(name)) cat = 'Web Fetch';
1718
- else cat = t.toolName || 'Other';
1719
-
1720
- if (!catMap[cat]) {
1721
- catMap[cat] = { category: cat, totalTime: 0, count: 0, errors: 0 };
1722
- }
1723
- catMap[cat].totalTime += (t.duration || 0);
1724
- catMap[cat].count++;
1725
- if (t.isError) catMap[cat].errors++;
1726
- });
1727
-
1728
- return Object.values(catMap).sort((a, b) => b.totalTime - a.totalTime);
1729
- });
1730
-
1731
- const maxCategoryTime = computed(() => {
1732
- return Math.max(...toolTimeByCategory.value.map(c => c.totalTime), 1);
1733
- });
1734
-
1735
- // ── Summary stats ──
1736
- // Wall-clock tool time: merge overlapping intervals to avoid double-counting parallel tools
1737
- const totalToolTime = computed(() => {
1738
- const intervals = toolAnalysis.value
1739
- .filter(t => t.duration && t.startTime && t.endTime)
1740
- .map(t => ({
1741
- start: new Date(t.startTime).getTime(),
1742
- end: new Date(t.endTime).getTime()
1743
- }))
1744
- .sort((a, b) => a.start - b.start);
1745
-
1746
- if (!intervals.length) return 0;
1747
-
1748
- // Merge overlapping intervals
1749
- let totalMs = 0;
1750
- let curStart = intervals[0].start;
1751
- let curEnd = intervals[0].end;
1752
-
1753
- for (let i = 1; i < intervals.length; i++) {
1754
- if (intervals[i].start <= curEnd) {
1755
- // Overlapping — extend current interval
1756
- curEnd = Math.max(curEnd, intervals[i].end);
1757
- } else {
1758
- // Gap — commit current interval and start new one
1759
- totalMs += curEnd - curStart;
1760
- curStart = intervals[i].start;
1761
- curEnd = intervals[i].end;
1762
- }
1763
- }
1764
- totalMs += curEnd - curStart; // commit last interval
1765
-
1766
- return totalMs;
1767
- });
1768
-
1769
- // ── Token Statistics ──
1770
- const tokenStats = computed(() => {
1771
- let totalTokens = 0;
1772
- let byCategory = {};
1773
-
1774
- for (const ev of events.value) {
1775
- if (ev.type === 'tool.execution_complete' && ev.data?.toolTelemetry?.metrics) {
1776
- const tokens = ev.data.toolTelemetry.metrics.resultForLlmLength || 0;
1777
- totalTokens += tokens;
1778
-
1779
- // Categorize by tool name
1780
- const toolName = ev.data.toolName || 'unknown';
1781
- if (!byCategory[toolName]) {
1782
- byCategory[toolName] = 0;
1783
- }
1784
- byCategory[toolName] += tokens;
1785
- }
1786
- }
1787
-
1788
- return {
1789
- total: totalTokens,
1790
- byCategory
1791
- };
1792
- });
1793
-
1794
- // ── Gap Analysis ──
1795
- const gapAnalysis = computed(() => {
1796
- const sorted = sortedEvents.value;
1797
- const gaps = [];
1798
-
1799
- // Track user messages and assistant responses
1800
- for (let i = 0; i < sorted.length - 1; i++) {
1801
- const current = sorted[i];
1802
- const next = sorted[i + 1];
1803
- const currentTime = new Date(current.timestamp).getTime();
1804
- const nextTime = new Date(next.timestamp).getTime();
1805
- const duration = nextTime - currentTime;
1806
-
1807
- // Only report gaps > 100ms
1808
- if (duration < 100) continue;
1809
-
1810
- let gapType = null;
1811
- let description = '';
1812
-
1813
- // User message → assistant.turn_start (input consumption)
1814
- if (current.type === 'user.message' && next.type === 'assistant.turn_start') {
1815
- gapType = 'input-consumption';
1816
- const msgLength = (current.data?.message || '').length;
1817
- description = `LLM reading user input (${msgLength} chars)`;
1818
- }
1819
-
1820
- // assistant.turn_start → assistant.message (generation)
1821
- else if (current.type === 'assistant.turn_start' && next.type === 'assistant.message') {
1822
- gapType = 'llm-generation';
1823
- const outputLength = (next.data?.content || '').length;
1824
- description = `LLM generating response (${outputLength} chars output)`;
1825
- }
1826
-
1827
- // assistant.turn_start → first tool (generation before tool call)
1828
- else if (current.type === 'assistant.turn_start' && next.type === 'tool.execution_start') {
1829
- gapType = 'llm-generation';
1830
- const toolName = next.data?.toolName || 'unknown';
1831
- description = `LLM deciding to call ${toolName}`;
1832
- }
1833
-
1834
- // assistant.message → assistant.turn_start (thinking between turns)
1835
- else if (current.type === 'assistant.message' && next.type === 'assistant.turn_start') {
1836
- gapType = 'turn-gap';
1837
- description = 'Gap between assistant response and next turn';
1838
- }
1839
-
1840
- // tool.execution_complete → next event (post-processing)
1841
- else if (current.type === 'tool.execution_complete' && duration > 500) {
1842
- gapType = 'post-tool';
1843
- const toolName = current.data?.toolName || 'unknown';
1844
- description = `Processing ${toolName} result`;
1845
- }
1846
-
1847
- // Large gaps between any events
1848
- else if (duration > 5000) {
1849
- gapType = 'idle';
1850
- description = `${current.type} → ${next.type}`;
1851
- }
1852
-
1853
- if (gapType) {
1854
- gaps.push({
1855
- type: gapType,
1856
- description,
1857
- startTime: current.timestamp,
1858
- endTime: next.timestamp,
1859
- duration,
1860
- fromEvent: current.type,
1861
- toEvent: next.type,
1862
- fromData: current.data,
1863
- toData: next.data
1864
- });
1865
- }
1866
- }
1867
-
1868
- return gaps.sort((a, b) => (b.duration || 0) - (a.duration || 0));
1869
- });
1870
-
1871
- const maxGapDuration = computed(() => {
1872
- return Math.max(...gapAnalysis.value.map(g => g.duration || 0), 1);
1873
- });
1874
-
1875
- const gapStats = computed(() => {
1876
- const stats = {
1877
- 'input-consumption': { count: 0, total: 0, avg: 0 },
1878
- 'llm-generation': { count: 0, total: 0, avg: 0 },
1879
- 'post-tool': { count: 0, total: 0, avg: 0 },
1880
- 'turn-gap': { count: 0, total: 0, avg: 0 },
1881
- 'idle': { count: 0, total: 0, avg: 0 }
1882
- };
1883
-
1884
- gapAnalysis.value.forEach(gap => {
1885
- if (stats[gap.type]) {
1886
- stats[gap.type].count++;
1887
- stats[gap.type].total += gap.duration;
1888
- }
1889
- });
1890
-
1891
- Object.keys(stats).forEach(key => {
1892
- if (stats[key].count > 0) {
1893
- stats[key].avg = stats[key].total / stats[key].count;
1894
- }
1895
- });
1896
-
1897
- return stats;
1898
- });
1899
-
1900
- const successRate = computed(() => {
1901
- const total = toolAnalysis.value.length;
1902
- if (total === 0) return 100;
1903
- const errors = toolAnalysis.value.filter(t => t.isError).length;
1904
- return ((total - errors) / total * 100).toFixed(1);
1905
- });
1906
-
1907
- const errorCount = computed(() => {
1908
- return toolAnalysis.value.filter(t => t.isError).length;
1909
- });
1910
-
1911
- // Time breakdown: user thinking vs agent working
1912
- // "User thinking" = gaps where the agent has finished and is waiting for the next user message
1913
- // "Agent working" = totalDuration - userThinkingTime
1914
- const timeBreakdown = computed(() => {
1915
- const sorted = sortedEvents.value;
1916
- let userThinkingTime = 0;
1917
-
1918
- for (let i = 0; i < sorted.length - 1; i++) {
1919
- const current = sorted[i];
1920
- const next = sorted[i + 1];
1921
-
1922
- // User thinking = from any non-user event to the next user.message
1923
- // This captures when the agent is done and waiting for the user to type
1924
- if (next.type === 'user.message' && current.type !== 'user.message') {
1925
- const gap = new Date(next.timestamp).getTime() - new Date(current.timestamp).getTime();
1926
- if (gap > 1000) { // Only count gaps > 1s as intentional user thinking
1927
- userThinkingTime += gap;
1928
- }
1929
- }
1930
- }
1931
-
1932
- const total = totalDuration.value || 0;
1933
- const agentWorkingTime = Math.max(total - userThinkingTime, 0);
1934
- // LLM time = agent working time minus tool wall-clock time
1935
- const llmTime = Math.max(agentWorkingTime - totalToolTime.value, 0);
1936
-
1937
- return {
1938
- userThinkingTime,
1939
- agentWorkingTime,
1940
- llmTime,
1941
- userThinkingPct: total > 0 ? (userThinkingTime / total * 100).toFixed(0) : 0,
1942
- agentWorkingPct: total > 0 ? (agentWorkingTime / total * 100).toFixed(0) : 0,
1943
- llmPct: total > 0 ? (llmTime / total * 100).toFixed(0) : 0,
1944
- toolPct: total > 0 ? (totalToolTime.value / total * 100).toFixed(0) : 0,
1945
- };
1946
- });
1947
-
1948
-
1949
-
1950
- const totalToolCount = computed(() => toolAnalysis.value.length);
1951
-
1952
- const avgToolDuration = computed(() => {
1953
- if (!totalToolCount.value) return 0;
1954
- const rawSum = toolAnalysis.value.reduce((acc, t) => acc + (t.duration || 0), 0);
1955
- return rawSum / totalToolCount.value;
1956
- });
1957
-
1958
- const longestTool = computed(() => {
1959
- if (!toolAnalysis.value.length) return null;
1960
- return toolAnalysis.value.reduce((max, t) => (t.duration || 0) > (max.duration || 0) ? t : max);
1961
- });
1962
-
1963
- // ── VS Code Session Detection ──
1964
- const isVSCodeSession = computed(() => {
1965
- // Detect VS Code sessions: they have events with data.source === 'vscode'
1966
- // OR they have assistant.message events with data.subAgentName but no subagent.started events
1967
- const sorted = sortedEvents.value;
1968
- const hasVSCodeSource = sorted.some(ev => ev.data?.source === 'vscode');
1969
- if (hasVSCodeSource) return true;
1970
-
1971
- // Alternative check: has subAgentName but no subagent events
1972
- const hasSubAgentName = sorted.some(ev =>
1973
- ev.type === 'assistant.message' && ev.data?.subAgentName
1974
- );
1975
- const hasSubagentEvents = sorted.some(ev =>
1976
- ev.type === 'subagent.started' || ev.type === 'subagent.completed' || ev.type === 'subagent.failed'
1977
- );
1978
- return hasSubAgentName && !hasSubagentEvents;
1979
- });
1980
-
1981
- // ── VS Code Subagents ──
1982
- const vsCodeSubagents = computed(() => {
1983
- if (!isVSCodeSession.value) return [];
1984
-
1985
- const sorted = sortedEvents.value;
1986
- const subagentMap = new Map(); // subAgentId -> { events, toolCount, firstIndex, status, name }
1987
-
1988
- for (let i = 0; i < sorted.length; i++) {
1989
- const ev = sorted[i];
1990
- if (ev.type === 'assistant.message' && ev.data?.subAgentName) {
1991
- const id = ev.data.subAgentId || ev.data.subAgentName;
1992
- if (!subagentMap.has(id)) {
1993
- subagentMap.set(id, {
1994
- name: ev.data.subAgentName,
1995
- events: [],
1996
- toolCount: 0,
1997
- firstIndex: i, // use array index as stable position
1998
- status: 'completed',
1999
- subAgentId: ev.data.subAgentId
2000
- });
2001
- }
2002
- const entry = subagentMap.get(id);
2003
- entry.events.push(ev);
2004
-
2005
- // Count tools
2006
- if (ev.data.tools && Array.isArray(ev.data.tools)) {
2007
- entry.toolCount += ev.data.tools.length;
2008
- }
2009
-
2010
- // Update status if there are errors
2011
- if (ev.data.error || ev.data.status === 'error') {
2012
- entry.status = 'failed';
2013
- }
2014
- }
2015
- }
2016
-
2017
- // Convert to array and sort by firstIndex
2018
- return Array.from(subagentMap.values()).sort((a, b) => a.firstIndex - b.firstIndex);
2019
- });
2020
-
2021
- // ── Unified Timeline Items ──
2022
- const unifiedTimelineItems = computed(() => {
2023
- const items = [];
2024
- const groups = groupedTurns.value;
2025
- const agents = subagentAnalysis.value;
2026
- const sorted = sortedEvents.value;
2027
-
2028
- // Check if this is a VS Code session and use sequence-based layout
2029
- if (isVSCodeSession.value) {
2030
- const vsAgents = vsCodeSubagents.value;
2031
-
2032
- // Collect user messages with their array index positions
2033
- const userMessages = [];
2034
- for (let i = 0; i < sorted.length; i++) {
2035
- if (sorted[i].type === 'user.message') {
2036
- userMessages.push({ event: sorted[i], sortedIndex: i });
2037
- }
2038
- }
2039
-
2040
- if (userMessages.length > 0 && vsAgents.length > 0) {
2041
- // Build user-req groups: each user message owns the subagents that follow it
2042
- // until the next user message (using sorted array indices)
2043
- for (let ui = 0; ui < userMessages.length; ui++) {
2044
- const { event: userMsg, sortedIndex: userIdx } = userMessages[ui];
2045
- const nextIdx = userMessages[ui + 1] ? userMessages[ui + 1].sortedIndex : Infinity;
2046
-
2047
- // Find subagents belonging to this user request (by sorted array index)
2048
- const reqAgents = vsAgents.filter(sa =>
2049
- sa.firstIndex >= userIdx && sa.firstIndex < nextIdx
2050
- );
2051
-
2052
- // Calculate total tool count for this user request
2053
- const totalTools = reqAgents.reduce((s, a) => s + a.toolCount, 0);
2054
-
2055
- const msg = userMsg.data?.message || userMsg.data?.content || '';
2056
-
2057
- // Push user-req header
2058
- items.push({
2059
- rowType: 'user-req',
2060
- userReqNumber: ui + 1,
2061
- message: typeof msg === 'string' ? msg.substring(0, 120) : String(msg).substring(0, 120),
2062
- toolCount: totalTools,
2063
- sequenceIndex: userIdx,
2064
- isSequenceEstimated: true,
2065
- duration: totalTools,
2066
- });
2067
-
2068
- // Push subagent rows under this user request
2069
- for (const vsAgent of reqAgents) {
2070
- items.push({
2071
- rowType: 'subagent',
2072
- itemType: 'subagent',
2073
- name: vsAgent.name,
2074
- status: vsAgent.status,
2075
- toolCount: vsAgent.toolCount,
2076
- sequenceIndex: vsAgent.firstIndex,
2077
- isSequenceEstimated: true,
2078
- duration: vsAgent.toolCount,
2079
- indented: true,
2080
- });
2081
- }
2082
- }
2083
- } else if (vsAgents.length > 0) {
2084
- // No user messages, just show subagents
2085
- for (const vsAgent of vsAgents) {
2086
- items.push({
2087
- rowType: 'subagent',
2088
- itemType: 'subagent',
2089
- name: vsAgent.name,
2090
- status: vsAgent.status,
2091
- toolCount: vsAgent.toolCount,
2092
- sequenceIndex: vsAgent.firstIndex,
2093
- isSequenceEstimated: true,
2094
- duration: vsAgent.toolCount,
2095
- });
2096
- }
2097
- }
2098
- return items;
2099
- }
2100
-
2101
- // Original Copilot CLI timeline logic
2102
- if (groups.length) {
2103
- for (let gi = 0; gi < groups.length; gi++) {
2104
- const group = groups[gi];
2105
- const turns = group.turns;
2106
- if (!turns.length) continue;
2107
-
2108
- const reqStart = new Date(turns[0].startTime).getTime();
2109
- const reqEnd = new Date(turns[turns.length - 1].endTime).getTime();
2110
-
2111
- // 1. Push user-req header row
2112
- items.push({
2113
- rowType: 'user-req',
2114
- userReqNumber: group.userReqNumber,
2115
- message: group.message,
2116
- startTime: turns[0].startTime,
2117
- endTime: turns[turns.length - 1].endTime,
2118
- duration: reqEnd - reqStart,
2119
- });
2120
-
2121
- // 2. Find subagents within this UserReq time range
2122
- const reqAgents = agents.filter(sa => {
2123
- if (!sa.startTime) return false;
2124
- const saStart = new Date(sa.startTime).getTime();
2125
- return saStart >= reqStart && saStart <= reqEnd;
2126
- });
2127
-
2128
- if (reqAgents.length) {
2129
- // Build subagent + gap items scoped to this UserReq
2130
- for (let i = 0; i < reqAgents.length; i++) {
2131
- const sa = reqAgents[i];
2132
-
2133
- // Gap before first subagent (from reqStart) or between subagents
2134
- const gapStart = i === 0
2135
- ? reqStart
2136
- : new Date(reqAgents[i - 1].endTime).getTime();
2137
- const gapEnd = new Date(sa.startTime).getTime();
2138
-
2139
- if (gapEnd - gapStart > 500) {
2140
- const agentOp = buildAgentOpItem(sorted, gapStart, gapEnd);
2141
- agentOp.rowType = 'main-agent';
2142
- items.push(agentOp);
2143
- }
2144
-
2145
- // Add subagent
2146
- items.push({
2147
- ...sa,
2148
- rowType: 'subagent',
2149
- itemType: 'subagent',
2150
- });
2151
-
2152
- // Gap after last subagent to reqEnd
2153
- if (i === reqAgents.length - 1) {
2154
- const trailingStart = new Date(sa.endTime).getTime();
2155
- const trailingEnd = reqEnd;
2156
- if (trailingEnd - trailingStart > 500) {
2157
- const agentOp = buildAgentOpItem(sorted, trailingStart, trailingEnd);
2158
- agentOp.rowType = 'main-agent';
2159
- items.push(agentOp);
2160
- }
2161
- }
2162
- }
2163
- } else {
2164
- // No subagents — show single Main Agent bar spanning entire UserReq
2165
- if (reqEnd - reqStart > 0) {
2166
- const agentOp = buildAgentOpItem(sorted, reqStart, reqEnd);
2167
- agentOp.rowType = 'main-agent';
2168
- items.push(agentOp);
2169
- }
2170
- }
2171
- }
2172
- } else if (subagentTimelineItems.value.length) {
2173
- // No UserReq groups, but subagents exist — flat fallback
2174
- for (const item of subagentTimelineItems.value) {
2175
- items.push({
2176
- ...item,
2177
- rowType: item.itemType === 'agent-op' ? 'main-agent' : 'subagent',
2178
- });
2179
- }
2180
- }
2181
-
2182
- return items;
2183
- });
2184
-
2185
- // ── Gantt chart positioning ──
2186
- const ganttPosition = (startTs, endTs) => {
2187
- if (!sessionStart.value || !totalDuration.value || !startTs) return { left: '0%', width: '0%' };
2188
- const s = new Date(startTs).getTime();
2189
- const e = endTs ? new Date(endTs).getTime() : s + 1000;
2190
- const left = ((s - sessionStart.value) / totalDuration.value) * 100;
2191
- const width = Math.max(((e - s) / totalDuration.value) * 100, 0.5);
2192
- return {
2193
- left: left + '%',
2194
- width: Math.min(width, 100 - left) + '%'
2195
- };
2196
- };
2197
-
2198
- // ── VS Code Sequence-based positioning ──
2199
- const ganttSequencePosition = (item) => {
2200
- const items = unifiedTimelineItems.value;
2201
- if (items.length === 0) return { left: '0%', width: '0%' };
2202
-
2203
- // For user-req rows, span across all its child subagent rows
2204
- if (item.rowType === 'user-req') {
2205
- const idx = items.findIndex(it => it === item);
2206
- if (idx === -1) return { left: '0%', width: '0%' };
2207
-
2208
- // Find child subagents (indented rows immediately following this user-req)
2209
- const children = [];
2210
- for (let i = idx + 1; i < items.length; i++) {
2211
- if (items[i].rowType === 'user-req') break; // next user-req
2212
- if (items[i].rowType === 'subagent') children.push(items[i]);
2213
- }
2214
- if (children.length === 0) {
2215
- // No subagents — position this user-req at the end of previous subagents
2216
- // by finding cumulative tool count up to this point
2217
- const subagentItems = items.filter(it => it.rowType !== 'user-req');
2218
- const totalToolCount = subagentItems.reduce((sum, it) => sum + (it.toolCount || 0), 0);
2219
- if (totalToolCount === 0) return { left: '0%', width: '0%' };
2220
- // Sum tools of all subagents before this user-req in the items array
2221
- let cumTools = 0;
2222
- for (let i = 0; i < idx; i++) {
2223
- if (items[i].rowType === 'subagent') cumTools += (items[i].toolCount || 0);
2224
- }
2225
- const leftPct = (cumTools / totalToolCount) * 100;
2226
- // Minimal width bar (at least 1%)
2227
- return { left: leftPct + '%', width: Math.max(1, (1 / totalToolCount) * 100) + '%' };
2228
- }
2229
-
2230
- const firstPos = ganttSequencePosition(children[0]);
2231
- const lastPos = ganttSequencePosition(children[children.length - 1]);
2232
- const startPct = parseFloat(firstPos.left);
2233
- const endPct = parseFloat(lastPos.left) + parseFloat(lastPos.width);
2234
- return {
2235
- left: startPct + '%',
2236
- width: (endPct - startPct) + '%'
2237
- };
2238
- }
2239
-
2240
- // Find index of this item
2241
- const idx = items.findIndex(it => it === item);
2242
- if (idx === -1) return { left: '0%', width: '0%' };
2243
-
2244
- // Calculate total tool count across subagent items only (exclude user-req)
2245
- const subagentItems = items.filter(it => it.rowType !== 'user-req');
2246
- const totalToolCount = subagentItems.reduce((sum, it) => sum + (it.toolCount || 0), 0);
2247
- if (totalToolCount === 0) return { left: '0%', width: '0%' };
2248
-
2249
- // Calculate cumulative tool count up to this item (among subagent items only)
2250
- const subIdx = subagentItems.findIndex(it => it === item);
2251
- if (subIdx === -1) return { left: '0%', width: '0%' };
2252
-
2253
- let cumulativeToolCount = 0;
2254
- for (let i = 0; i < subIdx; i++) {
2255
- cumulativeToolCount += subagentItems[i].toolCount || 0;
2256
- }
2257
-
2258
- // Position based on sequence
2259
- const left = (cumulativeToolCount / totalToolCount) * 100;
2260
-
2261
- // Width based on tool count with minimum width
2262
- const itemToolCount = item.toolCount || 0;
2263
- const width = Math.max((itemToolCount / totalToolCount) * 100, 2); // Minimum 2% width
2264
-
2265
- return {
2266
- left: left + '%',
2267
- width: Math.min(width, 100 - left) + '%'
2268
- };
2269
- };
2270
-
2271
- // ── Sort control ──
2272
- const toggleSort = (field) => {
2273
- if (sortField.value === field) {
2274
- sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
2275
- } else {
2276
- sortField.value = field;
2277
- sortDir.value = field === 'duration' ? 'desc' : 'asc';
2278
- }
2279
- };
2280
-
2281
- const sortIcon = (field) => {
2282
- if (sortField.value !== field) return '↕';
2283
- return sortDir.value === 'asc' ? '↑' : '↓';
2284
- };
2285
-
2286
- const getToolBadgeClass = (toolName) => {
2287
- const lower = (toolName || '').toLowerCase();
2288
- if (['bash', 'exec'].includes(lower)) return 'badge-bash';
2289
- if (lower === 'read') return 'badge-read';
2290
- if (lower === 'write' || lower === 'notebookedit') return 'badge-write';
2291
- if (lower === 'edit') return 'badge-edit';
2292
- if (lower === 'glob' || lower === 'grep') return 'badge-search';
2293
- if (lower === 'task') return 'badge-subagent';
2294
- return 'badge-other';
2295
- };
2296
-
2297
- const getOpBadgeClass = (opType) => {
2298
- const classes = {
2299
- read: 'badge-read',
2300
- write: 'badge-write',
2301
- edit: 'badge-edit',
2302
- create: 'badge-create',
2303
- search: 'badge-search'
2304
- };
2305
- return classes[opType] || 'badge-other';
2306
- };
2307
-
2308
- // ── Load events ──
2309
- onMounted(async () => {
2310
- try {
2311
- const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
2312
- if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
2313
- const data = await resp.json();
2314
- console.log('[TIME-ANALYZE] Loaded events:', data.length);
2315
- console.log('[TIME-ANALYZE] Event types:', [...new Set(data.map(e => e.type))]);
2316
- console.log('[TIME-ANALYZE] Turn starts:', data.filter(e => e.type === 'assistant.turn_start').length);
2317
- console.log('[TIME-ANALYZE] User messages:', data.filter(e => e.type === 'user.message').length);
2318
- events.value = data.sort((a, b) => {
2319
- const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
2320
- const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
2321
- if (timeA !== timeB) return timeA - timeB;
2322
- return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
2323
- });
2324
- console.log('[TIME-ANALYZE] Events set, length:', events.value.length);
2325
- } catch (err) {
2326
- console.error('[TIME-ANALYZE] Error loading events:', err);
2327
- error.value = err.message;
2328
- } finally {
2329
- loading.value = false;
2330
- }
2331
- });
2332
-
2333
- // ── Copilot Insight ──
2334
- const insightStatus = ref('not_started'); // completed | generating | timeout | not_started
2335
- const insightLastUpdate = ref(null);
2336
- const insightStartedAt = ref(null);
2337
- const insightAgeMs = ref(0);
2338
- let pollInterval = null;
2339
-
2340
- const renderedInsight = computed(() => {
2341
- if (!insightReport.value) return '';
2342
- return marked.parse(insightReport.value);
2343
- });
2344
-
2345
- const checkExistingInsight = async () => {
2346
- try {
2347
- const resp = await fetch(`/session/${sessionId.value}/insight`);
2348
- const data = await resp.json();
2349
-
2350
- insightStatus.value = data.status;
2351
-
2352
- if (data.status === 'completed') {
2353
- insightReport.value = data.report;
2354
- insightLog.value = null;
2355
- insightGeneratedAt.value = data.generatedAt;
2356
- stopPolling();
2357
- } else if (data.status === 'generating') {
2358
- insightLog.value = data.log || null;
2359
- insightStartedAt.value = data.startedAt;
2360
- insightLastUpdate.value = data.lastUpdate;
2361
- insightAgeMs.value = data.ageMs;
2362
- startPolling();
2363
- // Auto-scroll log to bottom
2364
- Vue.nextTick(() => {
2365
- const el = document.getElementById('insight-log');
2366
- if (el) el.scrollTop = el.scrollHeight;
2367
- });
2368
- } else if (data.status === 'timeout') {
2369
- insightLog.value = data.log || null;
2370
- insightStartedAt.value = data.startedAt;
2371
- insightLastUpdate.value = data.lastUpdate;
2372
- insightAgeMs.value = data.ageMs;
2373
- // Keep polling — the process may still finish and write the report
2374
- startPolling();
2375
- }
2376
- } catch (err) {
2377
- console.error('Failed to check insight:', err);
2378
- }
2379
- };
2380
-
2381
- const startPolling = () => {
2382
- stopPolling();
2383
- pollInterval = setInterval(checkExistingInsight, 2000); // Poll every 2 seconds
2384
- };
2385
-
2386
- const stopPolling = () => {
2387
- if (pollInterval) {
2388
- clearInterval(pollInterval);
2389
- pollInterval = null;
2390
- }
2391
- };
2392
-
2393
- const generateInsight = async (force = false) => {
2394
- insightLoading.value = true;
2395
- insightError.value = null;
2396
- insightLog.value = null;
2397
-
2398
- try {
2399
- const resp = await fetch(`/session/${sessionId.value}/insight`, {
2400
- method: 'POST',
2401
- headers: { 'Content-Type': 'application/json' },
2402
- body: JSON.stringify({ force })
2403
- });
2404
-
2405
- if (!resp.ok) {
2406
- const err = await resp.json();
2407
- throw new Error(err.error || 'Failed to generate insight');
2408
- }
2409
-
2410
- const data = await resp.json();
2411
- insightStatus.value = data.status;
2412
-
2413
- if (data.status === 'generating') {
2414
- insightStartedAt.value = data.startedAt;
2415
- startPolling();
2416
- } else if (data.status === 'completed') {
2417
- insightReport.value = data.report;
2418
- insightGeneratedAt.value = data.generatedAt;
2419
- }
2420
- } catch (err) {
2421
- insightError.value = err.message;
2422
- } finally {
2423
- insightLoading.value = false;
2424
- }
2425
- };
2426
-
2427
- const regenerateInsight = async () => {
2428
- await generateInsight(true);
2429
- };
2430
-
2431
- // Check for existing insight on mount
2432
- onMounted(async () => {
2433
- await checkExistingInsight();
2434
- });
2435
-
2436
- // Clean up polling on unmount
2437
- onUnmounted(() => {
2438
- stopPolling();
2439
- });
2440
-
2441
- return {
2442
- sessionId, metadata, events, loading, error, activeTab,
2443
- sortField, sortDir,
2444
- insightReport, insightLog, insightLoading, insightError, insightGeneratedAt,
2445
- insightStatus, insightLastUpdate, insightStartedAt, insightAgeMs,
2446
- renderedInsight, generateInsight, regenerateInsight,
2447
- formatDuration, formatTime, formatDateTime,
2448
- sessionStart, sessionEnd, totalDuration,
2449
- subagentAnalysis, maxSubagentDuration, subagentTimelineItems, subagentStats,
2450
- EVENT_MARKER_CATEGORIES, showMarkerLegend,
2451
- copyLabel, copyTimelineMarkdown,
2452
- ganttCrosshairX, ganttCrosshairTime, onGanttMouseMove, onGanttMouseLeave,
2453
- turnAnalysis, maxTurnDuration, groupedTurns,
2454
- unifiedTimelineItems,
2455
- toolAnalysis, sortedToolAnalysis, maxToolDuration,
2456
- fileOperations, fileStats,
2457
- toolTimeByCategory, maxCategoryTime,
2458
- totalToolTime, totalToolCount, avgToolDuration, longestTool,
2459
- successRate, errorCount, timeBreakdown,
2460
- gapAnalysis, maxGapDuration, gapStats,
2461
- ganttPosition, ganttSequencePosition, toggleSort, sortIcon,
2462
- getToolBadgeClass, getOpBadgeClass,
2463
- isVSCodeSession, vsCodeSubagents
2464
- };
2465
- },
2466
-
2467
- template: `
2468
- <div v-if="loading" class="empty-state" style="padding: 60px;">
2469
- ⏳ Loading events...
2470
- </div>
2471
-
2472
- <div v-else-if="error" class="empty-state" style="padding: 60px; color: #f85149;">
2473
- ❌ {{ error }}
2474
- </div>
2475
-
2476
- <div v-else>
2477
- <!-- Summary Cards -->
2478
- <div class="summary-grid">
2479
- <div class="summary-card" title="Wall-clock time from first event to last event in this session.">
2480
- <div class="summary-card-label">Total Duration</div>
2481
- <div class="summary-card-value">{{ formatDuration(totalDuration) }}</div>
2482
- <div class="summary-card-sub" v-if="sessionStart" style="margin-top: 2px; font-size: 10px; opacity: 0.7;">
2483
- {{ formatDateTime(sessionStart) }} → {{ formatDateTime(sessionEnd) }}
2484
- </div>
2485
- </div>
2486
- <div class="summary-card" title="Number of user messages that triggered agent work. Each request may involve multiple LLM turns.">
2487
- <div class="summary-card-label">User Requests</div>
2488
- <div class="summary-card-value">{{ groupedTurns.length }}</div>
2489
- <div class="summary-card-sub">{{ turnAnalysis.length }} turn{{ turnAnalysis.length !== 1 ? 's' : '' }}</div>
2490
- </div>
2491
- <div class="summary-card" title="Total tool executions (Read, Write, Edit, Bash, Grep, etc.) across the entire session, including subagent tools.">
2492
- <div class="summary-card-label">Tool Calls</div>
2493
- <div class="summary-card-value">{{ totalToolCount }}</div>
2494
- <div class="summary-card-sub">
2495
- <span :style="{ color: successRate >= 95 ? '#3fb950' : successRate >= 80 ? '#d29922' : '#f85149' }">{{ successRate }}%</span> success
2496
- <span v-if="errorCount > 0" style="color: #f85149;"> · {{ errorCount }} error{{ errorCount !== 1 ? 's' : '' }}</span>
2497
- </div>
2498
- </div>
2499
- <div class="summary-card" title="Spawned subagents (via Task tool). Shows completed/failed/incomplete counts, total wall-clock time, and tool calls attributed to subagents.">
2500
- <div class="summary-card-label">Sub-Agents</div>
2501
- <div class="summary-card-value">{{ subagentAnalysis.length || vsCodeSubagents.length }}</div>
2502
- <div class="summary-card-sub" v-if="subagentAnalysis.length > 0">
2503
- <span :style="{ color: subagentStats.successRate >= 95 ? '#3fb950' : subagentStats.successRate >= 80 ? '#d29922' : '#f85149' }">{{ subagentStats.completed }}✓</span>
2504
- <span v-if="subagentStats.failed > 0" style="color: #f85149;"> · {{ subagentStats.failed }}✗</span>
2505
- <span v-if="subagentStats.incomplete > 0" style="color: #d29922;"> · {{ subagentStats.incomplete }}⏳</span>
2506
- · {{ formatDuration(subagentStats.totalTime) }}
2507
- · {{ subagentStats.totalTools }} tools
2508
- </div>
2509
- <div class="summary-card-sub" v-else-if="vsCodeSubagents.length > 0">
2510
- <span style="color: #3fb950;">{{ vsCodeSubagents.length }}✓</span>
2511
- · {{ vsCodeSubagents.reduce((s, a) => s + a.toolCount, 0) }} tools
2512
- </div>
2513
- </div>
2514
- <div class="summary-card" title="Estimated LLM reasoning time (total duration minus tool execution and user thinking time). Breakdown shows LLM percentage, tool wall-clock time, and user idle time.">
2515
- <div class="summary-card-label">Time Breakdown</div>
2516
- <div class="summary-card-value">{{ formatDuration(timeBreakdown.llmTime) }} <span style="font-size: 14px; opacity: 0.6;">({{ timeBreakdown.llmPct }}% LLM Reasoning)</span></div>
2517
- <div class="summary-card-sub">
2518
- Tools {{ formatDuration(totalToolTime) }} ({{ timeBreakdown.toolPct }}%)
2519
- <span v-if="timeBreakdown.userThinkingTime > 1000"> · User {{ formatDuration(timeBreakdown.userThinkingTime) }} ({{ timeBreakdown.userThinkingPct }}%)</span>
2520
- </div>
2521
- </div>
2522
- <div class="summary-card" title="File system operations: reads (Read/Glob), edits (Edit), writes (Write), and searches (Grep).">
2523
- <div class="summary-card-label">File Operations</div>
2524
- <div class="summary-card-value">{{ fileStats.totalOps }}</div>
2525
- <div class="summary-card-sub">
2526
- {{ fileStats.reads }} reads · {{ fileStats.edits }} edits · {{ fileStats.writes }} writes · {{ fileStats.searches }} searches
2527
- </div>
2528
- </div>
2529
- </div>
2530
-
2531
- <!-- Tabs -->
2532
- <div class="tabs">
2533
- <button :class="['tab', { active: activeTab === 'timeline' }]" @click="activeTab = 'timeline'">
2534
- 📊 Timeline
2535
- </button>
2536
- <button :class="['tab', { active: activeTab === 'insight' }]" @click="activeTab = 'insight'">
2537
- 💡 Agent Review
2538
- </button>
2539
- </div>
2540
-
2541
- <!-- ═══ Unified Timeline Tab ═══ -->
2542
- <div v-if="activeTab === 'timeline'" class="section">
2543
- <div v-if="error" class="empty-state" style="color: #f85149;">
2544
- Error loading timeline: {{ error }}
2545
- </div>
2546
- <div v-else-if="!unifiedTimelineItems.length" class="empty-state">
2547
- No timeline data found in this session.
2548
- </div>
2549
- <div v-else>
2550
- <!-- Section A: Gantt Chart -->
2551
- <div class="section-title" style="display: flex; align-items: center;">
2552
- Timeline
2553
- <button class="legend-toggle-btn" @click="showMarkerLegend = !showMarkerLegend">
2554
- {{ showMarkerLegend ? 'Hide Legend' : 'Show Legend' }}
2555
- </button>
2556
- <button class="legend-toggle-btn" @click="copyTimelineMarkdown">
2557
- {{ copyLabel }}
2558
- </button>
2559
- </div>
2560
-
2561
- <!-- Event Legend -->
2562
- <div v-show="showMarkerLegend" class="event-legend">
2563
- <div class="event-legend-item">
2564
- <span class="event-legend-swatch" style="background: rgba(88, 166, 255, 0.5);"></span>
2565
- <span>User Request</span>
2566
- </div>
2567
- <div class="event-legend-item">
2568
- <span class="event-legend-swatch" style="background: rgba(63, 185, 80, 0.8);"></span>
2569
- <span>Sub-Agent</span>
2570
- </div>
2571
- <div class="event-legend-item">
2572
- <span class="event-legend-swatch" style="background: rgba(139, 148, 158, 0.3); border: 1px dashed rgba(139, 148, 158, 0.5);"></span>
2573
- <span>Main Agent</span>
2574
- </div>
2575
- <div class="event-legend-item">
2576
- <span class="event-legend-swatch" style="background: #d29922;"></span>
2577
- <span>Tool (no errors)</span>
2578
- </div>
2579
- <div class="event-legend-item">
2580
- <span class="event-legend-swatch" style="background: linear-gradient(to right, #d29922, #f85149);"></span>
2581
- <span>Tool (error gradient)</span>
2582
- </div>
2583
- <div class="event-legend-item">
2584
- <span class="event-legend-swatch" style="background: #f85149;"></span>
2585
- <span>Tool Error (100%)</span>
2586
- </div>
2587
- <template v-for="(cat, type) in EVENT_MARKER_CATEGORIES" :key="type">
2588
- <div v-if="type && !type.startsWith('tool.')" class="event-legend-item">
2589
- <span class="event-legend-swatch" :style="{ background: cat.color, borderRadius: cat.shape === 'circle' ? '50%' : cat.shape === 'diamond' ? '1px' : '2px', transform: cat.shape === 'diamond' ? 'rotate(45deg)' : 'none' }"></span>
2590
- <span>{{ cat.label }}</span>
2591
- </div>
2592
- </template>
2593
- </div>
2594
-
2595
- <!-- VS Code Session Banner -->
2596
- <div v-if="isVSCodeSession" style="
2597
- background: rgba(88, 166, 255, 0.1);
2598
- border: 1px solid rgba(88, 166, 255, 0.3);
2599
- border-radius: 6px;
2600
- padding: 12px 16px;
2601
- margin-bottom: 16px;
2602
- display: flex;
2603
- align-items: center;
2604
- gap: 8px;
2605
- color: #58a6ff;
2606
- font-size: 13px;
2607
- ">
2608
- <span style="font-size: 16px;">ⓘ</span>
2609
- <span>Sequence layout — bar widths represent tool count, not elapsed time</span>
2610
- </div>
2611
-
2612
- <div class="gantt-container" @mousemove="onGanttMouseMove" @mouseleave="onGanttMouseLeave">
2613
- <!-- Crosshair -->
2614
- <div v-if="ganttCrosshairX !== null" class="gantt-crosshair" :style="{ left: ganttCrosshairX + 'px' }">
2615
- <div class="gantt-crosshair-label">{{ ganttCrosshairTime }}</div>
2616
- </div>
2617
- <template v-for="(item, idx) in unifiedTimelineItems" :key="'utl-' + idx">
2618
-
2619
- <!-- Divider row -->
2620
- <div v-if="item.rowType === 'divider'" class="gantt-divider">
2621
- Tool Summary
2622
- </div>
2623
-
2624
- <!-- User Request row -->
2625
- <div v-else-if="item.rowType === 'user-req'" class="gantt-row">
2626
- <div class="gantt-label user-req" :title="item.message || 'No message'">
2627
- <span class="user-req-badge">UserReq {{ item.userReqNumber }}</span>
2628
- <span class="user-req-msg">{{ (item.message || '').substring(0, 40) }}{{ (item.message || '').length > 40 ? '...' : '' }}</span>
2629
- </div>
2630
- <div class="gantt-bar-area">
2631
- <div
2632
- class="gantt-bar user-req"
2633
- :style="item.isSequenceEstimated ? ganttSequencePosition(item) : ganttPosition(item.startTime, item.endTime)"
2634
- :title="item.isSequenceEstimated ? ('UserReq ' + item.userReqNumber + ' — ' + item.toolCount + ' tools') : ('UserReq ' + item.userReqNumber + ' — ' + formatDuration(item.duration))"
2635
- >
2636
- {{ item.isSequenceEstimated ? (item.toolCount + ' tools') : formatDuration(item.duration) }}
2637
- </div>
2638
- </div>
2639
- </div>
2640
-
2641
- <!-- Sub-Agent row (indented for CLI, not indented for VS Code) -->
2642
- <div v-else-if="item.rowType === 'subagent'" :class="['gantt-row', item.indented ? 'indented' : (item.isSequenceEstimated ? '' : 'indented')]">
2643
- <div class="gantt-label" :title="item.name">
2644
- <a
2645
- :href="'/session/' + sessionId + '?eventType=subagent.started&eventName=' + encodeURIComponent(item.name) + '&eventTimestamp=' + encodeURIComponent(item.startTime || '')"
2646
- class="subagent-link"
2647
- :title="'View events from here'"
2648
- >
2649
- <span :style="{ color: item.status === 'completed' ? '#3fb950' : item.status === 'failed' ? '#f85149' : '#d29922' }">
2650
- {{ item.status === 'completed' ? '✓' : item.status === 'failed' ? '✗' : '⏳' }}
2651
- </span>
2652
- {{ item.name }}
2653
- </a>
2654
- </div>
2655
- <div class="gantt-bar-area">
2656
- <div
2657
- :class="[
2658
- 'gantt-bar',
2659
- item.isSequenceEstimated ? 'sequence-estimated' : '',
2660
- item.status === 'completed' ? 'subagent' : item.status === 'failed' ? 'subagent-failed' : 'subagent-incomplete'
2661
- ]"
2662
- :style="item.isSequenceEstimated ? ganttSequencePosition(item) : ganttPosition(item.startTime, item.endTime)"
2663
- :title="item.isSequenceEstimated ? (item.name + ' — ' + item.toolCount + ' tools') : (item.name + ' — ' + formatDuration(item.duration))"
2664
- >
2665
- {{ item.isSequenceEstimated ? (item.toolCount + ' tools') : formatDuration(item.duration) }}
2666
-
2667
- <!-- Event markers (only for non-sequence bars) -->
2668
- <template v-if="!item.isSequenceEstimated && item.innerEventMarkers && item.innerEventMarkers.length">
2669
- <span
2670
- v-for="(marker, midx) in item.innerEventMarkers"
2671
- :key="'m-' + midx"
2672
- class="event-marker"
2673
- :style="{ left: marker.position + '%' }"
2674
- >
2675
- <template v-if="marker.shape === 'cluster'">
2676
- <span class="event-marker--cluster" :style="{ background: marker.color }">{{ marker.count }}</span>
2677
- </template>
2678
- <template v-else-if="marker.shape === 'circle'">
2679
- <span class="event-marker--circle" :style="{ background: marker.color }"></span>
2680
- </template>
2681
- <template v-else-if="marker.shape === 'diamond'">
2682
- <span class="event-marker--diamond" :style="{ background: marker.color }"></span>
2683
- </template>
2684
- <template v-else-if="marker.shape === 'square'">
2685
- <span class="event-marker--square" :style="{ background: marker.color }"></span>
2686
- </template>
2687
- <template v-else-if="marker.shape === 'triangle'">
2688
- <span class="event-marker--triangle" :style="{ color: marker.color }"></span>
2689
- </template>
2690
- <span class="event-marker-tooltip">
2691
- <template v-if="marker.shape === 'cluster'">{{ marker.count }} events: {{ marker.label }}</template>
2692
- <template v-else>{{ marker.label }}<span v-if="marker.toolName"> ({{ marker.toolName }})</span></template>
2693
- </span>
2694
- </span>
2695
- </template>
2696
- </div>
2697
- </div>
2698
- </div>
2699
-
2700
- <!-- Main Agent gap row (indented) -->
2701
- <div v-else-if="item.rowType === 'main-agent'" class="gantt-row indented">
2702
- <div class="gantt-label agent-op" :title="item.summary">
2703
- <span class="agent-op-icon">⚙</span>
2704
- <span>Main Agent</span>
2705
- <span class="agent-op-summary">{{ item.summary }}</span>
2706
- </div>
2707
- <div class="gantt-bar-area">
2708
- <div
2709
- class="gantt-bar agent-op"
2710
- :style="ganttPosition(item.startTime, item.endTime)"
2711
- :title="'Main Agent — ' + formatDuration(item.duration)"
2712
- >
2713
- {{ formatDuration(item.duration) }}
2714
-
2715
- <!-- Event markers -->
2716
- <template v-if="item.innerEventMarkers && item.innerEventMarkers.length">
2717
- <span
2718
- v-for="(marker, midx) in item.innerEventMarkers"
2719
- :key="'m-' + midx"
2720
- class="event-marker"
2721
- :style="{ left: marker.position + '%' }"
2722
- >
2723
- <template v-if="marker.shape === 'cluster'">
2724
- <span class="event-marker--cluster" :style="{ background: marker.color }">{{ marker.count }}</span>
2725
- </template>
2726
- <template v-else-if="marker.shape === 'circle'">
2727
- <span class="event-marker--circle" :style="{ background: marker.color }"></span>
2728
- </template>
2729
- <template v-else-if="marker.shape === 'diamond'">
2730
- <span class="event-marker--diamond" :style="{ background: marker.color }"></span>
2731
- </template>
2732
- <template v-else-if="marker.shape === 'square'">
2733
- <span class="event-marker--square" :style="{ background: marker.color }"></span>
2734
- </template>
2735
- <template v-else-if="marker.shape === 'triangle'">
2736
- <span class="event-marker--triangle" :style="{ color: marker.color }"></span>
2737
- </template>
2738
- <span class="event-marker-tooltip">
2739
- <template v-if="marker.shape === 'cluster'">{{ marker.count }} events: {{ marker.label }}</template>
2740
- <template v-else>{{ marker.label }}<span v-if="marker.toolName"> ({{ marker.toolName }})</span></template>
2741
- </span>
2742
- </span>
2743
- </template>
2744
- </div>
2745
- </div>
2746
- </div>
2747
-
2748
- </template>
2749
-
2750
- <div class="gantt-time-axis">
2751
- <span>{{ formatTime(events[0]?.timestamp) }}</span>
2752
- <span>{{ formatTime(events[events.length-1]?.timestamp) }}</span>
2753
- </div>
2754
- </div>
2755
-
2756
- <!-- Tool Summary -->
2757
- <div v-if="toolTimeByCategory.length" style="margin-top: 24px;">
2758
- <h3 style="color: #e6edf3; font-size: 14px; margin-bottom: 12px;">🔧 Tool Summary</h3>
2759
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px;">
2760
- <div
2761
- v-for="cat in toolTimeByCategory"
2762
- :key="cat.category"
2763
- style="background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; display: flex; align-items: center; gap: 10px;"
2764
- >
2765
- <div style="flex: 1; min-width: 0;">
2766
- <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px;">
2767
- <span style="color: #d29922; font-weight: 500; font-size: 13px;">{{ cat.category }}</span>
2768
- <span style="color: #7d8590; font-size: 11px;">{{ cat.count }} call{{ cat.count !== 1 ? 's' : '' }}<span v-if="cat.errors" style="color: #f85149;"> · {{ cat.errors }} err</span></span>
2769
- </div>
2770
- <div style="background: #21262d; border-radius: 3px; height: 6px; overflow: hidden;">
2771
- <div :style="{ width: (cat.totalTime / maxCategoryTime * 100) + '%', height: '100%', background: 'rgba(158, 106, 3, 0.7)', borderRadius: '3px' }"></div>
2772
- </div>
2773
- <div style="color: #7d8590; font-size: 11px; margin-top: 3px;">{{ formatDuration(cat.totalTime) }}</div>
2774
- </div>
2775
- </div>
2776
- </div>
2777
- </div>
2778
-
2779
- </div>
2780
- </div>
2781
-
2782
- <!-- ═══ Copilot Insight Tab ═══ -->
2783
- <div v-if="activeTab === 'insight'" class="section">
2784
- <!-- Error State -->
2785
- <div v-if="insightError" class="empty-state" style="padding: 60px; color: #f85149;">
2786
- ❌ {{ insightError }}
2787
- </div>
2788
-
2789
- <!-- Generating State -->
2790
- <div v-else-if="insightStatus === 'generating'" style="padding: 20px;">
2791
- <div :style="{
2792
- background: '#0d1117',
2793
- border: '1px solid ' + (insightAgeMs > 300000 ? '#d29922' : '#30363d'),
2794
- borderRadius: '6px',
2795
- padding: '20px',
2796
- marginBottom: '20px',
2797
- }">
2798
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
2799
- <div style="display: flex; align-items: center;">
2800
- <span style="font-size: 24px; margin-right: 10px;">⏳</span>
2801
- <div>
2802
- <div style="font-weight: 600; color: #58a6ff; margin-bottom: 5px;">
2803
- Generating Agent Review...
2804
- </div>
2805
- <div style="font-size: 13px; color: #7d8590;">
2806
- Started: {{ formatDateTime(insightStartedAt) }} •
2807
- Age: {{ Math.floor(insightAgeMs / 1000) }}s
2808
- </div>
2809
- </div>
2810
- </div>
2811
- <button
2812
- v-if="insightAgeMs > 300000"
2813
- @click="regenerateInsight"
2814
- style="
2815
- background: #d29922;
2816
- color: #fff;
2817
- border: none;
2818
- padding: 8px 16px;
2819
- border-radius: 6px;
2820
- font-size: 13px;
2821
- cursor: pointer;
2822
- font-weight: 500;
2823
- white-space: nowrap;
2824
- "
2825
- @mouseover="$event.target.style.background='#e3b341'"
2826
- @mouseleave="$event.target.style.background='#d29922'"
2827
- >
2828
- 🔄 Stop &amp; Retry
2829
- </button>
2830
- </div>
2831
- <!-- Slow generation warning -->
2832
- <div v-if="insightAgeMs > 300000" style="
2833
- background: rgba(210, 153, 34, 0.1);
2834
- border: 1px solid rgba(210, 153, 34, 0.3);
2835
- border-radius: 6px;
2836
- padding: 10px 14px;
2837
- margin-bottom: 12px;
2838
- font-size: 13px;
2839
- color: #d29922;
2840
- ">
2841
- ⚠️ Generation is taking longer than 5 minutes. For large sessions this is normal — the agent needs to read and analyze all events. If it appears stuck, you can click <strong>Stop &amp; Retry</strong> to cancel and start fresh.
2842
- </div>
2843
- <div v-if="insightLog" id="insight-log" style="
2844
- background: #161b22;
2845
- border: 1px solid #30363d;
2846
- border-radius: 6px;
2847
- padding: 14px 16px;
2848
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
2849
- font-size: 12px;
2850
- line-height: 1.6;
2851
- color: #8b949e;
2852
- white-space: pre-wrap;
2853
- word-break: break-word;
2854
- max-height: 400px;
2855
- overflow-y: auto;
2856
- ">{{ insightLog }}</div>
2857
- </div>
2858
- </div>
2859
-
2860
- <!-- Timeout State -->
2861
- <div v-else-if="insightStatus === 'timeout'" style="padding: 20px;">
2862
- <div style="
2863
- background: #0d1117;
2864
- border: 1px solid #d29922;
2865
- border-radius: 6px;
2866
- padding: 20px;
2867
- margin-bottom: 20px;
2868
- ">
2869
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
2870
- <div style="display: flex; align-items: center;">
2871
- <span style="font-size: 24px; margin-right: 10px;">⏳</span>
2872
- <div>
2873
- <div style="font-weight: 600; color: #d29922; margin-bottom: 5px;">
2874
- Still generating... ({{ Math.floor(insightAgeMs / 1000 / 60) }}m elapsed)
2875
- </div>
2876
- <div style="font-size: 13px; color: #7d8590;">
2877
- Large sessions with sub-agents may take 10–15 minutes. Still polling for completion.
2878
- </div>
2879
- </div>
2880
- </div>
2881
- <button
2882
- @click="regenerateInsight"
2883
- style="
2884
- background: #d29922;
2885
- color: #fff;
2886
- border: none;
2887
- padding: 8px 16px;
2888
- border-radius: 6px;
2889
- font-size: 13px;
2890
- cursor: pointer;
2891
- font-weight: 500;
2892
- white-space: nowrap;
2893
- "
2894
- @mouseover="$event.target.style.background='#e3b341'"
2895
- @mouseleave="$event.target.style.background='#d29922'"
2896
- >
2897
- 🔄 Stop &amp; Retry
2898
- </button>
2899
- </div>
2900
- <div v-if="insightLog" style="
2901
- background: #161b22;
2902
- border: 1px solid #30363d;
2903
- border-radius: 6px;
2904
- padding: 14px 16px;
2905
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
2906
- font-size: 12px;
2907
- line-height: 1.6;
2908
- color: #8b949e;
2909
- white-space: pre-wrap;
2910
- word-break: break-word;
2911
- max-height: 400px;
2912
- overflow-y: auto;
2913
- ">{{ insightLog }}</div>
2914
- </div>
2915
- </div>
2916
-
2917
- <!-- Not Started State -->
2918
- <div v-else-if="insightStatus === 'not_started'" style="padding: 40px; text-align: center;">
2919
- <p style="margin-bottom: 20px; color: #7d8590;">
2920
- Generate an AI-powered quality & performance review of how the agent used its tools, prompts, and workflow in this session
2921
- </p>
2922
- <button
2923
- @click="generateInsight(false)"
2924
- :disabled="insightLoading"
2925
- style="
2926
- background: #238636;
2927
- color: #fff;
2928
- border: none;
2929
- padding: 10px 20px;
2930
- border-radius: 6px;
2931
- font-size: 14px;
2932
- cursor: pointer;
2933
- font-weight: 500;
2934
- "
2935
- @mouseover="$event.target.style.background='#2ea043'"
2936
- @mouseleave="$event.target.style.background='#238636'"
2937
- >
2938
- 💡 Generate Agent Review
2939
- </button>
2940
- <p style="margin-top: 12px; font-size: 12px; color: #6e7681;">
2941
- For large sessions this may take several minutes — you'll see a live progress log while the review is being generated.
2942
- </p>
2943
- </div>
2944
-
2945
- <!-- Completed State -->
2946
- <div v-else-if="insightStatus === 'completed'" style="padding: 20px;">
2947
- <div style="
2948
- background: #161b22;
2949
- border: 1px solid #30363d;
2950
- border-radius: 6px;
2951
- padding: 20px;
2952
- margin-bottom: 20px;
2953
- ">
2954
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
2955
- <span style="color: #7d8590; font-size: 13px;">
2956
- Generated: {{ formatDateTime(insightGeneratedAt) }}
2957
- </span>
2958
- <button
2959
- @click="regenerateInsight"
2960
- style="
2961
- background: transparent;
2962
- color: #58a6ff;
2963
- border: 1px solid #58a6ff;
2964
- padding: 5px 12px;
2965
- border-radius: 6px;
2966
- font-size: 12px;
2967
- cursor: pointer;
2968
- "
2969
- @mouseover="$event.target.style.background='rgba(88, 166, 255, 0.1)'"
2970
- @mouseleave="$event.target.style.background='transparent'"
2971
- >
2972
- 🔄 Regenerate
2973
- </button>
2974
- </div>
2975
- <div v-html="renderedInsight" style="
2976
- color: #c9d1d9;
2977
- line-height: 1.6;
2978
- "></div>
2979
- </div>
2980
- </div>
2981
- </div>
2982
- </div>
2983
- `
2984
- });
2985
-
2986
- // Global error handler
2987
- app.config.errorHandler = (err, instance, info) => {
2988
- console.error('[Vue Error]', err, info);
2989
- const errorDiv = document.createElement('div');
2990
- errorDiv.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #f85149; color: white; padding: 20px; border-radius: 6px; z-index: 9999; max-width: 80%; font-family: monospace; font-size: 14px;';
2991
- errorDiv.innerHTML = `<strong>Vue Error:</strong><br>${err.message}<br><br><small>${info}</small>`;
2992
- document.body.appendChild(errorDiv);
2993
- };
2994
-
2995
- app.mount('#app');
2996
- </script>
781
+ <script src="/public/js/time-analyze.min.js"></script>
2997
782
  </body>
2998
783
  </html>