@qiaolei81/copilot-session-viewer 0.1.7 → 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 +33 -2
- package/views/session-vue.ejs +162 -15
- package/views/time-analyze.ejs +290 -100
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 [];
|
|
@@ -104,6 +119,22 @@ class SessionService {
|
|
|
104
119
|
metadata.model = modelChangeEvent.data.newModel || modelChangeEvent.data.model;
|
|
105
120
|
}
|
|
106
121
|
|
|
122
|
+
// Derive "updated" from last event timestamp (more accurate than filesystem mtime)
|
|
123
|
+
if (events.length) {
|
|
124
|
+
const lastEvent = events[events.length - 1];
|
|
125
|
+
if (lastEvent?.timestamp) {
|
|
126
|
+
metadata.updated = lastEvent.timestamp;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Derive "created" from first event timestamp if available
|
|
131
|
+
if (events.length) {
|
|
132
|
+
const firstEvent = events[0];
|
|
133
|
+
if (firstEvent?.timestamp) {
|
|
134
|
+
metadata.created = firstEvent.timestamp;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
return { session, events, metadata };
|
|
108
139
|
}
|
|
109
140
|
}
|
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
|
@@ -61,10 +61,9 @@
|
|
|
61
61
|
/* Summary cards */
|
|
62
62
|
.summary-grid {
|
|
63
63
|
display: grid;
|
|
64
|
-
grid-template-columns: repeat(
|
|
64
|
+
grid-template-columns: repeat(6, 1fr);
|
|
65
65
|
gap: 12px;
|
|
66
66
|
margin: 20px auto;
|
|
67
|
-
max-width: 1200px;
|
|
68
67
|
}
|
|
69
68
|
.summary-card {
|
|
70
69
|
background: #161b22;
|
|
@@ -359,7 +358,32 @@
|
|
|
359
358
|
/* Gantt-like chart for subagents */
|
|
360
359
|
.gantt-container {
|
|
361
360
|
overflow-x: auto;
|
|
361
|
+
overflow-y: visible;
|
|
362
362
|
padding-bottom: 8px;
|
|
363
|
+
padding-top: 22px;
|
|
364
|
+
position: relative;
|
|
365
|
+
}
|
|
366
|
+
.gantt-crosshair {
|
|
367
|
+
position: absolute;
|
|
368
|
+
top: 0;
|
|
369
|
+
bottom: 0;
|
|
370
|
+
width: 1px;
|
|
371
|
+
background: rgba(139, 148, 158, 0.5);
|
|
372
|
+
pointer-events: none;
|
|
373
|
+
z-index: 10;
|
|
374
|
+
}
|
|
375
|
+
.gantt-crosshair-label {
|
|
376
|
+
position: absolute;
|
|
377
|
+
top: 4px;
|
|
378
|
+
left: 50%;
|
|
379
|
+
transform: translateX(-50%);
|
|
380
|
+
background: #30363d;
|
|
381
|
+
color: #e6edf3;
|
|
382
|
+
font-size: 10px;
|
|
383
|
+
padding: 2px 6px;
|
|
384
|
+
border-radius: 3px;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
pointer-events: none;
|
|
363
387
|
}
|
|
364
388
|
.gantt-row {
|
|
365
389
|
display: flex;
|
|
@@ -740,6 +764,38 @@
|
|
|
740
764
|
const showMarkerLegend = ref(false);
|
|
741
765
|
const copyLabel = ref('📊 Copy as Mermaid Gantt');
|
|
742
766
|
|
|
767
|
+
// Gantt crosshair
|
|
768
|
+
const ganttCrosshairX = ref(null); // px from left of gantt-container
|
|
769
|
+
const ganttCrosshairTime = ref('');
|
|
770
|
+
const onGanttMouseMove = (e) => {
|
|
771
|
+
const container = e.currentTarget;
|
|
772
|
+
// Find the first gantt-bar-area to get the bar column bounds
|
|
773
|
+
const barArea = container.querySelector('.gantt-bar-area');
|
|
774
|
+
if (!barArea) return;
|
|
775
|
+
const barRect = barArea.getBoundingClientRect();
|
|
776
|
+
const containerRect = container.getBoundingClientRect();
|
|
777
|
+
const barLeft = barRect.left - containerRect.left;
|
|
778
|
+
const barRight = barLeft + barRect.width;
|
|
779
|
+
const mouseX = e.clientX - containerRect.left;
|
|
780
|
+
|
|
781
|
+
if (mouseX >= barLeft && mouseX <= barRight) {
|
|
782
|
+
ganttCrosshairX.value = mouseX;
|
|
783
|
+
// Compute timestamp from position
|
|
784
|
+
const pct = (mouseX - barLeft) / barRect.width;
|
|
785
|
+
const ts = sessionStart.value + pct * totalDuration.value;
|
|
786
|
+
const d = new Date(ts);
|
|
787
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
788
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
789
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
790
|
+
ganttCrosshairTime.value = h + ':' + m + ':' + s;
|
|
791
|
+
} else {
|
|
792
|
+
ganttCrosshairX.value = null;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const onGanttMouseLeave = () => {
|
|
796
|
+
ganttCrosshairX.value = null;
|
|
797
|
+
};
|
|
798
|
+
|
|
743
799
|
const copyTimelineMarkdown = async () => {
|
|
744
800
|
const items = unifiedTimelineItems.value;
|
|
745
801
|
if (!items.length) return;
|
|
@@ -883,8 +939,70 @@
|
|
|
883
939
|
});
|
|
884
940
|
|
|
885
941
|
// ── Shared sorted events (computed once, reused everywhere) ──
|
|
942
|
+
// Stable sort: use _fileIndex (set by backend) as tiebreaker for identical timestamps
|
|
886
943
|
const sortedEvents = computed(() => {
|
|
887
|
-
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
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// ── Map tool.execution_start events to owning subagent via parentToolCallId ──
|
|
953
|
+
const subagentToolMap = computed(() => {
|
|
954
|
+
const sorted = sortedEvents.value;
|
|
955
|
+
|
|
956
|
+
// 1. Collect all subagent toolCallIds
|
|
957
|
+
const subagentToolCallIds = new Set();
|
|
958
|
+
for (const ev of sorted) {
|
|
959
|
+
if (ev.type === 'subagent.started') {
|
|
960
|
+
const tcid = ev.data?.toolCallId;
|
|
961
|
+
if (tcid) subagentToolCallIds.add(tcid);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// 2. Build id → event lookup
|
|
966
|
+
const idMap = new Map();
|
|
967
|
+
for (const ev of sorted) {
|
|
968
|
+
if (ev.id) idMap.set(ev.id, ev);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// 3. For each tool.execution_start, walk parentId to find assistant.message,
|
|
972
|
+
// then read data.parentToolCallId
|
|
973
|
+
const toolToSubagent = new Map(); // tool event id → subagent toolCallId
|
|
974
|
+
const startIdByToolCallId = new Map(); // data.toolCallId → subagent toolCallId (for matching complete events)
|
|
975
|
+
for (const ev of sorted) {
|
|
976
|
+
if (ev.type !== 'tool.execution_start') continue;
|
|
977
|
+
let current = ev.parentId;
|
|
978
|
+
let depth = 0;
|
|
979
|
+
while (current && depth < 10) {
|
|
980
|
+
const parent = idMap.get(current);
|
|
981
|
+
if (!parent) break;
|
|
982
|
+
if (parent.type === 'assistant.message') {
|
|
983
|
+
const ptcid = parent.data?.parentToolCallId;
|
|
984
|
+
if (ptcid && subagentToolCallIds.has(ptcid)) {
|
|
985
|
+
toolToSubagent.set(ev.id, ptcid);
|
|
986
|
+
const tcid = ev.data?.toolCallId;
|
|
987
|
+
if (tcid) startIdByToolCallId.set(tcid, ptcid);
|
|
988
|
+
}
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
current = parent.parentId;
|
|
992
|
+
depth++;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// 4. Map tool.execution_complete events via their toolCallId
|
|
997
|
+
for (const ev of sorted) {
|
|
998
|
+
if (ev.type !== 'tool.execution_complete') continue;
|
|
999
|
+
const tcid = ev.data?.toolCallId;
|
|
1000
|
+
if (tcid && startIdByToolCallId.has(tcid)) {
|
|
1001
|
+
toolToSubagent.set(ev.id, startIdByToolCallId.get(tcid));
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return toolToSubagent;
|
|
888
1006
|
});
|
|
889
1007
|
|
|
890
1008
|
// ── Sub-agent analysis ──
|
|
@@ -898,13 +1016,25 @@
|
|
|
898
1016
|
startStack.push(ev);
|
|
899
1017
|
} else if (ev.type === 'subagent.completed' || ev.type === 'subagent.failed') {
|
|
900
1018
|
const name = ev.data?.agentDisplayName || ev.data?.agentName || 'SubAgent';
|
|
901
|
-
// Find matching start
|
|
1019
|
+
// Find matching start by toolCallId, with name-based LIFO fallback
|
|
1020
|
+
const tcid = ev.data?.toolCallId;
|
|
902
1021
|
let startIdx = -1;
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1022
|
+
if (tcid) {
|
|
1023
|
+
for (let i = startStack.length - 1; i >= 0; i--) {
|
|
1024
|
+
if (startStack[i].data?.toolCallId === tcid) {
|
|
1025
|
+
startIdx = i;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Fallback to name-based LIFO if no toolCallId match
|
|
1031
|
+
if (startIdx < 0) {
|
|
1032
|
+
for (let i = startStack.length - 1; i >= 0; i--) {
|
|
1033
|
+
const sName = startStack[i].data?.agentDisplayName || startStack[i].data?.agentName || 'SubAgent';
|
|
1034
|
+
if (sName === name) {
|
|
1035
|
+
startIdx = i;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
908
1038
|
}
|
|
909
1039
|
}
|
|
910
1040
|
const startEv = startIdx >= 0 ? startStack.splice(startIdx, 1)[0] : null;
|
|
@@ -912,16 +1042,28 @@
|
|
|
912
1042
|
const endTime = new Date(ev.timestamp).getTime();
|
|
913
1043
|
const duration = startTime ? endTime - startTime : null;
|
|
914
1044
|
|
|
915
|
-
// Count tool calls
|
|
1045
|
+
// Count tool calls using parentToolCallId attribution (not time-window)
|
|
1046
|
+
const subagentTcid = startEv?.data?.toolCallId;
|
|
916
1047
|
let toolCalls = 0;
|
|
917
1048
|
const innerEvents = [];
|
|
918
1049
|
if (startEv) {
|
|
919
1050
|
for (const e of sorted) {
|
|
1051
|
+
if (e.type === 'tool.execution_start') {
|
|
1052
|
+
if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1053
|
+
toolCalls++;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Keep time-window for innerEvents/markers (visual only)
|
|
920
1057
|
const t = new Date(e.timestamp).getTime();
|
|
921
1058
|
if (t >= startTime && t <= endTime) {
|
|
922
|
-
if (e.type === 'tool.execution_start') toolCalls++;
|
|
923
1059
|
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
924
|
-
|
|
1060
|
+
if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
|
|
1061
|
+
// Non-tool trackable events: use time-window
|
|
1062
|
+
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1063
|
+
} else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1064
|
+
// Tool events: only include if attributed to this subagent
|
|
1065
|
+
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1066
|
+
}
|
|
925
1067
|
}
|
|
926
1068
|
}
|
|
927
1069
|
}
|
|
@@ -949,15 +1091,25 @@
|
|
|
949
1091
|
const startTime = new Date(startEv.timestamp).getTime();
|
|
950
1092
|
const duration = sessionEndTime - startTime;
|
|
951
1093
|
|
|
952
|
-
// Count tool calls
|
|
1094
|
+
// Count tool calls using parentToolCallId attribution (not time-window)
|
|
1095
|
+
const subagentTcid = startEv.data?.toolCallId;
|
|
953
1096
|
let toolCalls = 0;
|
|
954
1097
|
const innerEvents = [];
|
|
955
1098
|
for (const e of sorted) {
|
|
1099
|
+
if (e.type === 'tool.execution_start') {
|
|
1100
|
+
if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1101
|
+
toolCalls++;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// Keep time-window for innerEvents/markers (visual only)
|
|
956
1105
|
const t = new Date(e.timestamp).getTime();
|
|
957
1106
|
if (t >= startTime && t <= sessionEndTime) {
|
|
958
|
-
if (e.type === 'tool.execution_start') toolCalls++;
|
|
959
1107
|
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
960
|
-
|
|
1108
|
+
if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
|
|
1109
|
+
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1110
|
+
} else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1111
|
+
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1112
|
+
}
|
|
961
1113
|
}
|
|
962
1114
|
}
|
|
963
1115
|
}
|
|
@@ -994,7 +1146,6 @@
|
|
|
994
1146
|
const successRate = agents.length ? ((completed / agents.length) * 100).toFixed(0) : 100;
|
|
995
1147
|
|
|
996
1148
|
// Merge overlapping intervals to get actual wall-clock time in subagents
|
|
997
|
-
// and build merged interval list for tool counting (single pass)
|
|
998
1149
|
const intervals = agents
|
|
999
1150
|
.filter(a => a.startTime && a.endTime)
|
|
1000
1151
|
.map(a => [new Date(a.startTime).getTime(), new Date(a.endTime).getTime()])
|
|
@@ -1009,19 +1160,8 @@
|
|
|
1009
1160
|
}
|
|
1010
1161
|
const totalTime = mergedIntervals.reduce((sum, iv) => sum + (iv.e - iv.s), 0);
|
|
1011
1162
|
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
for (const ev of sortedEvents.value) {
|
|
1015
|
-
if (ev.type !== 'tool.execution_start') continue;
|
|
1016
|
-
const t = new Date(ev.timestamp).getTime();
|
|
1017
|
-
let lo = 0, hi = mergedIntervals.length - 1;
|
|
1018
|
-
while (lo <= hi) {
|
|
1019
|
-
const mid = (lo + hi) >> 1;
|
|
1020
|
-
if (t < mergedIntervals[mid].s) hi = mid - 1;
|
|
1021
|
-
else if (t > mergedIntervals[mid].e) lo = mid + 1;
|
|
1022
|
-
else { totalTools++; break; }
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1163
|
+
// Sum per-subagent tool counts (already correctly attributed via parentToolCallId)
|
|
1164
|
+
const totalTools = agents.reduce((sum, a) => sum + (a.toolCalls || 0), 0);
|
|
1025
1165
|
|
|
1026
1166
|
return { completed, failed, incomplete, totalTime, totalTools, successRate };
|
|
1027
1167
|
});
|
|
@@ -1074,21 +1214,40 @@
|
|
|
1074
1214
|
buckets.get(bucketIdx).push(ev);
|
|
1075
1215
|
}
|
|
1076
1216
|
|
|
1217
|
+
// Helper: interpolate color from tool yellow to error red based on error ratio
|
|
1218
|
+
const toolErrorColor = (errorRatio) => {
|
|
1219
|
+
// 0 = #d29922 (yellow), 1 = #f85149 (red)
|
|
1220
|
+
const r = Math.round(210 + (248 - 210) * errorRatio);
|
|
1221
|
+
const g = Math.round(153 + (81 - 153) * errorRatio);
|
|
1222
|
+
const b = Math.round(34 + (73 - 34) * errorRatio);
|
|
1223
|
+
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
const isToolError = (ev) => {
|
|
1227
|
+
return ev.type === 'tool.execution_complete' && (ev.data?.isError || !!ev.data?.error);
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1077
1230
|
const toolMarkers = [];
|
|
1078
1231
|
for (const [bucketIdx, group] of buckets) {
|
|
1079
1232
|
const bucketMid = startTime + (bucketIdx + 0.5) * bucketSize;
|
|
1080
1233
|
const relPos = ((bucketMid - startTime) / duration) * 100;
|
|
1081
1234
|
const pos = Math.max(0, Math.min(100, relPos));
|
|
1082
1235
|
|
|
1236
|
+
// Count errors in this bucket
|
|
1237
|
+
const errorCount = group.filter(isToolError).length;
|
|
1238
|
+
const completeCount = group.filter(ev => ev.type === 'tool.execution_complete').length;
|
|
1239
|
+
const errorRatio = completeCount > 0 ? errorCount / completeCount : 0;
|
|
1240
|
+
|
|
1083
1241
|
if (group.length === 1) {
|
|
1084
1242
|
const ev = group[0];
|
|
1085
1243
|
const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
|
|
1244
|
+
const color = isToolError(ev) ? '#f85149' : cat.color || '#8b949e';
|
|
1086
1245
|
toolMarkers.push({
|
|
1087
1246
|
type: ev.type,
|
|
1088
1247
|
position: pos,
|
|
1089
|
-
color
|
|
1248
|
+
color,
|
|
1090
1249
|
shape: cat.shape || 'circle',
|
|
1091
|
-
label: cat.label || ev.type,
|
|
1250
|
+
label: isToolError(ev) ? (cat.label || ev.type) + ' (error)' : (cat.label || ev.type),
|
|
1092
1251
|
timestamp: ev.timestamp,
|
|
1093
1252
|
toolName: ev.data?.toolName || null,
|
|
1094
1253
|
});
|
|
@@ -1100,17 +1259,20 @@
|
|
|
1100
1259
|
const lbl = cat.label || ev.type;
|
|
1101
1260
|
typeCounts[lbl] = (typeCounts[lbl] || 0) + 1;
|
|
1102
1261
|
});
|
|
1262
|
+
if (errorCount > 0) typeCounts['Errors'] = errorCount;
|
|
1103
1263
|
const summaryParts = Object.entries(typeCounts).map(([l, c]) => c + ' ' + l);
|
|
1104
|
-
const
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1264
|
+
const clusterColor = errorRatio > 0 ? toolErrorColor(errorRatio) : ((() => {
|
|
1265
|
+
const dominantType = group.reduce((best, ev) => {
|
|
1266
|
+
const cnt = group.filter(e => e.type === ev.type).length;
|
|
1267
|
+
return cnt > best.cnt ? { type: ev.type, cnt } : best;
|
|
1268
|
+
}, { type: group[0].type, cnt: 0 }).type;
|
|
1269
|
+
return (EVENT_MARKER_CATEGORIES[dominantType] || {}).color || '#8b949e';
|
|
1270
|
+
})());
|
|
1109
1271
|
|
|
1110
1272
|
toolMarkers.push({
|
|
1111
1273
|
type: 'cluster',
|
|
1112
1274
|
position: pos,
|
|
1113
|
-
color:
|
|
1275
|
+
color: clusterColor,
|
|
1114
1276
|
shape: 'cluster',
|
|
1115
1277
|
label: summaryParts.join(', '),
|
|
1116
1278
|
count: group.length,
|
|
@@ -1128,25 +1290,38 @@
|
|
|
1128
1290
|
const duration = gapEnd - gapStart;
|
|
1129
1291
|
const gapEvents = [];
|
|
1130
1292
|
const eventCounts = {};
|
|
1293
|
+
let toolCalls = 0;
|
|
1131
1294
|
for (const e of sorted) {
|
|
1132
1295
|
const t = new Date(e.timestamp).getTime();
|
|
1133
1296
|
if (t >= gapStart && t <= gapEnd) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1297
|
+
// For tool events, only include those NOT attributed to a subagent
|
|
1298
|
+
if (e.type.startsWith('tool.')) {
|
|
1299
|
+
const isSubagentTool = e.id && subagentToolMap.value.has(e.id);
|
|
1300
|
+
if (!isSubagentTool) {
|
|
1301
|
+
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1302
|
+
gapEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1303
|
+
}
|
|
1304
|
+
if (e.type === 'tool.execution_start') {
|
|
1305
|
+
toolCalls++;
|
|
1306
|
+
}
|
|
1307
|
+
eventCounts.tool = (eventCounts.tool || 0) + 1;
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1311
|
+
gapEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1312
|
+
}
|
|
1313
|
+
let cat = 'other';
|
|
1314
|
+
if (e.type.startsWith('assistant.')) cat = 'message';
|
|
1315
|
+
else if (e.type.startsWith('user.')) cat = 'user';
|
|
1316
|
+
else if (e.type.startsWith('session.')) cat = 'session';
|
|
1317
|
+
eventCounts[cat] = (eventCounts[cat] || 0) + 1;
|
|
1136
1318
|
}
|
|
1137
|
-
// Count by category for summary
|
|
1138
|
-
let cat = 'other';
|
|
1139
|
-
if (e.type.startsWith('tool.')) cat = 'tool';
|
|
1140
|
-
else if (e.type.startsWith('assistant.')) cat = 'message';
|
|
1141
|
-
else if (e.type.startsWith('user.')) cat = 'user';
|
|
1142
|
-
else if (e.type.startsWith('session.')) cat = 'session';
|
|
1143
|
-
eventCounts[cat] = (eventCounts[cat] || 0) + 1;
|
|
1144
1319
|
}
|
|
1145
1320
|
}
|
|
1146
1321
|
|
|
1147
1322
|
// Build summary string
|
|
1148
1323
|
const parts = [];
|
|
1149
|
-
if (
|
|
1324
|
+
if (toolCalls) parts.push(toolCalls + ' tool' + (toolCalls > 1 ? 's' : ''));
|
|
1150
1325
|
if (eventCounts.message) parts.push(eventCounts.message + ' message' + (eventCounts.message > 1 ? 's' : ''));
|
|
1151
1326
|
if (eventCounts.user) parts.push(eventCounts.user + ' user msg');
|
|
1152
1327
|
if (eventCounts.session) parts.push(eventCounts.session + ' session event' + (eventCounts.session > 1 ? 's' : ''));
|
|
@@ -1159,6 +1334,7 @@
|
|
|
1159
1334
|
itemType: 'agent-op',
|
|
1160
1335
|
name: 'Main Agent',
|
|
1161
1336
|
summary,
|
|
1337
|
+
toolCalls,
|
|
1162
1338
|
startTime: new Date(gapStart).toISOString(),
|
|
1163
1339
|
endTime: new Date(gapEnd).toISOString(),
|
|
1164
1340
|
duration,
|
|
@@ -1650,6 +1826,8 @@
|
|
|
1650
1826
|
llmTime,
|
|
1651
1827
|
userThinkingPct: total > 0 ? (userThinkingTime / total * 100).toFixed(0) : 0,
|
|
1652
1828
|
agentWorkingPct: total > 0 ? (agentWorkingTime / total * 100).toFixed(0) : 0,
|
|
1829
|
+
llmPct: total > 0 ? (llmTime / total * 100).toFixed(0) : 0,
|
|
1830
|
+
toolPct: total > 0 ? (totalToolTime.value / total * 100).toFixed(0) : 0,
|
|
1653
1831
|
};
|
|
1654
1832
|
});
|
|
1655
1833
|
|
|
@@ -1755,25 +1933,6 @@
|
|
|
1755
1933
|
}
|
|
1756
1934
|
}
|
|
1757
1935
|
|
|
1758
|
-
// Tool summary section
|
|
1759
|
-
const cats = toolTimeByCategory.value;
|
|
1760
|
-
if (cats.length) {
|
|
1761
|
-
items.push({ rowType: 'divider' });
|
|
1762
|
-
|
|
1763
|
-
// Compute maxTime for proportional bars
|
|
1764
|
-
const maxTime = Math.max(...cats.map(c => c.totalTime), 1);
|
|
1765
|
-
for (const cat of cats) {
|
|
1766
|
-
items.push({
|
|
1767
|
-
rowType: 'tool-summary',
|
|
1768
|
-
category: cat.category,
|
|
1769
|
-
count: cat.count,
|
|
1770
|
-
totalTime: cat.totalTime,
|
|
1771
|
-
errors: cat.errors,
|
|
1772
|
-
barWidthPct: (cat.totalTime / maxTime) * 100,
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
1936
|
return items;
|
|
1778
1937
|
});
|
|
1779
1938
|
|
|
@@ -1833,7 +1992,12 @@
|
|
|
1833
1992
|
const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
|
|
1834
1993
|
if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
|
|
1835
1994
|
const data = await resp.json();
|
|
1836
|
-
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
|
+
});
|
|
1837
2001
|
} catch (err) {
|
|
1838
2002
|
error.value = err.message;
|
|
1839
2003
|
} finally {
|
|
@@ -1960,6 +2124,7 @@
|
|
|
1960
2124
|
subagentAnalysis, maxSubagentDuration, subagentTimelineItems, subagentStats,
|
|
1961
2125
|
EVENT_MARKER_CATEGORIES, showMarkerLegend,
|
|
1962
2126
|
copyLabel, copyTimelineMarkdown,
|
|
2127
|
+
ganttCrosshairX, ganttCrosshairTime, onGanttMouseMove, onGanttMouseLeave,
|
|
1963
2128
|
turnAnalysis, maxTurnDuration, groupedTurns,
|
|
1964
2129
|
unifiedTimelineItems,
|
|
1965
2130
|
toolAnalysis, sortedToolAnalysis, maxToolDuration,
|
|
@@ -1985,21 +2150,19 @@
|
|
|
1985
2150
|
<div v-else>
|
|
1986
2151
|
<!-- Summary Cards -->
|
|
1987
2152
|
<div class="summary-grid">
|
|
1988
|
-
<div class="summary-card">
|
|
2153
|
+
<div class="summary-card" title="Wall-clock time from first event to last event in this session.">
|
|
1989
2154
|
<div class="summary-card-label">Total Duration</div>
|
|
1990
2155
|
<div class="summary-card-value">{{ formatDuration(totalDuration) }}</div>
|
|
1991
|
-
<div class="summary-card-sub">
|
|
1992
|
-
|
|
1993
|
-
· Tools {{ formatDuration(totalToolTime) }}
|
|
1994
|
-
<span v-if="timeBreakdown.userThinkingTime > 1000"> · User {{ formatDuration(timeBreakdown.userThinkingTime) }}</span>
|
|
2156
|
+
<div class="summary-card-sub" v-if="sessionStart" style="margin-top: 2px; font-size: 10px; opacity: 0.7;">
|
|
2157
|
+
{{ formatDateTime(sessionStart) }} → {{ formatDateTime(sessionEnd) }}
|
|
1995
2158
|
</div>
|
|
1996
2159
|
</div>
|
|
1997
|
-
<div class="summary-card">
|
|
2160
|
+
<div class="summary-card" title="Number of user messages that triggered agent work. Each request may involve multiple LLM turns.">
|
|
1998
2161
|
<div class="summary-card-label">User Requests</div>
|
|
1999
2162
|
<div class="summary-card-value">{{ groupedTurns.length }}</div>
|
|
2000
2163
|
<div class="summary-card-sub">{{ turnAnalysis.length }} turn{{ turnAnalysis.length !== 1 ? 's' : '' }}</div>
|
|
2001
2164
|
</div>
|
|
2002
|
-
<div class="summary-card">
|
|
2165
|
+
<div class="summary-card" title="Total tool executions (Read, Write, Edit, Bash, Grep, etc.) across the entire session, including subagent tools.">
|
|
2003
2166
|
<div class="summary-card-label">Tool Calls</div>
|
|
2004
2167
|
<div class="summary-card-value">{{ totalToolCount }}</div>
|
|
2005
2168
|
<div class="summary-card-sub">
|
|
@@ -2007,7 +2170,7 @@
|
|
|
2007
2170
|
<span v-if="errorCount > 0" style="color: #f85149;"> · {{ errorCount }} error{{ errorCount !== 1 ? 's' : '' }}</span>
|
|
2008
2171
|
</div>
|
|
2009
2172
|
</div>
|
|
2010
|
-
<div class="summary-card">
|
|
2173
|
+
<div class="summary-card" title="Spawned subagents (via Task tool). Shows completed/failed/incomplete counts, total wall-clock time, and tool calls attributed to subagents.">
|
|
2011
2174
|
<div class="summary-card-label">Sub-Agents</div>
|
|
2012
2175
|
<div class="summary-card-value">{{ subagentAnalysis.length }}</div>
|
|
2013
2176
|
<div class="summary-card-sub">
|
|
@@ -2018,7 +2181,15 @@
|
|
|
2018
2181
|
· {{ subagentStats.totalTools }} tools
|
|
2019
2182
|
</div>
|
|
2020
2183
|
</div>
|
|
2021
|
-
<div class="summary-card">
|
|
2184
|
+
<div class="summary-card" title="Estimated LLM reasoning time (total duration minus tool execution and user thinking time). Breakdown shows LLM percentage, tool wall-clock time, and user idle time.">
|
|
2185
|
+
<div class="summary-card-label">Time Breakdown</div>
|
|
2186
|
+
<div class="summary-card-value">{{ formatDuration(timeBreakdown.llmTime) }} <span style="font-size: 14px; opacity: 0.6;">({{ timeBreakdown.llmPct }}% LLM Reasoning)</span></div>
|
|
2187
|
+
<div class="summary-card-sub">
|
|
2188
|
+
Tools {{ formatDuration(totalToolTime) }} ({{ timeBreakdown.toolPct }}%)
|
|
2189
|
+
<span v-if="timeBreakdown.userThinkingTime > 1000"> · User {{ formatDuration(timeBreakdown.userThinkingTime) }} ({{ timeBreakdown.userThinkingPct }}%)</span>
|
|
2190
|
+
</div>
|
|
2191
|
+
</div>
|
|
2192
|
+
<div class="summary-card" title="File system operations: reads (Read/Glob), edits (Edit), writes (Write), and searches (Grep).">
|
|
2022
2193
|
<div class="summary-card-label">File Operations</div>
|
|
2023
2194
|
<div class="summary-card-value">{{ fileStats.totalOps }}</div>
|
|
2024
2195
|
<div class="summary-card-sub">
|
|
@@ -2069,16 +2240,30 @@
|
|
|
2069
2240
|
<span>Main Agent</span>
|
|
2070
2241
|
</div>
|
|
2071
2242
|
<div class="event-legend-item">
|
|
2072
|
-
<span class="event-legend-swatch" style="background:
|
|
2073
|
-
<span>Tool
|
|
2243
|
+
<span class="event-legend-swatch" style="background: #d29922;"></span>
|
|
2244
|
+
<span>Tool (no errors)</span>
|
|
2074
2245
|
</div>
|
|
2075
|
-
<div
|
|
2076
|
-
<span class="event-legend-swatch"
|
|
2077
|
-
<span>
|
|
2246
|
+
<div class="event-legend-item">
|
|
2247
|
+
<span class="event-legend-swatch" style="background: linear-gradient(to right, #d29922, #f85149);"></span>
|
|
2248
|
+
<span>Tool (error gradient)</span>
|
|
2078
2249
|
</div>
|
|
2250
|
+
<div class="event-legend-item">
|
|
2251
|
+
<span class="event-legend-swatch" style="background: #f85149;"></span>
|
|
2252
|
+
<span>Tool Error (100%)</span>
|
|
2253
|
+
</div>
|
|
2254
|
+
<template v-for="(cat, type) in EVENT_MARKER_CATEGORIES" :key="type">
|
|
2255
|
+
<div v-if="type && !type.startsWith('tool.')" class="event-legend-item">
|
|
2256
|
+
<span class="event-legend-swatch" :style="{ background: cat.color, borderRadius: cat.shape === 'circle' ? '50%' : cat.shape === 'diamond' ? '1px' : '2px', transform: cat.shape === 'diamond' ? 'rotate(45deg)' : 'none' }"></span>
|
|
2257
|
+
<span>{{ cat.label }}</span>
|
|
2258
|
+
</div>
|
|
2259
|
+
</template>
|
|
2079
2260
|
</div>
|
|
2080
2261
|
|
|
2081
|
-
<div class="gantt-container">
|
|
2262
|
+
<div class="gantt-container" @mousemove="onGanttMouseMove" @mouseleave="onGanttMouseLeave">
|
|
2263
|
+
<!-- Crosshair -->
|
|
2264
|
+
<div v-if="ganttCrosshairX !== null" class="gantt-crosshair" :style="{ left: ganttCrosshairX + 'px' }">
|
|
2265
|
+
<div class="gantt-crosshair-label">{{ ganttCrosshairTime }}</div>
|
|
2266
|
+
</div>
|
|
2082
2267
|
<template v-for="(item, idx) in unifiedTimelineItems" :key="'utl-' + idx">
|
|
2083
2268
|
|
|
2084
2269
|
<!-- Divider row -->
|
|
@@ -2206,24 +2391,6 @@
|
|
|
2206
2391
|
</div>
|
|
2207
2392
|
</div>
|
|
2208
2393
|
|
|
2209
|
-
<!-- Tool Summary row -->
|
|
2210
|
-
<div v-else-if="item.rowType === 'tool-summary'" class="gantt-row">
|
|
2211
|
-
<div class="gantt-label tool-summary" :title="item.category + ' (' + item.count + ' calls)'">
|
|
2212
|
-
<span class="tool-cat-icon">🔧</span>
|
|
2213
|
-
{{ item.category }}
|
|
2214
|
-
<span class="tool-cat-count">({{ item.count }})</span>
|
|
2215
|
-
</div>
|
|
2216
|
-
<div class="gantt-bar-area">
|
|
2217
|
-
<div
|
|
2218
|
-
class="gantt-bar tool-summary"
|
|
2219
|
-
:style="{ left: '0%', width: item.barWidthPct + '%' }"
|
|
2220
|
-
:title="item.category + ' — ' + formatDuration(item.totalTime) + ' (' + item.count + ' calls)'"
|
|
2221
|
-
>
|
|
2222
|
-
{{ formatDuration(item.totalTime) }}
|
|
2223
|
-
</div>
|
|
2224
|
-
</div>
|
|
2225
|
-
</div>
|
|
2226
|
-
|
|
2227
2394
|
</template>
|
|
2228
2395
|
|
|
2229
2396
|
<div class="gantt-time-axis">
|
|
@@ -2232,6 +2399,29 @@
|
|
|
2232
2399
|
</div>
|
|
2233
2400
|
</div>
|
|
2234
2401
|
|
|
2402
|
+
<!-- Tool Summary -->
|
|
2403
|
+
<div v-if="toolTimeByCategory.length" style="margin-top: 24px;">
|
|
2404
|
+
<h3 style="color: #e6edf3; font-size: 14px; margin-bottom: 12px;">🔧 Tool Summary</h3>
|
|
2405
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px;">
|
|
2406
|
+
<div
|
|
2407
|
+
v-for="cat in toolTimeByCategory"
|
|
2408
|
+
:key="cat.category"
|
|
2409
|
+
style="background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; display: flex; align-items: center; gap: 10px;"
|
|
2410
|
+
>
|
|
2411
|
+
<div style="flex: 1; min-width: 0;">
|
|
2412
|
+
<div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px;">
|
|
2413
|
+
<span style="color: #d29922; font-weight: 500; font-size: 13px;">{{ cat.category }}</span>
|
|
2414
|
+
<span style="color: #7d8590; font-size: 11px;">{{ cat.count }} call{{ cat.count !== 1 ? 's' : '' }}<span v-if="cat.errors" style="color: #f85149;"> · {{ cat.errors }} err</span></span>
|
|
2415
|
+
</div>
|
|
2416
|
+
<div style="background: #21262d; border-radius: 3px; height: 6px; overflow: hidden;">
|
|
2417
|
+
<div :style="{ width: (cat.totalTime / maxCategoryTime * 100) + '%', height: '100%', background: 'rgba(158, 106, 3, 0.7)', borderRadius: '3px' }"></div>
|
|
2418
|
+
</div>
|
|
2419
|
+
<div style="color: #7d8590; font-size: 11px; margin-top: 3px;">{{ formatDuration(cat.totalTime) }}</div>
|
|
2420
|
+
</div>
|
|
2421
|
+
</div>
|
|
2422
|
+
</div>
|
|
2423
|
+
</div>
|
|
2424
|
+
|
|
2235
2425
|
</div>
|
|
2236
2426
|
</div>
|
|
2237
2427
|
|