agentacta 2026.3.12 → 2026.3.27
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/README.md +16 -2
- package/config.js +2 -0
- package/db.js +12 -0
- package/delta-attribution-context.js +57 -0
- package/index.js +93 -13
- package/indexer.js +31 -4
- package/insights.js +260 -0
- package/package.json +4 -1
- package/project-attribution.js +443 -0
- package/public/app.js +313 -22
- package/public/index.html +13 -10
- package/public/style.css +197 -16
package/public/app.js
CHANGED
|
@@ -239,6 +239,16 @@ function updateNavActive(view) {
|
|
|
239
239
|
$$('.nav-item').forEach(i => i.classList.remove('active'));
|
|
240
240
|
const navItem = $(`.nav-item[data-view="${view}"]`);
|
|
241
241
|
if (navItem) navItem.classList.add('active');
|
|
242
|
+
// Settings gear buttons
|
|
243
|
+
const isSettings = view === 'stats';
|
|
244
|
+
document.getElementById('settings-btn')?.classList.toggle('active', isSettings);
|
|
245
|
+
document.getElementById('settings-btn-mobile')?.classList.toggle('active', isSettings);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function updateMobileNavActive(view) {
|
|
249
|
+
$$('.mobile-nav-btn').forEach(b => b.classList.remove('active'));
|
|
250
|
+
const btn = $(`.mobile-nav-btn[data-view="${view}"]`);
|
|
251
|
+
if (btn) btn.classList.add('active');
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
function handleRoute() {
|
|
@@ -253,13 +263,15 @@ function handleRoute() {
|
|
|
253
263
|
}
|
|
254
264
|
|
|
255
265
|
const normalized = raw === 'search' ? 'overview' : raw;
|
|
256
|
-
const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' ? normalized : 'overview';
|
|
266
|
+
const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' || normalized === 'insights' ? normalized : 'overview';
|
|
257
267
|
window._lastView = view;
|
|
258
268
|
updateNavActive(view);
|
|
269
|
+
updateMobileNavActive(view);
|
|
259
270
|
if (view === 'overview') viewOverview();
|
|
260
271
|
else if (view === 'sessions') viewSessions();
|
|
261
272
|
else if (view === 'files') viewFiles();
|
|
262
273
|
else if (view === 'timeline') viewTimeline();
|
|
274
|
+
else if (view === 'insights') viewInsights();
|
|
263
275
|
else viewStats();
|
|
264
276
|
}
|
|
265
277
|
|
|
@@ -647,7 +659,9 @@ async function viewSession(id) {
|
|
|
647
659
|
if (data._error || data.error) { content.innerHTML = `<div class="empty"><h2>${escHtml(data.error || 'Unable to load')}</h2></div>`; return; }
|
|
648
660
|
|
|
649
661
|
const s = data.session;
|
|
650
|
-
const
|
|
662
|
+
const projectFilters = Array.isArray(data.projectFilters)
|
|
663
|
+
? data.projectFilters.filter(p => p && p.project && Number.isFinite(Number(p.eventCount)))
|
|
664
|
+
: [];
|
|
651
665
|
let html = `
|
|
652
666
|
<div class="back-btn" id="backBtn">\u2190 Back</div>
|
|
653
667
|
<div class="page-title">Session</div>
|
|
@@ -667,7 +681,6 @@ async function viewSession(id) {
|
|
|
667
681
|
<div class="session-header" style="margin-bottom:12px">
|
|
668
682
|
<span class="session-time">${fmtDate(s.start_time)} \u00b7 ${fmtTimeShort(s.start_time)} \u2013 ${fmtTimeShort(s.end_time)}</span>
|
|
669
683
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
670
|
-
${renderProjectTags(s)}
|
|
671
684
|
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
672
685
|
${s.session_type && s.session_type !== normalizeAgentLabel(s.agent || '') ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
673
686
|
${renderModelTags(s)}
|
|
@@ -678,16 +691,63 @@ async function viewSession(id) {
|
|
|
678
691
|
<span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span> ${s.tool_count} tools</span>
|
|
679
692
|
${s.output_tokens ? `<span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/></svg></span> ${fmtTokens(s.output_tokens)} output</span><span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></span> ${fmtTokens(s.input_tokens + s.cache_read_tokens)} input</span>` : s.total_tokens ? `<span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg></span> ${fmtTokens(s.total_tokens)} tokens</span><span></span>` : '<span></span><span></span>'}
|
|
680
693
|
</div>
|
|
694
|
+
${projectFilters.length ? `
|
|
695
|
+
<div class="section-label" style="margin-top:14px">Project Filter</div>
|
|
696
|
+
<div class="filters" id="sessionProjectFilters" style="margin-bottom:4px">
|
|
697
|
+
<span class="filter-chip active" data-project-filter="all">All</span>
|
|
698
|
+
${projectFilters.map(p => `<span class="filter-chip" data-project-filter="${escHtml(p.project)}">${escHtml(p.project)} · ${p.eventCount}</span>`).join('')}
|
|
699
|
+
</div>
|
|
700
|
+
` : ''}
|
|
681
701
|
</div>
|
|
682
|
-
<div class="section-label">Events</div>
|
|
702
|
+
<div class="section-label" id="sessionEventsLabel">Events</div>
|
|
703
|
+
<div id="eventsContainer"></div>
|
|
704
|
+
<div class="empty" id="sessionEventsEmpty" style="display:none"><h2>No events</h2><p>This session has no events to display.</p></div>
|
|
705
|
+
<div id="sessionInsightsPanel" class="loading" style="margin-top:var(--space-xl)">Loading insights...</div>
|
|
683
706
|
`;
|
|
684
707
|
|
|
685
708
|
const PAGE_SIZE = 50;
|
|
686
|
-
const allEvents = data.events;
|
|
709
|
+
const allEvents = Array.isArray(data.events) ? [...data.events] : [];
|
|
710
|
+
let activeProjectFilter = 'all';
|
|
687
711
|
let rendered = 0;
|
|
712
|
+
let onScroll = null;
|
|
713
|
+
let pendingNewCount = 0;
|
|
714
|
+
|
|
715
|
+
const getFilteredEvents = () => {
|
|
716
|
+
if (activeProjectFilter === 'all') return allEvents;
|
|
717
|
+
return allEvents.filter(ev => ev.project === activeProjectFilter);
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const updateEventsLabel = () => {
|
|
721
|
+
const el = $('#sessionEventsLabel');
|
|
722
|
+
if (!el) return;
|
|
723
|
+
const count = getFilteredEvents().length;
|
|
724
|
+
if (activeProjectFilter === 'all') {
|
|
725
|
+
el.textContent = `Events (${count})`;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
el.textContent = `Events (${count}) · ${activeProjectFilter}`;
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const updateEventsEmptyState = () => {
|
|
732
|
+
const empty = $('#sessionEventsEmpty');
|
|
733
|
+
if (!empty) return;
|
|
734
|
+
empty.style.display = getFilteredEvents().length ? 'none' : 'block';
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const setProjectFilter = (nextFilter) => {
|
|
738
|
+
if (nextFilter === activeProjectFilter) return;
|
|
739
|
+
activeProjectFilter = nextFilter;
|
|
740
|
+
const chips = $$('#sessionProjectFilters .filter-chip');
|
|
741
|
+
chips.forEach(node => node.classList.toggle('active', node.dataset.projectFilter === activeProjectFilter));
|
|
742
|
+
pendingNewCount = 0;
|
|
743
|
+
const indicator = document.getElementById('newEventsIndicator');
|
|
744
|
+
if (indicator) indicator.remove();
|
|
745
|
+
resetRenderedEvents();
|
|
746
|
+
};
|
|
688
747
|
|
|
689
748
|
function renderBatch() {
|
|
690
|
-
const
|
|
749
|
+
const filtered = getFilteredEvents();
|
|
750
|
+
const batch = filtered.slice(rendered, rendered + PAGE_SIZE);
|
|
691
751
|
if (!batch.length) return;
|
|
692
752
|
const frag = document.createElement('div');
|
|
693
753
|
frag.innerHTML = batch.map(renderEvent).join('');
|
|
@@ -696,33 +756,61 @@ async function viewSession(id) {
|
|
|
696
756
|
while (frag.firstChild) container.appendChild(frag.firstChild);
|
|
697
757
|
}
|
|
698
758
|
rendered += batch.length;
|
|
699
|
-
|
|
700
759
|
}
|
|
701
760
|
|
|
702
|
-
html += '<div id="eventsContainer">' + allEvents.slice(0, PAGE_SIZE).map(renderEvent).join('') + '</div>';
|
|
703
|
-
rendered = Math.min(PAGE_SIZE, allEvents.length);
|
|
704
761
|
content.innerHTML = html;
|
|
705
762
|
transitionView();
|
|
706
763
|
|
|
707
|
-
|
|
708
|
-
|
|
764
|
+
const syncScrollHandler = () => {
|
|
765
|
+
const total = getFilteredEvents().length;
|
|
766
|
+
if (total <= rendered) {
|
|
767
|
+
if (onScroll) {
|
|
768
|
+
window.removeEventListener('scroll', onScroll);
|
|
769
|
+
onScroll = null;
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (onScroll) return;
|
|
775
|
+
|
|
709
776
|
let loading = false;
|
|
710
777
|
onScroll = () => {
|
|
711
|
-
if (loading || rendered >=
|
|
778
|
+
if (loading || rendered >= getFilteredEvents().length) return;
|
|
712
779
|
const scrollBottom = window.innerHeight + window.scrollY;
|
|
713
780
|
const threshold = document.body.offsetHeight - 300;
|
|
714
781
|
if (scrollBottom >= threshold) {
|
|
715
782
|
loading = true;
|
|
716
783
|
renderBatch();
|
|
717
784
|
loading = false;
|
|
718
|
-
if (rendered >=
|
|
785
|
+
if (rendered >= getFilteredEvents().length) {
|
|
719
786
|
window.removeEventListener('scroll', onScroll);
|
|
720
787
|
onScroll = null;
|
|
721
788
|
}
|
|
722
789
|
}
|
|
723
790
|
};
|
|
724
791
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
725
|
-
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const resetRenderedEvents = () => {
|
|
795
|
+
const container = document.getElementById('eventsContainer');
|
|
796
|
+
if (!container) return;
|
|
797
|
+
container.innerHTML = '';
|
|
798
|
+
rendered = 0;
|
|
799
|
+
renderBatch();
|
|
800
|
+
syncScrollHandler();
|
|
801
|
+
updateEventsLabel();
|
|
802
|
+
updateEventsEmptyState();
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
resetRenderedEvents();
|
|
806
|
+
|
|
807
|
+
const filterChips = $$('#sessionProjectFilters .filter-chip');
|
|
808
|
+
filterChips.forEach(chip => {
|
|
809
|
+
chip.addEventListener('click', () => {
|
|
810
|
+
const nextFilter = chip.dataset.projectFilter || 'all';
|
|
811
|
+
setProjectFilter(nextFilter);
|
|
812
|
+
});
|
|
813
|
+
});
|
|
726
814
|
|
|
727
815
|
$('#backBtn').addEventListener('click', () => {
|
|
728
816
|
clearJumpUi();
|
|
@@ -778,22 +866,35 @@ async function viewSession(id) {
|
|
|
778
866
|
if (jumpBtn) {
|
|
779
867
|
jumpBtn.addEventListener('click', async () => {
|
|
780
868
|
const fromY = window.scrollY || window.pageYOffset || 0;
|
|
869
|
+
const firstMessageId = s.first_message_id;
|
|
781
870
|
|
|
782
871
|
jumpBtn.classList.add('jumping');
|
|
783
872
|
jumpBtn.disabled = true;
|
|
784
873
|
|
|
874
|
+
if (!firstMessageId) {
|
|
875
|
+
jumpBtn.classList.remove('jumping');
|
|
876
|
+
jumpBtn.disabled = false;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Resolve from full session context, not just project-filtered events.
|
|
881
|
+
const inCurrentFilter = getFilteredEvents().some(ev => ev.id === firstMessageId);
|
|
882
|
+
if (!inCurrentFilter && activeProjectFilter !== 'all') {
|
|
883
|
+
setProjectFilter('all');
|
|
884
|
+
}
|
|
885
|
+
|
|
785
886
|
// Let button state paint before heavy DOM work.
|
|
786
887
|
await new Promise(requestAnimationFrame);
|
|
787
888
|
|
|
788
889
|
// Load remaining events in chunks so UI stays responsive.
|
|
789
890
|
let loops = 0;
|
|
790
|
-
while (rendered <
|
|
891
|
+
while (rendered < getFilteredEvents().length) {
|
|
791
892
|
renderBatch();
|
|
792
893
|
loops += 1;
|
|
793
894
|
if (loops % 2 === 0) await new Promise(requestAnimationFrame);
|
|
794
895
|
}
|
|
795
896
|
|
|
796
|
-
const firstMessage = document.querySelector(`[data-event-id="${
|
|
897
|
+
const firstMessage = document.querySelector(`[data-event-id="${firstMessageId}"]`);
|
|
797
898
|
if (!firstMessage) {
|
|
798
899
|
jumpBtn.classList.remove('jumping');
|
|
799
900
|
jumpBtn.disabled = false;
|
|
@@ -838,9 +939,8 @@ async function viewSession(id) {
|
|
|
838
939
|
});
|
|
839
940
|
}
|
|
840
941
|
|
|
841
|
-
|
|
942
|
+
// --- Lightweight realtime updates (polling fallback first) ---
|
|
842
943
|
const knownIds = new Set(allEvents.map(e => e.id));
|
|
843
|
-
let pendingNewCount = 0;
|
|
844
944
|
|
|
845
945
|
const applyIncomingEvents = (incoming) => {
|
|
846
946
|
const container = document.getElementById('eventsContainer');
|
|
@@ -850,8 +950,22 @@ async function viewSession(id) {
|
|
|
850
950
|
if (!fresh.length) return;
|
|
851
951
|
fresh.forEach(e => knownIds.add(e.id));
|
|
852
952
|
|
|
953
|
+
// Delta endpoint returns oldest -> newest, and view is newest-first.
|
|
954
|
+
for (const ev of fresh) allEvents.unshift(ev);
|
|
955
|
+
|
|
956
|
+
const visibleFresh = activeProjectFilter === 'all'
|
|
957
|
+
? fresh
|
|
958
|
+
: fresh.filter(ev => ev.project === activeProjectFilter);
|
|
959
|
+
|
|
960
|
+
if (!visibleFresh.length) {
|
|
961
|
+
updateEventsLabel();
|
|
962
|
+
updateEventsEmptyState();
|
|
963
|
+
syncScrollHandler();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
853
967
|
const isAtTop = window.scrollY < 100;
|
|
854
|
-
for (const ev of
|
|
968
|
+
for (const ev of visibleFresh) {
|
|
855
969
|
const div = document.createElement('div');
|
|
856
970
|
div.innerHTML = renderEvent(ev);
|
|
857
971
|
const el = div.firstElementChild;
|
|
@@ -859,9 +973,13 @@ async function viewSession(id) {
|
|
|
859
973
|
container.insertBefore(el, container.firstChild);
|
|
860
974
|
setTimeout(() => el.classList.remove('event-highlight'), 2000);
|
|
861
975
|
}
|
|
976
|
+
rendered += visibleFresh.length;
|
|
977
|
+
updateEventsLabel();
|
|
978
|
+
updateEventsEmptyState();
|
|
979
|
+
syncScrollHandler();
|
|
862
980
|
|
|
863
981
|
if (!isAtTop) {
|
|
864
|
-
pendingNewCount +=
|
|
982
|
+
pendingNewCount += visibleFresh.length;
|
|
865
983
|
let indicator = document.getElementById('newEventsIndicator');
|
|
866
984
|
if (!indicator) {
|
|
867
985
|
indicator = document.createElement('div');
|
|
@@ -913,6 +1031,12 @@ async function viewSession(id) {
|
|
|
913
1031
|
const ind = document.getElementById('newEventsIndicator');
|
|
914
1032
|
if (ind) ind.remove();
|
|
915
1033
|
};
|
|
1034
|
+
|
|
1035
|
+
// Load insights panel
|
|
1036
|
+
api(`/insights/session/${id}`).then(insights => {
|
|
1037
|
+
const panel = document.getElementById('sessionInsightsPanel');
|
|
1038
|
+
if (panel) panel.outerHTML = renderInsightsPanel(insights._error ? null : insights);
|
|
1039
|
+
});
|
|
916
1040
|
}
|
|
917
1041
|
|
|
918
1042
|
async function viewTimeline(date) {
|
|
@@ -1091,7 +1215,7 @@ async function viewStats() {
|
|
|
1091
1215
|
<div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
|
|
1092
1216
|
<div class="config-card"><div class="config-label">DB Size</div><div class="config-value" id="dbSizeValue">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
|
|
1093
1217
|
</div>
|
|
1094
|
-
<p class="settings-help" style="margin-bottom:var(--space-sm)">Date range: ${fmtDate(data.dateRange?.earliest)}
|
|
1218
|
+
<p class="settings-help" style="margin-bottom:var(--space-sm)">Date range: ${fmtDate(data.dateRange?.earliest)} to ${fmtDate(data.dateRange?.latest)}</p>
|
|
1095
1219
|
<div class="settings-maintenance">
|
|
1096
1220
|
<button class="export-btn" id="optimizeDbBtn">Optimize Database</button>
|
|
1097
1221
|
<span id="optimizeDbStatus" class="settings-maintenance-status"></span>
|
|
@@ -1419,6 +1543,166 @@ async function viewFileDetail(filePath) {
|
|
|
1419
1543
|
});
|
|
1420
1544
|
}
|
|
1421
1545
|
|
|
1546
|
+
// --- Insights helpers ---
|
|
1547
|
+
const SIGNAL_LABELS = {
|
|
1548
|
+
tool_retry_loop: 'Repeated Actions',
|
|
1549
|
+
session_bail: 'No Output Produced',
|
|
1550
|
+
high_error_rate: 'Frequent Errors',
|
|
1551
|
+
long_prompt_short_session: 'Vague Instructions',
|
|
1552
|
+
no_completion: 'Incomplete Session'
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const SIGNAL_DESCRIPTIONS = {
|
|
1556
|
+
tool_retry_loop: 'The agent called the same tool many times in a row, suggesting it was stuck in a retry loop',
|
|
1557
|
+
session_bail: 'The agent ran many actions but never wrote or edited any files',
|
|
1558
|
+
high_error_rate: 'More than 30% of tool calls returned errors',
|
|
1559
|
+
long_prompt_short_session: 'A very short prompt led to a long session, suggesting the agent lacked sufficient context',
|
|
1560
|
+
no_completion: 'The session ended mid-action instead of finishing with a response'
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
const SIGNAL_COLORS = {
|
|
1564
|
+
tool_retry_loop: 'amber',
|
|
1565
|
+
high_error_rate: 'red',
|
|
1566
|
+
no_completion: 'purple',
|
|
1567
|
+
session_bail: 'teal',
|
|
1568
|
+
long_prompt_short_session: 'accent'
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
function renderSignalTag(sig) {
|
|
1572
|
+
const label = SIGNAL_LABELS[sig.type] || sig.type;
|
|
1573
|
+
const color = SIGNAL_COLORS[sig.type] || 'muted';
|
|
1574
|
+
const desc = SIGNAL_DESCRIPTIONS[sig.type] || '';
|
|
1575
|
+
return `<span class="signal-tag signal-${color}"${desc ? ` title="${escHtml(desc)}"` : ''}><span class="signal-dot"></span>${escHtml(label)}</span>`;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function renderReliabilityBadge(score) {
|
|
1579
|
+
const reliability = 100 - score;
|
|
1580
|
+
return `<span class="insight-score-value" title="Reliability score: higher means fewer errors">${reliability}</span>`;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function renderIssueRateBadge(signals) {
|
|
1584
|
+
const TOTAL_SIGNAL_TYPES = 5;
|
|
1585
|
+
const uniqueTypes = new Set((signals || []).map(s => s.type)).size;
|
|
1586
|
+
const rate = Math.round((uniqueTypes / TOTAL_SIGNAL_TYPES) * 100);
|
|
1587
|
+
return `<span class="insight-score-value" title="${uniqueTypes} of ${TOTAL_SIGNAL_TYPES} issue types detected">${rate}%</span>`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function renderInsightsPanel(insights) {
|
|
1591
|
+
if (!insights || !insights.signals || !insights.signals.length) {
|
|
1592
|
+
return `<div class="insights-panel insights-clean"><span style="color:var(--text-tertiary);font-size:13px">No issues detected</span></div>`;
|
|
1593
|
+
}
|
|
1594
|
+
return `<div class="insights-panel">
|
|
1595
|
+
<div class="section-label" style="margin-top:0">Session Health ${renderReliabilityBadge(insights.confusion_score)}</div>
|
|
1596
|
+
<div class="insights-signals">
|
|
1597
|
+
${insights.signals.map(sig => {
|
|
1598
|
+
let detail = '';
|
|
1599
|
+
if (sig.type === 'tool_retry_loop') detail = `${escHtml(sig.tool)} called ${sig.count}x consecutively`;
|
|
1600
|
+
else if (sig.type === 'session_bail') detail = `${sig.tool_calls} tool calls with no file writes`;
|
|
1601
|
+
else if (sig.type === 'high_error_rate') detail = `${sig.rate}% error rate (${sig.error_count}/${sig.total})`;
|
|
1602
|
+
else if (sig.type === 'long_prompt_short_session') detail = `${sig.prompt_words} word prompt, ${sig.tool_calls} tool calls`;
|
|
1603
|
+
else if (sig.type === 'no_completion') detail = `Ended on ${escHtml(sig.last_event_type)}${sig.last_tool ? ': ' + escHtml(sig.last_tool) : ''}`;
|
|
1604
|
+
return `<div class="insight-callout">
|
|
1605
|
+
${renderSignalTag(sig)}
|
|
1606
|
+
<span class="insight-detail">${detail}</span>
|
|
1607
|
+
</div>`;
|
|
1608
|
+
}).join('')}
|
|
1609
|
+
</div>
|
|
1610
|
+
</div>`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
async function viewInsights() {
|
|
1614
|
+
clearJumpUi();
|
|
1615
|
+
window._lastView = 'insights';
|
|
1616
|
+
content.innerHTML = `<div class="page-title">Insights</div><div class="stat-grid">${skeletonRows(3, 'stats')}</div>`;
|
|
1617
|
+
transitionView();
|
|
1618
|
+
|
|
1619
|
+
const data = await api('/insights');
|
|
1620
|
+
if (data._error) {
|
|
1621
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Signal breakdown data — sorted by count descending
|
|
1626
|
+
const signalTypes = Object.keys(SIGNAL_LABELS).sort((a, b) => (data.signal_counts[b] || 0) - (data.signal_counts[a] || 0));
|
|
1627
|
+
const maxSignalCount = Math.max(1, ...signalTypes.map(t => data.signal_counts[t] || 0));
|
|
1628
|
+
|
|
1629
|
+
// Issue rate
|
|
1630
|
+
const issueRate = data.total_sessions > 0 ? Math.round((data.flagged_count / data.total_sessions) * 100) : 0;
|
|
1631
|
+
|
|
1632
|
+
// Reliability score (inverted struggle score)
|
|
1633
|
+
const reliabilityScore = 100 - (data.avg_confusion_score || 0);
|
|
1634
|
+
|
|
1635
|
+
let html = `<div class="page-title">Insights</div>
|
|
1636
|
+
|
|
1637
|
+
<div class="stat-grid">
|
|
1638
|
+
<div class="stat-card accent-amber"><div class="label">Issue Rate</div><div class="value">${issueRate}%</div><div class="stat-desc">${data.flagged_count} of ${data.total_sessions} sessions had at least one detected issue</div></div>
|
|
1639
|
+
<div class="stat-card accent-purple"><div class="label">Reliability Score</div><div class="value">${reliabilityScore}</div><div class="stat-desc">How often the agent completed tasks cleanly, out of 100</div></div>
|
|
1640
|
+
</div>
|
|
1641
|
+
|
|
1642
|
+
<div class="section-label">Issue Types</div>
|
|
1643
|
+
<div class="signal-chart">
|
|
1644
|
+
${signalTypes.map(type => {
|
|
1645
|
+
const count = data.signal_counts[type] || 0;
|
|
1646
|
+
const pct = Math.round((count / maxSignalCount) * 100);
|
|
1647
|
+
const color = SIGNAL_COLORS[type] || 'muted';
|
|
1648
|
+
const desc = SIGNAL_DESCRIPTIONS[type] || '';
|
|
1649
|
+
const barColor = color === 'muted' ? 'var(--text-tertiary)' : `var(--${color})`;
|
|
1650
|
+
return `<div class="signal-lollipop-row${desc ? ' signal-lollipop-expandable' : ''}" data-desc="${escHtml(desc)}">
|
|
1651
|
+
<div class="signal-lollipop-main">
|
|
1652
|
+
<span class="signal-bar-label">${SIGNAL_LABELS[type]}</span>
|
|
1653
|
+
<div class="signal-lollipop-track">
|
|
1654
|
+
<svg width="100%" height="20" class="signal-lollipop-svg">
|
|
1655
|
+
<line x1="0" y1="10" x2="${pct}%" y2="10" stroke="${barColor}" stroke-width="2"/>
|
|
1656
|
+
<circle cx="${pct}%" cy="10" r="5" fill="${barColor}"/>
|
|
1657
|
+
</svg>
|
|
1658
|
+
</div>
|
|
1659
|
+
<span class="signal-bar-count">${count}</span>
|
|
1660
|
+
</div>
|
|
1661
|
+
${desc ? `<div class="signal-lollipop-desc">${escHtml(desc)}</div>` : ''}
|
|
1662
|
+
</div>`;
|
|
1663
|
+
}).join('')}
|
|
1664
|
+
</div>
|
|
1665
|
+
|
|
1666
|
+
<div class="section-label">Sessions with Issues (${data.flagged_count})</div>
|
|
1667
|
+
<div id="insightsList">
|
|
1668
|
+
${data.top_flagged.length ? [...data.top_flagged].sort((a, b) => {
|
|
1669
|
+
const aRate = new Set((a.signals||[]).map(s=>s.type)).size;
|
|
1670
|
+
const bRate = new Set((b.signals||[]).map(s=>s.type)).size;
|
|
1671
|
+
return bRate - aRate || b.confusion_score - a.confusion_score;
|
|
1672
|
+
}).map(s => {
|
|
1673
|
+
const summary = cleanSessionSummary(s.summary, '');
|
|
1674
|
+
return `<div class="session-item insight-row" data-id="${escHtml(s.session_id)}">
|
|
1675
|
+
<div class="session-header">
|
|
1676
|
+
<span class="session-time">${fmtTime(s.start_time)}</span>
|
|
1677
|
+
<span class="insight-scores">issue rate ${renderIssueRateBadge(s.signals)} · reliability ${renderReliabilityBadge(s.confusion_score)}</span>
|
|
1678
|
+
</div>
|
|
1679
|
+
<div class="session-summary">${escHtml(truncate(summary, 120))}</div>
|
|
1680
|
+
<div class="session-meta">
|
|
1681
|
+
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
1682
|
+
${s.agent ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
1683
|
+
${s.model ? `<span class="session-model">${escHtml(s.model)}</span>` : ''}
|
|
1684
|
+
</span>
|
|
1685
|
+
<span class="insight-signal-tags">${s.signals.map(sig => renderSignalTag(sig)).join('')}</span>
|
|
1686
|
+
</div>
|
|
1687
|
+
</div>`;
|
|
1688
|
+
}).join('') : '<div class="empty"><p>No flagged sessions found</p></div>'}
|
|
1689
|
+
</div>
|
|
1690
|
+
`;
|
|
1691
|
+
|
|
1692
|
+
content.innerHTML = html;
|
|
1693
|
+
transitionView();
|
|
1694
|
+
|
|
1695
|
+
$$('.session-item', content).forEach(item => {
|
|
1696
|
+
item.addEventListener('click', () => viewSession(item.dataset.id));
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
$$('.signal-lollipop-expandable', content).forEach(row => {
|
|
1700
|
+
row.addEventListener('click', () => {
|
|
1701
|
+
row.classList.toggle('signal-lollipop-open');
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1422
1706
|
// --- Navigation ---
|
|
1423
1707
|
window._searchType = '';
|
|
1424
1708
|
window._searchRole = '';
|
|
@@ -1437,6 +1721,7 @@ $$('.nav-item').forEach(item => {
|
|
|
1437
1721
|
else if (view === 'sessions') viewSessions();
|
|
1438
1722
|
else if (view === 'files') viewFiles();
|
|
1439
1723
|
else if (view === 'timeline') viewTimeline();
|
|
1724
|
+
else if (view === 'insights') viewInsights();
|
|
1440
1725
|
else if (view === 'stats') viewStats();
|
|
1441
1726
|
});
|
|
1442
1727
|
});
|
|
@@ -1683,8 +1968,14 @@ function openCmdk() {
|
|
|
1683
1968
|
initTheme();
|
|
1684
1969
|
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
|
|
1685
1970
|
document.getElementById('theme-toggle-mobile')?.addEventListener('click', toggleTheme);
|
|
1686
|
-
document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
|
|
1687
1971
|
document.getElementById('mobile-search-btn')?.addEventListener('click', () => openCmdk());
|
|
1972
|
+
document.getElementById('settings-btn')?.addEventListener('click', () => { window._lastView = 'stats'; updateNavActive('stats'); setHash('stats'); viewStats(); });
|
|
1973
|
+
document.getElementById('settings-btn-mobile')?.addEventListener('click', () => { window._lastView = 'stats'; updateNavActive('stats'); setHash('stats'); viewStats(); });
|
|
1974
|
+
document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
|
|
1688
1979
|
document.addEventListener('keydown', e => {
|
|
1689
1980
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
1690
1981
|
e.preventDefault();
|
package/public/index.html
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
<nav class="sidebar">
|
|
22
22
|
<div class="sidebar-header">
|
|
23
23
|
<h1>Agent<span>Acta</span></h1>
|
|
24
|
-
<
|
|
24
|
+
<div class="sidebar-header-actions">
|
|
25
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme"><svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg><svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
|
|
26
|
+
<button class="theme-toggle" id="settings-btn" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
|
27
|
+
</div>
|
|
25
28
|
</div>
|
|
26
29
|
<button class="cmdk-trigger" id="cmdkBtn" aria-label="Search" title="Search (⌘K)">
|
|
27
30
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
@@ -29,26 +32,26 @@
|
|
|
29
32
|
<kbd>⌘K</kbd>
|
|
30
33
|
</button>
|
|
31
34
|
<div class="nav-section">
|
|
35
|
+
<div class="nav-item active" data-view="overview">
|
|
36
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h7V3H3z"/><path d="M14 21h7v-7h-7z"/><path d="M14 10h7V3h-7z"/><path d="M3 21h7v-5H3z"/></svg>
|
|
37
|
+
<span>Overview</span>
|
|
38
|
+
</div>
|
|
32
39
|
<div class="nav-item" data-view="sessions">
|
|
33
40
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
34
41
|
<span>Sessions</span>
|
|
35
42
|
</div>
|
|
43
|
+
<div class="nav-item" data-view="insights">
|
|
44
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
45
|
+
<span>Insights</span>
|
|
46
|
+
</div>
|
|
36
47
|
<div class="nav-item" data-view="timeline">
|
|
37
48
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
38
49
|
<span>Timeline</span>
|
|
39
50
|
</div>
|
|
40
|
-
<div class="nav-item active" data-view="overview">
|
|
41
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h7V3H3z"/><path d="M14 21h7v-7h-7z"/><path d="M14 10h7V3h-7z"/><path d="M3 21h7v-5H3z"/></svg>
|
|
42
|
-
<span>Overview</span>
|
|
43
|
-
</div>
|
|
44
51
|
<div class="nav-item" data-view="files">
|
|
45
52
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
46
53
|
<span>Files</span>
|
|
47
54
|
</div>
|
|
48
|
-
<div class="nav-item" data-view="stats">
|
|
49
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
50
|
-
<span>Settings</span>
|
|
51
|
-
</div>
|
|
52
55
|
|
|
53
56
|
</div>
|
|
54
57
|
</nav>
|
|
@@ -56,8 +59,8 @@
|
|
|
56
59
|
<div class="mobile-toolbar">
|
|
57
60
|
<button class="mobile-search-btn" id="mobile-search-btn" title="Search" aria-label="Search"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></button>
|
|
58
61
|
<button class="theme-toggle-mobile" id="theme-toggle-mobile" title="Toggle theme" aria-label="Toggle theme"><svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg><svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
|
|
62
|
+
<button class="theme-toggle-mobile" id="settings-btn-mobile" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
|
59
63
|
</div>
|
|
60
|
-
|
|
61
64
|
</div>
|
|
62
65
|
<script src="/app.js"></script>
|
|
63
66
|
<script>
|