@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.
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +435 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +1236 -0
- package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +1177 -0
- package/.nyc_output/coverage-e2e-merged.json +1 -0
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +435 -0
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +435 -0
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +1134 -0
- package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +435 -0
- 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
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +471 -0
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +471 -0
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +471 -0
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +1633 -0
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +471 -0
- package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +471 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +1255 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +1156 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +701 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +1182 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +1245 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +701 -0
- package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +1177 -0
- package/.nyc_output/coverage-unit.json +21 -0
- package/.nycrc +29 -0
- package/CHANGELOG.md +36 -0
- package/README.md +154 -15
- package/examples/parser-usage.js +114 -0
- package/lib/parsers/README.md +239 -0
- package/lib/parsers/base-parser.js +53 -0
- package/lib/parsers/claude-parser.js +181 -0
- package/lib/parsers/copilot-parser.js +143 -0
- package/lib/parsers/index.js +13 -0
- package/lib/parsers/parser-factory.js +77 -0
- package/lib/parsers/pi-mono-parser.js +119 -0
- package/package.json +12 -4
- package/server.js +17 -2
- package/src/app.js +45 -20
- package/src/controllers/insightController.js +44 -8
- package/src/controllers/sessionController.js +217 -3
- package/src/controllers/uploadController.js +447 -7
- package/src/middleware/rateLimiting.js +7 -1
- package/src/models/Session.js +26 -0
- package/src/schemas/event.schema.js +73 -0
- package/src/services/eventNormalizer.js +291 -0
- package/src/services/insightService.js +140 -48
- package/src/services/sessionRepository.js +584 -49
- package/src/services/sessionService.js +1594 -27
- package/src/utils/helpers.js +6 -1
- package/views/index.ejs +111 -4
- package/views/session-vue.ejs +425 -71
- package/views/time-analyze.ejs +140 -57
package/views/time-analyze.ejs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
1025
|
-
if (startIdx < 0) {
|
|
1026
|
-
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
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="
|
|
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>
|