@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/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 {
|
|
@@ -904,14 +922,30 @@
|
|
|
904
922
|
|
|
905
923
|
<!-- Vue App -->
|
|
906
924
|
<script>
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
+
},
|
|
915
949
|
|
|
916
950
|
setup() {
|
|
917
951
|
const sessionId = ref(window.sessionData.sessionId);
|
|
@@ -930,6 +964,24 @@
|
|
|
930
964
|
|
|
931
965
|
const expandedTools = ref({});
|
|
932
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
|
+
|
|
933
985
|
const currentFilter = ref('all');
|
|
934
986
|
const searchText = ref('');
|
|
935
987
|
const debouncedSearchText = ref('');
|
|
@@ -946,6 +998,15 @@
|
|
|
946
998
|
}, 300);
|
|
947
999
|
});
|
|
948
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
|
+
|
|
949
1010
|
// Async loading state
|
|
950
1011
|
const loadedEvents = ref([]);
|
|
951
1012
|
const eventsLoading = ref(true);
|
|
@@ -1168,43 +1229,6 @@
|
|
|
1168
1229
|
};
|
|
1169
1230
|
|
|
1170
1231
|
// Tool call map
|
|
1171
|
-
const toolCallMap = computed(() => {
|
|
1172
|
-
const map = new Map();
|
|
1173
|
-
const toolGroups = new Map();
|
|
1174
|
-
|
|
1175
|
-
flatEvents.value.forEach(event => {
|
|
1176
|
-
if (event.type === 'tool.execution_start') {
|
|
1177
|
-
const toolId = event.data?.toolCallId;
|
|
1178
|
-
if (toolId) {
|
|
1179
|
-
if (!toolGroups.has(toolId)) {
|
|
1180
|
-
toolGroups.set(toolId, { tool: event.data.tool, start: event });
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
} else if (event.type === 'tool.execution_complete') {
|
|
1184
|
-
const toolId = event.data?.toolCallId;
|
|
1185
|
-
if (toolId && toolGroups.has(toolId)) {
|
|
1186
|
-
toolGroups.get(toolId).complete = event;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
flatEvents.value.forEach(event => {
|
|
1192
|
-
if (event.type === 'assistant.message') {
|
|
1193
|
-
const groups = [];
|
|
1194
|
-
toolGroups.forEach((group, toolId) => {
|
|
1195
|
-
if (group.start?.parentId === event.id) {
|
|
1196
|
-
groups.push(group);
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
if (groups.length > 0) {
|
|
1200
|
-
map.set(event.id || event.virtualIndex, groups);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
return map;
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
1232
|
// Subagent ownership: attribute events to their owning subagent
|
|
1209
1233
|
const subagentOwnership = computed(() => {
|
|
1210
1234
|
const sorted = flatEvents.value;
|
|
@@ -1307,8 +1331,18 @@
|
|
|
1307
1331
|
return `${hours}:${minutes}:${seconds}`;
|
|
1308
1332
|
};
|
|
1309
1333
|
|
|
1334
|
+
// Performance fix: Cache markdown rendering results
|
|
1335
|
+
const markdownCache = new Map();
|
|
1336
|
+
const MAX_CACHE_SIZE = 200;
|
|
1337
|
+
|
|
1310
1338
|
const renderMarkdown = (text) => {
|
|
1311
1339
|
if (!text) return '';
|
|
1340
|
+
|
|
1341
|
+
// Check cache first
|
|
1342
|
+
if (markdownCache.has(text)) {
|
|
1343
|
+
return markdownCache.get(text);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1312
1346
|
try {
|
|
1313
1347
|
// 处理转义序列:将 \r\n、\n、\t 等转换为实际字符
|
|
1314
1348
|
let processedText = text
|
|
@@ -1318,6 +1352,14 @@
|
|
|
1318
1352
|
.replace(/\\"/g, '"') // \" → 引号
|
|
1319
1353
|
.replace(/\\\\/g, '\\'); // \\ → 反斜杠
|
|
1320
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
|
+
|
|
1321
1363
|
// Parse YAML frontmatter
|
|
1322
1364
|
const frontmatterMatch = processedText.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1323
1365
|
if (frontmatterMatch) {
|
|
@@ -1332,18 +1374,42 @@
|
|
|
1332
1374
|
return { key, value };
|
|
1333
1375
|
});
|
|
1334
1376
|
|
|
1335
|
-
// Render frontmatter as table
|
|
1377
|
+
// Render frontmatter as table (sanitize key/value)
|
|
1336
1378
|
let tableHTML = '<table style="margin-bottom: 16px; border-collapse: collapse;"><tbody>';
|
|
1337
1379
|
pairs.forEach(pair => {
|
|
1338
|
-
|
|
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>`;
|
|
1339
1383
|
});
|
|
1340
1384
|
tableHTML += '</tbody></table>';
|
|
1341
1385
|
|
|
1342
|
-
// Render remaining content
|
|
1343
|
-
|
|
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;
|
|
1344
1399
|
}
|
|
1345
1400
|
|
|
1346
|
-
|
|
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;
|
|
1347
1413
|
} catch (e) {
|
|
1348
1414
|
return text;
|
|
1349
1415
|
}
|
|
@@ -1414,7 +1480,18 @@
|
|
|
1414
1480
|
return lines.slice(0, 20).join('\n') + '\n\n...';
|
|
1415
1481
|
};
|
|
1416
1482
|
|
|
1417
|
-
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
|
+
|
|
1418
1495
|
// Special case for specific event types
|
|
1419
1496
|
if (type === 'session.model_change') {
|
|
1420
1497
|
return { label: 'MODEL CHANGE', class: 'badge-session' };
|
|
@@ -1535,11 +1612,37 @@
|
|
|
1535
1612
|
};
|
|
1536
1613
|
|
|
1537
1614
|
const hasTools = (event) => {
|
|
1538
|
-
|
|
1615
|
+
// Unified format: check data.tools (works for both Copilot and Claude)
|
|
1616
|
+
return event.data?.tools && event.data.tools.length > 0;
|
|
1539
1617
|
};
|
|
1540
1618
|
|
|
1541
1619
|
const getToolGroups = (event) => {
|
|
1542
|
-
|
|
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 [];
|
|
1543
1646
|
};
|
|
1544
1647
|
|
|
1545
1648
|
// Subagent color palette for parallel subagent distinction
|
|
@@ -1554,6 +1657,17 @@
|
|
|
1554
1657
|
'#56d4dd' // teal
|
|
1555
1658
|
];
|
|
1556
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
|
+
|
|
1557
1671
|
const getSubagentInfo = (event) => {
|
|
1558
1672
|
const { ownerMap, subagentInfo } = subagentOwnership.value;
|
|
1559
1673
|
// For subagent dividers, use their own toolCallId
|
|
@@ -1565,7 +1679,19 @@
|
|
|
1565
1679
|
}
|
|
1566
1680
|
return null;
|
|
1567
1681
|
}
|
|
1568
|
-
// For regular events,
|
|
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)
|
|
1569
1695
|
const tcid = ownerMap.get(event.stableId);
|
|
1570
1696
|
if (!tcid) return null;
|
|
1571
1697
|
const info = subagentInfo.get(tcid);
|
|
@@ -1649,28 +1775,53 @@
|
|
|
1649
1775
|
};
|
|
1650
1776
|
|
|
1651
1777
|
const exportSession = async () => {
|
|
1778
|
+
console.log('[Export] exportSession called');
|
|
1652
1779
|
exporting.value = true;
|
|
1653
1780
|
try {
|
|
1781
|
+
console.log('[Export] Fetching:', `/session/${sessionId.value}/export`);
|
|
1654
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);
|
|
1655
1785
|
if (!response.ok) {
|
|
1656
1786
|
throw new Error('Share failed');
|
|
1657
1787
|
}
|
|
1658
1788
|
|
|
1659
1789
|
// Download the file
|
|
1790
|
+
console.log('[Export] Creating blob...');
|
|
1660
1791
|
const blob = await response.blob();
|
|
1792
|
+
console.log('[Export] Blob size:', blob.size, 'type:', blob.type);
|
|
1661
1793
|
const url = window.URL.createObjectURL(blob);
|
|
1794
|
+
console.log('[Export] Creating download link...');
|
|
1662
1795
|
const a = document.createElement('a');
|
|
1663
1796
|
a.href = url;
|
|
1664
1797
|
a.download = `session-${sessionId.value}.zip`;
|
|
1665
1798
|
document.body.appendChild(a);
|
|
1666
1799
|
a.click();
|
|
1800
|
+
console.log('[Export] Download triggered');
|
|
1667
1801
|
window.URL.revokeObjectURL(url);
|
|
1668
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
|
+
}
|
|
1669
1819
|
} catch (err) {
|
|
1670
|
-
console.error('Share session error:', err);
|
|
1820
|
+
console.error('[Export] Share session error:', err);
|
|
1671
1821
|
alert('Failed to share session: ' + err.message);
|
|
1672
1822
|
} finally {
|
|
1673
1823
|
exporting.value = false;
|
|
1824
|
+
console.log('[Export] Export complete');
|
|
1674
1825
|
}
|
|
1675
1826
|
};
|
|
1676
1827
|
|
|
@@ -1684,7 +1835,20 @@
|
|
|
1684
1835
|
if (!response.ok) {
|
|
1685
1836
|
throw new Error(`Failed to load events: ${response.statusText}`);
|
|
1686
1837
|
}
|
|
1687
|
-
|
|
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
|
+
|
|
1688
1852
|
console.log('[Navigation] Events loaded:', loadedEvents.value.length);
|
|
1689
1853
|
|
|
1690
1854
|
// Check for URL query parameters and jump to event AFTER events are loaded
|
|
@@ -1835,9 +1999,23 @@
|
|
|
1835
1999
|
|
|
1836
2000
|
// Cleanup on unmount
|
|
1837
2001
|
onBeforeUnmount(() => {
|
|
2002
|
+
// Clear search timeout (memory leak fix)
|
|
2003
|
+
if (searchTimeout) {
|
|
2004
|
+
clearTimeout(searchTimeout);
|
|
2005
|
+
searchTimeout = null;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Clean scroll listeners
|
|
1838
2009
|
if (scrollCleanup) {
|
|
1839
2010
|
scrollCleanup();
|
|
1840
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();
|
|
1841
2019
|
});
|
|
1842
2020
|
});
|
|
1843
2021
|
|
|
@@ -1864,7 +2042,6 @@
|
|
|
1864
2042
|
turns,
|
|
1865
2043
|
userReqs,
|
|
1866
2044
|
truncateText,
|
|
1867
|
-
toolCallMap,
|
|
1868
2045
|
formatTime,
|
|
1869
2046
|
formatDateTime,
|
|
1870
2047
|
renderMarkdown,
|
|
@@ -1914,6 +2091,15 @@
|
|
|
1914
2091
|
<div v-if="metadata.summary" class="session-summary-block">{{ metadata.summary }}</div>
|
|
1915
2092
|
<table class="session-info-table">
|
|
1916
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>
|
|
1917
2103
|
<tr v-if="metadata.copilotVersion">
|
|
1918
2104
|
<td>CLI Version</td>
|
|
1919
2105
|
<td>{{ metadata.copilotVersion }}</td>
|
|
@@ -2078,8 +2264,8 @@
|
|
|
2078
2264
|
:style="getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}"
|
|
2079
2265
|
>
|
|
2080
2266
|
<div class="event-header">
|
|
2081
|
-
<span :class="['event-badge', getBadgeInfo(item.type).class]">
|
|
2082
|
-
{{ getBadgeInfo(item.type).label }}
|
|
2267
|
+
<span :class="['event-badge', getBadgeInfo(item.type, item).class]">
|
|
2268
|
+
{{ getBadgeInfo(item.type, item).label }}
|
|
2083
2269
|
</span>
|
|
2084
2270
|
<span
|
|
2085
2271
|
v-if="getSubagentInfo(item)"
|
|
@@ -2170,7 +2356,7 @@
|
|
|
2170
2356
|
</div>
|
|
2171
2357
|
</div>
|
|
2172
2358
|
|
|
2173
|
-
<!-- Regular content -->
|
|
2359
|
+
<!-- Regular content (unified format from server) -->
|
|
2174
2360
|
<div v-else-if="item.data?.message || item.data?.text || item.data?.content || item.data?.transformedContent">
|
|
2175
2361
|
<div
|
|
2176
2362
|
class="event-content"
|
|
@@ -2197,6 +2383,12 @@
|
|
|
2197
2383
|
</div>
|
|
2198
2384
|
</div>
|
|
2199
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) -->
|
|
2200
2392
|
<div v-if="hasTools(item)" class="tool-list">
|
|
2201
2393
|
<div
|
|
2202
2394
|
v-for="(group, idx) in getToolGroups(item)"
|
|
@@ -2250,8 +2442,23 @@
|
|
|
2250
2442
|
</div>
|
|
2251
2443
|
`
|
|
2252
2444
|
});
|
|
2253
|
-
|
|
2254
|
-
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
|
|
2255
2462
|
</script>
|
|
2256
2463
|
</body>
|
|
2257
2464
|
</html>
|