@qiaolei81/copilot-session-viewer 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +435 -0
  2. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +435 -0
  3. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +435 -0
  4. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +435 -0
  5. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +435 -0
  6. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +435 -0
  7. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +1236 -0
  8. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +1177 -0
  9. package/.nyc_output/coverage-e2e-merged.json +1 -0
  10. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +435 -0
  11. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +435 -0
  12. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +1134 -0
  13. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +435 -0
  14. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-display-Load-More-Sessions-button-when-there-are-more-sessions-1771605468486.json +435 -0
  15. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +471 -0
  16. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +471 -0
  17. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +471 -0
  18. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +1633 -0
  19. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +471 -0
  20. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +471 -0
  21. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +1255 -0
  22. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +1156 -0
  23. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +701 -0
  24. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +1182 -0
  25. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +1245 -0
  26. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +701 -0
  27. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +1177 -0
  28. package/.nyc_output/coverage-unit.json +21 -0
  29. package/.nycrc +29 -0
  30. package/CHANGELOG.md +36 -0
  31. package/README.md +154 -15
  32. package/examples/parser-usage.js +114 -0
  33. package/lib/parsers/README.md +239 -0
  34. package/lib/parsers/base-parser.js +53 -0
  35. package/lib/parsers/claude-parser.js +181 -0
  36. package/lib/parsers/copilot-parser.js +143 -0
  37. package/lib/parsers/index.js +13 -0
  38. package/lib/parsers/parser-factory.js +77 -0
  39. package/lib/parsers/pi-mono-parser.js +119 -0
  40. package/package.json +12 -4
  41. package/server.js +17 -2
  42. package/src/app.js +45 -20
  43. package/src/controllers/insightController.js +44 -8
  44. package/src/controllers/sessionController.js +217 -3
  45. package/src/controllers/uploadController.js +447 -7
  46. package/src/middleware/rateLimiting.js +7 -1
  47. package/src/models/Session.js +26 -0
  48. package/src/schemas/event.schema.js +73 -0
  49. package/src/services/eventNormalizer.js +291 -0
  50. package/src/services/insightService.js +140 -48
  51. package/src/services/sessionRepository.js +584 -49
  52. package/src/services/sessionService.js +1594 -27
  53. package/src/utils/helpers.js +6 -1
  54. package/views/index.ejs +111 -4
  55. package/views/session-vue.ejs +425 -71
  56. package/views/time-analyze.ejs +140 -57
@@ -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(() => {
@@ -939,8 +972,14 @@
939
972
  });
940
973
 
941
974
  // ── Shared sorted events (computed once, reused everywhere) ──
975
+ // Stable sort: use _fileIndex (set by backend) as tiebreaker for identical timestamps
942
976
  const sortedEvents = computed(() => {
943
- return [...events.value].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
977
+ return [...events.value].sort((a, b) => {
978
+ const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
979
+ const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
980
+ if (timeA !== timeB) return timeA - timeB;
981
+ return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
982
+ });
944
983
  });
945
984
 
946
985
  // ── Map tool.execution_start events to owning subagent via parentToolCallId ──
@@ -1009,8 +1048,7 @@
1009
1048
  if (ev.type === 'subagent.started') {
1010
1049
  startStack.push(ev);
1011
1050
  } else if (ev.type === 'subagent.completed' || ev.type === 'subagent.failed') {
1012
- const name = ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent';
1013
- // Find matching start by toolCallId, with name-based LIFO fallback
1051
+ // Find matching start by toolCallId
1014
1052
  const tcid = ev.data?.toolCallId;
1015
1053
  let startIdx = -1;
1016
1054
  if (tcid) {
@@ -1021,17 +1059,13 @@
1021
1059
  }
1022
1060
  }
1023
1061
  }
1024
- // Fallback to name-based LIFO if no toolCallId match
1025
- if (startIdx < 0) {
1026
- for (let i = startStack.length - 1; i >= 0; i--) {
1027
- const sName = startStack[i].data?.agentDisplayName || startStack[i].data?.agentName || 'SubAgent';
1028
- if (sName === name) {
1029
- startIdx = i;
1030
- break;
1031
- }
1032
- }
1062
+ // Fallback to LIFO if no toolCallId match (pop last started)
1063
+ if (startIdx < 0 && startStack.length > 0) {
1064
+ startIdx = startStack.length - 1;
1033
1065
  }
1034
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';
1035
1069
  const startTime = startEv ? new Date(startEv.timestamp).getTime() : null;
1036
1070
  const endTime = new Date(ev.timestamp).getTime();
1037
1071
  const duration = startTime ? endTime - startTime : null;
@@ -1377,48 +1411,74 @@
1377
1411
 
1378
1412
  // ── Turn analysis ──
1379
1413
  const turnAnalysis = computed(() => {
1380
- const sorted = sortedEvents.value;
1381
- const turnStarts = sorted.filter(e => e.type === 'assistant.turn_start');
1382
- const allUserMessages = sorted.filter(e => e.type === 'user.message');
1383
-
1384
- return turnStarts.map((turn, idx) => {
1385
- const startTime = new Date(turn.timestamp).getTime();
1386
- const nextTurn = turnStarts[idx + 1];
1387
- const endTime = nextTurn
1388
- ? new Date(nextTurn.timestamp).getTime()
1389
- : sessionEnd.value || startTime;
1390
- const duration = endTime - startTime;
1391
-
1392
- // Find user message before this turn
1393
- const turnIndex = sorted.indexOf(turn);
1394
- const userMessage = sorted
1395
- .slice(0, turnIndex)
1396
- .reverse()
1397
- .find(e => e.type === 'user.message');
1398
-
1399
- const userReqNumber = userMessage
1400
- ? allUserMessages.indexOf(userMessage) + 1
1401
- : 0;
1402
-
1403
- // Count tool calls in this turn
1404
- let toolCalls = 0;
1405
- for (const e of sorted) {
1406
- const t = new Date(e.timestamp).getTime();
1407
- if (t >= startTime && t <= endTime && e.type === 'tool.execution_start') {
1408
- 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)';
1409
1460
  }
1410
- }
1411
1461
 
1412
- return {
1413
- turnId: turn.data?.turnId ?? idx,
1414
- userReqNumber,
1415
- message: userMessage?.data?.content || userMessage?.data?.transformedContent || '',
1416
- startTime: turn.timestamp,
1417
- endTime: nextTurn?.timestamp || events.value[events.value.length - 1]?.timestamp,
1418
- duration,
1419
- toolCalls
1420
- };
1421
- });
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
+ }
1422
1482
  });
1423
1483
 
1424
1484
  const maxTurnDuration = computed(() => {
@@ -1986,8 +2046,19 @@
1986
2046
  const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
1987
2047
  if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
1988
2048
  const data = await resp.json();
1989
- events.value = data.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
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);
2053
+ events.value = data.sort((a, b) => {
2054
+ const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
2055
+ const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
2056
+ if (timeA !== timeB) return timeA - timeB;
2057
+ return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
2058
+ });
2059
+ console.log('[TIME-ANALYZE] Events set, length:', events.value.length);
1990
2060
  } catch (err) {
2061
+ console.error('[TIME-ANALYZE] Error loading events:', err);
1991
2062
  error.value = err.message;
1992
2063
  } finally {
1993
2064
  loading.value = false;
@@ -2199,7 +2270,10 @@
2199
2270
 
2200
2271
  <!-- ═══ Unified Timeline Tab ═══ -->
2201
2272
  <div v-if="activeTab === 'timeline'" class="section">
2202
- <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">
2203
2277
  No timeline data found in this session.
2204
2278
  </div>
2205
2279
  <div v-else>
@@ -2618,6 +2692,15 @@
2618
2692
  `
2619
2693
  });
2620
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
+
2621
2704
  app.mount('#app');
2622
2705
  </script>
2623
2706
  </body>