agentacta 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +69 -1
- package/package.json +1 -1
- package/public/app.js +134 -8
- package/public/style.css +28 -0
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { EventEmitter } = require('events');
|
|
5
6
|
|
|
6
7
|
// --demo flag: use demo session data (must run before config load)
|
|
7
8
|
if (process.argv.includes('--demo')) {
|
|
@@ -129,6 +130,10 @@ const db = open();
|
|
|
129
130
|
// Live re-indexing setup
|
|
130
131
|
const stmts = createStmts(db);
|
|
131
132
|
|
|
133
|
+
// SSE emitter: notifies connected clients when a session is re-indexed
|
|
134
|
+
const sseEmitter = new EventEmitter();
|
|
135
|
+
sseEmitter.setMaxListeners(100);
|
|
136
|
+
|
|
132
137
|
const sessionDirs = discoverSessionDirs(config);
|
|
133
138
|
|
|
134
139
|
// Initial indexing pass
|
|
@@ -163,7 +168,10 @@ for (const dir of sessionDirs) {
|
|
|
163
168
|
_reindexTimers.delete(filePath);
|
|
164
169
|
try {
|
|
165
170
|
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE);
|
|
166
|
-
if (!result.skipped)
|
|
171
|
+
if (!result.skipped) {
|
|
172
|
+
console.log(`Live re-indexed: ${filename} (${dir.agent})`);
|
|
173
|
+
if (result.sessionId) sseEmitter.emit('session-update', result.sessionId);
|
|
174
|
+
}
|
|
167
175
|
} catch (err) {
|
|
168
176
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
169
177
|
}
|
|
@@ -252,6 +260,66 @@ const server = http.createServer((req, res) => {
|
|
|
252
260
|
const total = agent ? db.prepare(countSql).get(agent).c : db.prepare(countSql).get().c;
|
|
253
261
|
json(res, { sessions: rows, total, limit, offset });
|
|
254
262
|
}
|
|
263
|
+
|
|
264
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/events$/)) {
|
|
265
|
+
const id = pathname.split('/')[3];
|
|
266
|
+
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
|
|
267
|
+
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
268
|
+
|
|
269
|
+
const after = query.after || '1970-01-01T00:00:00.000Z';
|
|
270
|
+
const afterId = query.afterId || '';
|
|
271
|
+
const limit = Math.min(parseInt(query.limit || '50', 10) || 50, 200);
|
|
272
|
+
const rows = db.prepare(
|
|
273
|
+
`SELECT * FROM events
|
|
274
|
+
WHERE session_id = ?
|
|
275
|
+
AND (timestamp > ? OR (timestamp = ? AND id > ?))
|
|
276
|
+
ORDER BY timestamp ASC, id ASC
|
|
277
|
+
LIMIT ?`
|
|
278
|
+
).all(id, after, after, afterId, limit);
|
|
279
|
+
json(res, { events: rows, after, afterId, count: rows.length });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/stream$/)) {
|
|
283
|
+
const id = pathname.split('/')[3];
|
|
284
|
+
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
|
|
285
|
+
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
286
|
+
|
|
287
|
+
res.writeHead(200, {
|
|
288
|
+
'Content-Type': 'text/event-stream',
|
|
289
|
+
'Cache-Control': 'no-cache',
|
|
290
|
+
'Connection': 'keep-alive',
|
|
291
|
+
'X-Accel-Buffering': 'no'
|
|
292
|
+
});
|
|
293
|
+
res.write(': connected\n\n');
|
|
294
|
+
|
|
295
|
+
let lastTs = req.headers['last-event-id'] || query.after || new Date().toISOString();
|
|
296
|
+
|
|
297
|
+
const onUpdate = (sessionId) => {
|
|
298
|
+
if (sessionId !== id) return;
|
|
299
|
+
try {
|
|
300
|
+
const rows = db.prepare(
|
|
301
|
+
'SELECT * FROM events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp ASC'
|
|
302
|
+
).all(id, lastTs);
|
|
303
|
+
if (rows.length) {
|
|
304
|
+
lastTs = rows[rows.length - 1].timestamp;
|
|
305
|
+
res.write(`id: ${lastTs}\ndata: ${JSON.stringify(rows)}\n\n`);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error('SSE query error:', err.message);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
sseEmitter.on('session-update', onUpdate);
|
|
313
|
+
|
|
314
|
+
const ping = setInterval(() => {
|
|
315
|
+
try { res.write(': ping\n\n'); } catch {}
|
|
316
|
+
}, 30000);
|
|
317
|
+
|
|
318
|
+
req.on('close', () => {
|
|
319
|
+
sseEmitter.off('session-update', onUpdate);
|
|
320
|
+
clearInterval(ping);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
255
323
|
else if (pathname.match(/^\/api\/sessions\/[^/]+$/) && !pathname.includes('export')) {
|
|
256
324
|
const id = pathname.split('/')[3];
|
|
257
325
|
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -49,6 +49,7 @@ function dlExport(url, filename) {
|
|
|
49
49
|
|
|
50
50
|
function fmtTokens(n) {
|
|
51
51
|
if (!n) return '0';
|
|
52
|
+
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(4) + 'B';
|
|
52
53
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
53
54
|
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
54
55
|
return n.toLocaleString();
|
|
@@ -94,6 +95,50 @@ function transitionView() {
|
|
|
94
95
|
content.classList.add('view-enter');
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
// --- Hash routing ---
|
|
99
|
+
window._navDepth = 0;
|
|
100
|
+
|
|
101
|
+
function setHash(hash, replace) {
|
|
102
|
+
const target = '#' + hash;
|
|
103
|
+
if (window.location.hash === target) return;
|
|
104
|
+
if (replace) {
|
|
105
|
+
history.replaceState(null, '', target);
|
|
106
|
+
} else {
|
|
107
|
+
history.pushState(null, '', target);
|
|
108
|
+
window._navDepth++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateNavActive(view) {
|
|
113
|
+
$$('.nav-item').forEach(i => i.classList.remove('active'));
|
|
114
|
+
const navItem = $(`.nav-item[data-view="${view}"]`);
|
|
115
|
+
if (navItem) navItem.classList.add('active');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleRoute() {
|
|
119
|
+
const raw = (window.location.hash || '').slice(1) || 'search';
|
|
120
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
121
|
+
|
|
122
|
+
if (raw.startsWith('session/')) {
|
|
123
|
+
const id = decodeURIComponent(raw.slice('session/'.length));
|
|
124
|
+
if (id) { viewSession(id); return; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const view = raw === 'sessions' || raw === 'timeline' || raw === 'files' || raw === 'stats' ? raw : 'search';
|
|
128
|
+
window._lastView = view;
|
|
129
|
+
updateNavActive(view);
|
|
130
|
+
if (view === 'sessions') viewSessions();
|
|
131
|
+
else if (view === 'files') viewFiles();
|
|
132
|
+
else if (view === 'timeline') viewTimeline();
|
|
133
|
+
else if (view === 'stats') viewStats();
|
|
134
|
+
else viewSearch(window._lastSearchQuery || '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
window.addEventListener('popstate', () => {
|
|
138
|
+
if (window._navDepth > 0) window._navDepth--;
|
|
139
|
+
handleRoute();
|
|
140
|
+
});
|
|
141
|
+
|
|
97
142
|
function renderEvent(ev) {
|
|
98
143
|
const badge = `<span class="event-badge ${badgeClass(ev.type, ev.role)}">${ev.type === 'tool_call' ? 'tool' : ev.role || ev.type}</span>`;
|
|
99
144
|
let body = '';
|
|
@@ -383,7 +428,9 @@ async function viewSessions() {
|
|
|
383
428
|
}
|
|
384
429
|
|
|
385
430
|
async function viewSession(id) {
|
|
431
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
386
432
|
window._currentSessionId = id;
|
|
433
|
+
setHash('session/' + encodeURIComponent(id));
|
|
387
434
|
window.scrollTo(0, 0);
|
|
388
435
|
const data = await api(`/sessions/${id}`);
|
|
389
436
|
|
|
@@ -468,11 +515,13 @@ async function viewSession(id) {
|
|
|
468
515
|
}
|
|
469
516
|
|
|
470
517
|
$('#backBtn').addEventListener('click', () => {
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
else
|
|
474
|
-
|
|
475
|
-
|
|
518
|
+
if (window._navDepth > 0) {
|
|
519
|
+
history.back();
|
|
520
|
+
} else {
|
|
521
|
+
const view = window._lastView || 'sessions';
|
|
522
|
+
setHash(view, true);
|
|
523
|
+
handleRoute();
|
|
524
|
+
}
|
|
476
525
|
});
|
|
477
526
|
|
|
478
527
|
$('#copySessionId').addEventListener('click', async () => {
|
|
@@ -531,6 +580,82 @@ async function viewSession(id) {
|
|
|
531
580
|
}
|
|
532
581
|
});
|
|
533
582
|
}
|
|
583
|
+
|
|
584
|
+
// --- Lightweight realtime updates (polling fallback first) ---
|
|
585
|
+
const knownIds = new Set(allEvents.map(e => e.id));
|
|
586
|
+
let pendingNewCount = 0;
|
|
587
|
+
|
|
588
|
+
const applyIncomingEvents = (incoming) => {
|
|
589
|
+
const container = document.getElementById('eventsContainer');
|
|
590
|
+
if (!container || !incoming?.length) return;
|
|
591
|
+
|
|
592
|
+
const fresh = incoming.filter(e => !knownIds.has(e.id));
|
|
593
|
+
if (!fresh.length) return;
|
|
594
|
+
fresh.forEach(e => knownIds.add(e.id));
|
|
595
|
+
|
|
596
|
+
const isAtTop = window.scrollY < 100;
|
|
597
|
+
for (const ev of fresh) {
|
|
598
|
+
const div = document.createElement('div');
|
|
599
|
+
div.innerHTML = renderEvent(ev);
|
|
600
|
+
const el = div.firstElementChild;
|
|
601
|
+
el.classList.add('event-highlight');
|
|
602
|
+
container.insertBefore(el, container.firstChild);
|
|
603
|
+
setTimeout(() => el.classList.remove('event-highlight'), 2000);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!isAtTop) {
|
|
607
|
+
pendingNewCount += fresh.length;
|
|
608
|
+
let indicator = document.getElementById('newEventsIndicator');
|
|
609
|
+
if (!indicator) {
|
|
610
|
+
indicator = document.createElement('div');
|
|
611
|
+
indicator.id = 'newEventsIndicator';
|
|
612
|
+
indicator.className = 'new-events-indicator';
|
|
613
|
+
document.body.appendChild(indicator);
|
|
614
|
+
indicator.addEventListener('click', () => {
|
|
615
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
616
|
+
indicator.remove();
|
|
617
|
+
pendingNewCount = 0;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
indicator.textContent = `${pendingNewCount} new event${pendingNewCount !== 1 ? 's' : ''} ↑`;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Poll every 3s for new events using delta endpoint
|
|
625
|
+
let lastSeenTs = allEvents.length ? allEvents[0].timestamp : new Date(0).toISOString();
|
|
626
|
+
let lastSeenId = allEvents.length ? allEvents[0].id : '';
|
|
627
|
+
const pollNewEvents = async () => {
|
|
628
|
+
try {
|
|
629
|
+
const latest = await api(`/sessions/${id}/events?after=${encodeURIComponent(lastSeenTs)}&afterId=${encodeURIComponent(lastSeenId)}&limit=50`);
|
|
630
|
+
const incoming = latest.events || [];
|
|
631
|
+
if (incoming.length) {
|
|
632
|
+
const tail = incoming[incoming.length - 1];
|
|
633
|
+
lastSeenTs = tail.timestamp || lastSeenTs;
|
|
634
|
+
lastSeenId = tail.id || lastSeenId;
|
|
635
|
+
applyIncomingEvents(incoming);
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
// silent; next tick will retry
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const pollInterval = setInterval(pollNewEvents, 3000);
|
|
643
|
+
|
|
644
|
+
const sseScrollHandler = () => {
|
|
645
|
+
if (window.scrollY < 100) {
|
|
646
|
+
const ind = document.getElementById('newEventsIndicator');
|
|
647
|
+
if (ind) { ind.remove(); pendingNewCount = 0; }
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
window.addEventListener('scroll', sseScrollHandler, { passive: true });
|
|
651
|
+
|
|
652
|
+
window._sseCleanup = () => {
|
|
653
|
+
if (onScroll) { window.removeEventListener('scroll', onScroll); onScroll = null; }
|
|
654
|
+
clearInterval(pollInterval);
|
|
655
|
+
window.removeEventListener('scroll', sseScrollHandler);
|
|
656
|
+
const ind = document.getElementById('newEventsIndicator');
|
|
657
|
+
if (ind) ind.remove();
|
|
658
|
+
};
|
|
534
659
|
}
|
|
535
660
|
|
|
536
661
|
async function viewTimeline(date) {
|
|
@@ -828,10 +953,11 @@ window._lastView = 'sessions';
|
|
|
828
953
|
|
|
829
954
|
$$('.nav-item').forEach(item => {
|
|
830
955
|
item.addEventListener('click', () => {
|
|
831
|
-
|
|
832
|
-
item.classList.add('active');
|
|
956
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
833
957
|
const view = item.dataset.view;
|
|
834
958
|
window._lastView = view;
|
|
959
|
+
updateNavActive(view);
|
|
960
|
+
setHash(view);
|
|
835
961
|
if (view === 'search') viewSearch();
|
|
836
962
|
else if (view === 'sessions') viewSessions();
|
|
837
963
|
else if (view === 'files') viewFiles();
|
|
@@ -840,7 +966,7 @@ $$('.nav-item').forEach(item => {
|
|
|
840
966
|
});
|
|
841
967
|
});
|
|
842
968
|
|
|
843
|
-
|
|
969
|
+
handleRoute();
|
|
844
970
|
|
|
845
971
|
// Swipe right from left edge to go back
|
|
846
972
|
(function initSwipeBack() {
|
package/public/style.css
CHANGED
|
@@ -1077,6 +1077,34 @@ mark {
|
|
|
1077
1077
|
animation: highlightFade 2s var(--ease-out);
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
/* ---- New Events SSE Indicator ---- */
|
|
1081
|
+
.new-events-indicator {
|
|
1082
|
+
position: fixed;
|
|
1083
|
+
top: 20px;
|
|
1084
|
+
left: 50%;
|
|
1085
|
+
transform: translateX(-50%);
|
|
1086
|
+
background: var(--accent);
|
|
1087
|
+
color: #fff;
|
|
1088
|
+
padding: 8px 20px;
|
|
1089
|
+
border-radius: 20px;
|
|
1090
|
+
font-size: 12px;
|
|
1091
|
+
font-weight: 600;
|
|
1092
|
+
cursor: pointer;
|
|
1093
|
+
z-index: 90;
|
|
1094
|
+
box-shadow: 0 4px 16px rgba(99, 144, 240, 0.4);
|
|
1095
|
+
animation: slideDown 0.3s var(--ease-out);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.new-events-indicator:hover {
|
|
1099
|
+
background: #4a7ae8;
|
|
1100
|
+
transform: translateX(-50%) scale(1.05);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
@keyframes slideDown {
|
|
1104
|
+
from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
|
|
1105
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1080
1108
|
/* ---- Search Home Stats ---- */
|
|
1081
1109
|
.search-stats {
|
|
1082
1110
|
display: grid;
|