@qiaolei81/copilot-session-viewer 0.3.1 → 0.3.3
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/AGENTS.md +109 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +104 -0
- package/RELEASE.md +146 -0
- package/docs/API.md +471 -0
- package/docs/DEVELOPMENT.md +556 -0
- package/docs/INSTALLATION.md +329 -0
- package/docs/README.md +102 -0
- package/docs/TROUBLESHOOTING.md +630 -0
- package/docs/images/homepage.png +0 -0
- package/docs/images/session-detail.png +0 -0
- package/docs/images/time-analysis.png +0 -0
- package/docs/unified-event-format-design.md +844 -0
- package/docs/unified-event-format-implementation.md +350 -0
- package/eslint.config.mjs +133 -0
- package/package.json +10 -4
- package/public/js/homepage.min.js +35 -0
- package/public/js/session-detail.min.js +461 -0
- package/public/js/telemetry-browser.min.js +1 -0
- package/public/js/time-analyze.min.js +518 -0
- package/scripts/release.sh +43 -0
- package/server.js +3 -0
- package/src/app.js +2 -1
- package/src/controllers/insightController.js +31 -0
- package/src/controllers/sessionController.js +56 -0
- package/src/controllers/tagController.js +8 -0
- package/src/controllers/uploadController.js +32 -1
- package/src/middleware/common.js +20 -1
- package/src/models/Session.js +12 -2
- package/src/services/sessionRepository.js +98 -108
- package/src/telemetry.js +152 -0
- package/src/utils/fileUtils.js +2 -1
- package/views/index.ejs +9 -494
- package/views/session-vue.ejs +166 -1869
- package/views/telemetry-snippet.ejs +26 -0
- package/views/time-analyze.ejs +2 -2217
- package/.env.example +0 -14
package/views/time-analyze.ejs
CHANGED
|
@@ -773,2226 +773,11 @@
|
|
|
773
773
|
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.28/dist/vue.global.prod.js" integrity="sha384-EyKhbIJoP1t1fKIFRNEfYKy4uy8qxs7UNS4Cab53xyXqCTUB1PCoxeFsD0G/NX9W" crossorigin="anonymous"></script>
|
|
774
774
|
|
|
775
775
|
<script>
|
|
776
|
-
window.
|
|
776
|
+
window.__PAGE_DATA = {
|
|
777
777
|
sessionId: '<%= sessionId %>',
|
|
778
778
|
metadata: <%- JSON.stringify(metadata).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029') %>
|
|
779
779
|
};
|
|
780
780
|
</script>
|
|
781
|
-
|
|
782
|
-
<script>
|
|
783
|
-
const { createApp, ref, computed, onMounted, onUnmounted, reactive } = Vue;
|
|
784
|
-
|
|
785
|
-
const app = createApp({
|
|
786
|
-
setup() {
|
|
787
|
-
const sessionId = ref(window.sessionData.sessionId);
|
|
788
|
-
const metadata = ref(window.sessionData.metadata);
|
|
789
|
-
const events = ref([]);
|
|
790
|
-
const loading = ref(true);
|
|
791
|
-
const error = ref(null);
|
|
792
|
-
const activeTab = ref('timeline');
|
|
793
|
-
const sortField = ref('timestamp');
|
|
794
|
-
const sortDir = ref('asc');
|
|
795
|
-
const insightReport = ref(null);
|
|
796
|
-
const insightLog = ref(null);
|
|
797
|
-
const insightLoading = ref(false);
|
|
798
|
-
const insightError = ref(null);
|
|
799
|
-
const insightGeneratedAt = ref(null);
|
|
800
|
-
const showMarkerLegend = ref(false);
|
|
801
|
-
const copyLabel = ref('📊 Copy as Mermaid Gantt');
|
|
802
|
-
|
|
803
|
-
// Helper: normalize message to string (handle arrays/objects from Copilot)
|
|
804
|
-
const normalizeMessage = (msg) => {
|
|
805
|
-
if (!msg) return '';
|
|
806
|
-
if (typeof msg === 'string') return msg;
|
|
807
|
-
if (Array.isArray(msg)) {
|
|
808
|
-
// Handle content array (Copilot format)
|
|
809
|
-
return msg.map(c => c.text || c.content || '').join(' ');
|
|
810
|
-
}
|
|
811
|
-
if (typeof msg === 'object' && msg.text) return msg.text;
|
|
812
|
-
return String(msg);
|
|
813
|
-
};
|
|
814
|
-
|
|
815
|
-
// Gantt crosshair
|
|
816
|
-
const ganttCrosshairX = ref(null); // px from left of gantt-container
|
|
817
|
-
const ganttCrosshairTime = ref('');
|
|
818
|
-
const onGanttMouseMove = (e) => {
|
|
819
|
-
const container = e.currentTarget;
|
|
820
|
-
// Find the first gantt-bar-area to get the bar column bounds
|
|
821
|
-
const barArea = container.querySelector('.gantt-bar-area');
|
|
822
|
-
if (!barArea) return;
|
|
823
|
-
const barRect = barArea.getBoundingClientRect();
|
|
824
|
-
const containerRect = container.getBoundingClientRect();
|
|
825
|
-
const barLeft = barRect.left - containerRect.left;
|
|
826
|
-
const barRight = barLeft + barRect.width;
|
|
827
|
-
const mouseX = e.clientX - containerRect.left;
|
|
828
|
-
|
|
829
|
-
if (mouseX >= barLeft && mouseX <= barRight) {
|
|
830
|
-
ganttCrosshairX.value = mouseX;
|
|
831
|
-
// Compute timestamp from position
|
|
832
|
-
const pct = (mouseX - barLeft) / barRect.width;
|
|
833
|
-
const ts = sessionStart.value + pct * totalDuration.value;
|
|
834
|
-
const d = new Date(ts);
|
|
835
|
-
const h = String(d.getHours()).padStart(2, '0');
|
|
836
|
-
const m = String(d.getMinutes()).padStart(2, '0');
|
|
837
|
-
const s = String(d.getSeconds()).padStart(2, '0');
|
|
838
|
-
ganttCrosshairTime.value = h + ':' + m + ':' + s;
|
|
839
|
-
} else {
|
|
840
|
-
ganttCrosshairX.value = null;
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
const onGanttMouseLeave = () => {
|
|
844
|
-
ganttCrosshairX.value = null;
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
const copyTimelineMarkdown = async () => {
|
|
848
|
-
const items = unifiedTimelineItems.value;
|
|
849
|
-
if (!items.length) return;
|
|
850
|
-
|
|
851
|
-
// Helper: convert timestamp to Unix epoch milliseconds
|
|
852
|
-
const toEpochMs = (ts) => {
|
|
853
|
-
if (!ts) return 0;
|
|
854
|
-
return new Date(ts).getTime();
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
// Sanitize label for Mermaid: strip chars that break syntax or could escape the code block
|
|
858
|
-
const sanitize = (str) => (str || '').replace(/[`\n\r]/g, '').replace(/[:;#]/g, '-').replace(/\s+/g, ' ').trim().substring(0, 100);
|
|
859
|
-
const normalizeMessage = (msg) => {
|
|
860
|
-
if (!msg) return '';
|
|
861
|
-
if (typeof msg === 'string') return msg;
|
|
862
|
-
if (Array.isArray(msg)) {
|
|
863
|
-
// Handle content array (Copilot format)
|
|
864
|
-
return msg.map(c => c.text || c.content || '').join(' ');
|
|
865
|
-
}
|
|
866
|
-
if (typeof msg === 'object' && msg.text) return msg.text;
|
|
867
|
-
return String(msg);
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
// Deduplicate task IDs within the mermaid block
|
|
871
|
-
const usedIds = {};
|
|
872
|
-
const uniqueId = (base) => {
|
|
873
|
-
const clean = base.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
|
|
874
|
-
if (!usedIds[clean]) { usedIds[clean] = 1; return clean; }
|
|
875
|
-
usedIds[clean]++;
|
|
876
|
-
return clean + '_' + usedIds[clean];
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
const lines = [];
|
|
880
|
-
lines.push('```mermaid');
|
|
881
|
-
lines.push('gantt');
|
|
882
|
-
lines.push(' title Session Timeline – ' + sanitize(sessionId.value));
|
|
883
|
-
lines.push(' dateFormat x');
|
|
884
|
-
lines.push(' axisFormat %H:%M:%S');
|
|
885
|
-
lines.push('');
|
|
886
|
-
|
|
887
|
-
for (const item of items) {
|
|
888
|
-
if (item.rowType === 'user-req') {
|
|
889
|
-
const msg = sanitize(normalizeMessage(item.message) || 'No message').substring(0, 40);
|
|
890
|
-
const label = 'UserReq ' + item.userReqNumber + ' – ' + msg + ' (' + formatDuration(item.duration) + ')';
|
|
891
|
-
const id = uniqueId('userreq_' + item.userReqNumber);
|
|
892
|
-
const start = toEpochMs(item.startTime);
|
|
893
|
-
const end = toEpochMs(item.endTime);
|
|
894
|
-
lines.push(' ' + label + ' :milestone, ' + id + ', ' + start + ', ' + end);
|
|
895
|
-
} else if (item.rowType === 'subagent') {
|
|
896
|
-
const start = toEpochMs(item.startTime);
|
|
897
|
-
const end = toEpochMs(item.endTime);
|
|
898
|
-
const toolInfo = (item.toolCalls != null ? item.toolCalls : 0) + ' tools';
|
|
899
|
-
const label = sanitize(item.name) + ' – ' + formatDuration(item.duration) + ' (' + toolInfo + ')';
|
|
900
|
-
const id = uniqueId(item.name);
|
|
901
|
-
const tag = item.status === 'failed' ? 'crit, '
|
|
902
|
-
: item.status === 'incomplete' ? 'active, ' : '';
|
|
903
|
-
lines.push(' ' + label + ' :' + tag + id + ', ' + start + ', ' + end);
|
|
904
|
-
} else if (item.rowType === 'main-agent') {
|
|
905
|
-
const start = toEpochMs(item.startTime);
|
|
906
|
-
const end = toEpochMs(item.endTime);
|
|
907
|
-
const detail = sanitize(item.summary || 'idle');
|
|
908
|
-
const label = 'Main Agent – ' + formatDuration(item.duration) + ' (' + detail + ')';
|
|
909
|
-
const id = uniqueId('main_agent');
|
|
910
|
-
lines.push(' ' + label + ' :' + id + ', ' + start + ', ' + end);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
lines.push('```');
|
|
915
|
-
lines.push('');
|
|
916
|
-
|
|
917
|
-
const md = lines.join('\n');
|
|
918
|
-
|
|
919
|
-
try {
|
|
920
|
-
await navigator.clipboard.writeText(md);
|
|
921
|
-
copyLabel.value = '✅ Copied!';
|
|
922
|
-
} catch (err) {
|
|
923
|
-
// Fallback for non-secure contexts
|
|
924
|
-
const textarea = document.createElement('textarea');
|
|
925
|
-
textarea.value = md;
|
|
926
|
-
textarea.style.position = 'fixed';
|
|
927
|
-
textarea.style.opacity = '0';
|
|
928
|
-
document.body.appendChild(textarea);
|
|
929
|
-
textarea.select();
|
|
930
|
-
document.execCommand('copy');
|
|
931
|
-
document.body.removeChild(textarea);
|
|
932
|
-
copyLabel.value = '✅ Copied!';
|
|
933
|
-
}
|
|
934
|
-
setTimeout(() => { copyLabel.value = '📊 Copy as Mermaid Gantt'; }, 2000);
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
// 事件标记类别定义
|
|
938
|
-
const EVENT_MARKER_CATEGORIES = {
|
|
939
|
-
'tool.execution_start': { color: '#d29922', shape: 'diamond', label: 'Tool Start' },
|
|
940
|
-
'tool.execution_complete': { color: '#e3b341', shape: 'diamond', label: 'Tool Complete' },
|
|
941
|
-
'assistant.message': { color: '#8b949e', shape: 'circle', label: 'Message' },
|
|
942
|
-
'user.message': { color: '#79c0ff', shape: 'square', label: 'User Message' },
|
|
943
|
-
'session.start': { color: '#56d364', shape: 'square', label: 'Session Start' },
|
|
944
|
-
'session.resume': { color: '#56d364', shape: 'square', label: 'Session Resume' },
|
|
945
|
-
'session.error': { color: '#f85149', shape: 'triangle', label: 'Error' },
|
|
946
|
-
'session.truncation': { color: '#f0883e', shape: 'triangle', label: 'Truncation' },
|
|
947
|
-
'session.compaction_start': { color: '#a371f7', shape: 'square', label: 'Compaction Start' },
|
|
948
|
-
'session.compaction_complete': { color: '#bc8cff', shape: 'square', label: 'Compaction End' },
|
|
949
|
-
'session.model_change': { color: '#f778ba', shape: 'square', label: 'Model Change' },
|
|
950
|
-
'abort': { color: '#ff7b72', shape: 'triangle', label: 'Abort' },
|
|
951
|
-
};
|
|
952
|
-
const TRACKABLE_EVENT_TYPES = new Set(Object.keys(EVENT_MARKER_CATEGORIES));
|
|
953
|
-
|
|
954
|
-
// ── Helpers ──
|
|
955
|
-
const formatDuration = (ms) => {
|
|
956
|
-
if (ms == null || ms < 0) return '—';
|
|
957
|
-
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
958
|
-
const s = ms / 1000;
|
|
959
|
-
if (s < 60) {
|
|
960
|
-
const rounded = Math.round(s * 10) / 10; // 四舍五入到一位小数
|
|
961
|
-
return (rounded % 1 === 0 ? Math.round(rounded) : rounded.toFixed(1)) + 's';
|
|
962
|
-
}
|
|
963
|
-
const m = Math.floor(s / 60);
|
|
964
|
-
const remainder = Math.floor(s % 60);
|
|
965
|
-
if (m < 60) return m + 'm ' + remainder + 's';
|
|
966
|
-
const h = Math.floor(m / 60);
|
|
967
|
-
return h + 'h ' + (m % 60) + 'm';
|
|
968
|
-
};
|
|
969
|
-
|
|
970
|
-
const formatTime = (ts) => {
|
|
971
|
-
if (!ts) return '';
|
|
972
|
-
const d = new Date(ts);
|
|
973
|
-
return String(d.getHours()).padStart(2, '0') + ':' +
|
|
974
|
-
String(d.getMinutes()).padStart(2, '0') + ':' +
|
|
975
|
-
String(d.getSeconds()).padStart(2, '0');
|
|
976
|
-
};
|
|
977
|
-
|
|
978
|
-
const formatDateTime = (ts) => {
|
|
979
|
-
if (!ts) return '';
|
|
980
|
-
return new Date(ts).toLocaleString();
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
// ── Session timeline ──
|
|
984
|
-
const sessionStart = computed(() => {
|
|
985
|
-
if (!events.value.length) return null;
|
|
986
|
-
// Find first event with valid timestamp
|
|
987
|
-
for (const ev of events.value) {
|
|
988
|
-
const ts = ev.timestamp || ev.snapshot?.timestamp;
|
|
989
|
-
if (ts) return new Date(ts).getTime();
|
|
990
|
-
}
|
|
991
|
-
return null;
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
const sessionEnd = computed(() => {
|
|
995
|
-
if (!events.value.length) return null;
|
|
996
|
-
// Find last event with valid timestamp
|
|
997
|
-
for (let i = events.value.length - 1; i >= 0; i--) {
|
|
998
|
-
const ev = events.value[i];
|
|
999
|
-
const ts = ev.timestamp || ev.snapshot?.timestamp;
|
|
1000
|
-
if (ts) return new Date(ts).getTime();
|
|
1001
|
-
}
|
|
1002
|
-
return null;
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
const totalDuration = computed(() => {
|
|
1006
|
-
if (!sessionStart.value || !sessionEnd.value) return 0;
|
|
1007
|
-
return sessionEnd.value - sessionStart.value;
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
// ── Shared sorted events (computed once, reused everywhere) ──
|
|
1011
|
-
// Stable sort: use _fileIndex (set by backend) as tiebreaker for identical timestamps
|
|
1012
|
-
const sortedEvents = computed(() => {
|
|
1013
|
-
return [...events.value].sort((a, b) => {
|
|
1014
|
-
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
1015
|
-
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
1016
|
-
if (timeA !== timeB) return timeA - timeB;
|
|
1017
|
-
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
1018
|
-
});
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// ── Map tool.execution_start events to owning subagent via parentToolCallId ──
|
|
1022
|
-
const subagentToolMap = computed(() => {
|
|
1023
|
-
const sorted = sortedEvents.value;
|
|
1024
|
-
|
|
1025
|
-
// 1. Collect all subagent toolCallIds
|
|
1026
|
-
const subagentToolCallIds = new Set();
|
|
1027
|
-
for (const ev of sorted) {
|
|
1028
|
-
if (ev.type === 'subagent.started') {
|
|
1029
|
-
const tcid = ev.data?.toolCallId;
|
|
1030
|
-
if (tcid) subagentToolCallIds.add(tcid);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// 2. Build id → event lookup
|
|
1035
|
-
const idMap = new Map();
|
|
1036
|
-
for (const ev of sorted) {
|
|
1037
|
-
if (ev.id) idMap.set(ev.id, ev);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// 3. For each tool.execution_start, walk parentId to find assistant.message,
|
|
1041
|
-
// then read data.parentToolCallId
|
|
1042
|
-
const toolToSubagent = new Map(); // tool event id → subagent toolCallId
|
|
1043
|
-
const startIdByToolCallId = new Map(); // data.toolCallId → subagent toolCallId (for matching complete events)
|
|
1044
|
-
for (const ev of sorted) {
|
|
1045
|
-
if (ev.type !== 'tool.execution_start') continue;
|
|
1046
|
-
let current = ev.parentId;
|
|
1047
|
-
let depth = 0;
|
|
1048
|
-
while (current && depth < 10) {
|
|
1049
|
-
const parent = idMap.get(current);
|
|
1050
|
-
if (!parent) break;
|
|
1051
|
-
if (parent.type === 'assistant.message') {
|
|
1052
|
-
const ptcid = parent.data?.parentToolCallId;
|
|
1053
|
-
if (ptcid && subagentToolCallIds.has(ptcid)) {
|
|
1054
|
-
toolToSubagent.set(ev.id, ptcid);
|
|
1055
|
-
const tcid = ev.data?.toolCallId;
|
|
1056
|
-
if (tcid) startIdByToolCallId.set(tcid, ptcid);
|
|
1057
|
-
}
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
current = parent.parentId;
|
|
1061
|
-
depth++;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// 4. Map tool.execution_complete events via their toolCallId
|
|
1066
|
-
for (const ev of sorted) {
|
|
1067
|
-
if (ev.type !== 'tool.execution_complete') continue;
|
|
1068
|
-
const tcid = ev.data?.toolCallId;
|
|
1069
|
-
if (tcid && startIdByToolCallId.has(tcid)) {
|
|
1070
|
-
toolToSubagent.set(ev.id, startIdByToolCallId.get(tcid));
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
return toolToSubagent;
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
// ── Sub-agent analysis ──
|
|
1078
|
-
const subagentAnalysis = computed(() => {
|
|
1079
|
-
const sorted = sortedEvents.value;
|
|
1080
|
-
const results = [];
|
|
1081
|
-
const startStack = [];
|
|
1082
|
-
|
|
1083
|
-
for (const ev of sorted) {
|
|
1084
|
-
if (ev.type === 'subagent.started') {
|
|
1085
|
-
startStack.push(ev);
|
|
1086
|
-
} else if (ev.type === 'subagent.completed' || ev.type === 'subagent.failed') {
|
|
1087
|
-
// Find matching start by toolCallId
|
|
1088
|
-
const tcid = ev.data?.toolCallId;
|
|
1089
|
-
let startIdx = -1;
|
|
1090
|
-
if (tcid) {
|
|
1091
|
-
for (let i = startStack.length - 1; i >= 0; i--) {
|
|
1092
|
-
if (startStack[i].data?.toolCallId === tcid) {
|
|
1093
|
-
startIdx = i;
|
|
1094
|
-
break;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
// Fallback to LIFO if no toolCallId match (pop last started)
|
|
1099
|
-
if (startIdx < 0 && startStack.length > 0) {
|
|
1100
|
-
startIdx = startStack.length - 1;
|
|
1101
|
-
}
|
|
1102
|
-
const startEv = startIdx >= 0 ? startStack.splice(startIdx, 1)[0] : null;
|
|
1103
|
-
// Extract name from matched start event (completed event doesn't have name)
|
|
1104
|
-
const name = startEv?.data?.agentDisplayName || startEv?.data?.agentName || 'SubAgent';
|
|
1105
|
-
const startTime = startEv ? new Date(startEv.timestamp).getTime() : null;
|
|
1106
|
-
const endTime = new Date(ev.timestamp).getTime();
|
|
1107
|
-
const duration = startTime ? endTime - startTime : null;
|
|
1108
|
-
|
|
1109
|
-
// Count tool calls using parentToolCallId attribution (not time-window)
|
|
1110
|
-
const subagentTcid = startEv?.data?.toolCallId;
|
|
1111
|
-
let toolCalls = 0;
|
|
1112
|
-
const innerEvents = [];
|
|
1113
|
-
if (startEv) {
|
|
1114
|
-
for (const e of sorted) {
|
|
1115
|
-
if (e.type === 'tool.execution_start') {
|
|
1116
|
-
if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1117
|
-
toolCalls++;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
// Keep time-window for innerEvents/markers (visual only)
|
|
1121
|
-
const t = new Date(e.timestamp).getTime();
|
|
1122
|
-
if (t >= startTime && t <= endTime) {
|
|
1123
|
-
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1124
|
-
if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
|
|
1125
|
-
// Non-tool trackable events: use time-window
|
|
1126
|
-
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1127
|
-
} else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1128
|
-
// Tool events: only include if attributed to this subagent
|
|
1129
|
-
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Build event markers with clustering
|
|
1137
|
-
const innerEventMarkers = buildEventMarkers(innerEvents, startTime, duration);
|
|
1138
|
-
|
|
1139
|
-
results.push({
|
|
1140
|
-
name,
|
|
1141
|
-
status: ev.type === 'subagent.completed' ? 'completed' : 'failed',
|
|
1142
|
-
startTime: startEv?.timestamp || null,
|
|
1143
|
-
endTime: ev.timestamp,
|
|
1144
|
-
duration,
|
|
1145
|
-
toolCalls,
|
|
1146
|
-
innerEventMarkers
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// Handle incomplete sub-agents (started but never completed/failed)
|
|
1152
|
-
const sessionEndTime = sorted.length > 0 ? new Date(sorted[sorted.length - 1].timestamp).getTime() : Date.now();
|
|
1153
|
-
for (const startEv of startStack) {
|
|
1154
|
-
const name = startEv.data?.agentDisplayName || startEv.data?.agentName || 'SubAgent';
|
|
1155
|
-
const startTime = new Date(startEv.timestamp).getTime();
|
|
1156
|
-
const duration = sessionEndTime - startTime;
|
|
1157
|
-
|
|
1158
|
-
// Count tool calls using parentToolCallId attribution (not time-window)
|
|
1159
|
-
const subagentTcid = startEv.data?.toolCallId;
|
|
1160
|
-
let toolCalls = 0;
|
|
1161
|
-
const innerEvents = [];
|
|
1162
|
-
for (const e of sorted) {
|
|
1163
|
-
if (e.type === 'tool.execution_start') {
|
|
1164
|
-
if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1165
|
-
toolCalls++;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
// Keep time-window for innerEvents/markers (visual only)
|
|
1169
|
-
const t = new Date(e.timestamp).getTime();
|
|
1170
|
-
if (t >= startTime && t <= sessionEndTime) {
|
|
1171
|
-
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1172
|
-
if (e.type !== 'tool.execution_start' && e.type !== 'tool.execution_complete') {
|
|
1173
|
-
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1174
|
-
} else if (subagentTcid && subagentToolMap.value.get(e.id) === subagentTcid) {
|
|
1175
|
-
innerEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const innerEventMarkers = buildEventMarkers(innerEvents, startTime, duration);
|
|
1182
|
-
|
|
1183
|
-
results.push({
|
|
1184
|
-
name,
|
|
1185
|
-
status: 'incomplete',
|
|
1186
|
-
startTime: startEv.timestamp,
|
|
1187
|
-
endTime: sorted[sorted.length - 1]?.timestamp || startEv.timestamp,
|
|
1188
|
-
duration,
|
|
1189
|
-
toolCalls,
|
|
1190
|
-
innerEventMarkers
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
return results.sort((a, b) => {
|
|
1195
|
-
const tA = a.startTime ? new Date(a.startTime).getTime() : 0;
|
|
1196
|
-
const tB = b.startTime ? new Date(b.startTime).getTime() : 0;
|
|
1197
|
-
return tA - tB;
|
|
1198
|
-
});
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
const maxSubagentDuration = computed(() => {
|
|
1202
|
-
return Math.max(...subagentAnalysis.value.map(s => s.duration || 0), 1);
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
const subagentStats = computed(() => {
|
|
1206
|
-
const agents = subagentAnalysis.value;
|
|
1207
|
-
const completed = agents.filter(a => a.status === 'completed').length;
|
|
1208
|
-
const failed = agents.filter(a => a.status === 'failed').length;
|
|
1209
|
-
const incomplete = agents.filter(a => a.status === 'incomplete').length;
|
|
1210
|
-
const successRate = agents.length ? ((completed / agents.length) * 100).toFixed(0) : 100;
|
|
1211
|
-
|
|
1212
|
-
// Merge overlapping intervals to get actual wall-clock time in subagents
|
|
1213
|
-
const intervals = agents
|
|
1214
|
-
.filter(a => a.startTime && a.endTime)
|
|
1215
|
-
.map(a => [new Date(a.startTime).getTime(), new Date(a.endTime).getTime()])
|
|
1216
|
-
.sort((a, b) => a[0] - b[0]);
|
|
1217
|
-
const mergedIntervals = [];
|
|
1218
|
-
for (const [s, e] of intervals) {
|
|
1219
|
-
if (!mergedIntervals.length || s >= mergedIntervals[mergedIntervals.length - 1].e) {
|
|
1220
|
-
mergedIntervals.push({ s, e });
|
|
1221
|
-
} else if (e > mergedIntervals[mergedIntervals.length - 1].e) {
|
|
1222
|
-
mergedIntervals[mergedIntervals.length - 1].e = e;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
const totalTime = mergedIntervals.reduce((sum, iv) => sum + (iv.e - iv.s), 0);
|
|
1226
|
-
|
|
1227
|
-
// Sum per-subagent tool counts (already correctly attributed via parentToolCallId)
|
|
1228
|
-
const totalTools = agents.reduce((sum, a) => sum + (a.toolCalls || 0), 0);
|
|
1229
|
-
|
|
1230
|
-
return { completed, failed, incomplete, totalTime, totalTools, successRate };
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
// ── Event marker builder (shared by subagent + agent-op) ──
|
|
1234
|
-
const buildEventMarkers = (innerEvents, startTime, duration) => {
|
|
1235
|
-
if (!innerEvents.length || !duration) return [];
|
|
1236
|
-
|
|
1237
|
-
// Separate high-priority events (always shown individually) from tool events (clustered)
|
|
1238
|
-
const HIGH_PRIORITY_TYPES = new Set([
|
|
1239
|
-
'session.start', 'session.resume', 'session.error',
|
|
1240
|
-
'session.truncation', 'session.compaction_start', 'session.compaction_complete',
|
|
1241
|
-
'session.model_change', 'abort', 'user.message',
|
|
1242
|
-
]);
|
|
1243
|
-
|
|
1244
|
-
const hiPriEvents = [];
|
|
1245
|
-
const toolEvents = [];
|
|
1246
|
-
for (const ev of innerEvents) {
|
|
1247
|
-
if (HIGH_PRIORITY_TYPES.has(ev.type)) {
|
|
1248
|
-
hiPriEvents.push(ev);
|
|
1249
|
-
} else {
|
|
1250
|
-
toolEvents.push(ev);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Build individual markers for high-priority events (never clustered)
|
|
1255
|
-
const hiPriMarkers = hiPriEvents.map(ev => {
|
|
1256
|
-
const relPos = ((ev.timestamp - startTime) / duration) * 100;
|
|
1257
|
-
const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
|
|
1258
|
-
return {
|
|
1259
|
-
type: ev.type,
|
|
1260
|
-
position: Math.max(0, Math.min(100, relPos)),
|
|
1261
|
-
color: cat.color || '#8b949e',
|
|
1262
|
-
shape: cat.shape || 'circle',
|
|
1263
|
-
label: cat.label || ev.type,
|
|
1264
|
-
timestamp: ev.timestamp,
|
|
1265
|
-
toolName: ev.data?.toolName || null,
|
|
1266
|
-
};
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
// Cluster tool events into fixed time buckets
|
|
1270
|
-
// Adaptive: aim for ~20 buckets max, with a minimum of 5 minutes per bucket
|
|
1271
|
-
const MIN_BUCKET_MS = 5 * 60 * 1000; // 5 minutes
|
|
1272
|
-
const bucketSize = Math.max(MIN_BUCKET_MS, duration / 20);
|
|
1273
|
-
|
|
1274
|
-
const buckets = new Map(); // bucketIndex -> events[]
|
|
1275
|
-
for (const ev of toolEvents) {
|
|
1276
|
-
const bucketIdx = Math.floor((ev.timestamp - startTime) / bucketSize);
|
|
1277
|
-
if (!buckets.has(bucketIdx)) buckets.set(bucketIdx, []);
|
|
1278
|
-
buckets.get(bucketIdx).push(ev);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// Helper: interpolate color from tool yellow to error red based on error ratio
|
|
1282
|
-
const toolErrorColor = (errorRatio) => {
|
|
1283
|
-
// 0 = #d29922 (yellow), 1 = #f85149 (red)
|
|
1284
|
-
const r = Math.round(210 + (248 - 210) * errorRatio);
|
|
1285
|
-
const g = Math.round(153 + (81 - 153) * errorRatio);
|
|
1286
|
-
const b = Math.round(34 + (73 - 34) * errorRatio);
|
|
1287
|
-
return 'rgb(' + r + ',' + g + ',' + b + ')';
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1290
|
-
const isToolError = (ev) => {
|
|
1291
|
-
return ev.type === 'tool.execution_complete' && (ev.data?.isError || !!ev.data?.error);
|
|
1292
|
-
};
|
|
1293
|
-
|
|
1294
|
-
const toolMarkers = [];
|
|
1295
|
-
for (const [bucketIdx, group] of buckets) {
|
|
1296
|
-
const bucketMid = startTime + (bucketIdx + 0.5) * bucketSize;
|
|
1297
|
-
const relPos = ((bucketMid - startTime) / duration) * 100;
|
|
1298
|
-
const pos = Math.max(0, Math.min(100, relPos));
|
|
1299
|
-
|
|
1300
|
-
// Count errors in this bucket
|
|
1301
|
-
const errorCount = group.filter(isToolError).length;
|
|
1302
|
-
const completeCount = group.filter(ev => ev.type === 'tool.execution_complete').length;
|
|
1303
|
-
const errorRatio = completeCount > 0 ? errorCount / completeCount : 0;
|
|
1304
|
-
|
|
1305
|
-
if (group.length === 1) {
|
|
1306
|
-
const ev = group[0];
|
|
1307
|
-
const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
|
|
1308
|
-
const color = isToolError(ev) ? '#f85149' : cat.color || '#8b949e';
|
|
1309
|
-
toolMarkers.push({
|
|
1310
|
-
type: ev.type,
|
|
1311
|
-
position: pos,
|
|
1312
|
-
color,
|
|
1313
|
-
shape: cat.shape || 'circle',
|
|
1314
|
-
label: isToolError(ev) ? (cat.label || ev.type) + ' (error)' : (cat.label || ev.type),
|
|
1315
|
-
timestamp: ev.timestamp,
|
|
1316
|
-
toolName: ev.data?.toolName || null,
|
|
1317
|
-
});
|
|
1318
|
-
} else {
|
|
1319
|
-
// Summarize the cluster
|
|
1320
|
-
const typeCounts = {};
|
|
1321
|
-
group.forEach(ev => {
|
|
1322
|
-
const cat = EVENT_MARKER_CATEGORIES[ev.type] || {};
|
|
1323
|
-
const lbl = cat.label || ev.type;
|
|
1324
|
-
typeCounts[lbl] = (typeCounts[lbl] || 0) + 1;
|
|
1325
|
-
});
|
|
1326
|
-
if (errorCount > 0) typeCounts['Errors'] = errorCount;
|
|
1327
|
-
const summaryParts = Object.entries(typeCounts).map(([l, c]) => c + ' ' + l);
|
|
1328
|
-
const clusterColor = errorRatio > 0 ? toolErrorColor(errorRatio) : ((() => {
|
|
1329
|
-
const dominantType = group.reduce((best, ev) => {
|
|
1330
|
-
const cnt = group.filter(e => e.type === ev.type).length;
|
|
1331
|
-
return cnt > best.cnt ? { type: ev.type, cnt } : best;
|
|
1332
|
-
}, { type: group[0].type, cnt: 0 }).type;
|
|
1333
|
-
return (EVENT_MARKER_CATEGORIES[dominantType] || {}).color || '#8b949e';
|
|
1334
|
-
})());
|
|
1335
|
-
|
|
1336
|
-
toolMarkers.push({
|
|
1337
|
-
type: 'cluster',
|
|
1338
|
-
position: pos,
|
|
1339
|
-
color: clusterColor,
|
|
1340
|
-
shape: 'cluster',
|
|
1341
|
-
label: summaryParts.join(', '),
|
|
1342
|
-
count: group.length,
|
|
1343
|
-
items: group,
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// Merge and sort by position
|
|
1349
|
-
return [...hiPriMarkers, ...toolMarkers].sort((a, b) => a.position - b.position);
|
|
1350
|
-
};
|
|
1351
|
-
|
|
1352
|
-
// ── Build Agent Operation item for gaps ──
|
|
1353
|
-
const buildAgentOpItem = (sorted, gapStart, gapEnd) => {
|
|
1354
|
-
const duration = gapEnd - gapStart;
|
|
1355
|
-
const gapEvents = [];
|
|
1356
|
-
const eventCounts = {};
|
|
1357
|
-
let toolCalls = 0;
|
|
1358
|
-
for (const e of sorted) {
|
|
1359
|
-
const t = new Date(e.timestamp).getTime();
|
|
1360
|
-
if (t >= gapStart && t <= gapEnd) {
|
|
1361
|
-
// For tool events, only include those NOT attributed to a subagent
|
|
1362
|
-
if (e.type.startsWith('tool.')) {
|
|
1363
|
-
const isSubagentTool = e.id && subagentToolMap.value.has(e.id);
|
|
1364
|
-
if (!isSubagentTool) {
|
|
1365
|
-
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1366
|
-
gapEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1367
|
-
}
|
|
1368
|
-
if (e.type === 'tool.execution_start') {
|
|
1369
|
-
toolCalls++;
|
|
1370
|
-
}
|
|
1371
|
-
eventCounts.tool = (eventCounts.tool || 0) + 1;
|
|
1372
|
-
}
|
|
1373
|
-
} else {
|
|
1374
|
-
if (TRACKABLE_EVENT_TYPES.has(e.type)) {
|
|
1375
|
-
gapEvents.push({ type: e.type, timestamp: t, data: e.data });
|
|
1376
|
-
}
|
|
1377
|
-
let cat = 'other';
|
|
1378
|
-
if (e.type.startsWith('assistant.')) cat = 'message';
|
|
1379
|
-
else if (e.type.startsWith('user.')) cat = 'user';
|
|
1380
|
-
else if (e.type.startsWith('session.')) cat = 'session';
|
|
1381
|
-
eventCounts[cat] = (eventCounts[cat] || 0) + 1;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// Build summary string
|
|
1387
|
-
const parts = [];
|
|
1388
|
-
if (toolCalls) parts.push(toolCalls + ' tool' + (toolCalls > 1 ? 's' : ''));
|
|
1389
|
-
if (eventCounts.message) parts.push(eventCounts.message + ' message' + (eventCounts.message > 1 ? 's' : ''));
|
|
1390
|
-
if (eventCounts.user) parts.push(eventCounts.user + ' user msg');
|
|
1391
|
-
if (eventCounts.session) parts.push(eventCounts.session + ' session event' + (eventCounts.session > 1 ? 's' : ''));
|
|
1392
|
-
if (eventCounts.other) parts.push(eventCounts.other + ' other');
|
|
1393
|
-
const summary = parts.length ? parts.join(', ') : 'idle';
|
|
1394
|
-
|
|
1395
|
-
const innerEventMarkers = buildEventMarkers(gapEvents, gapStart, duration);
|
|
1396
|
-
|
|
1397
|
-
return {
|
|
1398
|
-
itemType: 'agent-op',
|
|
1399
|
-
name: 'Main Agent',
|
|
1400
|
-
summary,
|
|
1401
|
-
toolCalls,
|
|
1402
|
-
startTime: new Date(gapStart).toISOString(),
|
|
1403
|
-
endTime: new Date(gapEnd).toISOString(),
|
|
1404
|
-
duration,
|
|
1405
|
-
eventCounts,
|
|
1406
|
-
innerEventMarkers,
|
|
1407
|
-
};
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
// ── Subagent timeline items (subagent bars + agent-op gaps) ──
|
|
1411
|
-
const subagentTimelineItems = computed(() => {
|
|
1412
|
-
const agents = subagentAnalysis.value;
|
|
1413
|
-
if (!agents.length) return [];
|
|
1414
|
-
|
|
1415
|
-
const sorted = sortedEvents.value;
|
|
1416
|
-
const items = [];
|
|
1417
|
-
|
|
1418
|
-
for (let i = 0; i < agents.length; i++) {
|
|
1419
|
-
const sa = agents[i];
|
|
1420
|
-
|
|
1421
|
-
// Before first subagent: check gap from session start
|
|
1422
|
-
if (i === 0 && sa.startTime) {
|
|
1423
|
-
const gapStart = sessionStart.value;
|
|
1424
|
-
const gapEnd = new Date(sa.startTime).getTime();
|
|
1425
|
-
if (gapEnd - gapStart > 500) {
|
|
1426
|
-
items.push(buildAgentOpItem(sorted, gapStart, gapEnd));
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// Add subagent itself
|
|
1431
|
-
items.push({ ...sa, itemType: 'subagent' });
|
|
1432
|
-
|
|
1433
|
-
// Gap between this and next subagent (or session end)
|
|
1434
|
-
const nextSa = agents[i + 1];
|
|
1435
|
-
const gapStart = new Date(sa.endTime).getTime();
|
|
1436
|
-
const gapEnd = nextSa
|
|
1437
|
-
? new Date(nextSa.startTime).getTime()
|
|
1438
|
-
: sessionEnd.value;
|
|
1439
|
-
|
|
1440
|
-
if (gapEnd - gapStart > 500) {
|
|
1441
|
-
items.push(buildAgentOpItem(sorted, gapStart, gapEnd));
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
return items;
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
// ── Turn analysis ──
|
|
1449
|
-
const turnAnalysis = computed(() => {
|
|
1450
|
-
try {
|
|
1451
|
-
const sorted = sortedEvents.value;
|
|
1452
|
-
const assistantMessages = sorted.filter(e => e.type === 'assistant.message');
|
|
1453
|
-
const allUserMessages = sorted.filter(e => e.type === 'user.message');
|
|
1454
|
-
|
|
1455
|
-
return assistantMessages.map((msg, idx) => {
|
|
1456
|
-
const ts = msg.timestamp;
|
|
1457
|
-
if (!ts) {
|
|
1458
|
-
console.warn('[turnAnalysis] Message without timestamp:', msg);
|
|
1459
|
-
return null;
|
|
1460
|
-
}
|
|
1461
|
-
const startTime = new Date(ts).getTime();
|
|
1462
|
-
if (isNaN(startTime)) {
|
|
1463
|
-
console.warn('[turnAnalysis] Invalid timestamp:', ts, msg);
|
|
1464
|
-
return null;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
const nextMsg = assistantMessages[idx + 1];
|
|
1468
|
-
const endTime = nextMsg
|
|
1469
|
-
? new Date(nextMsg.timestamp).getTime()
|
|
1470
|
-
: sessionEnd.value || startTime;
|
|
1471
|
-
const duration = endTime - startTime;
|
|
1472
|
-
|
|
1473
|
-
// Find user message before this assistant message
|
|
1474
|
-
const msgIndex = sorted.indexOf(msg);
|
|
1475
|
-
const userMessage = sorted
|
|
1476
|
-
.slice(0, msgIndex)
|
|
1477
|
-
.reverse()
|
|
1478
|
-
.find(e => e.type === 'user.message');
|
|
1479
|
-
|
|
1480
|
-
const userReqNumber = userMessage
|
|
1481
|
-
? allUserMessages.indexOf(userMessage) + 1
|
|
1482
|
-
: 0;
|
|
1483
|
-
|
|
1484
|
-
// Extract display text
|
|
1485
|
-
let displayText = '';
|
|
1486
|
-
const hasText = msg.data?.message && msg.data.message.trim() !== '';
|
|
1487
|
-
|
|
1488
|
-
if (hasText) {
|
|
1489
|
-
displayText = normalizeMessage(msg.data.message);
|
|
1490
|
-
} else if (msg.data?.tools && msg.data.tools.length > 0) {
|
|
1491
|
-
// Only tool calls, show tool names
|
|
1492
|
-
const toolNames = msg.data.tools.map(t => t.name || 'unknown').join(', ');
|
|
1493
|
-
displayText = `Tool calls: ${toolNames}`;
|
|
1494
|
-
} else {
|
|
1495
|
-
displayText = '(empty assistant message)';
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Count tool calls
|
|
1499
|
-
const toolCalls = msg.data?.tools?.length || 0;
|
|
1500
|
-
|
|
1501
|
-
return {
|
|
1502
|
-
turnId: msg.id ?? `msg-${idx}`,
|
|
1503
|
-
userReqNumber,
|
|
1504
|
-
message: normalizeMessage(userMessage?.data?.message || userMessage?.data?.content || userMessage?.data?.transformedContent || ''),
|
|
1505
|
-
displayText,
|
|
1506
|
-
hasText,
|
|
1507
|
-
startTime: msg.timestamp,
|
|
1508
|
-
endTime: nextMsg?.timestamp || events.value[events.value.length - 1]?.timestamp,
|
|
1509
|
-
duration,
|
|
1510
|
-
toolCalls
|
|
1511
|
-
};
|
|
1512
|
-
}).filter(t => t !== null);
|
|
1513
|
-
} catch (err) {
|
|
1514
|
-
console.error('[turnAnalysis] Error:', err);
|
|
1515
|
-
error.value = 'Error analyzing turns: ' + err.message;
|
|
1516
|
-
return [];
|
|
1517
|
-
}
|
|
1518
|
-
});
|
|
1519
|
-
|
|
1520
|
-
const maxTurnDuration = computed(() => {
|
|
1521
|
-
return Math.max(...turnAnalysis.value.map(t => t.duration || 0), 1);
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
// ── Grouped turns by UserReq ──
|
|
1525
|
-
const groupedTurns = computed(() => {
|
|
1526
|
-
const groups = new Map();
|
|
1527
|
-
|
|
1528
|
-
for (const turn of turnAnalysis.value) {
|
|
1529
|
-
const reqNum = turn.userReqNumber || 0;
|
|
1530
|
-
if (!groups.has(reqNum)) {
|
|
1531
|
-
groups.set(reqNum, {
|
|
1532
|
-
userReqNumber: reqNum,
|
|
1533
|
-
message: turn.message,
|
|
1534
|
-
turns: []
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
groups.get(reqNum).turns.push(turn);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
return Array.from(groups.values()).sort((a, b) => a.userReqNumber - b.userReqNumber);
|
|
1541
|
-
});
|
|
1542
|
-
|
|
1543
|
-
// ── Tool operations analysis ──
|
|
1544
|
-
const toolAnalysis = computed(() => {
|
|
1545
|
-
const sorted = sortedEvents.value;
|
|
1546
|
-
const toolGroups = new Map();
|
|
1547
|
-
|
|
1548
|
-
for (const ev of sorted) {
|
|
1549
|
-
if (ev.type === 'tool.execution_start') {
|
|
1550
|
-
const toolId = ev.data?.toolCallId;
|
|
1551
|
-
if (toolId) {
|
|
1552
|
-
toolGroups.set(toolId, { start: ev });
|
|
1553
|
-
}
|
|
1554
|
-
} else if (ev.type === 'tool.execution_complete') {
|
|
1555
|
-
const toolId = ev.data?.toolCallId;
|
|
1556
|
-
if (toolId && toolGroups.has(toolId)) {
|
|
1557
|
-
toolGroups.get(toolId).complete = ev;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
const results = [];
|
|
1563
|
-
toolGroups.forEach((group, toolId) => {
|
|
1564
|
-
const startTime = new Date(group.start.timestamp).getTime();
|
|
1565
|
-
const endTime = group.complete
|
|
1566
|
-
? new Date(group.complete.timestamp).getTime()
|
|
1567
|
-
: null;
|
|
1568
|
-
const duration = endTime ? endTime - startTime : null;
|
|
1569
|
-
const toolName = group.start.data?.toolName || group.start.data?.tool || 'unknown';
|
|
1570
|
-
const args = group.start.data?.arguments || {};
|
|
1571
|
-
const isError = group.complete?.data?.isError || !!group.complete?.data?.error;
|
|
1572
|
-
|
|
1573
|
-
// Extract file path or command
|
|
1574
|
-
let description = '';
|
|
1575
|
-
if (toolName === 'Bash' || toolName === 'bash' || toolName === 'exec') {
|
|
1576
|
-
description = args.command || args.description || '';
|
|
1577
|
-
} else if (['Read', 'read', 'Write', 'write', 'Edit', 'edit'].includes(toolName)) {
|
|
1578
|
-
description = args.file_path || args.path || '';
|
|
1579
|
-
} else if (['Glob', 'glob'].includes(toolName)) {
|
|
1580
|
-
description = args.pattern || '';
|
|
1581
|
-
} else if (['Grep', 'grep'].includes(toolName)) {
|
|
1582
|
-
description = args.pattern || '';
|
|
1583
|
-
} else if (['Task', 'task'].includes(toolName)) {
|
|
1584
|
-
description = args.description || args.prompt?.substring(0, 80) || '';
|
|
1585
|
-
} else {
|
|
1586
|
-
description = args.description || args.command || args.file_path ||
|
|
1587
|
-
args.path || args.query || args.url || '';
|
|
1588
|
-
}
|
|
1589
|
-
if (description.length > 120) {
|
|
1590
|
-
description = description.substring(0, 120) + '...';
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
results.push({
|
|
1594
|
-
toolId,
|
|
1595
|
-
toolName,
|
|
1596
|
-
description,
|
|
1597
|
-
startTime: group.start.timestamp,
|
|
1598
|
-
endTime: group.complete?.timestamp || null,
|
|
1599
|
-
duration,
|
|
1600
|
-
isError,
|
|
1601
|
-
isRunning: !group.complete
|
|
1602
|
-
});
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
return results;
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
const sortedToolAnalysis = computed(() => {
|
|
1609
|
-
const items = [...toolAnalysis.value];
|
|
1610
|
-
items.sort((a, b) => {
|
|
1611
|
-
if (sortField.value === 'duration') {
|
|
1612
|
-
return sortDir.value === 'asc'
|
|
1613
|
-
? (a.duration || 0) - (b.duration || 0)
|
|
1614
|
-
: (b.duration || 0) - (a.duration || 0);
|
|
1615
|
-
}
|
|
1616
|
-
if (sortField.value === 'toolName') {
|
|
1617
|
-
const cmp = (a.toolName || '').localeCompare(b.toolName || '');
|
|
1618
|
-
return sortDir.value === 'asc' ? cmp : -cmp;
|
|
1619
|
-
}
|
|
1620
|
-
// default: timestamp
|
|
1621
|
-
const tA = new Date(a.startTime).getTime();
|
|
1622
|
-
const tB = new Date(b.startTime).getTime();
|
|
1623
|
-
return sortDir.value === 'asc' ? tA - tB : tB - tA;
|
|
1624
|
-
});
|
|
1625
|
-
return items;
|
|
1626
|
-
});
|
|
1627
|
-
|
|
1628
|
-
const maxToolDuration = computed(() => {
|
|
1629
|
-
return Math.max(...toolAnalysis.value.map(t => t.duration || 0), 1);
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// ── File operations ──
|
|
1633
|
-
const fileOperations = computed(() => {
|
|
1634
|
-
const fileTools = ['view', 'read', 'write', 'edit', 'create', 'glob', 'grep', 'notebookedit'];
|
|
1635
|
-
// VSCode Copilot Chat tool name mappings
|
|
1636
|
-
const vsCodeFileToolMap = {
|
|
1637
|
-
'copilot_readfile': 'read',
|
|
1638
|
-
'copilot_createfile': 'write',
|
|
1639
|
-
'copilot_createdirectory': 'write',
|
|
1640
|
-
'copilot_findfiles': 'search',
|
|
1641
|
-
'copilot_findtextinfiles': 'search',
|
|
1642
|
-
'copilot_listdirectory': 'read',
|
|
1643
|
-
'textedit': 'edit',
|
|
1644
|
-
'copilot_replacestring': 'edit',
|
|
1645
|
-
'copilot_multireplacestring': 'edit',
|
|
1646
|
-
};
|
|
1647
|
-
const ops = [];
|
|
1648
|
-
|
|
1649
|
-
for (const ev of events.value) {
|
|
1650
|
-
if (ev.type === 'tool.execution_start') {
|
|
1651
|
-
const toolName = ev.data?.toolName?.toLowerCase() || '';
|
|
1652
|
-
const args = ev.data?.arguments || {};
|
|
1653
|
-
const path = args.path || args.file || args.directory || args.pattern || '';
|
|
1654
|
-
|
|
1655
|
-
// Check standard file tools
|
|
1656
|
-
if (fileTools.includes(toolName)) {
|
|
1657
|
-
if (path) {
|
|
1658
|
-
let opType = 'other';
|
|
1659
|
-
if (toolName === 'view' || toolName === 'read') opType = 'read';
|
|
1660
|
-
else if (toolName === 'write' || toolName === 'notebookedit' || toolName === 'create') opType = 'write';
|
|
1661
|
-
else if (toolName === 'edit') opType = 'edit';
|
|
1662
|
-
else if (toolName === 'glob' || toolName === 'grep') opType = 'search';
|
|
1663
|
-
|
|
1664
|
-
ops.push({
|
|
1665
|
-
toolName: ev.data?.toolName || toolName,
|
|
1666
|
-
opType,
|
|
1667
|
-
filePath: path,
|
|
1668
|
-
timestamp: ev.timestamp,
|
|
1669
|
-
startTime: ev.timestamp
|
|
1670
|
-
});
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
// Check VSCode file tools
|
|
1674
|
-
else if (vsCodeFileToolMap[toolName]) {
|
|
1675
|
-
const opType = vsCodeFileToolMap[toolName];
|
|
1676
|
-
ops.push({
|
|
1677
|
-
toolName: ev.data?.toolName || toolName,
|
|
1678
|
-
opType,
|
|
1679
|
-
filePath: path || '(implicit)',
|
|
1680
|
-
timestamp: ev.timestamp,
|
|
1681
|
-
startTime: ev.timestamp
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
return ops.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
|
|
1688
|
-
});
|
|
1689
|
-
|
|
1690
|
-
const fileStats = computed(() => {
|
|
1691
|
-
const ops = fileOperations.value;
|
|
1692
|
-
const uniqueFiles = new Set(ops.map(o => o.filePath));
|
|
1693
|
-
return {
|
|
1694
|
-
uniqueCount: uniqueFiles.size,
|
|
1695
|
-
totalOps: ops.length,
|
|
1696
|
-
reads: ops.filter(o => o.opType === 'read').length,
|
|
1697
|
-
writes: ops.filter(o => o.opType === 'write').length,
|
|
1698
|
-
edits: ops.filter(o => o.opType === 'edit').length,
|
|
1699
|
-
searches: ops.filter(o => o.opType === 'search').length
|
|
1700
|
-
};
|
|
1701
|
-
});
|
|
1702
|
-
|
|
1703
|
-
// ── Tool time by category ──
|
|
1704
|
-
const toolTimeByCategory = computed(() => {
|
|
1705
|
-
const catMap = {};
|
|
1706
|
-
toolAnalysis.value.forEach(t => {
|
|
1707
|
-
const name = (t.toolName || 'unknown').toLowerCase();
|
|
1708
|
-
let cat;
|
|
1709
|
-
if (['bash', 'exec'].includes(name)) cat = 'Bash/Exec';
|
|
1710
|
-
else if (['read'].includes(name)) cat = 'Read';
|
|
1711
|
-
else if (['write'].includes(name)) cat = 'Write';
|
|
1712
|
-
else if (['edit'].includes(name)) cat = 'Edit';
|
|
1713
|
-
else if (['glob'].includes(name)) cat = 'Glob';
|
|
1714
|
-
else if (['grep'].includes(name)) cat = 'Grep';
|
|
1715
|
-
else if (['task'].includes(name)) cat = 'Task (SubAgent)';
|
|
1716
|
-
else if (['web_search', 'websearch'].includes(name)) cat = 'Web Search';
|
|
1717
|
-
else if (['web_fetch', 'webfetch'].includes(name)) cat = 'Web Fetch';
|
|
1718
|
-
else cat = t.toolName || 'Other';
|
|
1719
|
-
|
|
1720
|
-
if (!catMap[cat]) {
|
|
1721
|
-
catMap[cat] = { category: cat, totalTime: 0, count: 0, errors: 0 };
|
|
1722
|
-
}
|
|
1723
|
-
catMap[cat].totalTime += (t.duration || 0);
|
|
1724
|
-
catMap[cat].count++;
|
|
1725
|
-
if (t.isError) catMap[cat].errors++;
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
return Object.values(catMap).sort((a, b) => b.totalTime - a.totalTime);
|
|
1729
|
-
});
|
|
1730
|
-
|
|
1731
|
-
const maxCategoryTime = computed(() => {
|
|
1732
|
-
return Math.max(...toolTimeByCategory.value.map(c => c.totalTime), 1);
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
// ── Summary stats ──
|
|
1736
|
-
// Wall-clock tool time: merge overlapping intervals to avoid double-counting parallel tools
|
|
1737
|
-
const totalToolTime = computed(() => {
|
|
1738
|
-
const intervals = toolAnalysis.value
|
|
1739
|
-
.filter(t => t.duration && t.startTime && t.endTime)
|
|
1740
|
-
.map(t => ({
|
|
1741
|
-
start: new Date(t.startTime).getTime(),
|
|
1742
|
-
end: new Date(t.endTime).getTime()
|
|
1743
|
-
}))
|
|
1744
|
-
.sort((a, b) => a.start - b.start);
|
|
1745
|
-
|
|
1746
|
-
if (!intervals.length) return 0;
|
|
1747
|
-
|
|
1748
|
-
// Merge overlapping intervals
|
|
1749
|
-
let totalMs = 0;
|
|
1750
|
-
let curStart = intervals[0].start;
|
|
1751
|
-
let curEnd = intervals[0].end;
|
|
1752
|
-
|
|
1753
|
-
for (let i = 1; i < intervals.length; i++) {
|
|
1754
|
-
if (intervals[i].start <= curEnd) {
|
|
1755
|
-
// Overlapping — extend current interval
|
|
1756
|
-
curEnd = Math.max(curEnd, intervals[i].end);
|
|
1757
|
-
} else {
|
|
1758
|
-
// Gap — commit current interval and start new one
|
|
1759
|
-
totalMs += curEnd - curStart;
|
|
1760
|
-
curStart = intervals[i].start;
|
|
1761
|
-
curEnd = intervals[i].end;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
totalMs += curEnd - curStart; // commit last interval
|
|
1765
|
-
|
|
1766
|
-
return totalMs;
|
|
1767
|
-
});
|
|
1768
|
-
|
|
1769
|
-
// ── Token Statistics ──
|
|
1770
|
-
const tokenStats = computed(() => {
|
|
1771
|
-
let totalTokens = 0;
|
|
1772
|
-
let byCategory = {};
|
|
1773
|
-
|
|
1774
|
-
for (const ev of events.value) {
|
|
1775
|
-
if (ev.type === 'tool.execution_complete' && ev.data?.toolTelemetry?.metrics) {
|
|
1776
|
-
const tokens = ev.data.toolTelemetry.metrics.resultForLlmLength || 0;
|
|
1777
|
-
totalTokens += tokens;
|
|
1778
|
-
|
|
1779
|
-
// Categorize by tool name
|
|
1780
|
-
const toolName = ev.data.toolName || 'unknown';
|
|
1781
|
-
if (!byCategory[toolName]) {
|
|
1782
|
-
byCategory[toolName] = 0;
|
|
1783
|
-
}
|
|
1784
|
-
byCategory[toolName] += tokens;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
return {
|
|
1789
|
-
total: totalTokens,
|
|
1790
|
-
byCategory
|
|
1791
|
-
};
|
|
1792
|
-
});
|
|
1793
|
-
|
|
1794
|
-
// ── Gap Analysis ──
|
|
1795
|
-
const gapAnalysis = computed(() => {
|
|
1796
|
-
const sorted = sortedEvents.value;
|
|
1797
|
-
const gaps = [];
|
|
1798
|
-
|
|
1799
|
-
// Track user messages and assistant responses
|
|
1800
|
-
for (let i = 0; i < sorted.length - 1; i++) {
|
|
1801
|
-
const current = sorted[i];
|
|
1802
|
-
const next = sorted[i + 1];
|
|
1803
|
-
const currentTime = new Date(current.timestamp).getTime();
|
|
1804
|
-
const nextTime = new Date(next.timestamp).getTime();
|
|
1805
|
-
const duration = nextTime - currentTime;
|
|
1806
|
-
|
|
1807
|
-
// Only report gaps > 100ms
|
|
1808
|
-
if (duration < 100) continue;
|
|
1809
|
-
|
|
1810
|
-
let gapType = null;
|
|
1811
|
-
let description = '';
|
|
1812
|
-
|
|
1813
|
-
// User message → assistant.turn_start (input consumption)
|
|
1814
|
-
if (current.type === 'user.message' && next.type === 'assistant.turn_start') {
|
|
1815
|
-
gapType = 'input-consumption';
|
|
1816
|
-
const msgLength = (current.data?.message || '').length;
|
|
1817
|
-
description = `LLM reading user input (${msgLength} chars)`;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
// assistant.turn_start → assistant.message (generation)
|
|
1821
|
-
else if (current.type === 'assistant.turn_start' && next.type === 'assistant.message') {
|
|
1822
|
-
gapType = 'llm-generation';
|
|
1823
|
-
const outputLength = (next.data?.content || '').length;
|
|
1824
|
-
description = `LLM generating response (${outputLength} chars output)`;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
// assistant.turn_start → first tool (generation before tool call)
|
|
1828
|
-
else if (current.type === 'assistant.turn_start' && next.type === 'tool.execution_start') {
|
|
1829
|
-
gapType = 'llm-generation';
|
|
1830
|
-
const toolName = next.data?.toolName || 'unknown';
|
|
1831
|
-
description = `LLM deciding to call ${toolName}`;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// assistant.message → assistant.turn_start (thinking between turns)
|
|
1835
|
-
else if (current.type === 'assistant.message' && next.type === 'assistant.turn_start') {
|
|
1836
|
-
gapType = 'turn-gap';
|
|
1837
|
-
description = 'Gap between assistant response and next turn';
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// tool.execution_complete → next event (post-processing)
|
|
1841
|
-
else if (current.type === 'tool.execution_complete' && duration > 500) {
|
|
1842
|
-
gapType = 'post-tool';
|
|
1843
|
-
const toolName = current.data?.toolName || 'unknown';
|
|
1844
|
-
description = `Processing ${toolName} result`;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
// Large gaps between any events
|
|
1848
|
-
else if (duration > 5000) {
|
|
1849
|
-
gapType = 'idle';
|
|
1850
|
-
description = `${current.type} → ${next.type}`;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
if (gapType) {
|
|
1854
|
-
gaps.push({
|
|
1855
|
-
type: gapType,
|
|
1856
|
-
description,
|
|
1857
|
-
startTime: current.timestamp,
|
|
1858
|
-
endTime: next.timestamp,
|
|
1859
|
-
duration,
|
|
1860
|
-
fromEvent: current.type,
|
|
1861
|
-
toEvent: next.type,
|
|
1862
|
-
fromData: current.data,
|
|
1863
|
-
toData: next.data
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
return gaps.sort((a, b) => (b.duration || 0) - (a.duration || 0));
|
|
1869
|
-
});
|
|
1870
|
-
|
|
1871
|
-
const maxGapDuration = computed(() => {
|
|
1872
|
-
return Math.max(...gapAnalysis.value.map(g => g.duration || 0), 1);
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
const gapStats = computed(() => {
|
|
1876
|
-
const stats = {
|
|
1877
|
-
'input-consumption': { count: 0, total: 0, avg: 0 },
|
|
1878
|
-
'llm-generation': { count: 0, total: 0, avg: 0 },
|
|
1879
|
-
'post-tool': { count: 0, total: 0, avg: 0 },
|
|
1880
|
-
'turn-gap': { count: 0, total: 0, avg: 0 },
|
|
1881
|
-
'idle': { count: 0, total: 0, avg: 0 }
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
gapAnalysis.value.forEach(gap => {
|
|
1885
|
-
if (stats[gap.type]) {
|
|
1886
|
-
stats[gap.type].count++;
|
|
1887
|
-
stats[gap.type].total += gap.duration;
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
Object.keys(stats).forEach(key => {
|
|
1892
|
-
if (stats[key].count > 0) {
|
|
1893
|
-
stats[key].avg = stats[key].total / stats[key].count;
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
return stats;
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
|
-
const successRate = computed(() => {
|
|
1901
|
-
const total = toolAnalysis.value.length;
|
|
1902
|
-
if (total === 0) return 100;
|
|
1903
|
-
const errors = toolAnalysis.value.filter(t => t.isError).length;
|
|
1904
|
-
return ((total - errors) / total * 100).toFixed(1);
|
|
1905
|
-
});
|
|
1906
|
-
|
|
1907
|
-
const errorCount = computed(() => {
|
|
1908
|
-
return toolAnalysis.value.filter(t => t.isError).length;
|
|
1909
|
-
});
|
|
1910
|
-
|
|
1911
|
-
// Time breakdown: user thinking vs agent working
|
|
1912
|
-
// "User thinking" = gaps where the agent has finished and is waiting for the next user message
|
|
1913
|
-
// "Agent working" = totalDuration - userThinkingTime
|
|
1914
|
-
const timeBreakdown = computed(() => {
|
|
1915
|
-
const sorted = sortedEvents.value;
|
|
1916
|
-
let userThinkingTime = 0;
|
|
1917
|
-
|
|
1918
|
-
for (let i = 0; i < sorted.length - 1; i++) {
|
|
1919
|
-
const current = sorted[i];
|
|
1920
|
-
const next = sorted[i + 1];
|
|
1921
|
-
|
|
1922
|
-
// User thinking = from any non-user event to the next user.message
|
|
1923
|
-
// This captures when the agent is done and waiting for the user to type
|
|
1924
|
-
if (next.type === 'user.message' && current.type !== 'user.message') {
|
|
1925
|
-
const gap = new Date(next.timestamp).getTime() - new Date(current.timestamp).getTime();
|
|
1926
|
-
if (gap > 1000) { // Only count gaps > 1s as intentional user thinking
|
|
1927
|
-
userThinkingTime += gap;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
const total = totalDuration.value || 0;
|
|
1933
|
-
const agentWorkingTime = Math.max(total - userThinkingTime, 0);
|
|
1934
|
-
// LLM time = agent working time minus tool wall-clock time
|
|
1935
|
-
const llmTime = Math.max(agentWorkingTime - totalToolTime.value, 0);
|
|
1936
|
-
|
|
1937
|
-
return {
|
|
1938
|
-
userThinkingTime,
|
|
1939
|
-
agentWorkingTime,
|
|
1940
|
-
llmTime,
|
|
1941
|
-
userThinkingPct: total > 0 ? (userThinkingTime / total * 100).toFixed(0) : 0,
|
|
1942
|
-
agentWorkingPct: total > 0 ? (agentWorkingTime / total * 100).toFixed(0) : 0,
|
|
1943
|
-
llmPct: total > 0 ? (llmTime / total * 100).toFixed(0) : 0,
|
|
1944
|
-
toolPct: total > 0 ? (totalToolTime.value / total * 100).toFixed(0) : 0,
|
|
1945
|
-
};
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const totalToolCount = computed(() => toolAnalysis.value.length);
|
|
1951
|
-
|
|
1952
|
-
const avgToolDuration = computed(() => {
|
|
1953
|
-
if (!totalToolCount.value) return 0;
|
|
1954
|
-
const rawSum = toolAnalysis.value.reduce((acc, t) => acc + (t.duration || 0), 0);
|
|
1955
|
-
return rawSum / totalToolCount.value;
|
|
1956
|
-
});
|
|
1957
|
-
|
|
1958
|
-
const longestTool = computed(() => {
|
|
1959
|
-
if (!toolAnalysis.value.length) return null;
|
|
1960
|
-
return toolAnalysis.value.reduce((max, t) => (t.duration || 0) > (max.duration || 0) ? t : max);
|
|
1961
|
-
});
|
|
1962
|
-
|
|
1963
|
-
// ── VS Code Session Detection ──
|
|
1964
|
-
const isVSCodeSession = computed(() => {
|
|
1965
|
-
// Detect VS Code sessions: they have events with data.source === 'vscode'
|
|
1966
|
-
// OR they have assistant.message events with data.subAgentName but no subagent.started events
|
|
1967
|
-
const sorted = sortedEvents.value;
|
|
1968
|
-
const hasVSCodeSource = sorted.some(ev => ev.data?.source === 'vscode');
|
|
1969
|
-
if (hasVSCodeSource) return true;
|
|
1970
|
-
|
|
1971
|
-
// Alternative check: has subAgentName but no subagent events
|
|
1972
|
-
const hasSubAgentName = sorted.some(ev =>
|
|
1973
|
-
ev.type === 'assistant.message' && ev.data?.subAgentName
|
|
1974
|
-
);
|
|
1975
|
-
const hasSubagentEvents = sorted.some(ev =>
|
|
1976
|
-
ev.type === 'subagent.started' || ev.type === 'subagent.completed' || ev.type === 'subagent.failed'
|
|
1977
|
-
);
|
|
1978
|
-
return hasSubAgentName && !hasSubagentEvents;
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
// ── VS Code Subagents ──
|
|
1982
|
-
const vsCodeSubagents = computed(() => {
|
|
1983
|
-
if (!isVSCodeSession.value) return [];
|
|
1984
|
-
|
|
1985
|
-
const sorted = sortedEvents.value;
|
|
1986
|
-
const subagentMap = new Map(); // subAgentId -> { events, toolCount, firstIndex, status, name }
|
|
1987
|
-
|
|
1988
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
1989
|
-
const ev = sorted[i];
|
|
1990
|
-
if (ev.type === 'assistant.message' && ev.data?.subAgentName) {
|
|
1991
|
-
const id = ev.data.subAgentId || ev.data.subAgentName;
|
|
1992
|
-
if (!subagentMap.has(id)) {
|
|
1993
|
-
subagentMap.set(id, {
|
|
1994
|
-
name: ev.data.subAgentName,
|
|
1995
|
-
events: [],
|
|
1996
|
-
toolCount: 0,
|
|
1997
|
-
firstIndex: i, // use array index as stable position
|
|
1998
|
-
status: 'completed',
|
|
1999
|
-
subAgentId: ev.data.subAgentId
|
|
2000
|
-
});
|
|
2001
|
-
}
|
|
2002
|
-
const entry = subagentMap.get(id);
|
|
2003
|
-
entry.events.push(ev);
|
|
2004
|
-
|
|
2005
|
-
// Count tools
|
|
2006
|
-
if (ev.data.tools && Array.isArray(ev.data.tools)) {
|
|
2007
|
-
entry.toolCount += ev.data.tools.length;
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
// Update status if there are errors
|
|
2011
|
-
if (ev.data.error || ev.data.status === 'error') {
|
|
2012
|
-
entry.status = 'failed';
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// Convert to array and sort by firstIndex
|
|
2018
|
-
return Array.from(subagentMap.values()).sort((a, b) => a.firstIndex - b.firstIndex);
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
// ── Unified Timeline Items ──
|
|
2022
|
-
const unifiedTimelineItems = computed(() => {
|
|
2023
|
-
const items = [];
|
|
2024
|
-
const groups = groupedTurns.value;
|
|
2025
|
-
const agents = subagentAnalysis.value;
|
|
2026
|
-
const sorted = sortedEvents.value;
|
|
2027
|
-
|
|
2028
|
-
// Check if this is a VS Code session and use sequence-based layout
|
|
2029
|
-
if (isVSCodeSession.value) {
|
|
2030
|
-
const vsAgents = vsCodeSubagents.value;
|
|
2031
|
-
|
|
2032
|
-
// Collect user messages with their array index positions
|
|
2033
|
-
const userMessages = [];
|
|
2034
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
2035
|
-
if (sorted[i].type === 'user.message') {
|
|
2036
|
-
userMessages.push({ event: sorted[i], sortedIndex: i });
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
if (userMessages.length > 0 && vsAgents.length > 0) {
|
|
2041
|
-
// Build user-req groups: each user message owns the subagents that follow it
|
|
2042
|
-
// until the next user message (using sorted array indices)
|
|
2043
|
-
for (let ui = 0; ui < userMessages.length; ui++) {
|
|
2044
|
-
const { event: userMsg, sortedIndex: userIdx } = userMessages[ui];
|
|
2045
|
-
const nextIdx = userMessages[ui + 1] ? userMessages[ui + 1].sortedIndex : Infinity;
|
|
2046
|
-
|
|
2047
|
-
// Find subagents belonging to this user request (by sorted array index)
|
|
2048
|
-
const reqAgents = vsAgents.filter(sa =>
|
|
2049
|
-
sa.firstIndex >= userIdx && sa.firstIndex < nextIdx
|
|
2050
|
-
);
|
|
2051
|
-
|
|
2052
|
-
// Calculate total tool count for this user request
|
|
2053
|
-
const totalTools = reqAgents.reduce((s, a) => s + a.toolCount, 0);
|
|
2054
|
-
|
|
2055
|
-
const msg = userMsg.data?.message || userMsg.data?.content || '';
|
|
2056
|
-
|
|
2057
|
-
// Push user-req header
|
|
2058
|
-
items.push({
|
|
2059
|
-
rowType: 'user-req',
|
|
2060
|
-
userReqNumber: ui + 1,
|
|
2061
|
-
message: typeof msg === 'string' ? msg.substring(0, 120) : String(msg).substring(0, 120),
|
|
2062
|
-
toolCount: totalTools,
|
|
2063
|
-
sequenceIndex: userIdx,
|
|
2064
|
-
isSequenceEstimated: true,
|
|
2065
|
-
duration: totalTools,
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
// Push subagent rows under this user request
|
|
2069
|
-
for (const vsAgent of reqAgents) {
|
|
2070
|
-
items.push({
|
|
2071
|
-
rowType: 'subagent',
|
|
2072
|
-
itemType: 'subagent',
|
|
2073
|
-
name: vsAgent.name,
|
|
2074
|
-
status: vsAgent.status,
|
|
2075
|
-
toolCount: vsAgent.toolCount,
|
|
2076
|
-
sequenceIndex: vsAgent.firstIndex,
|
|
2077
|
-
isSequenceEstimated: true,
|
|
2078
|
-
duration: vsAgent.toolCount,
|
|
2079
|
-
indented: true,
|
|
2080
|
-
});
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
} else if (vsAgents.length > 0) {
|
|
2084
|
-
// No user messages, just show subagents
|
|
2085
|
-
for (const vsAgent of vsAgents) {
|
|
2086
|
-
items.push({
|
|
2087
|
-
rowType: 'subagent',
|
|
2088
|
-
itemType: 'subagent',
|
|
2089
|
-
name: vsAgent.name,
|
|
2090
|
-
status: vsAgent.status,
|
|
2091
|
-
toolCount: vsAgent.toolCount,
|
|
2092
|
-
sequenceIndex: vsAgent.firstIndex,
|
|
2093
|
-
isSequenceEstimated: true,
|
|
2094
|
-
duration: vsAgent.toolCount,
|
|
2095
|
-
});
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
return items;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
// Original Copilot CLI timeline logic
|
|
2102
|
-
if (groups.length) {
|
|
2103
|
-
for (let gi = 0; gi < groups.length; gi++) {
|
|
2104
|
-
const group = groups[gi];
|
|
2105
|
-
const turns = group.turns;
|
|
2106
|
-
if (!turns.length) continue;
|
|
2107
|
-
|
|
2108
|
-
const reqStart = new Date(turns[0].startTime).getTime();
|
|
2109
|
-
const reqEnd = new Date(turns[turns.length - 1].endTime).getTime();
|
|
2110
|
-
|
|
2111
|
-
// 1. Push user-req header row
|
|
2112
|
-
items.push({
|
|
2113
|
-
rowType: 'user-req',
|
|
2114
|
-
userReqNumber: group.userReqNumber,
|
|
2115
|
-
message: group.message,
|
|
2116
|
-
startTime: turns[0].startTime,
|
|
2117
|
-
endTime: turns[turns.length - 1].endTime,
|
|
2118
|
-
duration: reqEnd - reqStart,
|
|
2119
|
-
});
|
|
2120
|
-
|
|
2121
|
-
// 2. Find subagents within this UserReq time range
|
|
2122
|
-
const reqAgents = agents.filter(sa => {
|
|
2123
|
-
if (!sa.startTime) return false;
|
|
2124
|
-
const saStart = new Date(sa.startTime).getTime();
|
|
2125
|
-
return saStart >= reqStart && saStart <= reqEnd;
|
|
2126
|
-
});
|
|
2127
|
-
|
|
2128
|
-
if (reqAgents.length) {
|
|
2129
|
-
// Build subagent + gap items scoped to this UserReq
|
|
2130
|
-
for (let i = 0; i < reqAgents.length; i++) {
|
|
2131
|
-
const sa = reqAgents[i];
|
|
2132
|
-
|
|
2133
|
-
// Gap before first subagent (from reqStart) or between subagents
|
|
2134
|
-
const gapStart = i === 0
|
|
2135
|
-
? reqStart
|
|
2136
|
-
: new Date(reqAgents[i - 1].endTime).getTime();
|
|
2137
|
-
const gapEnd = new Date(sa.startTime).getTime();
|
|
2138
|
-
|
|
2139
|
-
if (gapEnd - gapStart > 500) {
|
|
2140
|
-
const agentOp = buildAgentOpItem(sorted, gapStart, gapEnd);
|
|
2141
|
-
agentOp.rowType = 'main-agent';
|
|
2142
|
-
items.push(agentOp);
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// Add subagent
|
|
2146
|
-
items.push({
|
|
2147
|
-
...sa,
|
|
2148
|
-
rowType: 'subagent',
|
|
2149
|
-
itemType: 'subagent',
|
|
2150
|
-
});
|
|
2151
|
-
|
|
2152
|
-
// Gap after last subagent to reqEnd
|
|
2153
|
-
if (i === reqAgents.length - 1) {
|
|
2154
|
-
const trailingStart = new Date(sa.endTime).getTime();
|
|
2155
|
-
const trailingEnd = reqEnd;
|
|
2156
|
-
if (trailingEnd - trailingStart > 500) {
|
|
2157
|
-
const agentOp = buildAgentOpItem(sorted, trailingStart, trailingEnd);
|
|
2158
|
-
agentOp.rowType = 'main-agent';
|
|
2159
|
-
items.push(agentOp);
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
} else {
|
|
2164
|
-
// No subagents — show single Main Agent bar spanning entire UserReq
|
|
2165
|
-
if (reqEnd - reqStart > 0) {
|
|
2166
|
-
const agentOp = buildAgentOpItem(sorted, reqStart, reqEnd);
|
|
2167
|
-
agentOp.rowType = 'main-agent';
|
|
2168
|
-
items.push(agentOp);
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
} else if (subagentTimelineItems.value.length) {
|
|
2173
|
-
// No UserReq groups, but subagents exist — flat fallback
|
|
2174
|
-
for (const item of subagentTimelineItems.value) {
|
|
2175
|
-
items.push({
|
|
2176
|
-
...item,
|
|
2177
|
-
rowType: item.itemType === 'agent-op' ? 'main-agent' : 'subagent',
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
return items;
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
// ── Gantt chart positioning ──
|
|
2186
|
-
const ganttPosition = (startTs, endTs) => {
|
|
2187
|
-
if (!sessionStart.value || !totalDuration.value || !startTs) return { left: '0%', width: '0%' };
|
|
2188
|
-
const s = new Date(startTs).getTime();
|
|
2189
|
-
const e = endTs ? new Date(endTs).getTime() : s + 1000;
|
|
2190
|
-
const left = ((s - sessionStart.value) / totalDuration.value) * 100;
|
|
2191
|
-
const width = Math.max(((e - s) / totalDuration.value) * 100, 0.5);
|
|
2192
|
-
return {
|
|
2193
|
-
left: left + '%',
|
|
2194
|
-
width: Math.min(width, 100 - left) + '%'
|
|
2195
|
-
};
|
|
2196
|
-
};
|
|
2197
|
-
|
|
2198
|
-
// ── VS Code Sequence-based positioning ──
|
|
2199
|
-
const ganttSequencePosition = (item) => {
|
|
2200
|
-
const items = unifiedTimelineItems.value;
|
|
2201
|
-
if (items.length === 0) return { left: '0%', width: '0%' };
|
|
2202
|
-
|
|
2203
|
-
// For user-req rows, span across all its child subagent rows
|
|
2204
|
-
if (item.rowType === 'user-req') {
|
|
2205
|
-
const idx = items.findIndex(it => it === item);
|
|
2206
|
-
if (idx === -1) return { left: '0%', width: '0%' };
|
|
2207
|
-
|
|
2208
|
-
// Find child subagents (indented rows immediately following this user-req)
|
|
2209
|
-
const children = [];
|
|
2210
|
-
for (let i = idx + 1; i < items.length; i++) {
|
|
2211
|
-
if (items[i].rowType === 'user-req') break; // next user-req
|
|
2212
|
-
if (items[i].rowType === 'subagent') children.push(items[i]);
|
|
2213
|
-
}
|
|
2214
|
-
if (children.length === 0) {
|
|
2215
|
-
// No subagents — position this user-req at the end of previous subagents
|
|
2216
|
-
// by finding cumulative tool count up to this point
|
|
2217
|
-
const subagentItems = items.filter(it => it.rowType !== 'user-req');
|
|
2218
|
-
const totalToolCount = subagentItems.reduce((sum, it) => sum + (it.toolCount || 0), 0);
|
|
2219
|
-
if (totalToolCount === 0) return { left: '0%', width: '0%' };
|
|
2220
|
-
// Sum tools of all subagents before this user-req in the items array
|
|
2221
|
-
let cumTools = 0;
|
|
2222
|
-
for (let i = 0; i < idx; i++) {
|
|
2223
|
-
if (items[i].rowType === 'subagent') cumTools += (items[i].toolCount || 0);
|
|
2224
|
-
}
|
|
2225
|
-
const leftPct = (cumTools / totalToolCount) * 100;
|
|
2226
|
-
// Minimal width bar (at least 1%)
|
|
2227
|
-
return { left: leftPct + '%', width: Math.max(1, (1 / totalToolCount) * 100) + '%' };
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
const firstPos = ganttSequencePosition(children[0]);
|
|
2231
|
-
const lastPos = ganttSequencePosition(children[children.length - 1]);
|
|
2232
|
-
const startPct = parseFloat(firstPos.left);
|
|
2233
|
-
const endPct = parseFloat(lastPos.left) + parseFloat(lastPos.width);
|
|
2234
|
-
return {
|
|
2235
|
-
left: startPct + '%',
|
|
2236
|
-
width: (endPct - startPct) + '%'
|
|
2237
|
-
};
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
// Find index of this item
|
|
2241
|
-
const idx = items.findIndex(it => it === item);
|
|
2242
|
-
if (idx === -1) return { left: '0%', width: '0%' };
|
|
2243
|
-
|
|
2244
|
-
// Calculate total tool count across subagent items only (exclude user-req)
|
|
2245
|
-
const subagentItems = items.filter(it => it.rowType !== 'user-req');
|
|
2246
|
-
const totalToolCount = subagentItems.reduce((sum, it) => sum + (it.toolCount || 0), 0);
|
|
2247
|
-
if (totalToolCount === 0) return { left: '0%', width: '0%' };
|
|
2248
|
-
|
|
2249
|
-
// Calculate cumulative tool count up to this item (among subagent items only)
|
|
2250
|
-
const subIdx = subagentItems.findIndex(it => it === item);
|
|
2251
|
-
if (subIdx === -1) return { left: '0%', width: '0%' };
|
|
2252
|
-
|
|
2253
|
-
let cumulativeToolCount = 0;
|
|
2254
|
-
for (let i = 0; i < subIdx; i++) {
|
|
2255
|
-
cumulativeToolCount += subagentItems[i].toolCount || 0;
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
// Position based on sequence
|
|
2259
|
-
const left = (cumulativeToolCount / totalToolCount) * 100;
|
|
2260
|
-
|
|
2261
|
-
// Width based on tool count with minimum width
|
|
2262
|
-
const itemToolCount = item.toolCount || 0;
|
|
2263
|
-
const width = Math.max((itemToolCount / totalToolCount) * 100, 2); // Minimum 2% width
|
|
2264
|
-
|
|
2265
|
-
return {
|
|
2266
|
-
left: left + '%',
|
|
2267
|
-
width: Math.min(width, 100 - left) + '%'
|
|
2268
|
-
};
|
|
2269
|
-
};
|
|
2270
|
-
|
|
2271
|
-
// ── Sort control ──
|
|
2272
|
-
const toggleSort = (field) => {
|
|
2273
|
-
if (sortField.value === field) {
|
|
2274
|
-
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc';
|
|
2275
|
-
} else {
|
|
2276
|
-
sortField.value = field;
|
|
2277
|
-
sortDir.value = field === 'duration' ? 'desc' : 'asc';
|
|
2278
|
-
}
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
const sortIcon = (field) => {
|
|
2282
|
-
if (sortField.value !== field) return '↕';
|
|
2283
|
-
return sortDir.value === 'asc' ? '↑' : '↓';
|
|
2284
|
-
};
|
|
2285
|
-
|
|
2286
|
-
const getToolBadgeClass = (toolName) => {
|
|
2287
|
-
const lower = (toolName || '').toLowerCase();
|
|
2288
|
-
if (['bash', 'exec'].includes(lower)) return 'badge-bash';
|
|
2289
|
-
if (lower === 'read') return 'badge-read';
|
|
2290
|
-
if (lower === 'write' || lower === 'notebookedit') return 'badge-write';
|
|
2291
|
-
if (lower === 'edit') return 'badge-edit';
|
|
2292
|
-
if (lower === 'glob' || lower === 'grep') return 'badge-search';
|
|
2293
|
-
if (lower === 'task') return 'badge-subagent';
|
|
2294
|
-
return 'badge-other';
|
|
2295
|
-
};
|
|
2296
|
-
|
|
2297
|
-
const getOpBadgeClass = (opType) => {
|
|
2298
|
-
const classes = {
|
|
2299
|
-
read: 'badge-read',
|
|
2300
|
-
write: 'badge-write',
|
|
2301
|
-
edit: 'badge-edit',
|
|
2302
|
-
create: 'badge-create',
|
|
2303
|
-
search: 'badge-search'
|
|
2304
|
-
};
|
|
2305
|
-
return classes[opType] || 'badge-other';
|
|
2306
|
-
};
|
|
2307
|
-
|
|
2308
|
-
// ── Load events ──
|
|
2309
|
-
onMounted(async () => {
|
|
2310
|
-
try {
|
|
2311
|
-
const resp = await fetch('/api/sessions/' + sessionId.value + '/events');
|
|
2312
|
-
if (!resp.ok) throw new Error('Failed to load events: ' + resp.statusText);
|
|
2313
|
-
const data = await resp.json();
|
|
2314
|
-
console.log('[TIME-ANALYZE] Loaded events:', data.length);
|
|
2315
|
-
console.log('[TIME-ANALYZE] Event types:', [...new Set(data.map(e => e.type))]);
|
|
2316
|
-
console.log('[TIME-ANALYZE] Turn starts:', data.filter(e => e.type === 'assistant.turn_start').length);
|
|
2317
|
-
console.log('[TIME-ANALYZE] User messages:', data.filter(e => e.type === 'user.message').length);
|
|
2318
|
-
events.value = data.sort((a, b) => {
|
|
2319
|
-
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
2320
|
-
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
2321
|
-
if (timeA !== timeB) return timeA - timeB;
|
|
2322
|
-
return (a._fileIndex ?? 0) - (b._fileIndex ?? 0);
|
|
2323
|
-
});
|
|
2324
|
-
console.log('[TIME-ANALYZE] Events set, length:', events.value.length);
|
|
2325
|
-
} catch (err) {
|
|
2326
|
-
console.error('[TIME-ANALYZE] Error loading events:', err);
|
|
2327
|
-
error.value = err.message;
|
|
2328
|
-
} finally {
|
|
2329
|
-
loading.value = false;
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// ── Copilot Insight ──
|
|
2334
|
-
const insightStatus = ref('not_started'); // completed | generating | timeout | not_started
|
|
2335
|
-
const insightLastUpdate = ref(null);
|
|
2336
|
-
const insightStartedAt = ref(null);
|
|
2337
|
-
const insightAgeMs = ref(0);
|
|
2338
|
-
let pollInterval = null;
|
|
2339
|
-
|
|
2340
|
-
const renderedInsight = computed(() => {
|
|
2341
|
-
if (!insightReport.value) return '';
|
|
2342
|
-
return marked.parse(insightReport.value);
|
|
2343
|
-
});
|
|
2344
|
-
|
|
2345
|
-
const checkExistingInsight = async () => {
|
|
2346
|
-
try {
|
|
2347
|
-
const resp = await fetch(`/session/${sessionId.value}/insight`);
|
|
2348
|
-
const data = await resp.json();
|
|
2349
|
-
|
|
2350
|
-
insightStatus.value = data.status;
|
|
2351
|
-
|
|
2352
|
-
if (data.status === 'completed') {
|
|
2353
|
-
insightReport.value = data.report;
|
|
2354
|
-
insightLog.value = null;
|
|
2355
|
-
insightGeneratedAt.value = data.generatedAt;
|
|
2356
|
-
stopPolling();
|
|
2357
|
-
} else if (data.status === 'generating') {
|
|
2358
|
-
insightLog.value = data.log || null;
|
|
2359
|
-
insightStartedAt.value = data.startedAt;
|
|
2360
|
-
insightLastUpdate.value = data.lastUpdate;
|
|
2361
|
-
insightAgeMs.value = data.ageMs;
|
|
2362
|
-
startPolling();
|
|
2363
|
-
// Auto-scroll log to bottom
|
|
2364
|
-
Vue.nextTick(() => {
|
|
2365
|
-
const el = document.getElementById('insight-log');
|
|
2366
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
2367
|
-
});
|
|
2368
|
-
} else if (data.status === 'timeout') {
|
|
2369
|
-
insightLog.value = data.log || null;
|
|
2370
|
-
insightStartedAt.value = data.startedAt;
|
|
2371
|
-
insightLastUpdate.value = data.lastUpdate;
|
|
2372
|
-
insightAgeMs.value = data.ageMs;
|
|
2373
|
-
// Keep polling — the process may still finish and write the report
|
|
2374
|
-
startPolling();
|
|
2375
|
-
}
|
|
2376
|
-
} catch (err) {
|
|
2377
|
-
console.error('Failed to check insight:', err);
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
|
|
2381
|
-
const startPolling = () => {
|
|
2382
|
-
stopPolling();
|
|
2383
|
-
pollInterval = setInterval(checkExistingInsight, 2000); // Poll every 2 seconds
|
|
2384
|
-
};
|
|
2385
|
-
|
|
2386
|
-
const stopPolling = () => {
|
|
2387
|
-
if (pollInterval) {
|
|
2388
|
-
clearInterval(pollInterval);
|
|
2389
|
-
pollInterval = null;
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
|
|
2393
|
-
const generateInsight = async (force = false) => {
|
|
2394
|
-
insightLoading.value = true;
|
|
2395
|
-
insightError.value = null;
|
|
2396
|
-
insightLog.value = null;
|
|
2397
|
-
|
|
2398
|
-
try {
|
|
2399
|
-
const resp = await fetch(`/session/${sessionId.value}/insight`, {
|
|
2400
|
-
method: 'POST',
|
|
2401
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2402
|
-
body: JSON.stringify({ force })
|
|
2403
|
-
});
|
|
2404
|
-
|
|
2405
|
-
if (!resp.ok) {
|
|
2406
|
-
const err = await resp.json();
|
|
2407
|
-
throw new Error(err.error || 'Failed to generate insight');
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
const data = await resp.json();
|
|
2411
|
-
insightStatus.value = data.status;
|
|
2412
|
-
|
|
2413
|
-
if (data.status === 'generating') {
|
|
2414
|
-
insightStartedAt.value = data.startedAt;
|
|
2415
|
-
startPolling();
|
|
2416
|
-
} else if (data.status === 'completed') {
|
|
2417
|
-
insightReport.value = data.report;
|
|
2418
|
-
insightGeneratedAt.value = data.generatedAt;
|
|
2419
|
-
}
|
|
2420
|
-
} catch (err) {
|
|
2421
|
-
insightError.value = err.message;
|
|
2422
|
-
} finally {
|
|
2423
|
-
insightLoading.value = false;
|
|
2424
|
-
}
|
|
2425
|
-
};
|
|
2426
|
-
|
|
2427
|
-
const regenerateInsight = async () => {
|
|
2428
|
-
await generateInsight(true);
|
|
2429
|
-
};
|
|
2430
|
-
|
|
2431
|
-
// Check for existing insight on mount
|
|
2432
|
-
onMounted(async () => {
|
|
2433
|
-
await checkExistingInsight();
|
|
2434
|
-
});
|
|
2435
|
-
|
|
2436
|
-
// Clean up polling on unmount
|
|
2437
|
-
onUnmounted(() => {
|
|
2438
|
-
stopPolling();
|
|
2439
|
-
});
|
|
2440
|
-
|
|
2441
|
-
return {
|
|
2442
|
-
sessionId, metadata, events, loading, error, activeTab,
|
|
2443
|
-
sortField, sortDir,
|
|
2444
|
-
insightReport, insightLog, insightLoading, insightError, insightGeneratedAt,
|
|
2445
|
-
insightStatus, insightLastUpdate, insightStartedAt, insightAgeMs,
|
|
2446
|
-
renderedInsight, generateInsight, regenerateInsight,
|
|
2447
|
-
formatDuration, formatTime, formatDateTime,
|
|
2448
|
-
sessionStart, sessionEnd, totalDuration,
|
|
2449
|
-
subagentAnalysis, maxSubagentDuration, subagentTimelineItems, subagentStats,
|
|
2450
|
-
EVENT_MARKER_CATEGORIES, showMarkerLegend,
|
|
2451
|
-
copyLabel, copyTimelineMarkdown,
|
|
2452
|
-
ganttCrosshairX, ganttCrosshairTime, onGanttMouseMove, onGanttMouseLeave,
|
|
2453
|
-
turnAnalysis, maxTurnDuration, groupedTurns,
|
|
2454
|
-
unifiedTimelineItems,
|
|
2455
|
-
toolAnalysis, sortedToolAnalysis, maxToolDuration,
|
|
2456
|
-
fileOperations, fileStats,
|
|
2457
|
-
toolTimeByCategory, maxCategoryTime,
|
|
2458
|
-
totalToolTime, totalToolCount, avgToolDuration, longestTool,
|
|
2459
|
-
successRate, errorCount, timeBreakdown,
|
|
2460
|
-
gapAnalysis, maxGapDuration, gapStats,
|
|
2461
|
-
ganttPosition, ganttSequencePosition, toggleSort, sortIcon,
|
|
2462
|
-
getToolBadgeClass, getOpBadgeClass,
|
|
2463
|
-
isVSCodeSession, vsCodeSubagents
|
|
2464
|
-
};
|
|
2465
|
-
},
|
|
2466
|
-
|
|
2467
|
-
template: `
|
|
2468
|
-
<div v-if="loading" class="empty-state" style="padding: 60px;">
|
|
2469
|
-
⏳ Loading events...
|
|
2470
|
-
</div>
|
|
2471
|
-
|
|
2472
|
-
<div v-else-if="error" class="empty-state" style="padding: 60px; color: #f85149;">
|
|
2473
|
-
❌ {{ error }}
|
|
2474
|
-
</div>
|
|
2475
|
-
|
|
2476
|
-
<div v-else>
|
|
2477
|
-
<!-- Summary Cards -->
|
|
2478
|
-
<div class="summary-grid">
|
|
2479
|
-
<div class="summary-card" title="Wall-clock time from first event to last event in this session.">
|
|
2480
|
-
<div class="summary-card-label">Total Duration</div>
|
|
2481
|
-
<div class="summary-card-value">{{ formatDuration(totalDuration) }}</div>
|
|
2482
|
-
<div class="summary-card-sub" v-if="sessionStart" style="margin-top: 2px; font-size: 10px; opacity: 0.7;">
|
|
2483
|
-
{{ formatDateTime(sessionStart) }} → {{ formatDateTime(sessionEnd) }}
|
|
2484
|
-
</div>
|
|
2485
|
-
</div>
|
|
2486
|
-
<div class="summary-card" title="Number of user messages that triggered agent work. Each request may involve multiple LLM turns.">
|
|
2487
|
-
<div class="summary-card-label">User Requests</div>
|
|
2488
|
-
<div class="summary-card-value">{{ groupedTurns.length }}</div>
|
|
2489
|
-
<div class="summary-card-sub">{{ turnAnalysis.length }} turn{{ turnAnalysis.length !== 1 ? 's' : '' }}</div>
|
|
2490
|
-
</div>
|
|
2491
|
-
<div class="summary-card" title="Total tool executions (Read, Write, Edit, Bash, Grep, etc.) across the entire session, including subagent tools.">
|
|
2492
|
-
<div class="summary-card-label">Tool Calls</div>
|
|
2493
|
-
<div class="summary-card-value">{{ totalToolCount }}</div>
|
|
2494
|
-
<div class="summary-card-sub">
|
|
2495
|
-
<span :style="{ color: successRate >= 95 ? '#3fb950' : successRate >= 80 ? '#d29922' : '#f85149' }">{{ successRate }}%</span> success
|
|
2496
|
-
<span v-if="errorCount > 0" style="color: #f85149;"> · {{ errorCount }} error{{ errorCount !== 1 ? 's' : '' }}</span>
|
|
2497
|
-
</div>
|
|
2498
|
-
</div>
|
|
2499
|
-
<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.">
|
|
2500
|
-
<div class="summary-card-label">Sub-Agents</div>
|
|
2501
|
-
<div class="summary-card-value">{{ subagentAnalysis.length || vsCodeSubagents.length }}</div>
|
|
2502
|
-
<div class="summary-card-sub" v-if="subagentAnalysis.length > 0">
|
|
2503
|
-
<span :style="{ color: subagentStats.successRate >= 95 ? '#3fb950' : subagentStats.successRate >= 80 ? '#d29922' : '#f85149' }">{{ subagentStats.completed }}✓</span>
|
|
2504
|
-
<span v-if="subagentStats.failed > 0" style="color: #f85149;"> · {{ subagentStats.failed }}✗</span>
|
|
2505
|
-
<span v-if="subagentStats.incomplete > 0" style="color: #d29922;"> · {{ subagentStats.incomplete }}⏳</span>
|
|
2506
|
-
· {{ formatDuration(subagentStats.totalTime) }}
|
|
2507
|
-
· {{ subagentStats.totalTools }} tools
|
|
2508
|
-
</div>
|
|
2509
|
-
<div class="summary-card-sub" v-else-if="vsCodeSubagents.length > 0">
|
|
2510
|
-
<span style="color: #3fb950;">{{ vsCodeSubagents.length }}✓</span>
|
|
2511
|
-
· {{ vsCodeSubagents.reduce((s, a) => s + a.toolCount, 0) }} tools
|
|
2512
|
-
</div>
|
|
2513
|
-
</div>
|
|
2514
|
-
<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.">
|
|
2515
|
-
<div class="summary-card-label">Time Breakdown</div>
|
|
2516
|
-
<div class="summary-card-value">{{ formatDuration(timeBreakdown.llmTime) }} <span style="font-size: 14px; opacity: 0.6;">({{ timeBreakdown.llmPct }}% LLM Reasoning)</span></div>
|
|
2517
|
-
<div class="summary-card-sub">
|
|
2518
|
-
Tools {{ formatDuration(totalToolTime) }} ({{ timeBreakdown.toolPct }}%)
|
|
2519
|
-
<span v-if="timeBreakdown.userThinkingTime > 1000"> · User {{ formatDuration(timeBreakdown.userThinkingTime) }} ({{ timeBreakdown.userThinkingPct }}%)</span>
|
|
2520
|
-
</div>
|
|
2521
|
-
</div>
|
|
2522
|
-
<div class="summary-card" title="File system operations: reads (Read/Glob), edits (Edit), writes (Write), and searches (Grep).">
|
|
2523
|
-
<div class="summary-card-label">File Operations</div>
|
|
2524
|
-
<div class="summary-card-value">{{ fileStats.totalOps }}</div>
|
|
2525
|
-
<div class="summary-card-sub">
|
|
2526
|
-
{{ fileStats.reads }} reads · {{ fileStats.edits }} edits · {{ fileStats.writes }} writes · {{ fileStats.searches }} searches
|
|
2527
|
-
</div>
|
|
2528
|
-
</div>
|
|
2529
|
-
</div>
|
|
2530
|
-
|
|
2531
|
-
<!-- Tabs -->
|
|
2532
|
-
<div class="tabs">
|
|
2533
|
-
<button :class="['tab', { active: activeTab === 'timeline' }]" @click="activeTab = 'timeline'">
|
|
2534
|
-
📊 Timeline
|
|
2535
|
-
</button>
|
|
2536
|
-
<button :class="['tab', { active: activeTab === 'insight' }]" @click="activeTab = 'insight'">
|
|
2537
|
-
💡 Agent Review
|
|
2538
|
-
</button>
|
|
2539
|
-
</div>
|
|
2540
|
-
|
|
2541
|
-
<!-- ═══ Unified Timeline Tab ═══ -->
|
|
2542
|
-
<div v-if="activeTab === 'timeline'" class="section">
|
|
2543
|
-
<div v-if="error" class="empty-state" style="color: #f85149;">
|
|
2544
|
-
Error loading timeline: {{ error }}
|
|
2545
|
-
</div>
|
|
2546
|
-
<div v-else-if="!unifiedTimelineItems.length" class="empty-state">
|
|
2547
|
-
No timeline data found in this session.
|
|
2548
|
-
</div>
|
|
2549
|
-
<div v-else>
|
|
2550
|
-
<!-- Section A: Gantt Chart -->
|
|
2551
|
-
<div class="section-title" style="display: flex; align-items: center;">
|
|
2552
|
-
Timeline
|
|
2553
|
-
<button class="legend-toggle-btn" @click="showMarkerLegend = !showMarkerLegend">
|
|
2554
|
-
{{ showMarkerLegend ? 'Hide Legend' : 'Show Legend' }}
|
|
2555
|
-
</button>
|
|
2556
|
-
<button class="legend-toggle-btn" @click="copyTimelineMarkdown">
|
|
2557
|
-
{{ copyLabel }}
|
|
2558
|
-
</button>
|
|
2559
|
-
</div>
|
|
2560
|
-
|
|
2561
|
-
<!-- Event Legend -->
|
|
2562
|
-
<div v-show="showMarkerLegend" class="event-legend">
|
|
2563
|
-
<div class="event-legend-item">
|
|
2564
|
-
<span class="event-legend-swatch" style="background: rgba(88, 166, 255, 0.5);"></span>
|
|
2565
|
-
<span>User Request</span>
|
|
2566
|
-
</div>
|
|
2567
|
-
<div class="event-legend-item">
|
|
2568
|
-
<span class="event-legend-swatch" style="background: rgba(63, 185, 80, 0.8);"></span>
|
|
2569
|
-
<span>Sub-Agent</span>
|
|
2570
|
-
</div>
|
|
2571
|
-
<div class="event-legend-item">
|
|
2572
|
-
<span class="event-legend-swatch" style="background: rgba(139, 148, 158, 0.3); border: 1px dashed rgba(139, 148, 158, 0.5);"></span>
|
|
2573
|
-
<span>Main Agent</span>
|
|
2574
|
-
</div>
|
|
2575
|
-
<div class="event-legend-item">
|
|
2576
|
-
<span class="event-legend-swatch" style="background: #d29922;"></span>
|
|
2577
|
-
<span>Tool (no errors)</span>
|
|
2578
|
-
</div>
|
|
2579
|
-
<div class="event-legend-item">
|
|
2580
|
-
<span class="event-legend-swatch" style="background: linear-gradient(to right, #d29922, #f85149);"></span>
|
|
2581
|
-
<span>Tool (error gradient)</span>
|
|
2582
|
-
</div>
|
|
2583
|
-
<div class="event-legend-item">
|
|
2584
|
-
<span class="event-legend-swatch" style="background: #f85149;"></span>
|
|
2585
|
-
<span>Tool Error (100%)</span>
|
|
2586
|
-
</div>
|
|
2587
|
-
<template v-for="(cat, type) in EVENT_MARKER_CATEGORIES" :key="type">
|
|
2588
|
-
<div v-if="type && !type.startsWith('tool.')" class="event-legend-item">
|
|
2589
|
-
<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>
|
|
2590
|
-
<span>{{ cat.label }}</span>
|
|
2591
|
-
</div>
|
|
2592
|
-
</template>
|
|
2593
|
-
</div>
|
|
2594
|
-
|
|
2595
|
-
<!-- VS Code Session Banner -->
|
|
2596
|
-
<div v-if="isVSCodeSession" style="
|
|
2597
|
-
background: rgba(88, 166, 255, 0.1);
|
|
2598
|
-
border: 1px solid rgba(88, 166, 255, 0.3);
|
|
2599
|
-
border-radius: 6px;
|
|
2600
|
-
padding: 12px 16px;
|
|
2601
|
-
margin-bottom: 16px;
|
|
2602
|
-
display: flex;
|
|
2603
|
-
align-items: center;
|
|
2604
|
-
gap: 8px;
|
|
2605
|
-
color: #58a6ff;
|
|
2606
|
-
font-size: 13px;
|
|
2607
|
-
">
|
|
2608
|
-
<span style="font-size: 16px;">ⓘ</span>
|
|
2609
|
-
<span>Sequence layout — bar widths represent tool count, not elapsed time</span>
|
|
2610
|
-
</div>
|
|
2611
|
-
|
|
2612
|
-
<div class="gantt-container" @mousemove="onGanttMouseMove" @mouseleave="onGanttMouseLeave">
|
|
2613
|
-
<!-- Crosshair -->
|
|
2614
|
-
<div v-if="ganttCrosshairX !== null" class="gantt-crosshair" :style="{ left: ganttCrosshairX + 'px' }">
|
|
2615
|
-
<div class="gantt-crosshair-label">{{ ganttCrosshairTime }}</div>
|
|
2616
|
-
</div>
|
|
2617
|
-
<template v-for="(item, idx) in unifiedTimelineItems" :key="'utl-' + idx">
|
|
2618
|
-
|
|
2619
|
-
<!-- Divider row -->
|
|
2620
|
-
<div v-if="item.rowType === 'divider'" class="gantt-divider">
|
|
2621
|
-
Tool Summary
|
|
2622
|
-
</div>
|
|
2623
|
-
|
|
2624
|
-
<!-- User Request row -->
|
|
2625
|
-
<div v-else-if="item.rowType === 'user-req'" class="gantt-row">
|
|
2626
|
-
<div class="gantt-label user-req" :title="item.message || 'No message'">
|
|
2627
|
-
<span class="user-req-badge">UserReq {{ item.userReqNumber }}</span>
|
|
2628
|
-
<span class="user-req-msg">{{ (item.message || '').substring(0, 40) }}{{ (item.message || '').length > 40 ? '...' : '' }}</span>
|
|
2629
|
-
</div>
|
|
2630
|
-
<div class="gantt-bar-area">
|
|
2631
|
-
<div
|
|
2632
|
-
class="gantt-bar user-req"
|
|
2633
|
-
:style="item.isSequenceEstimated ? ganttSequencePosition(item) : ganttPosition(item.startTime, item.endTime)"
|
|
2634
|
-
:title="item.isSequenceEstimated ? ('UserReq ' + item.userReqNumber + ' — ' + item.toolCount + ' tools') : ('UserReq ' + item.userReqNumber + ' — ' + formatDuration(item.duration))"
|
|
2635
|
-
>
|
|
2636
|
-
{{ item.isSequenceEstimated ? (item.toolCount + ' tools') : formatDuration(item.duration) }}
|
|
2637
|
-
</div>
|
|
2638
|
-
</div>
|
|
2639
|
-
</div>
|
|
2640
|
-
|
|
2641
|
-
<!-- Sub-Agent row (indented for CLI, not indented for VS Code) -->
|
|
2642
|
-
<div v-else-if="item.rowType === 'subagent'" :class="['gantt-row', item.indented ? 'indented' : (item.isSequenceEstimated ? '' : 'indented')]">
|
|
2643
|
-
<div class="gantt-label" :title="item.name">
|
|
2644
|
-
<a
|
|
2645
|
-
:href="'/session/' + sessionId + '?eventType=subagent.started&eventName=' + encodeURIComponent(item.name) + '&eventTimestamp=' + encodeURIComponent(item.startTime || '')"
|
|
2646
|
-
class="subagent-link"
|
|
2647
|
-
:title="'View events from here'"
|
|
2648
|
-
>
|
|
2649
|
-
<span :style="{ color: item.status === 'completed' ? '#3fb950' : item.status === 'failed' ? '#f85149' : '#d29922' }">
|
|
2650
|
-
{{ item.status === 'completed' ? '✓' : item.status === 'failed' ? '✗' : '⏳' }}
|
|
2651
|
-
</span>
|
|
2652
|
-
{{ item.name }}
|
|
2653
|
-
</a>
|
|
2654
|
-
</div>
|
|
2655
|
-
<div class="gantt-bar-area">
|
|
2656
|
-
<div
|
|
2657
|
-
:class="[
|
|
2658
|
-
'gantt-bar',
|
|
2659
|
-
item.isSequenceEstimated ? 'sequence-estimated' : '',
|
|
2660
|
-
item.status === 'completed' ? 'subagent' : item.status === 'failed' ? 'subagent-failed' : 'subagent-incomplete'
|
|
2661
|
-
]"
|
|
2662
|
-
:style="item.isSequenceEstimated ? ganttSequencePosition(item) : ganttPosition(item.startTime, item.endTime)"
|
|
2663
|
-
:title="item.isSequenceEstimated ? (item.name + ' — ' + item.toolCount + ' tools') : (item.name + ' — ' + formatDuration(item.duration))"
|
|
2664
|
-
>
|
|
2665
|
-
{{ item.isSequenceEstimated ? (item.toolCount + ' tools') : formatDuration(item.duration) }}
|
|
2666
|
-
|
|
2667
|
-
<!-- Event markers (only for non-sequence bars) -->
|
|
2668
|
-
<template v-if="!item.isSequenceEstimated && item.innerEventMarkers && item.innerEventMarkers.length">
|
|
2669
|
-
<span
|
|
2670
|
-
v-for="(marker, midx) in item.innerEventMarkers"
|
|
2671
|
-
:key="'m-' + midx"
|
|
2672
|
-
class="event-marker"
|
|
2673
|
-
:style="{ left: marker.position + '%' }"
|
|
2674
|
-
>
|
|
2675
|
-
<template v-if="marker.shape === 'cluster'">
|
|
2676
|
-
<span class="event-marker--cluster" :style="{ background: marker.color }">{{ marker.count }}</span>
|
|
2677
|
-
</template>
|
|
2678
|
-
<template v-else-if="marker.shape === 'circle'">
|
|
2679
|
-
<span class="event-marker--circle" :style="{ background: marker.color }"></span>
|
|
2680
|
-
</template>
|
|
2681
|
-
<template v-else-if="marker.shape === 'diamond'">
|
|
2682
|
-
<span class="event-marker--diamond" :style="{ background: marker.color }"></span>
|
|
2683
|
-
</template>
|
|
2684
|
-
<template v-else-if="marker.shape === 'square'">
|
|
2685
|
-
<span class="event-marker--square" :style="{ background: marker.color }"></span>
|
|
2686
|
-
</template>
|
|
2687
|
-
<template v-else-if="marker.shape === 'triangle'">
|
|
2688
|
-
<span class="event-marker--triangle" :style="{ color: marker.color }"></span>
|
|
2689
|
-
</template>
|
|
2690
|
-
<span class="event-marker-tooltip">
|
|
2691
|
-
<template v-if="marker.shape === 'cluster'">{{ marker.count }} events: {{ marker.label }}</template>
|
|
2692
|
-
<template v-else>{{ marker.label }}<span v-if="marker.toolName"> ({{ marker.toolName }})</span></template>
|
|
2693
|
-
</span>
|
|
2694
|
-
</span>
|
|
2695
|
-
</template>
|
|
2696
|
-
</div>
|
|
2697
|
-
</div>
|
|
2698
|
-
</div>
|
|
2699
|
-
|
|
2700
|
-
<!-- Main Agent gap row (indented) -->
|
|
2701
|
-
<div v-else-if="item.rowType === 'main-agent'" class="gantt-row indented">
|
|
2702
|
-
<div class="gantt-label agent-op" :title="item.summary">
|
|
2703
|
-
<span class="agent-op-icon">⚙</span>
|
|
2704
|
-
<span>Main Agent</span>
|
|
2705
|
-
<span class="agent-op-summary">{{ item.summary }}</span>
|
|
2706
|
-
</div>
|
|
2707
|
-
<div class="gantt-bar-area">
|
|
2708
|
-
<div
|
|
2709
|
-
class="gantt-bar agent-op"
|
|
2710
|
-
:style="ganttPosition(item.startTime, item.endTime)"
|
|
2711
|
-
:title="'Main Agent — ' + formatDuration(item.duration)"
|
|
2712
|
-
>
|
|
2713
|
-
{{ formatDuration(item.duration) }}
|
|
2714
|
-
|
|
2715
|
-
<!-- Event markers -->
|
|
2716
|
-
<template v-if="item.innerEventMarkers && item.innerEventMarkers.length">
|
|
2717
|
-
<span
|
|
2718
|
-
v-for="(marker, midx) in item.innerEventMarkers"
|
|
2719
|
-
:key="'m-' + midx"
|
|
2720
|
-
class="event-marker"
|
|
2721
|
-
:style="{ left: marker.position + '%' }"
|
|
2722
|
-
>
|
|
2723
|
-
<template v-if="marker.shape === 'cluster'">
|
|
2724
|
-
<span class="event-marker--cluster" :style="{ background: marker.color }">{{ marker.count }}</span>
|
|
2725
|
-
</template>
|
|
2726
|
-
<template v-else-if="marker.shape === 'circle'">
|
|
2727
|
-
<span class="event-marker--circle" :style="{ background: marker.color }"></span>
|
|
2728
|
-
</template>
|
|
2729
|
-
<template v-else-if="marker.shape === 'diamond'">
|
|
2730
|
-
<span class="event-marker--diamond" :style="{ background: marker.color }"></span>
|
|
2731
|
-
</template>
|
|
2732
|
-
<template v-else-if="marker.shape === 'square'">
|
|
2733
|
-
<span class="event-marker--square" :style="{ background: marker.color }"></span>
|
|
2734
|
-
</template>
|
|
2735
|
-
<template v-else-if="marker.shape === 'triangle'">
|
|
2736
|
-
<span class="event-marker--triangle" :style="{ color: marker.color }"></span>
|
|
2737
|
-
</template>
|
|
2738
|
-
<span class="event-marker-tooltip">
|
|
2739
|
-
<template v-if="marker.shape === 'cluster'">{{ marker.count }} events: {{ marker.label }}</template>
|
|
2740
|
-
<template v-else>{{ marker.label }}<span v-if="marker.toolName"> ({{ marker.toolName }})</span></template>
|
|
2741
|
-
</span>
|
|
2742
|
-
</span>
|
|
2743
|
-
</template>
|
|
2744
|
-
</div>
|
|
2745
|
-
</div>
|
|
2746
|
-
</div>
|
|
2747
|
-
|
|
2748
|
-
</template>
|
|
2749
|
-
|
|
2750
|
-
<div class="gantt-time-axis">
|
|
2751
|
-
<span>{{ formatTime(events[0]?.timestamp) }}</span>
|
|
2752
|
-
<span>{{ formatTime(events[events.length-1]?.timestamp) }}</span>
|
|
2753
|
-
</div>
|
|
2754
|
-
</div>
|
|
2755
|
-
|
|
2756
|
-
<!-- Tool Summary -->
|
|
2757
|
-
<div v-if="toolTimeByCategory.length" style="margin-top: 24px;">
|
|
2758
|
-
<h3 style="color: #e6edf3; font-size: 14px; margin-bottom: 12px;">🔧 Tool Summary</h3>
|
|
2759
|
-
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px;">
|
|
2760
|
-
<div
|
|
2761
|
-
v-for="cat in toolTimeByCategory"
|
|
2762
|
-
:key="cat.category"
|
|
2763
|
-
style="background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; display: flex; align-items: center; gap: 10px;"
|
|
2764
|
-
>
|
|
2765
|
-
<div style="flex: 1; min-width: 0;">
|
|
2766
|
-
<div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px;">
|
|
2767
|
-
<span style="color: #d29922; font-weight: 500; font-size: 13px;">{{ cat.category }}</span>
|
|
2768
|
-
<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>
|
|
2769
|
-
</div>
|
|
2770
|
-
<div style="background: #21262d; border-radius: 3px; height: 6px; overflow: hidden;">
|
|
2771
|
-
<div :style="{ width: (cat.totalTime / maxCategoryTime * 100) + '%', height: '100%', background: 'rgba(158, 106, 3, 0.7)', borderRadius: '3px' }"></div>
|
|
2772
|
-
</div>
|
|
2773
|
-
<div style="color: #7d8590; font-size: 11px; margin-top: 3px;">{{ formatDuration(cat.totalTime) }}</div>
|
|
2774
|
-
</div>
|
|
2775
|
-
</div>
|
|
2776
|
-
</div>
|
|
2777
|
-
</div>
|
|
2778
|
-
|
|
2779
|
-
</div>
|
|
2780
|
-
</div>
|
|
2781
|
-
|
|
2782
|
-
<!-- ═══ Copilot Insight Tab ═══ -->
|
|
2783
|
-
<div v-if="activeTab === 'insight'" class="section">
|
|
2784
|
-
<!-- Error State -->
|
|
2785
|
-
<div v-if="insightError" class="empty-state" style="padding: 60px; color: #f85149;">
|
|
2786
|
-
❌ {{ insightError }}
|
|
2787
|
-
</div>
|
|
2788
|
-
|
|
2789
|
-
<!-- Generating State -->
|
|
2790
|
-
<div v-else-if="insightStatus === 'generating'" style="padding: 20px;">
|
|
2791
|
-
<div :style="{
|
|
2792
|
-
background: '#0d1117',
|
|
2793
|
-
border: '1px solid ' + (insightAgeMs > 300000 ? '#d29922' : '#30363d'),
|
|
2794
|
-
borderRadius: '6px',
|
|
2795
|
-
padding: '20px',
|
|
2796
|
-
marginBottom: '20px',
|
|
2797
|
-
}">
|
|
2798
|
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
2799
|
-
<div style="display: flex; align-items: center;">
|
|
2800
|
-
<span style="font-size: 24px; margin-right: 10px;">⏳</span>
|
|
2801
|
-
<div>
|
|
2802
|
-
<div style="font-weight: 600; color: #58a6ff; margin-bottom: 5px;">
|
|
2803
|
-
Generating Agent Review...
|
|
2804
|
-
</div>
|
|
2805
|
-
<div style="font-size: 13px; color: #7d8590;">
|
|
2806
|
-
Started: {{ formatDateTime(insightStartedAt) }} •
|
|
2807
|
-
Age: {{ Math.floor(insightAgeMs / 1000) }}s
|
|
2808
|
-
</div>
|
|
2809
|
-
</div>
|
|
2810
|
-
</div>
|
|
2811
|
-
<button
|
|
2812
|
-
v-if="insightAgeMs > 300000"
|
|
2813
|
-
@click="regenerateInsight"
|
|
2814
|
-
style="
|
|
2815
|
-
background: #d29922;
|
|
2816
|
-
color: #fff;
|
|
2817
|
-
border: none;
|
|
2818
|
-
padding: 8px 16px;
|
|
2819
|
-
border-radius: 6px;
|
|
2820
|
-
font-size: 13px;
|
|
2821
|
-
cursor: pointer;
|
|
2822
|
-
font-weight: 500;
|
|
2823
|
-
white-space: nowrap;
|
|
2824
|
-
"
|
|
2825
|
-
@mouseover="$event.target.style.background='#e3b341'"
|
|
2826
|
-
@mouseleave="$event.target.style.background='#d29922'"
|
|
2827
|
-
>
|
|
2828
|
-
🔄 Stop & Retry
|
|
2829
|
-
</button>
|
|
2830
|
-
</div>
|
|
2831
|
-
<!-- Slow generation warning -->
|
|
2832
|
-
<div v-if="insightAgeMs > 300000" style="
|
|
2833
|
-
background: rgba(210, 153, 34, 0.1);
|
|
2834
|
-
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
2835
|
-
border-radius: 6px;
|
|
2836
|
-
padding: 10px 14px;
|
|
2837
|
-
margin-bottom: 12px;
|
|
2838
|
-
font-size: 13px;
|
|
2839
|
-
color: #d29922;
|
|
2840
|
-
">
|
|
2841
|
-
⚠️ Generation is taking longer than 5 minutes. For large sessions this is normal — the agent needs to read and analyze all events. If it appears stuck, you can click <strong>Stop & Retry</strong> to cancel and start fresh.
|
|
2842
|
-
</div>
|
|
2843
|
-
<div v-if="insightLog" id="insight-log" style="
|
|
2844
|
-
background: #161b22;
|
|
2845
|
-
border: 1px solid #30363d;
|
|
2846
|
-
border-radius: 6px;
|
|
2847
|
-
padding: 14px 16px;
|
|
2848
|
-
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
2849
|
-
font-size: 12px;
|
|
2850
|
-
line-height: 1.6;
|
|
2851
|
-
color: #8b949e;
|
|
2852
|
-
white-space: pre-wrap;
|
|
2853
|
-
word-break: break-word;
|
|
2854
|
-
max-height: 400px;
|
|
2855
|
-
overflow-y: auto;
|
|
2856
|
-
">{{ insightLog }}</div>
|
|
2857
|
-
</div>
|
|
2858
|
-
</div>
|
|
2859
|
-
|
|
2860
|
-
<!-- Timeout State -->
|
|
2861
|
-
<div v-else-if="insightStatus === 'timeout'" style="padding: 20px;">
|
|
2862
|
-
<div style="
|
|
2863
|
-
background: #0d1117;
|
|
2864
|
-
border: 1px solid #d29922;
|
|
2865
|
-
border-radius: 6px;
|
|
2866
|
-
padding: 20px;
|
|
2867
|
-
margin-bottom: 20px;
|
|
2868
|
-
">
|
|
2869
|
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
2870
|
-
<div style="display: flex; align-items: center;">
|
|
2871
|
-
<span style="font-size: 24px; margin-right: 10px;">⏳</span>
|
|
2872
|
-
<div>
|
|
2873
|
-
<div style="font-weight: 600; color: #d29922; margin-bottom: 5px;">
|
|
2874
|
-
Still generating... ({{ Math.floor(insightAgeMs / 1000 / 60) }}m elapsed)
|
|
2875
|
-
</div>
|
|
2876
|
-
<div style="font-size: 13px; color: #7d8590;">
|
|
2877
|
-
Large sessions with sub-agents may take 10–15 minutes. Still polling for completion.
|
|
2878
|
-
</div>
|
|
2879
|
-
</div>
|
|
2880
|
-
</div>
|
|
2881
|
-
<button
|
|
2882
|
-
@click="regenerateInsight"
|
|
2883
|
-
style="
|
|
2884
|
-
background: #d29922;
|
|
2885
|
-
color: #fff;
|
|
2886
|
-
border: none;
|
|
2887
|
-
padding: 8px 16px;
|
|
2888
|
-
border-radius: 6px;
|
|
2889
|
-
font-size: 13px;
|
|
2890
|
-
cursor: pointer;
|
|
2891
|
-
font-weight: 500;
|
|
2892
|
-
white-space: nowrap;
|
|
2893
|
-
"
|
|
2894
|
-
@mouseover="$event.target.style.background='#e3b341'"
|
|
2895
|
-
@mouseleave="$event.target.style.background='#d29922'"
|
|
2896
|
-
>
|
|
2897
|
-
🔄 Stop & Retry
|
|
2898
|
-
</button>
|
|
2899
|
-
</div>
|
|
2900
|
-
<div v-if="insightLog" style="
|
|
2901
|
-
background: #161b22;
|
|
2902
|
-
border: 1px solid #30363d;
|
|
2903
|
-
border-radius: 6px;
|
|
2904
|
-
padding: 14px 16px;
|
|
2905
|
-
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
2906
|
-
font-size: 12px;
|
|
2907
|
-
line-height: 1.6;
|
|
2908
|
-
color: #8b949e;
|
|
2909
|
-
white-space: pre-wrap;
|
|
2910
|
-
word-break: break-word;
|
|
2911
|
-
max-height: 400px;
|
|
2912
|
-
overflow-y: auto;
|
|
2913
|
-
">{{ insightLog }}</div>
|
|
2914
|
-
</div>
|
|
2915
|
-
</div>
|
|
2916
|
-
|
|
2917
|
-
<!-- Not Started State -->
|
|
2918
|
-
<div v-else-if="insightStatus === 'not_started'" style="padding: 40px; text-align: center;">
|
|
2919
|
-
<p style="margin-bottom: 20px; color: #7d8590;">
|
|
2920
|
-
Generate an AI-powered quality & performance review of how the agent used its tools, prompts, and workflow in this session
|
|
2921
|
-
</p>
|
|
2922
|
-
<button
|
|
2923
|
-
@click="generateInsight(false)"
|
|
2924
|
-
:disabled="insightLoading"
|
|
2925
|
-
style="
|
|
2926
|
-
background: #238636;
|
|
2927
|
-
color: #fff;
|
|
2928
|
-
border: none;
|
|
2929
|
-
padding: 10px 20px;
|
|
2930
|
-
border-radius: 6px;
|
|
2931
|
-
font-size: 14px;
|
|
2932
|
-
cursor: pointer;
|
|
2933
|
-
font-weight: 500;
|
|
2934
|
-
"
|
|
2935
|
-
@mouseover="$event.target.style.background='#2ea043'"
|
|
2936
|
-
@mouseleave="$event.target.style.background='#238636'"
|
|
2937
|
-
>
|
|
2938
|
-
💡 Generate Agent Review
|
|
2939
|
-
</button>
|
|
2940
|
-
<p style="margin-top: 12px; font-size: 12px; color: #6e7681;">
|
|
2941
|
-
For large sessions this may take several minutes — you'll see a live progress log while the review is being generated.
|
|
2942
|
-
</p>
|
|
2943
|
-
</div>
|
|
2944
|
-
|
|
2945
|
-
<!-- Completed State -->
|
|
2946
|
-
<div v-else-if="insightStatus === 'completed'" style="padding: 20px;">
|
|
2947
|
-
<div style="
|
|
2948
|
-
background: #161b22;
|
|
2949
|
-
border: 1px solid #30363d;
|
|
2950
|
-
border-radius: 6px;
|
|
2951
|
-
padding: 20px;
|
|
2952
|
-
margin-bottom: 20px;
|
|
2953
|
-
">
|
|
2954
|
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
2955
|
-
<span style="color: #7d8590; font-size: 13px;">
|
|
2956
|
-
Generated: {{ formatDateTime(insightGeneratedAt) }}
|
|
2957
|
-
</span>
|
|
2958
|
-
<button
|
|
2959
|
-
@click="regenerateInsight"
|
|
2960
|
-
style="
|
|
2961
|
-
background: transparent;
|
|
2962
|
-
color: #58a6ff;
|
|
2963
|
-
border: 1px solid #58a6ff;
|
|
2964
|
-
padding: 5px 12px;
|
|
2965
|
-
border-radius: 6px;
|
|
2966
|
-
font-size: 12px;
|
|
2967
|
-
cursor: pointer;
|
|
2968
|
-
"
|
|
2969
|
-
@mouseover="$event.target.style.background='rgba(88, 166, 255, 0.1)'"
|
|
2970
|
-
@mouseleave="$event.target.style.background='transparent'"
|
|
2971
|
-
>
|
|
2972
|
-
🔄 Regenerate
|
|
2973
|
-
</button>
|
|
2974
|
-
</div>
|
|
2975
|
-
<div v-html="renderedInsight" style="
|
|
2976
|
-
color: #c9d1d9;
|
|
2977
|
-
line-height: 1.6;
|
|
2978
|
-
"></div>
|
|
2979
|
-
</div>
|
|
2980
|
-
</div>
|
|
2981
|
-
</div>
|
|
2982
|
-
</div>
|
|
2983
|
-
`
|
|
2984
|
-
});
|
|
2985
|
-
|
|
2986
|
-
// Global error handler
|
|
2987
|
-
app.config.errorHandler = (err, instance, info) => {
|
|
2988
|
-
console.error('[Vue Error]', err, info);
|
|
2989
|
-
const errorDiv = document.createElement('div');
|
|
2990
|
-
errorDiv.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #f85149; color: white; padding: 20px; border-radius: 6px; z-index: 9999; max-width: 80%; font-family: monospace; font-size: 14px;';
|
|
2991
|
-
errorDiv.innerHTML = `<strong>Vue Error:</strong><br>${err.message}<br><br><small>${info}</small>`;
|
|
2992
|
-
document.body.appendChild(errorDiv);
|
|
2993
|
-
};
|
|
2994
|
-
|
|
2995
|
-
app.mount('#app');
|
|
2996
|
-
</script>
|
|
781
|
+
<script src="/public/js/time-analyze.min.js"></script>
|
|
2997
782
|
</body>
|
|
2998
783
|
</html>
|