@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/session-vue.ejs
CHANGED
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
<!-- Vue 3 -->
|
|
9
9
|
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.28/dist/vue.global.prod.js" integrity="sha384-EyKhbIJoP1t1fKIFRNEfYKy4uy8qxs7UNS4Cab53xyXqCTUB1PCoxeFsD0G/NX9W" crossorigin="anonymous"></script>
|
|
10
10
|
|
|
11
|
-
<!-- vue-virtual-scroller -->
|
|
12
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-virtual-scroller@
|
|
13
|
-
<script src="https://cdn.jsdelivr.net/npm/vue-virtual-scroller@
|
|
11
|
+
<!-- vue-virtual-scroller Vue 3 -->
|
|
12
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-virtual-scroller@next/dist/vue-virtual-scroller.css" crossorigin="anonymous">
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/vue-virtual-scroller@next/dist/vue-virtual-scroller.min.js" crossorigin="anonymous"></script>
|
|
14
14
|
|
|
15
15
|
<!-- Markdown rendering -->
|
|
16
16
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js" integrity="sha384-zbcZAIxlvJtNE3Dp5nxLXdXtXyxwOdnILY1TDPVmKFhl4r4nSUG1r8bcFXGVa4Te" crossorigin="anonymous"></script>
|
|
17
17
|
|
|
18
|
+
<!-- DOMPurify for XSS protection -->
|
|
19
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js" crossorigin="anonymous"></script>
|
|
20
|
+
|
|
18
21
|
<style>
|
|
19
22
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
20
23
|
body {
|
|
@@ -224,6 +227,21 @@
|
|
|
224
227
|
color: #58a6ff;
|
|
225
228
|
border: 1px solid rgba(31, 111, 235, 0.4);
|
|
226
229
|
}
|
|
230
|
+
.source-copilot {
|
|
231
|
+
background: rgba(88, 166, 255, 0.2);
|
|
232
|
+
color: #58a6ff;
|
|
233
|
+
border: 1px solid rgba(88, 166, 255, 0.4);
|
|
234
|
+
}
|
|
235
|
+
.source-claude {
|
|
236
|
+
background: rgba(210, 153, 34, 0.2);
|
|
237
|
+
color: #d29922;
|
|
238
|
+
border: 1px solid rgba(210, 153, 34, 0.4);
|
|
239
|
+
}
|
|
240
|
+
.source-pi-mono {
|
|
241
|
+
background: rgba(138, 102, 204, 0.2);
|
|
242
|
+
color: #a78bdb;
|
|
243
|
+
border: 1px solid rgba(138, 102, 204, 0.4);
|
|
244
|
+
}
|
|
227
245
|
|
|
228
246
|
/* Turn buttons */
|
|
229
247
|
.turn-buttons {
|
|
@@ -867,7 +885,7 @@
|
|
|
867
885
|
content: '';
|
|
868
886
|
flex: 1;
|
|
869
887
|
height: 1px;
|
|
870
|
-
background: #58a6ff;
|
|
888
|
+
background: var(--sa-color, #58a6ff);
|
|
871
889
|
}
|
|
872
890
|
.subagent-divider-text {
|
|
873
891
|
font-size: 12px;
|
|
@@ -886,6 +904,8 @@
|
|
|
886
904
|
.subagent-divider-line-right {
|
|
887
905
|
display: none;
|
|
888
906
|
}
|
|
907
|
+
.event.event-in-subagent { border-left-color: var(--subagent-border-color, #58a6ff); }
|
|
908
|
+
.subagent-owner-tag { font-size: 11px; padding: 1px 6px; border-radius: 8px; border: 1px solid; white-space: nowrap; opacity: 0.85; }
|
|
889
909
|
</style>
|
|
890
910
|
</head>
|
|
891
911
|
<body>
|
|
@@ -902,14 +922,30 @@
|
|
|
902
922
|
|
|
903
923
|
<!-- Vue App -->
|
|
904
924
|
<script>
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
925
|
+
// Immediate initialization (script is at bottom, DOM is ready)
|
|
926
|
+
(function() {
|
|
927
|
+
// Verify Vue is loaded
|
|
928
|
+
if (typeof Vue === 'undefined') {
|
|
929
|
+
console.error('Vue is not loaded');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Verify VueVirtualScroller is loaded
|
|
934
|
+
if (typeof window.VueVirtualScroller === 'undefined') {
|
|
935
|
+
console.error('VueVirtualScroller is not loaded');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.log('Initializing Vue app...');
|
|
940
|
+
|
|
941
|
+
const { createApp, ref, computed, onMounted, onBeforeUnmount, reactive, watch } = Vue;
|
|
942
|
+
const { DynamicScroller, DynamicScrollerItem } = window.VueVirtualScroller;
|
|
943
|
+
|
|
944
|
+
const app = createApp({
|
|
945
|
+
components: {
|
|
946
|
+
DynamicScroller,
|
|
947
|
+
DynamicScrollerItem
|
|
948
|
+
},
|
|
913
949
|
|
|
914
950
|
setup() {
|
|
915
951
|
const sessionId = ref(window.sessionData.sessionId);
|
|
@@ -928,6 +964,24 @@
|
|
|
928
964
|
|
|
929
965
|
const expandedTools = ref({});
|
|
930
966
|
const expandedContent = ref({});
|
|
967
|
+
const MAX_EXPANDED_ITEMS = 50; // Memory leak fix: Limit expanded items
|
|
968
|
+
|
|
969
|
+
// Clean up old expansion state to prevent memory leak
|
|
970
|
+
const cleanupExpansionState = () => {
|
|
971
|
+
const toolKeys = Object.keys(expandedTools.value);
|
|
972
|
+
if (toolKeys.length > MAX_EXPANDED_ITEMS) {
|
|
973
|
+
// Keep only recent 50 expanded items
|
|
974
|
+
const toRemove = toolKeys.slice(0, toolKeys.length - MAX_EXPANDED_ITEMS);
|
|
975
|
+
toRemove.forEach(key => delete expandedTools.value[key]);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const contentKeys = Object.keys(expandedContent.value);
|
|
979
|
+
if (contentKeys.length > MAX_EXPANDED_ITEMS) {
|
|
980
|
+
const toRemove = contentKeys.slice(0, contentKeys.length - MAX_EXPANDED_ITEMS);
|
|
981
|
+
toRemove.forEach(key => delete expandedContent.value[key]);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
931
985
|
const currentFilter = ref('all');
|
|
932
986
|
const searchText = ref('');
|
|
933
987
|
const debouncedSearchText = ref('');
|
|
@@ -944,19 +998,33 @@
|
|
|
944
998
|
}, 300);
|
|
945
999
|
});
|
|
946
1000
|
|
|
1001
|
+
// Memory leak fix: Clean up expansion state when filter/search changes
|
|
1002
|
+
watch(currentFilter, () => {
|
|
1003
|
+
cleanupExpansionState();
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
watch(debouncedSearchText, () => {
|
|
1007
|
+
cleanupExpansionState();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
947
1010
|
// Async loading state
|
|
948
1011
|
const loadedEvents = ref([]);
|
|
949
1012
|
const eventsLoading = ref(true);
|
|
950
1013
|
const eventsError = ref(null);
|
|
951
1014
|
|
|
952
|
-
// Flatten and sort events
|
|
1015
|
+
// Flatten and sort events (stable sort using _fileIndex tiebreaker)
|
|
953
1016
|
const flatEvents = computed(() => {
|
|
954
1017
|
const events = loadedEvents.value
|
|
955
|
-
.filter(e =>
|
|
956
|
-
e.type !== 'assistant.turn_end' &&
|
|
1018
|
+
.filter(e =>
|
|
1019
|
+
e.type !== 'assistant.turn_end' &&
|
|
957
1020
|
e.type !== 'assistant.turn_complete'
|
|
958
1021
|
)
|
|
959
|
-
.sort((a, b) =>
|
|
1022
|
+
.sort((a, b) => {
|
|
1023
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
1024
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
1025
|
+
if (timeA !== timeB) return timeA - timeB;
|
|
1026
|
+
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
1027
|
+
})
|
|
960
1028
|
.map((e, index) => ({
|
|
961
1029
|
...e,
|
|
962
1030
|
virtualIndex: index,
|
|
@@ -1004,12 +1072,12 @@
|
|
|
1004
1072
|
// Final filtered events (search + type filter)
|
|
1005
1073
|
const filteredEvents = computed(() => {
|
|
1006
1074
|
let events = searchFilteredEvents.value;
|
|
1007
|
-
|
|
1075
|
+
|
|
1008
1076
|
// Apply type filter
|
|
1009
1077
|
if (currentFilter.value !== 'all') {
|
|
1010
1078
|
events = events.filter(e => e.type === currentFilter.value);
|
|
1011
1079
|
}
|
|
1012
|
-
|
|
1080
|
+
|
|
1013
1081
|
// Divider types (no separator before these)
|
|
1014
1082
|
const dividerTypes = ['assistant.turn_start', 'subagent.started', 'subagent.completed', 'subagent.failed'];
|
|
1015
1083
|
|
|
@@ -1161,43 +1229,98 @@
|
|
|
1161
1229
|
};
|
|
1162
1230
|
|
|
1163
1231
|
// Tool call map
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1232
|
+
// Subagent ownership: attribute events to their owning subagent
|
|
1233
|
+
const subagentOwnership = computed(() => {
|
|
1234
|
+
const sorted = flatEvents.value;
|
|
1235
|
+
const ownerMap = new Map(); // stableId → toolCallId
|
|
1236
|
+
const subagentInfo = new Map(); // toolCallId → { name, colorIndex }
|
|
1237
|
+
|
|
1238
|
+
// 1. Collect all subagent.started toolCallIds + assign colorIndex
|
|
1239
|
+
let colorIdx = 0;
|
|
1240
|
+
for (const ev of sorted) {
|
|
1241
|
+
if (ev.type === 'subagent.started') {
|
|
1242
|
+
const tcid = ev.data?.toolCallId;
|
|
1243
|
+
if (tcid) {
|
|
1244
|
+
subagentInfo.set(tcid, {
|
|
1245
|
+
name: ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent',
|
|
1246
|
+
colorIndex: colorIdx++
|
|
1247
|
+
});
|
|
1175
1248
|
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (subagentInfo.size === 0) return { ownerMap, subagentInfo };
|
|
1253
|
+
|
|
1254
|
+
// 2. Build id → event lookup for parentId chain walking
|
|
1255
|
+
const idMap = new Map();
|
|
1256
|
+
for (const ev of sorted) {
|
|
1257
|
+
if (ev.id) idMap.set(ev.id, ev);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// 3. Attribute assistant.message events via data.parentToolCallId
|
|
1261
|
+
for (const ev of sorted) {
|
|
1262
|
+
if (ev.type === 'assistant.message') {
|
|
1263
|
+
const ptcid = ev.data?.parentToolCallId;
|
|
1264
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1265
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1180
1266
|
}
|
|
1181
1267
|
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// 4. Attribute reasoning events by walking parentId → assistant.message
|
|
1271
|
+
for (const ev of sorted) {
|
|
1272
|
+
if (ev.type !== 'reasoning') continue;
|
|
1273
|
+
let current = ev.parentId;
|
|
1274
|
+
let depth = 0;
|
|
1275
|
+
while (current && depth < 10) {
|
|
1276
|
+
const parent = idMap.get(current);
|
|
1277
|
+
if (!parent) break;
|
|
1278
|
+
if (parent.type === 'assistant.message') {
|
|
1279
|
+
const ptcid = parent.data?.parentToolCallId;
|
|
1280
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1281
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1190
1282
|
}
|
|
1191
|
-
|
|
1192
|
-
if (groups.length > 0) {
|
|
1193
|
-
map.set(event.id || event.virtualIndex, groups);
|
|
1283
|
+
break;
|
|
1194
1284
|
}
|
|
1285
|
+
current = parent.parentId;
|
|
1286
|
+
depth++;
|
|
1195
1287
|
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// 5. Attribute tool.execution_start/complete by walking parentId chain
|
|
1291
|
+
const startIdByToolCallId = new Map();
|
|
1292
|
+
for (const ev of sorted) {
|
|
1293
|
+
if (ev.type !== 'tool.execution_start') continue;
|
|
1294
|
+
let current = ev.parentId;
|
|
1295
|
+
let depth = 0;
|
|
1296
|
+
while (current && depth < 10) {
|
|
1297
|
+
const parent = idMap.get(current);
|
|
1298
|
+
if (!parent) break;
|
|
1299
|
+
if (parent.type === 'assistant.message') {
|
|
1300
|
+
const ptcid = parent.data?.parentToolCallId;
|
|
1301
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1302
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1303
|
+
const tcid = ev.data?.toolCallId;
|
|
1304
|
+
if (tcid) startIdByToolCallId.set(tcid, ptcid);
|
|
1305
|
+
}
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
current = parent.parentId;
|
|
1309
|
+
depth++;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
for (const ev of sorted) {
|
|
1314
|
+
if (ev.type !== 'tool.execution_complete') continue;
|
|
1315
|
+
const tcid = ev.data?.toolCallId;
|
|
1316
|
+
if (tcid && startIdByToolCallId.has(tcid)) {
|
|
1317
|
+
ownerMap.set(ev.stableId, startIdByToolCallId.get(tcid));
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
return { ownerMap, subagentInfo };
|
|
1199
1322
|
});
|
|
1200
|
-
|
|
1323
|
+
|
|
1201
1324
|
// Methods
|
|
1202
1325
|
const formatTime = (timestamp) => {
|
|
1203
1326
|
if (!timestamp) return '';
|
|
@@ -1208,8 +1331,18 @@
|
|
|
1208
1331
|
return `${hours}:${minutes}:${seconds}`;
|
|
1209
1332
|
};
|
|
1210
1333
|
|
|
1334
|
+
// Performance fix: Cache markdown rendering results
|
|
1335
|
+
const markdownCache = new Map();
|
|
1336
|
+
const MAX_CACHE_SIZE = 200;
|
|
1337
|
+
|
|
1211
1338
|
const renderMarkdown = (text) => {
|
|
1212
1339
|
if (!text) return '';
|
|
1340
|
+
|
|
1341
|
+
// Check cache first
|
|
1342
|
+
if (markdownCache.has(text)) {
|
|
1343
|
+
return markdownCache.get(text);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1213
1346
|
try {
|
|
1214
1347
|
// 处理转义序列:将 \r\n、\n、\t 等转换为实际字符
|
|
1215
1348
|
let processedText = text
|
|
@@ -1219,6 +1352,14 @@
|
|
|
1219
1352
|
.replace(/\\"/g, '"') // \" → 引号
|
|
1220
1353
|
.replace(/\\\\/g, '\\'); // \\ → 反斜杠
|
|
1221
1354
|
|
|
1355
|
+
// DOMPurify configuration for markdown content
|
|
1356
|
+
const purifyConfig = {
|
|
1357
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'del', 'span', 'div', 'mark'],
|
|
1358
|
+
ALLOWED_ATTR: ['href', 'style', 'class'],
|
|
1359
|
+
ALLOW_DATA_ATTR: false,
|
|
1360
|
+
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1222
1363
|
// Parse YAML frontmatter
|
|
1223
1364
|
const frontmatterMatch = processedText.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1224
1365
|
if (frontmatterMatch) {
|
|
@@ -1233,18 +1374,42 @@
|
|
|
1233
1374
|
return { key, value };
|
|
1234
1375
|
});
|
|
1235
1376
|
|
|
1236
|
-
// Render frontmatter as table
|
|
1377
|
+
// Render frontmatter as table (sanitize key/value)
|
|
1237
1378
|
let tableHTML = '<table style="margin-bottom: 16px; border-collapse: collapse;"><tbody>';
|
|
1238
1379
|
pairs.forEach(pair => {
|
|
1239
|
-
|
|
1380
|
+
const sanitizedKey = DOMPurify.sanitize(pair.key, { ALLOWED_TAGS: [] });
|
|
1381
|
+
const sanitizedValue = DOMPurify.sanitize(pair.value, { ALLOWED_TAGS: [] });
|
|
1382
|
+
tableHTML += `<tr><td style="padding: 4px 12px; border: 1px solid #30363d; font-weight: 600; color: #7d8590;">${sanitizedKey}</td><td style="padding: 4px 12px; border: 1px solid #30363d;">${sanitizedValue}</td></tr>`;
|
|
1240
1383
|
});
|
|
1241
1384
|
tableHTML += '</tbody></table>';
|
|
1242
1385
|
|
|
1243
|
-
// Render remaining content
|
|
1244
|
-
|
|
1386
|
+
// Render remaining content with sanitization
|
|
1387
|
+
const markdownHTML = marked.parse(content);
|
|
1388
|
+
const sanitizedMarkdown = DOMPurify.sanitize(markdownHTML, purifyConfig);
|
|
1389
|
+
const result = tableHTML + sanitizedMarkdown;
|
|
1390
|
+
|
|
1391
|
+
// Cache the result
|
|
1392
|
+
if (markdownCache.size >= MAX_CACHE_SIZE) {
|
|
1393
|
+
const firstKey = markdownCache.keys().next().value;
|
|
1394
|
+
markdownCache.delete(firstKey);
|
|
1395
|
+
}
|
|
1396
|
+
markdownCache.set(text, result);
|
|
1397
|
+
|
|
1398
|
+
return result;
|
|
1245
1399
|
}
|
|
1246
1400
|
|
|
1247
|
-
|
|
1401
|
+
// Regular markdown rendering with sanitization
|
|
1402
|
+
const markdownHTML = marked.parse(processedText);
|
|
1403
|
+
const result = DOMPurify.sanitize(markdownHTML, purifyConfig);
|
|
1404
|
+
|
|
1405
|
+
// Cache the result (with size limit to prevent memory leak)
|
|
1406
|
+
if (markdownCache.size >= MAX_CACHE_SIZE) {
|
|
1407
|
+
const firstKey = markdownCache.keys().next().value;
|
|
1408
|
+
markdownCache.delete(firstKey);
|
|
1409
|
+
}
|
|
1410
|
+
markdownCache.set(text, result);
|
|
1411
|
+
|
|
1412
|
+
return result;
|
|
1248
1413
|
} catch (e) {
|
|
1249
1414
|
return text;
|
|
1250
1415
|
}
|
|
@@ -1315,7 +1480,18 @@
|
|
|
1315
1480
|
return lines.slice(0, 20).join('\n') + '\n\n...';
|
|
1316
1481
|
};
|
|
1317
1482
|
|
|
1318
|
-
const getBadgeInfo = (type) => {
|
|
1483
|
+
const getBadgeInfo = (type, item) => {
|
|
1484
|
+
// Prefer backend-generated badge info (Violation #4 fix)
|
|
1485
|
+
if (item?.data?.badgeLabel && item?.data?.badgeClass) {
|
|
1486
|
+
return { label: item.data.badgeLabel, class: item.data.badgeClass };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Fallback: frontend logic for backward compatibility
|
|
1490
|
+
// Pi-Mono toolResult events: still use type='message' with role='toolResult'
|
|
1491
|
+
if (type === 'message' && item?.data?.role === 'toolResult') {
|
|
1492
|
+
return { label: 'TOOL RESULT', class: 'badge-tool' };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1319
1495
|
// Special case for specific event types
|
|
1320
1496
|
if (type === 'session.model_change') {
|
|
1321
1497
|
return { label: 'MODEL CHANGE', class: 'badge-session' };
|
|
@@ -1436,13 +1612,99 @@
|
|
|
1436
1612
|
};
|
|
1437
1613
|
|
|
1438
1614
|
const hasTools = (event) => {
|
|
1439
|
-
|
|
1615
|
+
// Unified format: check data.tools (works for both Copilot and Claude)
|
|
1616
|
+
return event.data?.tools && event.data.tools.length > 0;
|
|
1440
1617
|
};
|
|
1441
1618
|
|
|
1442
1619
|
const getToolGroups = (event) => {
|
|
1443
|
-
|
|
1620
|
+
// Unified format from server (both Copilot and Claude normalized to data.tools)
|
|
1621
|
+
if (event.data?.tools && Array.isArray(event.data.tools)) {
|
|
1622
|
+
return event.data.tools
|
|
1623
|
+
.filter(tool => tool && typeof tool === 'object' && tool.name) // Any tool object with a name
|
|
1624
|
+
.map(tool => {
|
|
1625
|
+
// Check if tool has result (works for all formats)
|
|
1626
|
+
const hasResult = tool.result !== undefined || tool.status === 'completed' || tool.status === 'error';
|
|
1627
|
+
return {
|
|
1628
|
+
tool: tool.name,
|
|
1629
|
+
start: {
|
|
1630
|
+
data: {
|
|
1631
|
+
toolName: tool.name,
|
|
1632
|
+
arguments: tool.input || tool.arguments || {}
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
complete: hasResult ? {
|
|
1636
|
+
data: {
|
|
1637
|
+
result: tool.result,
|
|
1638
|
+
error: tool.status === 'error' ? tool.error : null
|
|
1639
|
+
}
|
|
1640
|
+
} : null
|
|
1641
|
+
};
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return [];
|
|
1444
1646
|
};
|
|
1445
|
-
|
|
1647
|
+
|
|
1648
|
+
// Subagent color palette for parallel subagent distinction
|
|
1649
|
+
const SUBAGENT_COLORS = [
|
|
1650
|
+
'#58a6ff', // blue
|
|
1651
|
+
'#f0883e', // orange
|
|
1652
|
+
'#a371f7', // purple
|
|
1653
|
+
'#3fb950', // green
|
|
1654
|
+
'#f778ba', // pink
|
|
1655
|
+
'#79c0ff', // light blue
|
|
1656
|
+
'#d29922', // amber
|
|
1657
|
+
'#56d4dd' // teal
|
|
1658
|
+
];
|
|
1659
|
+
|
|
1660
|
+
// Hash function for generating consistent color indices
|
|
1661
|
+
const hashCode = (str) => {
|
|
1662
|
+
let hash = 0;
|
|
1663
|
+
for (let i = 0; i < str.length; i++) {
|
|
1664
|
+
const char = str.charCodeAt(i);
|
|
1665
|
+
hash = ((hash << 5) - hash) + char;
|
|
1666
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
1667
|
+
}
|
|
1668
|
+
return hash;
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
const getSubagentInfo = (event) => {
|
|
1672
|
+
const { ownerMap, subagentInfo } = subagentOwnership.value;
|
|
1673
|
+
// For subagent dividers, use their own toolCallId
|
|
1674
|
+
if (event.type === 'subagent.started' || event.type === 'subagent.completed' || event.type === 'subagent.failed') {
|
|
1675
|
+
const tcid = event.data?.toolCallId;
|
|
1676
|
+
if (tcid && subagentInfo.has(tcid)) {
|
|
1677
|
+
const info = subagentInfo.get(tcid);
|
|
1678
|
+
return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
// For regular events, first check _subagent metadata (Claude format)
|
|
1683
|
+
if (event._subagent) {
|
|
1684
|
+
const subagentId = event._subagent.id;
|
|
1685
|
+
const subagentName = event._subagent.name;
|
|
1686
|
+
// Use subagentId as toolCallId for consistency
|
|
1687
|
+
if (subagentInfo.has(subagentId)) {
|
|
1688
|
+
const info = subagentInfo.get(subagentId);
|
|
1689
|
+
return { name: info.name, toolCallId: subagentId, colorIndex: info.colorIndex };
|
|
1690
|
+
}
|
|
1691
|
+
// If not in subagentInfo, create a default entry
|
|
1692
|
+
return { name: subagentName, toolCallId: subagentId, colorIndex: Math.abs(hashCode(subagentId)) };
|
|
1693
|
+
}
|
|
1694
|
+
// For regular events, look up ownership (Copilot format)
|
|
1695
|
+
const tcid = ownerMap.get(event.stableId);
|
|
1696
|
+
if (!tcid) return null;
|
|
1697
|
+
const info = subagentInfo.get(tcid);
|
|
1698
|
+
if (!info) return null;
|
|
1699
|
+
return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
const getSubagentColor = (event) => {
|
|
1703
|
+
const info = getSubagentInfo(event);
|
|
1704
|
+
if (!info) return null;
|
|
1705
|
+
return SUBAGENT_COLORS[info.colorIndex % SUBAGENT_COLORS.length];
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1446
1708
|
const setFilter = (type) => {
|
|
1447
1709
|
currentFilter.value = type;
|
|
1448
1710
|
};
|
|
@@ -1513,28 +1775,53 @@
|
|
|
1513
1775
|
};
|
|
1514
1776
|
|
|
1515
1777
|
const exportSession = async () => {
|
|
1778
|
+
console.log('[Export] exportSession called');
|
|
1516
1779
|
exporting.value = true;
|
|
1517
1780
|
try {
|
|
1781
|
+
console.log('[Export] Fetching:', `/session/${sessionId.value}/export`);
|
|
1518
1782
|
const response = await fetch(`/session/${sessionId.value}/export`);
|
|
1783
|
+
console.log('[Export] Response received:', response.status, response.ok);
|
|
1784
|
+
console.log('[Export] Response received:', response.status, response.ok);
|
|
1519
1785
|
if (!response.ok) {
|
|
1520
1786
|
throw new Error('Share failed');
|
|
1521
1787
|
}
|
|
1522
1788
|
|
|
1523
1789
|
// Download the file
|
|
1790
|
+
console.log('[Export] Creating blob...');
|
|
1524
1791
|
const blob = await response.blob();
|
|
1792
|
+
console.log('[Export] Blob size:', blob.size, 'type:', blob.type);
|
|
1525
1793
|
const url = window.URL.createObjectURL(blob);
|
|
1794
|
+
console.log('[Export] Creating download link...');
|
|
1526
1795
|
const a = document.createElement('a');
|
|
1527
1796
|
a.href = url;
|
|
1528
1797
|
a.download = `session-${sessionId.value}.zip`;
|
|
1529
1798
|
document.body.appendChild(a);
|
|
1530
1799
|
a.click();
|
|
1800
|
+
console.log('[Export] Download triggered');
|
|
1531
1801
|
window.URL.revokeObjectURL(url);
|
|
1532
1802
|
document.body.removeChild(a);
|
|
1803
|
+
|
|
1804
|
+
// Show success feedback
|
|
1805
|
+
console.log('[Export] Showing success feedback...');
|
|
1806
|
+
const originalText = '📤 Share Session';
|
|
1807
|
+
const successText = '✓ Downloaded!';
|
|
1808
|
+
const btn = document.querySelector('.export-btn');
|
|
1809
|
+
if (btn) {
|
|
1810
|
+
btn.textContent = successText;
|
|
1811
|
+
btn.style.background = '#238636';
|
|
1812
|
+
console.log('[Export] Button text updated to:', btn.textContent);
|
|
1813
|
+
setTimeout(() => {
|
|
1814
|
+
btn.textContent = originalText;
|
|
1815
|
+
btn.style.background = '';
|
|
1816
|
+
console.log('[Export] Button text restored');
|
|
1817
|
+
}, 2000);
|
|
1818
|
+
}
|
|
1533
1819
|
} catch (err) {
|
|
1534
|
-
console.error('Share session error:', err);
|
|
1820
|
+
console.error('[Export] Share session error:', err);
|
|
1535
1821
|
alert('Failed to share session: ' + err.message);
|
|
1536
1822
|
} finally {
|
|
1537
1823
|
exporting.value = false;
|
|
1824
|
+
console.log('[Export] Export complete');
|
|
1538
1825
|
}
|
|
1539
1826
|
};
|
|
1540
1827
|
|
|
@@ -1548,7 +1835,20 @@
|
|
|
1548
1835
|
if (!response.ok) {
|
|
1549
1836
|
throw new Error(`Failed to load events: ${response.statusText}`);
|
|
1550
1837
|
}
|
|
1551
|
-
|
|
1838
|
+
const data = await response.json();
|
|
1839
|
+
|
|
1840
|
+
// Handle both old (array) and new (object with pagination) response formats
|
|
1841
|
+
if (Array.isArray(data)) {
|
|
1842
|
+
// Old format: direct array
|
|
1843
|
+
loadedEvents.value = data;
|
|
1844
|
+
} else if (data.events && Array.isArray(data.events)) {
|
|
1845
|
+
// New format: { events, pagination }
|
|
1846
|
+
loadedEvents.value = data.events;
|
|
1847
|
+
console.log('[Navigation] Pagination:', data.pagination);
|
|
1848
|
+
} else {
|
|
1849
|
+
throw new Error('Invalid response format');
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1552
1852
|
console.log('[Navigation] Events loaded:', loadedEvents.value.length);
|
|
1553
1853
|
|
|
1554
1854
|
// Check for URL query parameters and jump to event AFTER events are loaded
|
|
@@ -1699,9 +1999,23 @@
|
|
|
1699
1999
|
|
|
1700
2000
|
// Cleanup on unmount
|
|
1701
2001
|
onBeforeUnmount(() => {
|
|
2002
|
+
// Clear search timeout (memory leak fix)
|
|
2003
|
+
if (searchTimeout) {
|
|
2004
|
+
clearTimeout(searchTimeout);
|
|
2005
|
+
searchTimeout = null;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Clean scroll listeners
|
|
1702
2009
|
if (scrollCleanup) {
|
|
1703
2010
|
scrollCleanup();
|
|
1704
2011
|
}
|
|
2012
|
+
|
|
2013
|
+
// Clear expansion state (memory leak fix)
|
|
2014
|
+
expandedTools.value = {};
|
|
2015
|
+
expandedContent.value = {};
|
|
2016
|
+
|
|
2017
|
+
// Clear markdown cache (memory leak fix)
|
|
2018
|
+
markdownCache.clear();
|
|
1705
2019
|
});
|
|
1706
2020
|
});
|
|
1707
2021
|
|
|
@@ -1728,7 +2042,6 @@
|
|
|
1728
2042
|
turns,
|
|
1729
2043
|
userReqs,
|
|
1730
2044
|
truncateText,
|
|
1731
|
-
toolCallMap,
|
|
1732
2045
|
formatTime,
|
|
1733
2046
|
formatDateTime,
|
|
1734
2047
|
renderMarkdown,
|
|
@@ -1744,6 +2057,8 @@
|
|
|
1744
2057
|
getToolCommand,
|
|
1745
2058
|
hasTools,
|
|
1746
2059
|
getToolGroups,
|
|
2060
|
+
getSubagentInfo,
|
|
2061
|
+
getSubagentColor,
|
|
1747
2062
|
setFilter,
|
|
1748
2063
|
scrollToTurn,
|
|
1749
2064
|
jumpToTurn,
|
|
@@ -1776,6 +2091,15 @@
|
|
|
1776
2091
|
<div v-if="metadata.summary" class="session-summary-block">{{ metadata.summary }}</div>
|
|
1777
2092
|
<table class="session-info-table">
|
|
1778
2093
|
<tbody>
|
|
2094
|
+
<tr v-if="metadata.source">
|
|
2095
|
+
<td>Source</td>
|
|
2096
|
+
<td>
|
|
2097
|
+
<!-- Use backend-provided source metadata (Violation #3 fix) -->
|
|
2098
|
+
<span :class="['source-badge', metadata.sourceBadgeClass || 'source-copilot']">
|
|
2099
|
+
{{ metadata.sourceName || 'GitHub Copilot' }}
|
|
2100
|
+
</span>
|
|
2101
|
+
</td>
|
|
2102
|
+
</tr>
|
|
1779
2103
|
<tr v-if="metadata.copilotVersion">
|
|
1780
2104
|
<td>CLI Version</td>
|
|
1781
2105
|
<td>{{ metadata.copilotVersion }}</td>
|
|
@@ -1913,32 +2237,41 @@
|
|
|
1913
2237
|
</div>
|
|
1914
2238
|
|
|
1915
2239
|
<!-- Subagent Divider -->
|
|
1916
|
-
<div
|
|
2240
|
+
<div
|
|
1917
2241
|
v-else-if="item.type === 'subagent.started' || item.type === 'subagent.completed' || item.type === 'subagent.failed'"
|
|
1918
2242
|
:data-type="item.type"
|
|
1919
2243
|
:data-index="item.virtualIndex"
|
|
1920
2244
|
:class="['subagent-divider', item.type.split('.')[1]]"
|
|
2245
|
+
:style="{
|
|
2246
|
+
'--sa-color': getSubagentColor(item) || '#58a6ff'
|
|
2247
|
+
}"
|
|
1921
2248
|
>
|
|
1922
|
-
<div class="subagent-divider-line-left"></div>
|
|
1923
|
-
<span class="subagent-divider-text">
|
|
2249
|
+
<div class="subagent-divider-line-left" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
|
|
2250
|
+
<span class="subagent-divider-text" :style="{ color: getSubagentColor(item) || '#58a6ff', borderColor: getSubagentColor(item) || '#58a6ff', background: (getSubagentColor(item) || '#58a6ff') + '1a' }">
|
|
1924
2251
|
🤖 {{ item.data?.agentDisplayName || item.data?.agentName || 'SubAgent' }}
|
|
1925
2252
|
{{ item.type === 'subagent.started' ? 'Start ▶' : item.type === 'subagent.completed' ? 'Complete ✓' : 'Failed ✗' }}
|
|
1926
2253
|
</span>
|
|
1927
|
-
<div class="subagent-divider-line-right"></div>
|
|
2254
|
+
<div class="subagent-divider-line-right" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
|
|
1928
2255
|
<div class="divider-separator"></div>
|
|
1929
2256
|
</div>
|
|
1930
2257
|
|
|
1931
2258
|
<!-- Regular Event -->
|
|
1932
|
-
<div
|
|
2259
|
+
<div
|
|
1933
2260
|
v-else
|
|
1934
|
-
:class="['event']"
|
|
2261
|
+
:class="['event', getSubagentInfo(item) ? 'event-in-subagent' : '']"
|
|
1935
2262
|
:data-type="item.type"
|
|
1936
2263
|
:data-index="item.virtualIndex"
|
|
2264
|
+
:style="getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}"
|
|
1937
2265
|
>
|
|
1938
2266
|
<div class="event-header">
|
|
1939
|
-
<span :class="['event-badge', getBadgeInfo(item.type).class]">
|
|
1940
|
-
{{ getBadgeInfo(item.type).label }}
|
|
2267
|
+
<span :class="['event-badge', getBadgeInfo(item.type, item).class]">
|
|
2268
|
+
{{ getBadgeInfo(item.type, item).label }}
|
|
1941
2269
|
</span>
|
|
2270
|
+
<span
|
|
2271
|
+
v-if="getSubagentInfo(item)"
|
|
2272
|
+
class="subagent-owner-tag"
|
|
2273
|
+
:style="{ color: getSubagentColor(item), borderColor: getSubagentColor(item) }"
|
|
2274
|
+
>🤖 {{ getSubagentInfo(item).name }}</span>
|
|
1942
2275
|
<span class="event-timestamp">{{ formatTime(item.timestamp) }}</span>
|
|
1943
2276
|
</div>
|
|
1944
2277
|
|
|
@@ -2023,7 +2356,7 @@
|
|
|
2023
2356
|
</div>
|
|
2024
2357
|
</div>
|
|
2025
2358
|
|
|
2026
|
-
<!-- Regular content -->
|
|
2359
|
+
<!-- Regular content (unified format from server) -->
|
|
2027
2360
|
<div v-else-if="item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent">
|
|
2028
2361
|
<div
|
|
2029
2362
|
class="event-content"
|
|
@@ -2050,6 +2383,12 @@
|
|
|
2050
2383
|
</div>
|
|
2051
2384
|
</div>
|
|
2052
2385
|
|
|
2386
|
+
<!-- No content at all (no message and no tools) -->
|
|
2387
|
+
<div v-else-if="!hasTools(item)" class="event-content" style="color: #7d8590; font-style: italic;">
|
|
2388
|
+
No available message
|
|
2389
|
+
</div>
|
|
2390
|
+
|
|
2391
|
+
<!-- Tool calls section (independent of message content, but don't need "No available message" if tools exist) -->
|
|
2053
2392
|
<div v-if="hasTools(item)" class="tool-list">
|
|
2054
2393
|
<div
|
|
2055
2394
|
v-for="(group, idx) in getToolGroups(item)"
|
|
@@ -2103,8 +2442,23 @@
|
|
|
2103
2442
|
</div>
|
|
2104
2443
|
`
|
|
2105
2444
|
});
|
|
2106
|
-
|
|
2107
|
-
app
|
|
2445
|
+
|
|
2446
|
+
// Mount the app
|
|
2447
|
+
console.log('Mounting Vue app to #app...');
|
|
2448
|
+
console.log('App config:', app.config);
|
|
2449
|
+
console.log('Target element:', document.getElementById('app'));
|
|
2450
|
+
try {
|
|
2451
|
+
const vm = app.mount('#app');
|
|
2452
|
+
console.log('Vue app mounted successfully!', vm ? 'Instance created' : 'No instance');
|
|
2453
|
+
console.log('VM type:', typeof vm, 'Has exportSession:', typeof vm?.exportSession);
|
|
2454
|
+
console.log('VM keys:', vm ? Object.keys(vm).slice(0, 10) : 'NO_VM');
|
|
2455
|
+
console.log('#app innerHTML length:', document.getElementById('app').innerHTML.length);
|
|
2456
|
+
console.log('#app first 100 chars:', document.getElementById('app').innerHTML.substring(0, 100));
|
|
2457
|
+
} catch (error) {
|
|
2458
|
+
console.error('Mount failed:', error);
|
|
2459
|
+
console.error('Error stack:', error.stack);
|
|
2460
|
+
}
|
|
2461
|
+
})(); // End IIFE
|
|
2108
2462
|
</script>
|
|
2109
2463
|
</body>
|
|
2110
2464
|
</html>
|