agentacta 1.3.3 → 1.4.0
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 +102 -80
- package/db.js +1 -0
- package/index.js +57 -3
- package/indexer.js +281 -20
- package/package.json +1 -1
- package/public/app.js +231 -37
- package/public/index.html +4 -0
- package/public/style.css +144 -4
package/public/app.js
CHANGED
|
@@ -3,10 +3,31 @@ const $$ = (s, p = document) => [...p.querySelectorAll(s)];
|
|
|
3
3
|
const content = $('#content');
|
|
4
4
|
const API = '/api';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const THEME_KEY = 'agentacta-theme';
|
|
7
|
+
|
|
8
|
+
function applyTheme(theme) {
|
|
9
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
10
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
11
|
+
if (meta) meta.setAttribute('content', theme === 'light' ? '#f5f7fb' : '#0a0e1a');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function initTheme() {
|
|
15
|
+
const saved = localStorage.getItem(THEME_KEY);
|
|
16
|
+
const theme = saved === 'dark' || saved === 'light' ? saved : 'light';
|
|
17
|
+
applyTheme(theme);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toggleTheme() {
|
|
21
|
+
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
22
|
+
const next = current === 'light' ? 'dark' : 'light';
|
|
23
|
+
localStorage.setItem(THEME_KEY, next);
|
|
24
|
+
applyTheme(next);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function api(path, options = {}) {
|
|
7
28
|
let res;
|
|
8
29
|
try {
|
|
9
|
-
res = await fetch(API + path);
|
|
30
|
+
res = await fetch(API + path, options);
|
|
10
31
|
} catch (err) {
|
|
11
32
|
// Network error (server down, offline, etc.)
|
|
12
33
|
return { _error: true, error: 'Network error' };
|
|
@@ -71,6 +92,26 @@ function escHtml(s) {
|
|
|
71
92
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
function fmtToolName(name) {
|
|
96
|
+
if (!name) return '';
|
|
97
|
+
// MCP tools: mcp__provider__action → mcp_provider_action
|
|
98
|
+
const mcp = name.match(/^mcp__(.+?)__(.+)$/);
|
|
99
|
+
if (mcp) {
|
|
100
|
+
const provider = mcp[1].replace(/__/g, '_');
|
|
101
|
+
const action = mcp[2].replace(/__/g, '_');
|
|
102
|
+
return `mcp_${provider}_${action}`;
|
|
103
|
+
}
|
|
104
|
+
return name;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fmtToolGroup(name) {
|
|
108
|
+
if (!name) return '';
|
|
109
|
+
// MCP tools: collapse to mcp_provider (no action)
|
|
110
|
+
const mcp = name.match(/^mcp__(.+?)__/);
|
|
111
|
+
if (mcp) return 'mcp_' + mcp[1].replace(/__/g, '_');
|
|
112
|
+
return name;
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
function truncate(s, n = 200) {
|
|
75
116
|
if (!s) return '';
|
|
76
117
|
return s.length > n ? s.slice(0, n) + '\u2026' : s;
|
|
@@ -95,6 +136,48 @@ function transitionView() {
|
|
|
95
136
|
content.classList.add('view-enter');
|
|
96
137
|
}
|
|
97
138
|
|
|
139
|
+
function skeletonLine(width = '100%', height = '12px') {
|
|
140
|
+
return `<div class="skeleton-line" style="width:${width};height:${height}"></div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function skeletonRows(count = 6, kind = 'event') {
|
|
144
|
+
if (kind === 'session') {
|
|
145
|
+
return Array.from({ length: count }).map(() => `
|
|
146
|
+
<div class="session-item skeleton-card">
|
|
147
|
+
<div class="skeleton-line" style="width:58%;height:12px"></div>
|
|
148
|
+
<div class="skeleton-line" style="width:90%;height:14px"></div>
|
|
149
|
+
<div class="skeleton-line" style="width:72%;height:14px"></div>
|
|
150
|
+
</div>
|
|
151
|
+
`).join('');
|
|
152
|
+
}
|
|
153
|
+
if (kind === 'stats') {
|
|
154
|
+
return Array.from({ length: count }).map(() => `
|
|
155
|
+
<div class="stat-card skeleton-card">
|
|
156
|
+
<div class="skeleton-line" style="width:44%;height:10px"></div>
|
|
157
|
+
<div class="skeleton-line" style="width:66%;height:28px;margin-top:8px"></div>
|
|
158
|
+
</div>
|
|
159
|
+
`).join('');
|
|
160
|
+
}
|
|
161
|
+
if (kind === 'file') {
|
|
162
|
+
return Array.from({ length: count }).map(() => `
|
|
163
|
+
<div class="file-item skeleton-card">
|
|
164
|
+
<div class="skeleton-line" style="width:62%;height:13px"></div>
|
|
165
|
+
<div class="skeleton-line" style="width:86%;height:12px;margin-top:10px"></div>
|
|
166
|
+
</div>
|
|
167
|
+
`).join('');
|
|
168
|
+
}
|
|
169
|
+
return Array.from({ length: count }).map(() => `
|
|
170
|
+
<div class="event-item skeleton-row">
|
|
171
|
+
<div class="skeleton-line" style="width:72px;height:10px"></div>
|
|
172
|
+
<div class="skeleton-line" style="width:60px;height:16px"></div>
|
|
173
|
+
<div class="event-body">
|
|
174
|
+
<div class="skeleton-line" style="width:82%;height:12px"></div>
|
|
175
|
+
<div class="skeleton-line" style="width:66%;height:12px;margin-top:8px"></div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
`).join('');
|
|
179
|
+
}
|
|
180
|
+
|
|
98
181
|
// --- Hash routing ---
|
|
99
182
|
window._navDepth = 0;
|
|
100
183
|
|
|
@@ -118,6 +201,7 @@ function updateNavActive(view) {
|
|
|
118
201
|
function handleRoute() {
|
|
119
202
|
const raw = (window.location.hash || '').slice(1) || 'search';
|
|
120
203
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
204
|
+
if (window._timelineScrollHandler) { window.removeEventListener('scroll', window._timelineScrollHandler); window._timelineScrollHandler = null; }
|
|
121
205
|
|
|
122
206
|
if (raw.startsWith('session/')) {
|
|
123
207
|
const id = decodeURIComponent(raw.slice('session/'.length));
|
|
@@ -144,7 +228,7 @@ function renderEvent(ev) {
|
|
|
144
228
|
let body = '';
|
|
145
229
|
|
|
146
230
|
if (ev.type === 'tool_call') {
|
|
147
|
-
body = `<span class="tool-name">${escHtml(ev.tool_name)}</span>`;
|
|
231
|
+
body = `<span class="tool-name">${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
148
232
|
if (ev.tool_args) {
|
|
149
233
|
try {
|
|
150
234
|
const args = JSON.parse(ev.tool_args);
|
|
@@ -154,7 +238,7 @@ function renderEvent(ev) {
|
|
|
154
238
|
}
|
|
155
239
|
}
|
|
156
240
|
} else if (ev.type === 'tool_result') {
|
|
157
|
-
body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
|
|
241
|
+
body = `<span class="tool-name">\u2192 ${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
158
242
|
if (ev.content) {
|
|
159
243
|
body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
|
|
160
244
|
}
|
|
@@ -178,7 +262,7 @@ function renderTimelineEvent(ev) {
|
|
|
178
262
|
let body = '';
|
|
179
263
|
|
|
180
264
|
if (ev.type === 'tool_call') {
|
|
181
|
-
body = `<span class="tool-name">${escHtml(ev.tool_name)}</span>`;
|
|
265
|
+
body = `<span class="tool-name">${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
182
266
|
if (ev.tool_args) {
|
|
183
267
|
try {
|
|
184
268
|
const args = JSON.parse(ev.tool_args);
|
|
@@ -188,7 +272,7 @@ function renderTimelineEvent(ev) {
|
|
|
188
272
|
}
|
|
189
273
|
}
|
|
190
274
|
} else if (ev.type === 'tool_result') {
|
|
191
|
-
body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
|
|
275
|
+
body = `<span class="tool-name">\u2192 ${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
192
276
|
if (ev.content) {
|
|
193
277
|
body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
|
|
194
278
|
}
|
|
@@ -226,12 +310,20 @@ function normalizeAgentLabel(a) {
|
|
|
226
310
|
return a;
|
|
227
311
|
}
|
|
228
312
|
|
|
313
|
+
function isInternalProjectTag(tag) {
|
|
314
|
+
if (!tag) return true;
|
|
315
|
+
if (tag.startsWith('agent:')) return true;
|
|
316
|
+
if (tag.startsWith('claude:')) return true;
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
229
320
|
function renderProjectTags(s) {
|
|
230
321
|
let projects = [];
|
|
231
322
|
if (s.projects) {
|
|
232
323
|
try { projects = JSON.parse(s.projects); } catch {}
|
|
233
324
|
}
|
|
234
|
-
|
|
325
|
+
const visible = [...new Set(projects)].filter(p => !isInternalProjectTag(p));
|
|
326
|
+
return visible.map(p => `<span class="session-project">${escHtml(p)}</span>`).join('');
|
|
235
327
|
}
|
|
236
328
|
|
|
237
329
|
function renderModelTags(s) {
|
|
@@ -246,6 +338,8 @@ function renderModelTags(s) {
|
|
|
246
338
|
function renderSessionItem(s) {
|
|
247
339
|
const duration = fmtDuration(s.start_time, s.end_time);
|
|
248
340
|
const timeRange = `${fmtTime(s.start_time)} \u2192 ${s.end_time ? fmtTimeOnly(s.end_time) : 'now'}`;
|
|
341
|
+
const isSubagent = s.session_type === 'subagent';
|
|
342
|
+
const showAgentTag = s.agent && s.agent !== 'main' && !isSubagent;
|
|
249
343
|
|
|
250
344
|
return `
|
|
251
345
|
<div class="session-item" data-id="${s.id}">
|
|
@@ -253,8 +347,8 @@ function renderSessionItem(s) {
|
|
|
253
347
|
<span class="session-time">${timeRange} \u00b7 ${duration}</span>
|
|
254
348
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
255
349
|
${renderProjectTags(s)}
|
|
256
|
-
${
|
|
257
|
-
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
350
|
+
${showAgentTag ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
351
|
+
${s.session_type && s.session_type !== normalizeAgentLabel(s.agent || '') ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
258
352
|
${renderModelTags(s)}
|
|
259
353
|
</span>
|
|
260
354
|
</div>
|
|
@@ -286,7 +380,12 @@ async function viewSearch(query = '') {
|
|
|
286
380
|
<span class="filter-chip ${roleFilter==='user'?'active':''}" data-filter="role" data-val="user">User</span>
|
|
287
381
|
<span class="filter-chip ${roleFilter==='assistant'?'active':''}" data-filter="role" data-val="assistant">Assistant</span>
|
|
288
382
|
</div>
|
|
289
|
-
<div id="results"
|
|
383
|
+
<div id="results">
|
|
384
|
+
<div class="search-bar skeleton-card" style="margin-top:6px">
|
|
385
|
+
<div class="skeleton-line" style="height:16px;width:40%"></div>
|
|
386
|
+
</div>
|
|
387
|
+
${skeletonRows(4, 'session')}
|
|
388
|
+
</div>`;
|
|
290
389
|
|
|
291
390
|
content.innerHTML = html;
|
|
292
391
|
transitionView();
|
|
@@ -315,7 +414,7 @@ async function viewSearch(query = '') {
|
|
|
315
414
|
|
|
316
415
|
async function showSearchHome() {
|
|
317
416
|
const el = $('#results');
|
|
318
|
-
el.innerHTML =
|
|
417
|
+
el.innerHTML = `${skeletonRows(4, 'session')}`;
|
|
319
418
|
|
|
320
419
|
const stats = await api('/stats');
|
|
321
420
|
const sessions = await api('/sessions?limit=5');
|
|
@@ -366,7 +465,7 @@ async function doSearch(q) {
|
|
|
366
465
|
const el = $('#results');
|
|
367
466
|
if (!q.trim()) { el.innerHTML = '<div class="empty"><h2>Type to search</h2><p>Search across all sessions, messages, and tool calls</p></div>'; return; }
|
|
368
467
|
|
|
369
|
-
el.innerHTML =
|
|
468
|
+
el.innerHTML = `${skeletonRows(6, 'event')}`;
|
|
370
469
|
|
|
371
470
|
const type = window._searchType || '';
|
|
372
471
|
const role = window._searchRole || '';
|
|
@@ -392,7 +491,7 @@ async function doSearch(q) {
|
|
|
392
491
|
<div class="result-meta">
|
|
393
492
|
<span class="event-badge ${badgeClass(r.type, r.role)}">${r.type === 'tool_call' ? 'tool' : r.role || r.type}</span>
|
|
394
493
|
<span class="session-time">${fmtTime(r.timestamp)}</span>
|
|
395
|
-
${r.tool_name ? `<span class="tool-name">${escHtml(r.tool_name)}</span>` : ''}
|
|
494
|
+
${r.tool_name ? `<span class="tool-name">${escHtml(fmtToolName(r.tool_name))}</span>` : ''}
|
|
396
495
|
<span class="session-link" data-session="${r.session_id}">view session \u2192</span>
|
|
397
496
|
</div>
|
|
398
497
|
<div class="result-content">${escHtml(truncate(r.content || r.tool_args || r.tool_result || '', 400))}</div>
|
|
@@ -410,7 +509,8 @@ async function doSearch(q) {
|
|
|
410
509
|
|
|
411
510
|
async function viewSessions() {
|
|
412
511
|
window._currentSessionId = null;
|
|
413
|
-
content.innerHTML =
|
|
512
|
+
content.innerHTML = `<div class="page-title">Sessions</div>${skeletonRows(4, 'session')}`;
|
|
513
|
+
transitionView();
|
|
414
514
|
const data = await api('/sessions?limit=200');
|
|
415
515
|
if (data._error) {
|
|
416
516
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
@@ -432,6 +532,12 @@ async function viewSession(id) {
|
|
|
432
532
|
window._currentSessionId = id;
|
|
433
533
|
setHash('session/' + encodeURIComponent(id));
|
|
434
534
|
window.scrollTo(0, 0);
|
|
535
|
+
content.innerHTML = `
|
|
536
|
+
<div class="back-btn">← Back</div>
|
|
537
|
+
<div class="page-title">Session</div>
|
|
538
|
+
${skeletonRows(8, 'event')}
|
|
539
|
+
`;
|
|
540
|
+
transitionView();
|
|
435
541
|
const data = await api(`/sessions/${id}`);
|
|
436
542
|
|
|
437
543
|
if (data._error || data.error) { content.innerHTML = `<div class="empty"><h2>${escHtml(data.error || 'Unable to load')}</h2></div>`; return; }
|
|
@@ -459,7 +565,7 @@ async function viewSession(id) {
|
|
|
459
565
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
460
566
|
${renderProjectTags(s)}
|
|
461
567
|
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
462
|
-
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
568
|
+
${s.session_type && s.session_type !== normalizeAgentLabel(s.agent || '') ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
463
569
|
${renderModelTags(s)}
|
|
464
570
|
</span>
|
|
465
571
|
</div>
|
|
@@ -664,34 +770,93 @@ async function viewTimeline(date) {
|
|
|
664
770
|
date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
|
|
665
771
|
}
|
|
666
772
|
window._lastView = 'timeline';
|
|
773
|
+
window._timelineState = { date, limit: 100, offset: 0, hasMore: true, loading: false };
|
|
667
774
|
|
|
668
|
-
|
|
775
|
+
content.innerHTML = `<div class="page-title">Timeline</div>
|
|
669
776
|
<input type="date" class="date-input" id="dateInput" value="${date}">
|
|
670
|
-
<div id="timelineContent"
|
|
671
|
-
|
|
777
|
+
<div id="timelineContent">${skeletonRows(8, 'event')}</div>
|
|
778
|
+
<div id="timelineLoadMore" class="loading-more" style="display:none">Loading more…</div>`;
|
|
672
779
|
transitionView();
|
|
673
780
|
|
|
674
|
-
const data = await api(`/timeline?date=${date}`);
|
|
675
|
-
if (data._error) {
|
|
676
|
-
$('#timelineContent').innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
781
|
const el = $('#timelineContent');
|
|
782
|
+
const state = window._timelineState;
|
|
680
783
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
784
|
+
async function loadTimelinePage(append = false) {
|
|
785
|
+
if (state.loading || (!state.hasMore && append)) return;
|
|
786
|
+
state.loading = true;
|
|
787
|
+
if (append) $('#timelineLoadMore').style.display = 'block';
|
|
788
|
+
|
|
789
|
+
const data = await api(`/timeline?date=${state.date}&limit=${state.limit}&offset=${state.offset}`);
|
|
790
|
+
state.loading = false;
|
|
791
|
+
$('#timelineLoadMore').style.display = 'none';
|
|
792
|
+
|
|
793
|
+
if (data._error) {
|
|
794
|
+
if (!append) el.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
state.hasMore = !!data.hasMore;
|
|
799
|
+
state.offset += (data.events || []).length;
|
|
800
|
+
|
|
801
|
+
if (!append) {
|
|
802
|
+
if (!data.events.length) {
|
|
803
|
+
el.innerHTML = '<div class="empty"><h2>No activity</h2><p>Nothing recorded on this day</p></div>';
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
el.innerHTML = `<div class="timeline-events-wrap" id="timelineWrap"><div class="timeline-line"></div>${data.events.map(renderTimelineEvent).join('')}</div>`;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const wrap = $('#timelineWrap');
|
|
811
|
+
if (wrap) {
|
|
812
|
+
wrap.insertAdjacentHTML('beforeend', data.events.map(renderTimelineEvent).join(''));
|
|
813
|
+
}
|
|
688
814
|
}
|
|
689
815
|
|
|
690
|
-
|
|
816
|
+
await loadTimelinePage(false);
|
|
817
|
+
|
|
818
|
+
if (window._timelineScrollHandler) window.removeEventListener('scroll', window._timelineScrollHandler);
|
|
819
|
+
window._timelineScrollHandler = () => {
|
|
820
|
+
const st = window._timelineState;
|
|
821
|
+
if (!st || st.loading || !st.hasMore) return;
|
|
822
|
+
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
|
|
823
|
+
if (nearBottom) loadTimelinePage(true);
|
|
824
|
+
};
|
|
825
|
+
window.addEventListener('scroll', window._timelineScrollHandler, { passive: true });
|
|
826
|
+
|
|
827
|
+
// Live updates via SSE (only for today)
|
|
828
|
+
const today = new Date();
|
|
829
|
+
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
|
830
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
831
|
+
if (date === todayStr) {
|
|
832
|
+
const sse = new EventSource(`/api/timeline/stream?after=${encodeURIComponent(new Date().toISOString())}`);
|
|
833
|
+
window._timelineSse = sse;
|
|
834
|
+
sse.onmessage = (evt) => {
|
|
835
|
+
try {
|
|
836
|
+
const rows = JSON.parse(evt.data);
|
|
837
|
+
const wrap = $('#timelineWrap');
|
|
838
|
+
if (wrap && rows.length) {
|
|
839
|
+
const html = rows.map(renderTimelineEvent).join('');
|
|
840
|
+
wrap.insertAdjacentHTML('afterbegin', html);
|
|
841
|
+
// Flash new events
|
|
842
|
+
rows.forEach(r => {
|
|
843
|
+
const el = wrap.querySelector(`[data-event-id="${r.id}"]`);
|
|
844
|
+
if (el) { el.classList.add('event-highlight'); setTimeout(() => el.classList.remove('event-highlight'), 2000); }
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
} catch {}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
$('#dateInput').addEventListener('change', e => {
|
|
852
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
853
|
+
viewTimeline(e.target.value);
|
|
854
|
+
});
|
|
691
855
|
}
|
|
692
856
|
|
|
693
857
|
async function viewStats() {
|
|
694
|
-
content.innerHTML =
|
|
858
|
+
content.innerHTML = `<div class="page-title">Stats</div><div class="stat-grid">${skeletonRows(5, 'stats')}</div>`;
|
|
859
|
+
transitionView();
|
|
695
860
|
const data = await api('/stats');
|
|
696
861
|
if (data._error) {
|
|
697
862
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
@@ -703,14 +868,19 @@ async function viewStats() {
|
|
|
703
868
|
<div class="stat-card accent-blue"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
|
|
704
869
|
<div class="stat-card accent-green"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
|
|
705
870
|
<div class="stat-card accent-amber"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
|
|
706
|
-
<div class="stat-card accent-purple"><div class="label">Unique Tools</div><div class="value">${data.
|
|
871
|
+
<div class="stat-card accent-purple"><div class="label">Unique Tools</div><div class="value">${new Set((data.tools||[]).filter(t=>t).map(t=>fmtToolGroup(t))).size}</div></div>
|
|
707
872
|
<div class="stat-card accent-teal"><div class="label">Total Tokens</div><div class="value">${(data.totalTokens || 0).toLocaleString()}</div></div>
|
|
708
873
|
</div>
|
|
709
874
|
|
|
710
875
|
<div class="section-label">Configuration</div>
|
|
711
|
-
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:var(--space-md);margin-bottom:var(--space-
|
|
876
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:var(--space-md);margin-bottom:var(--space-md)">
|
|
712
877
|
<div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
|
|
713
|
-
<div class="config-card"><div class="config-label">DB Size</div><div class="config-value">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
|
|
878
|
+
<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>
|
|
879
|
+
</div>
|
|
880
|
+
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:var(--space-xl)">
|
|
881
|
+
<button class="export-btn" id="optimizeDbBtn">Optimize Database</button>
|
|
882
|
+
<span style="color:var(--text-tertiary);font-size:12px;line-height:1.5">Reclaims unused space and merges pending writes. Safe to run anytime, doesn't delete any data.</span>
|
|
883
|
+
<span id="optimizeDbStatus" style="color:var(--text-tertiary);font-size:12px"></span>
|
|
714
884
|
</div>
|
|
715
885
|
|
|
716
886
|
${data.sessionDirs && data.sessionDirs.length ? (() => {
|
|
@@ -745,16 +915,35 @@ async function viewStats() {
|
|
|
745
915
|
<div class="section-label">Date Range</div>
|
|
746
916
|
<p style="color:var(--text-secondary);font-size:13px;margin-bottom:var(--space-xl)">${fmtDate(data.dateRange?.earliest)} \u2014 ${fmtDate(data.dateRange?.latest)}</p>
|
|
747
917
|
<div class="section-label">Tools Used</div>
|
|
748
|
-
<div class="tools-grid">${(data.tools||[]).filter(t => t).sort().map(t => `<span class="tool-chip">${escHtml(t)}</span>`).join('')}</div>
|
|
918
|
+
<div class="tools-grid">${[...new Set((data.tools||[]).filter(t => t).map(t => fmtToolGroup(t)))].sort().map(t => `<span class="tool-chip">${escHtml(t)}</span>`).join('')}</div>
|
|
749
919
|
`;
|
|
750
920
|
|
|
751
921
|
content.innerHTML = html;
|
|
752
922
|
transitionView();
|
|
923
|
+
|
|
924
|
+
const optimizeBtn = $('#optimizeDbBtn');
|
|
925
|
+
const optimizeStatus = $('#optimizeDbStatus');
|
|
926
|
+
if (optimizeBtn) {
|
|
927
|
+
optimizeBtn.addEventListener('click', async () => {
|
|
928
|
+
optimizeBtn.disabled = true;
|
|
929
|
+
optimizeStatus.textContent = 'Optimizing…';
|
|
930
|
+
const result = await api('/maintenance', { method: 'POST' });
|
|
931
|
+
if (result._error || !result.ok) {
|
|
932
|
+
optimizeStatus.textContent = `Failed: ${result.error || 'Unknown error'}`;
|
|
933
|
+
} else {
|
|
934
|
+
optimizeStatus.textContent = `${result.sizeBefore?.display || 'N/A'} → ${result.sizeAfter?.display || 'N/A'}`;
|
|
935
|
+
const dbSizeValue = $('#dbSizeValue');
|
|
936
|
+
if (dbSizeValue) dbSizeValue.textContent = result.sizeAfter?.display || 'N/A';
|
|
937
|
+
}
|
|
938
|
+
optimizeBtn.disabled = false;
|
|
939
|
+
});
|
|
940
|
+
}
|
|
753
941
|
}
|
|
754
942
|
|
|
755
943
|
async function viewFiles() {
|
|
756
944
|
window._lastView = 'files';
|
|
757
|
-
content.innerHTML =
|
|
945
|
+
content.innerHTML = `<div class="page-title">Files</div>${skeletonRows(6, 'file')}`;
|
|
946
|
+
transitionView();
|
|
758
947
|
const data = await api('/files?limit=500');
|
|
759
948
|
if (data._error) {
|
|
760
949
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
@@ -954,6 +1143,8 @@ window._lastView = 'sessions';
|
|
|
954
1143
|
$$('.nav-item').forEach(item => {
|
|
955
1144
|
item.addEventListener('click', () => {
|
|
956
1145
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
1146
|
+
if (window._timelineScrollHandler) { window.removeEventListener('scroll', window._timelineScrollHandler); window._timelineScrollHandler = null; }
|
|
1147
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
957
1148
|
const view = item.dataset.view;
|
|
958
1149
|
window._lastView = view;
|
|
959
1150
|
updateNavActive(view);
|
|
@@ -966,6 +1157,9 @@ $$('.nav-item').forEach(item => {
|
|
|
966
1157
|
});
|
|
967
1158
|
});
|
|
968
1159
|
|
|
1160
|
+
initTheme();
|
|
1161
|
+
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
|
|
1162
|
+
document.getElementById('theme-toggle-mobile')?.addEventListener('click', toggleTheme);
|
|
969
1163
|
handleRoute();
|
|
970
1164
|
|
|
971
1165
|
// Swipe right from left edge to go back
|
package/public/index.html
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<nav class="sidebar">
|
|
22
22
|
<div class="sidebar-header">
|
|
23
23
|
<h1>Agent<span>Acta</span></h1>
|
|
24
|
+
<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>
|
|
24
25
|
</div>
|
|
25
26
|
<div class="nav-section">
|
|
26
27
|
<div class="nav-item" data-view="sessions">
|
|
@@ -43,9 +44,12 @@
|
|
|
43
44
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
|
|
44
45
|
<span>Stats</span>
|
|
45
46
|
</div>
|
|
47
|
+
|
|
46
48
|
</div>
|
|
47
49
|
</nav>
|
|
48
50
|
<main class="main" id="content"></main>
|
|
51
|
+
<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>
|
|
52
|
+
|
|
49
53
|
</div>
|
|
50
54
|
<script src="/app.js"></script>
|
|
51
55
|
<script>
|
package/public/style.css
CHANGED
|
@@ -71,6 +71,47 @@
|
|
|
71
71
|
|
|
72
72
|
/* Layout */
|
|
73
73
|
--sidebar-width: 240px;
|
|
74
|
+
--input-scheme: dark;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[data-theme="light"] {
|
|
78
|
+
--bg-base: #f5f7fb;
|
|
79
|
+
--bg-surface: #ffffff;
|
|
80
|
+
--bg-elevated: #f8fafc;
|
|
81
|
+
--bg-hover: #eef2f7;
|
|
82
|
+
--bg-active: #e8edf6;
|
|
83
|
+
|
|
84
|
+
--border-subtle: rgba(15, 23, 42, 0.06);
|
|
85
|
+
--border-default: rgba(15, 23, 42, 0.1);
|
|
86
|
+
--border-hover: rgba(15, 23, 42, 0.16);
|
|
87
|
+
--border-focus: rgba(36, 89, 214, 0.45);
|
|
88
|
+
|
|
89
|
+
--text-primary: #111827;
|
|
90
|
+
--text-secondary: #5b6678;
|
|
91
|
+
--text-tertiary: #8793a7;
|
|
92
|
+
--text-inverse: #ffffff;
|
|
93
|
+
|
|
94
|
+
--accent: #2459d6;
|
|
95
|
+
--accent-soft: rgba(36, 89, 214, 0.12);
|
|
96
|
+
--accent-medium: rgba(36, 89, 214, 0.2);
|
|
97
|
+
|
|
98
|
+
--green: #158f69;
|
|
99
|
+
--green-soft: rgba(21, 143, 105, 0.12);
|
|
100
|
+
|
|
101
|
+
--purple: #7d62eb;
|
|
102
|
+
--purple-soft: rgba(125, 98, 235, 0.12);
|
|
103
|
+
|
|
104
|
+
--amber: #b7791f;
|
|
105
|
+
--amber-soft: rgba(183, 121, 31, 0.12);
|
|
106
|
+
|
|
107
|
+
--red: #c24141;
|
|
108
|
+
--red-soft: rgba(194, 65, 65, 0.12);
|
|
109
|
+
|
|
110
|
+
--teal: #0f9f9a;
|
|
111
|
+
--teal-soft: rgba(15, 159, 154, 0.12);
|
|
112
|
+
|
|
113
|
+
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
|
114
|
+
--input-scheme: light;
|
|
74
115
|
}
|
|
75
116
|
|
|
76
117
|
/* ---- Reset ---- */
|
|
@@ -125,6 +166,10 @@ body {
|
|
|
125
166
|
|
|
126
167
|
.sidebar-header {
|
|
127
168
|
padding: var(--space-xl) var(--space-xl) var(--space-lg);
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: space-between;
|
|
172
|
+
gap: var(--space-sm);
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
.sidebar h1 {
|
|
@@ -141,6 +186,63 @@ body {
|
|
|
141
186
|
background-clip: text;
|
|
142
187
|
}
|
|
143
188
|
|
|
189
|
+
.theme-toggle {
|
|
190
|
+
width: 30px;
|
|
191
|
+
height: 30px;
|
|
192
|
+
border-radius: var(--radius-md);
|
|
193
|
+
border: 1px solid var(--border-default);
|
|
194
|
+
background: var(--bg-elevated);
|
|
195
|
+
color: var(--text-secondary);
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
padding: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.theme-toggle:hover {
|
|
204
|
+
background: var(--bg-hover);
|
|
205
|
+
color: var(--text-primary);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.theme-toggle svg,
|
|
209
|
+
.theme-toggle-mobile svg {
|
|
210
|
+
width: 16px;
|
|
211
|
+
height: 16px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.theme-icon-moon { display: none; }
|
|
215
|
+
[data-theme="light"] .theme-icon-sun { display: none; }
|
|
216
|
+
[data-theme="light"] .theme-icon-moon { display: block; }
|
|
217
|
+
|
|
218
|
+
.theme-toggle-mobile {
|
|
219
|
+
display: none;
|
|
220
|
+
width: 32px;
|
|
221
|
+
height: 32px;
|
|
222
|
+
border-radius: var(--radius-md);
|
|
223
|
+
border: 1px solid var(--border-default);
|
|
224
|
+
background: var(--bg-surface);
|
|
225
|
+
color: var(--text-secondary);
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
padding: 0;
|
|
230
|
+
position: absolute;
|
|
231
|
+
top: calc(var(--space-xl) + env(safe-area-inset-top, 0px));
|
|
232
|
+
right: var(--space-lg);
|
|
233
|
+
z-index: 10;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.theme-toggle-mobile:hover {
|
|
237
|
+
background: var(--bg-hover);
|
|
238
|
+
color: var(--text-primary);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.theme-toggle-mobile svg {
|
|
242
|
+
width: 16px;
|
|
243
|
+
height: 16px;
|
|
244
|
+
}
|
|
245
|
+
|
|
144
246
|
.nav-section {
|
|
145
247
|
display: flex;
|
|
146
248
|
flex-direction: column;
|
|
@@ -203,7 +305,7 @@ body {
|
|
|
203
305
|
flex: 1;
|
|
204
306
|
margin-left: var(--sidebar-width);
|
|
205
307
|
padding: var(--space-2xl) var(--space-3xl);
|
|
206
|
-
max-width:
|
|
308
|
+
max-width: none;
|
|
207
309
|
min-height: 100vh;
|
|
208
310
|
}
|
|
209
311
|
|
|
@@ -405,8 +507,8 @@ body {
|
|
|
405
507
|
.session-project {
|
|
406
508
|
font-size: 11px;
|
|
407
509
|
font-weight: 500;
|
|
408
|
-
color:
|
|
409
|
-
background:
|
|
510
|
+
color: #7fb4ff;
|
|
511
|
+
background: rgba(39, 94, 182, 0.18);
|
|
410
512
|
padding: 2px 10px;
|
|
411
513
|
border-radius: 10px;
|
|
412
514
|
}
|
|
@@ -655,6 +757,40 @@ mark {
|
|
|
655
757
|
font-weight: 500;
|
|
656
758
|
}
|
|
657
759
|
|
|
760
|
+
/* ---- Skeletons ---- */
|
|
761
|
+
.skeleton-card,
|
|
762
|
+
.skeleton-row {
|
|
763
|
+
border-color: var(--border-subtle);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.skeleton-line {
|
|
767
|
+
position: relative;
|
|
768
|
+
border-radius: var(--radius-sm);
|
|
769
|
+
background: var(--bg-elevated);
|
|
770
|
+
border: 1px solid var(--border-subtle);
|
|
771
|
+
overflow: hidden;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.skeleton-line::after {
|
|
775
|
+
content: '';
|
|
776
|
+
position: absolute;
|
|
777
|
+
inset: 0;
|
|
778
|
+
transform: translateX(-100%);
|
|
779
|
+
background: linear-gradient(90deg, transparent, var(--bg-hover), transparent);
|
|
780
|
+
animation: skeletonShimmer 1.4s ease-in-out infinite;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
@keyframes skeletonShimmer {
|
|
784
|
+
100% { transform: translateX(100%); }
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.loading-more {
|
|
788
|
+
text-align: center;
|
|
789
|
+
color: var(--text-tertiary);
|
|
790
|
+
font-size: 12px;
|
|
791
|
+
padding: var(--space-md) 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
658
794
|
@keyframes pulse {
|
|
659
795
|
0%, 100% { opacity: 0.4; }
|
|
660
796
|
50% { opacity: 1; }
|
|
@@ -683,7 +819,7 @@ mark {
|
|
|
683
819
|
outline: none;
|
|
684
820
|
margin-bottom: var(--space-lg);
|
|
685
821
|
transition: all var(--duration-normal) var(--ease-out);
|
|
686
|
-
color-scheme:
|
|
822
|
+
color-scheme: var(--input-scheme);
|
|
687
823
|
}
|
|
688
824
|
|
|
689
825
|
.date-input:hover {
|
|
@@ -1230,6 +1366,10 @@ mark {
|
|
|
1230
1366
|
|
|
1231
1367
|
.sidebar-header { display: none; }
|
|
1232
1368
|
|
|
1369
|
+
.theme-toggle-mobile {
|
|
1370
|
+
display: flex;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1233
1373
|
.nav-section {
|
|
1234
1374
|
display: flex;
|
|
1235
1375
|
flex-direction: row;
|