@qiaolei81/copilot-session-viewer 0.1.8 → 0.1.9
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/package.json +1 -1
- package/src/services/sessionService.js +17 -2
- package/views/session-vue.ejs +162 -15
- package/views/time-analyze.ejs +13 -2
package/package.json
CHANGED
|
@@ -70,14 +70,29 @@ class SessionService {
|
|
|
70
70
|
try {
|
|
71
71
|
const content = await fs.promises.readFile(eventsFile, 'utf-8');
|
|
72
72
|
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
73
|
-
|
|
73
|
+
const events = lines.map((line, index) => {
|
|
74
74
|
try {
|
|
75
|
-
|
|
75
|
+
const event = JSON.parse(line);
|
|
76
|
+
// Preserve original file order as _fileIndex for stable sorting
|
|
77
|
+
event._fileIndex = index;
|
|
78
|
+
return event;
|
|
76
79
|
} catch (err) {
|
|
77
80
|
console.error(`Error parsing line ${index + 1}:`, err.message);
|
|
78
81
|
return null;
|
|
79
82
|
}
|
|
80
83
|
}).filter(event => event !== null);
|
|
84
|
+
|
|
85
|
+
// Sort by timestamp with stable tiebreaker on original file order.
|
|
86
|
+
// This ensures events with identical timestamps (e.g. an assistant.message
|
|
87
|
+
// followed by its tool.execution_start events) keep their logical order.
|
|
88
|
+
events.sort((a, b) => {
|
|
89
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
90
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
91
|
+
if (timeA !== timeB) return timeA - timeB;
|
|
92
|
+
return a._fileIndex - b._fileIndex;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return events;
|
|
81
96
|
} catch (err) {
|
|
82
97
|
console.error('Error reading events:', err);
|
|
83
98
|
return [];
|
package/views/session-vue.ejs
CHANGED
|
@@ -867,7 +867,7 @@
|
|
|
867
867
|
content: '';
|
|
868
868
|
flex: 1;
|
|
869
869
|
height: 1px;
|
|
870
|
-
background: #58a6ff;
|
|
870
|
+
background: var(--sa-color, #58a6ff);
|
|
871
871
|
}
|
|
872
872
|
.subagent-divider-text {
|
|
873
873
|
font-size: 12px;
|
|
@@ -886,6 +886,8 @@
|
|
|
886
886
|
.subagent-divider-line-right {
|
|
887
887
|
display: none;
|
|
888
888
|
}
|
|
889
|
+
.event.event-in-subagent { border-left-color: var(--subagent-border-color, #58a6ff); }
|
|
890
|
+
.subagent-owner-tag { font-size: 11px; padding: 1px 6px; border-radius: 8px; border: 1px solid; white-space: nowrap; opacity: 0.85; }
|
|
889
891
|
</style>
|
|
890
892
|
</head>
|
|
891
893
|
<body>
|
|
@@ -949,14 +951,19 @@
|
|
|
949
951
|
const eventsLoading = ref(true);
|
|
950
952
|
const eventsError = ref(null);
|
|
951
953
|
|
|
952
|
-
// Flatten and sort events
|
|
954
|
+
// Flatten and sort events (stable sort using _fileIndex tiebreaker)
|
|
953
955
|
const flatEvents = computed(() => {
|
|
954
956
|
const events = loadedEvents.value
|
|
955
|
-
.filter(e =>
|
|
956
|
-
e.type !== 'assistant.turn_end' &&
|
|
957
|
+
.filter(e =>
|
|
958
|
+
e.type !== 'assistant.turn_end' &&
|
|
957
959
|
e.type !== 'assistant.turn_complete'
|
|
958
960
|
)
|
|
959
|
-
.sort((a, b) =>
|
|
961
|
+
.sort((a, b) => {
|
|
962
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
963
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
964
|
+
if (timeA !== timeB) return timeA - timeB;
|
|
965
|
+
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
966
|
+
})
|
|
960
967
|
.map((e, index) => ({
|
|
961
968
|
...e,
|
|
962
969
|
virtualIndex: index,
|
|
@@ -1004,12 +1011,12 @@
|
|
|
1004
1011
|
// Final filtered events (search + type filter)
|
|
1005
1012
|
const filteredEvents = computed(() => {
|
|
1006
1013
|
let events = searchFilteredEvents.value;
|
|
1007
|
-
|
|
1014
|
+
|
|
1008
1015
|
// Apply type filter
|
|
1009
1016
|
if (currentFilter.value !== 'all') {
|
|
1010
1017
|
events = events.filter(e => e.type === currentFilter.value);
|
|
1011
1018
|
}
|
|
1012
|
-
|
|
1019
|
+
|
|
1013
1020
|
// Divider types (no separator before these)
|
|
1014
1021
|
const dividerTypes = ['assistant.turn_start', 'subagent.started', 'subagent.completed', 'subagent.failed'];
|
|
1015
1022
|
|
|
@@ -1197,7 +1204,99 @@
|
|
|
1197
1204
|
|
|
1198
1205
|
return map;
|
|
1199
1206
|
});
|
|
1200
|
-
|
|
1207
|
+
|
|
1208
|
+
// Subagent ownership: attribute events to their owning subagent
|
|
1209
|
+
const subagentOwnership = computed(() => {
|
|
1210
|
+
const sorted = flatEvents.value;
|
|
1211
|
+
const ownerMap = new Map(); // stableId → toolCallId
|
|
1212
|
+
const subagentInfo = new Map(); // toolCallId → { name, colorIndex }
|
|
1213
|
+
|
|
1214
|
+
// 1. Collect all subagent.started toolCallIds + assign colorIndex
|
|
1215
|
+
let colorIdx = 0;
|
|
1216
|
+
for (const ev of sorted) {
|
|
1217
|
+
if (ev.type === 'subagent.started') {
|
|
1218
|
+
const tcid = ev.data?.toolCallId;
|
|
1219
|
+
if (tcid) {
|
|
1220
|
+
subagentInfo.set(tcid, {
|
|
1221
|
+
name: ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent',
|
|
1222
|
+
colorIndex: colorIdx++
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (subagentInfo.size === 0) return { ownerMap, subagentInfo };
|
|
1229
|
+
|
|
1230
|
+
// 2. Build id → event lookup for parentId chain walking
|
|
1231
|
+
const idMap = new Map();
|
|
1232
|
+
for (const ev of sorted) {
|
|
1233
|
+
if (ev.id) idMap.set(ev.id, ev);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// 3. Attribute assistant.message events via data.parentToolCallId
|
|
1237
|
+
for (const ev of sorted) {
|
|
1238
|
+
if (ev.type === 'assistant.message') {
|
|
1239
|
+
const ptcid = ev.data?.parentToolCallId;
|
|
1240
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1241
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// 4. Attribute reasoning events by walking parentId → assistant.message
|
|
1247
|
+
for (const ev of sorted) {
|
|
1248
|
+
if (ev.type !== 'reasoning') continue;
|
|
1249
|
+
let current = ev.parentId;
|
|
1250
|
+
let depth = 0;
|
|
1251
|
+
while (current && depth < 10) {
|
|
1252
|
+
const parent = idMap.get(current);
|
|
1253
|
+
if (!parent) break;
|
|
1254
|
+
if (parent.type === 'assistant.message') {
|
|
1255
|
+
const ptcid = parent.data?.parentToolCallId;
|
|
1256
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1257
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1258
|
+
}
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
current = parent.parentId;
|
|
1262
|
+
depth++;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// 5. Attribute tool.execution_start/complete by walking parentId chain
|
|
1267
|
+
const startIdByToolCallId = new Map();
|
|
1268
|
+
for (const ev of sorted) {
|
|
1269
|
+
if (ev.type !== 'tool.execution_start') continue;
|
|
1270
|
+
let current = ev.parentId;
|
|
1271
|
+
let depth = 0;
|
|
1272
|
+
while (current && depth < 10) {
|
|
1273
|
+
const parent = idMap.get(current);
|
|
1274
|
+
if (!parent) break;
|
|
1275
|
+
if (parent.type === 'assistant.message') {
|
|
1276
|
+
const ptcid = parent.data?.parentToolCallId;
|
|
1277
|
+
if (ptcid && subagentInfo.has(ptcid)) {
|
|
1278
|
+
ownerMap.set(ev.stableId, ptcid);
|
|
1279
|
+
const tcid = ev.data?.toolCallId;
|
|
1280
|
+
if (tcid) startIdByToolCallId.set(tcid, ptcid);
|
|
1281
|
+
}
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
current = parent.parentId;
|
|
1285
|
+
depth++;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
for (const ev of sorted) {
|
|
1290
|
+
if (ev.type !== 'tool.execution_complete') continue;
|
|
1291
|
+
const tcid = ev.data?.toolCallId;
|
|
1292
|
+
if (tcid && startIdByToolCallId.has(tcid)) {
|
|
1293
|
+
ownerMap.set(ev.stableId, startIdByToolCallId.get(tcid));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return { ownerMap, subagentInfo };
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1201
1300
|
// Methods
|
|
1202
1301
|
const formatTime = (timestamp) => {
|
|
1203
1302
|
if (!timestamp) return '';
|
|
@@ -1442,7 +1541,44 @@
|
|
|
1442
1541
|
const getToolGroups = (event) => {
|
|
1443
1542
|
return toolCallMap.value.get(event.id || event.virtualIndex) || [];
|
|
1444
1543
|
};
|
|
1445
|
-
|
|
1544
|
+
|
|
1545
|
+
// Subagent color palette for parallel subagent distinction
|
|
1546
|
+
const SUBAGENT_COLORS = [
|
|
1547
|
+
'#58a6ff', // blue
|
|
1548
|
+
'#f0883e', // orange
|
|
1549
|
+
'#a371f7', // purple
|
|
1550
|
+
'#3fb950', // green
|
|
1551
|
+
'#f778ba', // pink
|
|
1552
|
+
'#79c0ff', // light blue
|
|
1553
|
+
'#d29922', // amber
|
|
1554
|
+
'#56d4dd' // teal
|
|
1555
|
+
];
|
|
1556
|
+
|
|
1557
|
+
const getSubagentInfo = (event) => {
|
|
1558
|
+
const { ownerMap, subagentInfo } = subagentOwnership.value;
|
|
1559
|
+
// For subagent dividers, use their own toolCallId
|
|
1560
|
+
if (event.type === 'subagent.started' || event.type === 'subagent.completed' || event.type === 'subagent.failed') {
|
|
1561
|
+
const tcid = event.data?.toolCallId;
|
|
1562
|
+
if (tcid && subagentInfo.has(tcid)) {
|
|
1563
|
+
const info = subagentInfo.get(tcid);
|
|
1564
|
+
return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };
|
|
1565
|
+
}
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
// For regular events, look up ownership
|
|
1569
|
+
const tcid = ownerMap.get(event.stableId);
|
|
1570
|
+
if (!tcid) return null;
|
|
1571
|
+
const info = subagentInfo.get(tcid);
|
|
1572
|
+
if (!info) return null;
|
|
1573
|
+
return { name: info.name, toolCallId: tcid, colorIndex: info.colorIndex };
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const getSubagentColor = (event) => {
|
|
1577
|
+
const info = getSubagentInfo(event);
|
|
1578
|
+
if (!info) return null;
|
|
1579
|
+
return SUBAGENT_COLORS[info.colorIndex % SUBAGENT_COLORS.length];
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1446
1582
|
const setFilter = (type) => {
|
|
1447
1583
|
currentFilter.value = type;
|
|
1448
1584
|
};
|
|
@@ -1744,6 +1880,8 @@
|
|
|
1744
1880
|
getToolCommand,
|
|
1745
1881
|
hasTools,
|
|
1746
1882
|
getToolGroups,
|
|
1883
|
+
getSubagentInfo,
|
|
1884
|
+
getSubagentColor,
|
|
1747
1885
|
setFilter,
|
|
1748
1886
|
scrollToTurn,
|
|
1749
1887
|
jumpToTurn,
|
|
@@ -1913,32 +2051,41 @@
|
|
|
1913
2051
|
</div>
|
|
1914
2052
|
|
|
1915
2053
|
<!-- Subagent Divider -->
|
|
1916
|
-
<div
|
|
2054
|
+
<div
|
|
1917
2055
|
v-else-if="item.type === 'subagent.started' || item.type === 'subagent.completed' || item.type === 'subagent.failed'"
|
|
1918
2056
|
:data-type="item.type"
|
|
1919
2057
|
:data-index="item.virtualIndex"
|
|
1920
2058
|
:class="['subagent-divider', item.type.split('.')[1]]"
|
|
2059
|
+
:style="{
|
|
2060
|
+
'--sa-color': getSubagentColor(item) || '#58a6ff'
|
|
2061
|
+
}"
|
|
1921
2062
|
>
|
|
1922
|
-
<div class="subagent-divider-line-left"></div>
|
|
1923
|
-
<span class="subagent-divider-text">
|
|
2063
|
+
<div class="subagent-divider-line-left" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
|
|
2064
|
+
<span class="subagent-divider-text" :style="{ color: getSubagentColor(item) || '#58a6ff', borderColor: getSubagentColor(item) || '#58a6ff', background: (getSubagentColor(item) || '#58a6ff') + '1a' }">
|
|
1924
2065
|
🤖 {{ item.data?.agentDisplayName || item.data?.agentName || 'SubAgent' }}
|
|
1925
2066
|
{{ item.type === 'subagent.started' ? 'Start ▶' : item.type === 'subagent.completed' ? 'Complete ✓' : 'Failed ✗' }}
|
|
1926
2067
|
</span>
|
|
1927
|
-
<div class="subagent-divider-line-right"></div>
|
|
2068
|
+
<div class="subagent-divider-line-right" :style="{ background: getSubagentColor(item) || '#58a6ff' }"></div>
|
|
1928
2069
|
<div class="divider-separator"></div>
|
|
1929
2070
|
</div>
|
|
1930
2071
|
|
|
1931
2072
|
<!-- Regular Event -->
|
|
1932
|
-
<div
|
|
2073
|
+
<div
|
|
1933
2074
|
v-else
|
|
1934
|
-
:class="['event']"
|
|
2075
|
+
:class="['event', getSubagentInfo(item) ? 'event-in-subagent' : '']"
|
|
1935
2076
|
:data-type="item.type"
|
|
1936
2077
|
:data-index="item.virtualIndex"
|
|
2078
|
+
:style="getSubagentColor(item) ? { '--subagent-border-color': getSubagentColor(item) } : {}"
|
|
1937
2079
|
>
|
|
1938
2080
|
<div class="event-header">
|
|
1939
2081
|
<span :class="['event-badge', getBadgeInfo(item.type).class]">
|
|
1940
2082
|
{{ getBadgeInfo(item.type).label }}
|
|
1941
2083
|
</span>
|
|
2084
|
+
<span
|
|
2085
|
+
v-if="getSubagentInfo(item)"
|
|
2086
|
+
class="subagent-owner-tag"
|
|
2087
|
+
:style="{ color: getSubagentColor(item), borderColor: getSubagentColor(item) }"
|
|
2088
|
+
>🤖 {{ getSubagentInfo(item).name }}</span>
|
|
1942
2089
|
<span class="event-timestamp">{{ formatTime(item.timestamp) }}</span>
|
|
1943
2090
|
</div>
|
|
1944
2091
|
|
package/views/time-analyze.ejs
CHANGED
|
@@ -939,8 +939,14 @@
|
|
|
939
939
|
});
|
|
940
940
|
|
|
941
941
|
// ── Shared sorted events (computed once, reused everywhere) ──
|
|
942
|
+
// Stable sort: use _fileIndex (set by backend) as tiebreaker for identical timestamps
|
|
942
943
|
const sortedEvents = computed(() => {
|
|
943
|
-
return [...events.value].sort((a, b) =>
|
|
944
|
+
return [...events.value].sort((a, b) => {
|
|
945
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
946
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
947
|
+
if (timeA !== timeB) return timeA - timeB;
|
|
948
|
+
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
949
|
+
});
|
|
944
950
|
});
|
|
945
951
|
|
|
946
952
|
// ── Map tool.execution_start events to owning subagent via parentToolCallId ──
|
|
@@ -1986,7 +1992,12 @@
|
|
|
1986
1992
|
const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
|
|
1987
1993
|
if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
|
|
1988
1994
|
const data = await resp.json();
|
|
1989
|
-
events.value = data.sort((a, b) =>
|
|
1995
|
+
events.value = data.sort((a, b) => {
|
|
1996
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
1997
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
1998
|
+
if (timeA !== timeB) return timeA - timeB;
|
|
1999
|
+
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
2000
|
+
});
|
|
1990
2001
|
} catch (err) {
|
|
1991
2002
|
error.value = err.message;
|
|
1992
2003
|
} finally {
|