@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.
- package/.nycrc +29 -0
- package/CHANGELOG.md +48 -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 +1588 -36
- package/src/utils/helpers.js +6 -1
- package/views/index.ejs +111 -4
- package/views/session-vue.ejs +272 -65
- package/views/time-analyze.ejs +127 -55
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(() => {
|
|
@@ -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
|
-
|
|
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
|
|
1031
|
-
if (startIdx < 0) {
|
|
1032
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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="
|
|
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>
|