@qiaolei81/copilot-session-viewer 0.1.9 → 0.2.1

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.
@@ -764,6 +764,18 @@
764
764
  const showMarkerLegend = ref(false);
765
765
  const copyLabel = ref('📊 Copy as Mermaid Gantt');
766
766
 
767
+ // Helper: normalize message to string (handle arrays/objects from Copilot)
768
+ const normalizeMessage = (msg) => {
769
+ if (!msg) return '';
770
+ if (typeof msg === 'string') return msg;
771
+ if (Array.isArray(msg)) {
772
+ // Handle content array (Copilot format)
773
+ return msg.map(c => c.text || c.content || '').join(' ');
774
+ }
775
+ if (typeof msg === 'object' && msg.text) return msg.text;
776
+ return String(msg);
777
+ };
778
+
767
779
  // Gantt crosshair
768
780
  const ganttCrosshairX = ref(null); // px from left of gantt-container
769
781
  const ganttCrosshairTime = ref('');
@@ -808,6 +820,16 @@
808
820
 
809
821
  // Sanitize label for Mermaid: strip chars that break syntax or could escape the code block
810
822
  const sanitize = (str) => (str || '').replace(/[`\n\r]/g, '').replace(/[:;#]/g, '-').replace(/\s+/g, ' ').trim().substring(0, 100);
823
+ const normalizeMessage = (msg) => {
824
+ if (!msg) return '';
825
+ if (typeof msg === 'string') return msg;
826
+ if (Array.isArray(msg)) {
827
+ // Handle content array (Copilot format)
828
+ return msg.map(c => c.text || c.content || '').join(' ');
829
+ }
830
+ if (typeof msg === 'object' && msg.text) return msg.text;
831
+ return String(msg);
832
+ };
811
833
 
812
834
  // Deduplicate task IDs within the mermaid block
813
835
  const usedIds = {};
@@ -828,7 +850,7 @@
828
850
 
829
851
  for (const item of items) {
830
852
  if (item.rowType === 'user-req') {
831
- const msg = sanitize(item.message || 'No message').substring(0, 40);
853
+ const msg = sanitize(normalizeMessage(item.message) || 'No message').substring(0, 40);
832
854
  const label = 'UserReq ' + item.userReqNumber + ' – ' + msg + ' (' + formatDuration(item.duration) + ')';
833
855
  const id = uniqueId('userreq_' + item.userReqNumber);
834
856
  const start = toEpochMs(item.startTime);
@@ -925,12 +947,23 @@
925
947
  // ── Session timeline ──
926
948
  const sessionStart = computed(() => {
927
949
  if (!events.value.length) return null;
928
- return new Date(events.value[0].timestamp).getTime();
950
+ // Find first event with valid timestamp
951
+ for (const ev of events.value) {
952
+ const ts = ev.timestamp || ev.snapshot?.timestamp;
953
+ if (ts) return new Date(ts).getTime();
954
+ }
955
+ return null;
929
956
  });
930
957
 
931
958
  const sessionEnd = computed(() => {
932
959
  if (!events.value.length) return null;
933
- return new Date(events.value[events.value.length - 1].timestamp).getTime();
960
+ // Find last event with valid timestamp
961
+ for (let i = events.value.length - 1; i >= 0; i--) {
962
+ const ev = events.value[i];
963
+ const ts = ev.timestamp || ev.snapshot?.timestamp;
964
+ if (ts) return new Date(ts).getTime();
965
+ }
966
+ return null;
934
967
  });
935
968
 
936
969
  const totalDuration = computed(() => {
@@ -1015,8 +1048,7 @@
1015
1048
  if (ev.type === 'subagent.started') {
1016
1049
  startStack.push(ev);
1017
1050
  } else if (ev.type === 'subagent.completed' || ev.type === 'subagent.failed') {
1018
- const name = ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent';
1019
- // Find matching start by toolCallId, with name-based LIFO fallback
1051
+ // Find matching start by toolCallId
1020
1052
  const tcid = ev.data?.toolCallId;
1021
1053
  let startIdx = -1;
1022
1054
  if (tcid) {
@@ -1027,17 +1059,13 @@
1027
1059
  }
1028
1060
  }
1029
1061
  }
1030
- // Fallback to name-based LIFO if no toolCallId match
1031
- if (startIdx < 0) {
1032
- for (let i = startStack.length - 1; i >= 0; i--) {
1033
- const sName = startStack[i].data?.agentDisplayName || startStack[i].data?.agentName || 'SubAgent';
1034
- if (sName === name) {
1035
- startIdx = i;
1036
- break;
1037
- }
1038
- }
1062
+ // Fallback to LIFO if no toolCallId match (pop last started)
1063
+ if (startIdx < 0 && startStack.length > 0) {
1064
+ startIdx = startStack.length - 1;
1039
1065
  }
1040
1066
  const startEv = startIdx >= 0 ? startStack.splice(startIdx, 1)[0] : null;
1067
+ // Extract name from matched start event (completed event doesn't have name)
1068
+ const name = startEv?.data?.agentDisplayName || startEv?.data?.agentName || 'SubAgent';
1041
1069
  const startTime = startEv ? new Date(startEv.timestamp).getTime() : null;
1042
1070
  const endTime = new Date(ev.timestamp).getTime();
1043
1071
  const duration = startTime ? endTime - startTime : null;
@@ -1383,48 +1411,74 @@
1383
1411
 
1384
1412
  // ── Turn analysis ──
1385
1413
  const turnAnalysis = computed(() => {
1386
- const sorted = sortedEvents.value;
1387
- const turnStarts = sorted.filter(e => e.type === 'assistant.turn_start');
1388
- const allUserMessages = sorted.filter(e => e.type === 'user.message');
1389
-
1390
- return turnStarts.map((turn, idx) => {
1391
- const startTime = new Date(turn.timestamp).getTime();
1392
- const nextTurn = turnStarts[idx + 1];
1393
- const endTime = nextTurn
1394
- ? new Date(nextTurn.timestamp).getTime()
1395
- : sessionEnd.value || startTime;
1396
- const duration = endTime - startTime;
1397
-
1398
- // Find user message before this turn
1399
- const turnIndex = sorted.indexOf(turn);
1400
- const userMessage = sorted
1401
- .slice(0, turnIndex)
1402
- .reverse()
1403
- .find(e => e.type === 'user.message');
1404
-
1405
- const userReqNumber = userMessage
1406
- ? allUserMessages.indexOf(userMessage) + 1
1407
- : 0;
1408
-
1409
- // Count tool calls in this turn
1410
- let toolCalls = 0;
1411
- for (const e of sorted) {
1412
- const t = new Date(e.timestamp).getTime();
1413
- if (t >= startTime && t <= endTime && e.type === 'tool.execution_start') {
1414
- toolCalls++;
1414
+ try {
1415
+ const sorted = sortedEvents.value;
1416
+ const assistantMessages = sorted.filter(e => e.type === 'assistant.message');
1417
+ const allUserMessages = sorted.filter(e => e.type === 'user.message');
1418
+
1419
+ return assistantMessages.map((msg, idx) => {
1420
+ const ts = msg.timestamp;
1421
+ if (!ts) {
1422
+ console.warn('[turnAnalysis] Message without timestamp:', msg);
1423
+ return null;
1424
+ }
1425
+ const startTime = new Date(ts).getTime();
1426
+ if (isNaN(startTime)) {
1427
+ console.warn('[turnAnalysis] Invalid timestamp:', ts, msg);
1428
+ return null;
1429
+ }
1430
+
1431
+ const nextMsg = assistantMessages[idx + 1];
1432
+ const endTime = nextMsg
1433
+ ? new Date(nextMsg.timestamp).getTime()
1434
+ : sessionEnd.value || startTime;
1435
+ const duration = endTime - startTime;
1436
+
1437
+ // Find user message before this assistant message
1438
+ const msgIndex = sorted.indexOf(msg);
1439
+ const userMessage = sorted
1440
+ .slice(0, msgIndex)
1441
+ .reverse()
1442
+ .find(e => e.type === 'user.message');
1443
+
1444
+ const userReqNumber = userMessage
1445
+ ? allUserMessages.indexOf(userMessage) + 1
1446
+ : 0;
1447
+
1448
+ // Extract display text
1449
+ let displayText = '';
1450
+ const hasText = msg.data?.message && msg.data.message.trim() !== '';
1451
+
1452
+ if (hasText) {
1453
+ displayText = normalizeMessage(msg.data.message);
1454
+ } else if (msg.data?.tools && msg.data.tools.length > 0) {
1455
+ // Only tool calls, show tool names
1456
+ const toolNames = msg.data.tools.map(t => t.name || 'unknown').join(', ');
1457
+ displayText = `Tool calls: ${toolNames}`;
1458
+ } else {
1459
+ displayText = '(empty assistant message)';
1415
1460
  }
1416
- }
1417
1461
 
1418
- return {
1419
- turnId: turn.data?.turnId ?? idx,
1420
- userReqNumber,
1421
- message: userMessage?.data?.content || userMessage?.data?.transformedContent || '',
1422
- startTime: turn.timestamp,
1423
- endTime: nextTurn?.timestamp || events.value[events.value.length - 1]?.timestamp,
1424
- duration,
1425
- toolCalls
1426
- };
1427
- });
1462
+ // Count tool calls
1463
+ const toolCalls = msg.data?.tools?.length || 0;
1464
+
1465
+ return {
1466
+ turnId: msg.id ?? `msg-${idx}`,
1467
+ userReqNumber,
1468
+ message: normalizeMessage(userMessage?.data?.message || userMessage?.data?.content || userMessage?.data?.transformedContent || ''),
1469
+ displayText,
1470
+ hasText,
1471
+ startTime: msg.timestamp,
1472
+ endTime: nextMsg?.timestamp || events.value[events.value.length - 1]?.timestamp,
1473
+ duration,
1474
+ toolCalls
1475
+ };
1476
+ }).filter(t => t !== null);
1477
+ } catch (err) {
1478
+ console.error('[turnAnalysis] Error:', err);
1479
+ error.value = 'Error analyzing turns: ' + err.message;
1480
+ return [];
1481
+ }
1428
1482
  });
1429
1483
 
1430
1484
  const maxTurnDuration = computed(() => {
@@ -1992,13 +2046,19 @@
1992
2046
  const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
1993
2047
  if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
1994
2048
  const data = await resp.json();
2049
+ console.log('[TIME-ANALYZE] Loaded events:', data.length);
2050
+ console.log('[TIME-ANALYZE] Event types:', [...new Set(data.map(e => e.type))]);
2051
+ console.log('[TIME-ANALYZE] Turn starts:', data.filter(e => e.type === 'assistant.turn_start').length);
2052
+ console.log('[TIME-ANALYZE] User messages:', data.filter(e => e.type === 'user.message').length);
1995
2053
  events.value = data.sort((a, b) => {
1996
2054
  const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
1997
2055
  const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
1998
2056
  if (timeA !== timeB) return timeA - timeB;
1999
2057
  return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
2000
2058
  });
2059
+ console.log('[TIME-ANALYZE] Events set, length:', events.value.length);
2001
2060
  } catch (err) {
2061
+ console.error('[TIME-ANALYZE] Error loading events:', err);
2002
2062
  error.value = err.message;
2003
2063
  } finally {
2004
2064
  loading.value = false;
@@ -2210,7 +2270,10 @@
2210
2270
 
2211
2271
  <!-- ═══ Unified Timeline Tab ═══ -->
2212
2272
  <div v-if="activeTab === 'timeline'" class="section">
2213
- <div v-if="!unifiedTimelineItems.length" class="empty-state">
2273
+ <div v-if="error" class="empty-state" style="color: #f85149;">
2274
+ Error loading timeline: {{ error }}
2275
+ </div>
2276
+ <div v-else-if="!unifiedTimelineItems.length" class="empty-state">
2214
2277
  No timeline data found in this session.
2215
2278
  </div>
2216
2279
  <div v-else>
@@ -2629,6 +2692,15 @@
2629
2692
  `
2630
2693
  });
2631
2694
 
2695
+ // Global error handler
2696
+ app.config.errorHandler = (err, instance, info) => {
2697
+ console.error('[Vue Error]', err, info);
2698
+ const errorDiv = document.createElement('div');
2699
+ 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;';
2700
+ errorDiv.innerHTML = `<strong>Vue Error:</strong><br>${err.message}<br><br><small>${info}</small>`;
2701
+ document.body.appendChild(errorDiv);
2702
+ };
2703
+
2632
2704
  app.mount('#app');
2633
2705
  </script>
2634
2706
  </body>