agentacta 1.3.4 → 2026.3.5
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 +100 -79
- package/db.js +1 -0
- package/index.js +60 -3
- package/package.json +1 -1
- package/public/app.js +740 -55
- package/public/index.html +17 -5
- package/public/style.css +519 -3
package/public/app.js
CHANGED
|
@@ -3,10 +3,74 @@ 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'; // legacy
|
|
7
|
+
const THEME_MODE_KEY = 'agentacta-theme-mode'; // system | light | dark
|
|
8
|
+
const THEME_DARK_VARIANT_KEY = 'agentacta-dark-variant'; // default | trueblack
|
|
9
|
+
|
|
10
|
+
function lsGet(key) {
|
|
11
|
+
try { return localStorage.getItem(key); } catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function lsSet(key, value) {
|
|
15
|
+
try { localStorage.setItem(key, value); } catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveTheme(mode, darkVariant) {
|
|
19
|
+
if (mode === 'light') return 'light';
|
|
20
|
+
if (mode === 'dark') return darkVariant === 'trueblack' ? 'oled' : 'dark';
|
|
21
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
22
|
+
return prefersDark ? (darkVariant === 'trueblack' ? 'oled' : 'dark') : 'light';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function applyTheme(theme) {
|
|
26
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
27
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
28
|
+
if (meta) {
|
|
29
|
+
const color = theme === 'light' ? '#f5f7fb' : (theme === 'oled' ? '#000000' : '#0a0e1a');
|
|
30
|
+
meta.setAttribute('content', color);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyThemeFromPrefs() {
|
|
35
|
+
const mode = lsGet(THEME_MODE_KEY) || 'light';
|
|
36
|
+
const darkVariant = lsGet(THEME_DARK_VARIANT_KEY) || 'default';
|
|
37
|
+
window._themeMode = mode;
|
|
38
|
+
window._themeDarkVariant = darkVariant;
|
|
39
|
+
applyTheme(resolveTheme(mode, darkVariant));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function initTheme() {
|
|
43
|
+
// Migrate legacy key if present.
|
|
44
|
+
const legacy = lsGet(THEME_KEY);
|
|
45
|
+
if (!lsGet(THEME_MODE_KEY) && (legacy === 'light' || legacy === 'dark')) {
|
|
46
|
+
lsSet(THEME_MODE_KEY, legacy);
|
|
47
|
+
}
|
|
48
|
+
if (!lsGet(THEME_DARK_VARIANT_KEY)) {
|
|
49
|
+
lsSet(THEME_DARK_VARIANT_KEY, 'default');
|
|
50
|
+
}
|
|
51
|
+
applyThemeFromPrefs();
|
|
52
|
+
|
|
53
|
+
if (!window._themeMediaBound) {
|
|
54
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
55
|
+
media.addEventListener?.('change', () => {
|
|
56
|
+
if ((lsGet(THEME_MODE_KEY) || 'light') === 'system') applyThemeFromPrefs();
|
|
57
|
+
});
|
|
58
|
+
window._themeMediaBound = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toggleTheme() {
|
|
63
|
+
const currentApplied = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
64
|
+
const nextMode = currentApplied === 'light' ? 'dark' : 'light';
|
|
65
|
+
lsSet(THEME_MODE_KEY, nextMode);
|
|
66
|
+
window._themeMode = nextMode;
|
|
67
|
+
applyThemeFromPrefs();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function api(path, options = {}) {
|
|
7
71
|
let res;
|
|
8
72
|
try {
|
|
9
|
-
res = await fetch(API + path);
|
|
73
|
+
res = await fetch(API + path, options);
|
|
10
74
|
} catch (err) {
|
|
11
75
|
// Network error (server down, offline, etc.)
|
|
12
76
|
return { _error: true, error: 'Network error' };
|
|
@@ -71,6 +135,26 @@ function escHtml(s) {
|
|
|
71
135
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
72
136
|
}
|
|
73
137
|
|
|
138
|
+
function fmtToolName(name) {
|
|
139
|
+
if (!name) return '';
|
|
140
|
+
// MCP tools: mcp__provider__action → mcp_provider_action
|
|
141
|
+
const mcp = name.match(/^mcp__(.+?)__(.+)$/);
|
|
142
|
+
if (mcp) {
|
|
143
|
+
const provider = mcp[1].replace(/__/g, '_');
|
|
144
|
+
const action = mcp[2].replace(/__/g, '_');
|
|
145
|
+
return `mcp_${provider}_${action}`;
|
|
146
|
+
}
|
|
147
|
+
return name;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function fmtToolGroup(name) {
|
|
151
|
+
if (!name) return '';
|
|
152
|
+
// MCP tools: collapse to mcp_provider (no action)
|
|
153
|
+
const mcp = name.match(/^mcp__(.+?)__/);
|
|
154
|
+
if (mcp) return 'mcp_' + mcp[1].replace(/__/g, '_');
|
|
155
|
+
return name;
|
|
156
|
+
}
|
|
157
|
+
|
|
74
158
|
function truncate(s, n = 200) {
|
|
75
159
|
if (!s) return '';
|
|
76
160
|
return s.length > n ? s.slice(0, n) + '\u2026' : s;
|
|
@@ -95,6 +179,48 @@ function transitionView() {
|
|
|
95
179
|
content.classList.add('view-enter');
|
|
96
180
|
}
|
|
97
181
|
|
|
182
|
+
function skeletonLine(width = '100%', height = '12px') {
|
|
183
|
+
return `<div class="skeleton-line" style="width:${width};height:${height}"></div>`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function skeletonRows(count = 6, kind = 'event') {
|
|
187
|
+
if (kind === 'session') {
|
|
188
|
+
return Array.from({ length: count }).map(() => `
|
|
189
|
+
<div class="session-item skeleton-card">
|
|
190
|
+
<div class="skeleton-line" style="width:58%;height:12px"></div>
|
|
191
|
+
<div class="skeleton-line" style="width:90%;height:14px"></div>
|
|
192
|
+
<div class="skeleton-line" style="width:72%;height:14px"></div>
|
|
193
|
+
</div>
|
|
194
|
+
`).join('');
|
|
195
|
+
}
|
|
196
|
+
if (kind === 'stats') {
|
|
197
|
+
return Array.from({ length: count }).map(() => `
|
|
198
|
+
<div class="stat-card skeleton-card">
|
|
199
|
+
<div class="skeleton-line" style="width:44%;height:10px"></div>
|
|
200
|
+
<div class="skeleton-line" style="width:66%;height:28px;margin-top:8px"></div>
|
|
201
|
+
</div>
|
|
202
|
+
`).join('');
|
|
203
|
+
}
|
|
204
|
+
if (kind === 'file') {
|
|
205
|
+
return Array.from({ length: count }).map(() => `
|
|
206
|
+
<div class="file-item skeleton-card">
|
|
207
|
+
<div class="skeleton-line" style="width:62%;height:13px"></div>
|
|
208
|
+
<div class="skeleton-line" style="width:86%;height:12px;margin-top:10px"></div>
|
|
209
|
+
</div>
|
|
210
|
+
`).join('');
|
|
211
|
+
}
|
|
212
|
+
return Array.from({ length: count }).map(() => `
|
|
213
|
+
<div class="event-item skeleton-row">
|
|
214
|
+
<div class="skeleton-line" style="width:72px;height:10px"></div>
|
|
215
|
+
<div class="skeleton-line" style="width:60px;height:16px"></div>
|
|
216
|
+
<div class="event-body">
|
|
217
|
+
<div class="skeleton-line" style="width:82%;height:12px"></div>
|
|
218
|
+
<div class="skeleton-line" style="width:66%;height:12px;margin-top:8px"></div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
`).join('');
|
|
222
|
+
}
|
|
223
|
+
|
|
98
224
|
// --- Hash routing ---
|
|
99
225
|
window._navDepth = 0;
|
|
100
226
|
|
|
@@ -116,22 +242,25 @@ function updateNavActive(view) {
|
|
|
116
242
|
}
|
|
117
243
|
|
|
118
244
|
function handleRoute() {
|
|
119
|
-
|
|
245
|
+
clearJumpUi();
|
|
246
|
+
const raw = (window.location.hash || '').slice(1) || 'overview';
|
|
120
247
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
248
|
+
if (window._timelineScrollHandler) { window.removeEventListener('scroll', window._timelineScrollHandler); window._timelineScrollHandler = null; }
|
|
121
249
|
|
|
122
250
|
if (raw.startsWith('session/')) {
|
|
123
251
|
const id = decodeURIComponent(raw.slice('session/'.length));
|
|
124
252
|
if (id) { viewSession(id); return; }
|
|
125
253
|
}
|
|
126
254
|
|
|
127
|
-
const
|
|
255
|
+
const normalized = raw === 'search' ? 'overview' : raw;
|
|
256
|
+
const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' ? normalized : 'overview';
|
|
128
257
|
window._lastView = view;
|
|
129
258
|
updateNavActive(view);
|
|
130
|
-
if (view === '
|
|
259
|
+
if (view === 'overview') viewOverview();
|
|
260
|
+
else if (view === 'sessions') viewSessions();
|
|
131
261
|
else if (view === 'files') viewFiles();
|
|
132
262
|
else if (view === 'timeline') viewTimeline();
|
|
133
|
-
else
|
|
134
|
-
else viewSearch(window._lastSearchQuery || '');
|
|
263
|
+
else viewStats();
|
|
135
264
|
}
|
|
136
265
|
|
|
137
266
|
window.addEventListener('popstate', () => {
|
|
@@ -144,7 +273,7 @@ function renderEvent(ev) {
|
|
|
144
273
|
let body = '';
|
|
145
274
|
|
|
146
275
|
if (ev.type === 'tool_call') {
|
|
147
|
-
body = `<span class="tool-name">${escHtml(ev.tool_name)}</span>`;
|
|
276
|
+
body = `<span class="tool-name">${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
148
277
|
if (ev.tool_args) {
|
|
149
278
|
try {
|
|
150
279
|
const args = JSON.parse(ev.tool_args);
|
|
@@ -154,7 +283,7 @@ function renderEvent(ev) {
|
|
|
154
283
|
}
|
|
155
284
|
}
|
|
156
285
|
} else if (ev.type === 'tool_result') {
|
|
157
|
-
body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
|
|
286
|
+
body = `<span class="tool-name">\u2192 ${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
158
287
|
if (ev.content) {
|
|
159
288
|
body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
|
|
160
289
|
}
|
|
@@ -178,7 +307,7 @@ function renderTimelineEvent(ev) {
|
|
|
178
307
|
let body = '';
|
|
179
308
|
|
|
180
309
|
if (ev.type === 'tool_call') {
|
|
181
|
-
body = `<span class="tool-name">${escHtml(ev.tool_name)}</span>`;
|
|
310
|
+
body = `<span class="tool-name">${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
182
311
|
if (ev.tool_args) {
|
|
183
312
|
try {
|
|
184
313
|
const args = JSON.parse(ev.tool_args);
|
|
@@ -188,7 +317,7 @@ function renderTimelineEvent(ev) {
|
|
|
188
317
|
}
|
|
189
318
|
}
|
|
190
319
|
} else if (ev.type === 'tool_result') {
|
|
191
|
-
body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
|
|
320
|
+
body = `<span class="tool-name">\u2192 ${escHtml(fmtToolName(ev.tool_name))}</span>`;
|
|
192
321
|
if (ev.content) {
|
|
193
322
|
body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
|
|
194
323
|
}
|
|
@@ -226,6 +355,56 @@ function normalizeAgentLabel(a) {
|
|
|
226
355
|
return a;
|
|
227
356
|
}
|
|
228
357
|
|
|
358
|
+
function clearJumpUi() {
|
|
359
|
+
const indicator = document.getElementById('jumpIndicator');
|
|
360
|
+
if (indicator) indicator.remove();
|
|
361
|
+
const returnBtn = document.getElementById('returnJumpBtn');
|
|
362
|
+
if (returnBtn) returnBtn.remove();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function cleanSessionSummary(text, fallbackText = '') {
|
|
366
|
+
const pick = (input) => {
|
|
367
|
+
const raw = (input || '').trim();
|
|
368
|
+
if (!raw) return '';
|
|
369
|
+
|
|
370
|
+
const jsonFence = /```json[\s\S]*?```/gi;
|
|
371
|
+
const fences = [...raw.matchAll(jsonFence)];
|
|
372
|
+
if (fences.length >= 2) {
|
|
373
|
+
const second = fences[1];
|
|
374
|
+
const after = raw.slice(second.index + second[0].length).trim();
|
|
375
|
+
if (after) {
|
|
376
|
+
const line = after.split(/\r?\n/).map(l => l.trim()).find(Boolean);
|
|
377
|
+
if (line) return line;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const lines = raw.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
382
|
+
const skip = [
|
|
383
|
+
/^conversation info/i,
|
|
384
|
+
/^sender/i,
|
|
385
|
+
/^```/,
|
|
386
|
+
/^\{/, /^\}/,
|
|
387
|
+
/^"(message_id|sender_id|sender|timestamp|label|id|name)"/i,
|
|
388
|
+
/^untrusted metadata/i
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
let candidate = lines.find(l => !skip.some(rx => rx.test(l)));
|
|
392
|
+
if (candidate) {
|
|
393
|
+
candidate = candidate
|
|
394
|
+
.replace(/^\[cron:[^\]]+\]\s*/i, '')
|
|
395
|
+
.replace(/^\[heartbeat:[^\]]+\]\s*/i, '')
|
|
396
|
+
.trim();
|
|
397
|
+
}
|
|
398
|
+
if (!candidate || skip.some(rx => rx.test(candidate))) {
|
|
399
|
+
if (/heartbeat session/i.test(raw)) return 'Heartbeat session';
|
|
400
|
+
return '';
|
|
401
|
+
}
|
|
402
|
+
return candidate;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
return pick(text) || pick(fallbackText) || 'Session activity';
|
|
406
|
+
}
|
|
407
|
+
|
|
229
408
|
function isInternalProjectTag(tag) {
|
|
230
409
|
if (!tag) return true;
|
|
231
410
|
if (tag.startsWith('agent:')) return true;
|
|
@@ -268,7 +447,7 @@ function renderSessionItem(s) {
|
|
|
268
447
|
${renderModelTags(s)}
|
|
269
448
|
</span>
|
|
270
449
|
</div>
|
|
271
|
-
<div class="session-summary">${escHtml(truncate(s.summary
|
|
450
|
+
<div class="session-summary">${escHtml(truncate(cleanSessionSummary(s.summary, s.initial_prompt), 120))}</div>
|
|
272
451
|
<div class="session-meta">
|
|
273
452
|
<span><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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> ${s.message_count}</span>
|
|
274
453
|
<span><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> ${s.tool_count}</span>
|
|
@@ -280,6 +459,7 @@ function renderSessionItem(s) {
|
|
|
280
459
|
// --- Views ---
|
|
281
460
|
|
|
282
461
|
async function viewSearch(query = '') {
|
|
462
|
+
clearJumpUi();
|
|
283
463
|
const typeFilter = window._searchType || '';
|
|
284
464
|
const roleFilter = window._searchRole || '';
|
|
285
465
|
|
|
@@ -296,7 +476,12 @@ async function viewSearch(query = '') {
|
|
|
296
476
|
<span class="filter-chip ${roleFilter==='user'?'active':''}" data-filter="role" data-val="user">User</span>
|
|
297
477
|
<span class="filter-chip ${roleFilter==='assistant'?'active':''}" data-filter="role" data-val="assistant">Assistant</span>
|
|
298
478
|
</div>
|
|
299
|
-
<div id="results"
|
|
479
|
+
<div id="results">
|
|
480
|
+
<div class="search-bar skeleton-card" style="margin-top:6px">
|
|
481
|
+
<div class="skeleton-line" style="height:16px;width:40%"></div>
|
|
482
|
+
</div>
|
|
483
|
+
${skeletonRows(4, 'session')}
|
|
484
|
+
</div>`;
|
|
300
485
|
|
|
301
486
|
content.innerHTML = html;
|
|
302
487
|
transitionView();
|
|
@@ -325,10 +510,12 @@ async function viewSearch(query = '') {
|
|
|
325
510
|
|
|
326
511
|
async function showSearchHome() {
|
|
327
512
|
const el = $('#results');
|
|
328
|
-
|
|
513
|
+
const reqId = (window._searchReqSeq = (window._searchReqSeq || 0) + 1);
|
|
514
|
+
el.innerHTML = `${skeletonRows(4, 'session')}`;
|
|
329
515
|
|
|
330
516
|
const stats = await api('/stats');
|
|
331
517
|
const sessions = await api('/sessions?limit=5');
|
|
518
|
+
if (reqId !== window._searchReqSeq) return;
|
|
332
519
|
if (stats._error || sessions._error) {
|
|
333
520
|
el.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
334
521
|
return;
|
|
@@ -336,6 +523,7 @@ async function showSearchHome() {
|
|
|
336
523
|
|
|
337
524
|
let suggestions = [];
|
|
338
525
|
try { const r = await fetch('/api/suggestions'); const d = await r.json(); suggestions = d.suggestions || []; } catch(e) { suggestions = []; }
|
|
526
|
+
if (reqId !== window._searchReqSeq) return;
|
|
339
527
|
|
|
340
528
|
let html = `
|
|
341
529
|
<div class="search-stats" style="margin-top:8px">
|
|
@@ -374,9 +562,10 @@ async function showSearchHome() {
|
|
|
374
562
|
|
|
375
563
|
async function doSearch(q) {
|
|
376
564
|
const el = $('#results');
|
|
565
|
+
const reqId = (window._searchReqSeq = (window._searchReqSeq || 0) + 1);
|
|
377
566
|
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; }
|
|
378
567
|
|
|
379
|
-
el.innerHTML =
|
|
568
|
+
el.innerHTML = `${skeletonRows(6, 'event')}`;
|
|
380
569
|
|
|
381
570
|
const type = window._searchType || '';
|
|
382
571
|
const role = window._searchRole || '';
|
|
@@ -385,6 +574,7 @@ async function doSearch(q) {
|
|
|
385
574
|
if (role) url += `&role=${role}`;
|
|
386
575
|
|
|
387
576
|
const data = await api(url);
|
|
577
|
+
if (reqId !== window._searchReqSeq) return;
|
|
388
578
|
|
|
389
579
|
if (data._error || data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error || 'Server error')}</p></div>`; return; }
|
|
390
580
|
if (!data.results.length) { el.innerHTML = '<div class="empty"><h2>No results</h2><p>Try a different search term or adjust filters</p></div>'; return; }
|
|
@@ -402,7 +592,7 @@ async function doSearch(q) {
|
|
|
402
592
|
<div class="result-meta">
|
|
403
593
|
<span class="event-badge ${badgeClass(r.type, r.role)}">${r.type === 'tool_call' ? 'tool' : r.role || r.type}</span>
|
|
404
594
|
<span class="session-time">${fmtTime(r.timestamp)}</span>
|
|
405
|
-
${r.tool_name ? `<span class="tool-name">${escHtml(r.tool_name)}</span>` : ''}
|
|
595
|
+
${r.tool_name ? `<span class="tool-name">${escHtml(fmtToolName(r.tool_name))}</span>` : ''}
|
|
406
596
|
<span class="session-link" data-session="${r.session_id}">view session \u2192</span>
|
|
407
597
|
</div>
|
|
408
598
|
<div class="result-content">${escHtml(truncate(r.content || r.tool_args || r.tool_result || '', 400))}</div>
|
|
@@ -419,8 +609,10 @@ async function doSearch(q) {
|
|
|
419
609
|
}
|
|
420
610
|
|
|
421
611
|
async function viewSessions() {
|
|
612
|
+
clearJumpUi();
|
|
422
613
|
window._currentSessionId = null;
|
|
423
|
-
content.innerHTML =
|
|
614
|
+
content.innerHTML = `<div class="page-title">Sessions</div>${skeletonRows(4, 'session')}`;
|
|
615
|
+
transitionView();
|
|
424
616
|
const data = await api('/sessions?limit=200');
|
|
425
617
|
if (data._error) {
|
|
426
618
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
@@ -438,10 +630,18 @@ async function viewSessions() {
|
|
|
438
630
|
}
|
|
439
631
|
|
|
440
632
|
async function viewSession(id) {
|
|
633
|
+
clearJumpUi();
|
|
634
|
+
window._recentSessionIds = [id, ...(window._recentSessionIds || []).filter(x => x !== id)].slice(0, 5);
|
|
441
635
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
442
636
|
window._currentSessionId = id;
|
|
443
637
|
setHash('session/' + encodeURIComponent(id));
|
|
444
638
|
window.scrollTo(0, 0);
|
|
639
|
+
content.innerHTML = `
|
|
640
|
+
<div class="back-btn">← Back</div>
|
|
641
|
+
<div class="page-title">Session</div>
|
|
642
|
+
${skeletonRows(8, 'event')}
|
|
643
|
+
`;
|
|
644
|
+
transitionView();
|
|
445
645
|
const data = await api(`/sessions/${id}`);
|
|
446
646
|
|
|
447
647
|
if (data._error || data.error) { content.innerHTML = `<div class="empty"><h2>${escHtml(data.error || 'Unable to load')}</h2></div>`; return; }
|
|
@@ -525,6 +725,7 @@ async function viewSession(id) {
|
|
|
525
725
|
}
|
|
526
726
|
|
|
527
727
|
$('#backBtn').addEventListener('click', () => {
|
|
728
|
+
clearJumpUi();
|
|
528
729
|
if (window._navDepth > 0) {
|
|
529
730
|
history.back();
|
|
530
731
|
} else {
|
|
@@ -575,19 +776,65 @@ async function viewSession(id) {
|
|
|
575
776
|
|
|
576
777
|
const jumpBtn = $('#jumpToStartBtn');
|
|
577
778
|
if (jumpBtn) {
|
|
578
|
-
jumpBtn.addEventListener('click', () => {
|
|
579
|
-
|
|
779
|
+
jumpBtn.addEventListener('click', async () => {
|
|
780
|
+
const fromY = window.scrollY || window.pageYOffset || 0;
|
|
781
|
+
|
|
782
|
+
jumpBtn.classList.add('jumping');
|
|
783
|
+
jumpBtn.disabled = true;
|
|
784
|
+
|
|
785
|
+
// Let button state paint before heavy DOM work.
|
|
786
|
+
await new Promise(requestAnimationFrame);
|
|
787
|
+
|
|
788
|
+
// Load remaining events in chunks so UI stays responsive.
|
|
789
|
+
let loops = 0;
|
|
580
790
|
while (rendered < allEvents.length) {
|
|
581
791
|
renderBatch();
|
|
792
|
+
loops += 1;
|
|
793
|
+
if (loops % 2 === 0) await new Promise(requestAnimationFrame);
|
|
582
794
|
}
|
|
795
|
+
|
|
583
796
|
const firstMessage = document.querySelector(`[data-event-id="${s.first_message_id}"]`);
|
|
584
|
-
if (firstMessage) {
|
|
585
|
-
|
|
797
|
+
if (!firstMessage) {
|
|
798
|
+
jumpBtn.classList.remove('jumping');
|
|
799
|
+
jumpBtn.disabled = false;
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const targetY = Math.max(0, firstMessage.getBoundingClientRect().top + fromY - (window.innerHeight * 0.35));
|
|
804
|
+
const distance = Math.abs(targetY - fromY);
|
|
805
|
+
const useSmooth = distance < 1800;
|
|
806
|
+
|
|
807
|
+
window.scrollTo({ top: targetY, behavior: useSmooth ? 'smooth' : 'auto' });
|
|
808
|
+
|
|
809
|
+
const doneDelay = useSmooth ? 500 : 120;
|
|
810
|
+
setTimeout(() => {
|
|
586
811
|
firstMessage.classList.add('event-highlight');
|
|
812
|
+
setTimeout(() => firstMessage.classList.remove('event-highlight'), 2000);
|
|
813
|
+
|
|
814
|
+
jumpBtn.classList.remove('jumping');
|
|
815
|
+
jumpBtn.disabled = false;
|
|
816
|
+
|
|
817
|
+
let returnBtn = document.getElementById('returnJumpBtn');
|
|
818
|
+
if (!returnBtn) {
|
|
819
|
+
returnBtn = document.createElement('button');
|
|
820
|
+
returnBtn.id = 'returnJumpBtn';
|
|
821
|
+
returnBtn.className = 'return-jump-btn';
|
|
822
|
+
returnBtn.innerHTML = `<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="M12 19V5"></path><polyline points="5 12 12 5 19 12"></polyline></svg>`;
|
|
823
|
+
returnBtn.setAttribute('aria-label', 'Back to previous spot');
|
|
824
|
+
returnBtn.title = 'Back to previous spot';
|
|
825
|
+
document.body.appendChild(returnBtn);
|
|
826
|
+
}
|
|
827
|
+
returnBtn.classList.add('show');
|
|
828
|
+
|
|
829
|
+
returnBtn.onclick = () => {
|
|
830
|
+
window.scrollTo({ top: fromY, behavior: 'smooth' });
|
|
831
|
+
returnBtn.classList.remove('show');
|
|
832
|
+
};
|
|
833
|
+
|
|
587
834
|
setTimeout(() => {
|
|
588
|
-
|
|
589
|
-
},
|
|
590
|
-
}
|
|
835
|
+
if (returnBtn) returnBtn.classList.remove('show');
|
|
836
|
+
}, 9000);
|
|
837
|
+
}, doneDelay);
|
|
591
838
|
});
|
|
592
839
|
}
|
|
593
840
|
|
|
@@ -669,58 +916,205 @@ async function viewSession(id) {
|
|
|
669
916
|
}
|
|
670
917
|
|
|
671
918
|
async function viewTimeline(date) {
|
|
919
|
+
clearJumpUi();
|
|
672
920
|
if (!date) {
|
|
673
921
|
const now = new Date();
|
|
674
922
|
date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
|
|
675
923
|
}
|
|
676
924
|
window._lastView = 'timeline';
|
|
925
|
+
window._timelineState = { date, limit: 100, offset: 0, hasMore: true, loading: false, seenEventIds: new Set() };
|
|
677
926
|
|
|
678
|
-
|
|
927
|
+
content.innerHTML = `<div class="page-title">Timeline</div>
|
|
679
928
|
<input type="date" class="date-input" id="dateInput" value="${date}">
|
|
680
|
-
<div id="timelineContent"
|
|
681
|
-
|
|
929
|
+
<div id="timelineContent">${skeletonRows(8, 'event')}</div>
|
|
930
|
+
<div id="timelineLoadMore" class="loading-more" style="display:none">Loading more…</div>`;
|
|
682
931
|
transitionView();
|
|
683
932
|
|
|
684
|
-
const data = await api(`/timeline?date=${date}`);
|
|
685
|
-
if (data._error) {
|
|
686
|
-
$('#timelineContent').innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
933
|
const el = $('#timelineContent');
|
|
934
|
+
const state = window._timelineState;
|
|
690
935
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
936
|
+
async function loadTimelinePage(append = false) {
|
|
937
|
+
if (state.loading || (!state.hasMore && append)) return;
|
|
938
|
+
state.loading = true;
|
|
939
|
+
if (append) $('#timelineLoadMore').style.display = 'block';
|
|
940
|
+
|
|
941
|
+
const data = await api(`/timeline?date=${state.date}&limit=${state.limit}&offset=${state.offset}`);
|
|
942
|
+
state.loading = false;
|
|
943
|
+
$('#timelineLoadMore').style.display = 'none';
|
|
944
|
+
|
|
945
|
+
if (data._error) {
|
|
946
|
+
if (!append) el.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
state.hasMore = !!data.hasMore;
|
|
951
|
+
state.offset += (data.events || []).length;
|
|
952
|
+
(data.events || []).forEach(ev => state.seenEventIds.add(ev.id));
|
|
953
|
+
|
|
954
|
+
if (!append) {
|
|
955
|
+
if (!data.events.length) {
|
|
956
|
+
el.innerHTML = `<div class="timeline-events-wrap" id="timelineWrap"><div class="timeline-line"></div><div class="empty" id="timelineEmpty"><h2>No activity</h2><p>Nothing recorded on this day</p></div></div>`;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
el.innerHTML = `<div class="timeline-events-wrap" id="timelineWrap"><div class="timeline-line"></div>${data.events.map(renderTimelineEvent).join('')}</div>`;
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const wrap = $('#timelineWrap');
|
|
964
|
+
if (wrap) {
|
|
965
|
+
wrap.insertAdjacentHTML('beforeend', data.events.map(renderTimelineEvent).join(''));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
await loadTimelinePage(false);
|
|
970
|
+
|
|
971
|
+
if (window._timelineScrollHandler) window.removeEventListener('scroll', window._timelineScrollHandler);
|
|
972
|
+
window._timelineScrollHandler = () => {
|
|
973
|
+
const st = window._timelineState;
|
|
974
|
+
if (!st || st.loading || !st.hasMore) return;
|
|
975
|
+
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
|
|
976
|
+
if (nearBottom) loadTimelinePage(true);
|
|
977
|
+
};
|
|
978
|
+
window.addEventListener('scroll', window._timelineScrollHandler, { passive: true });
|
|
979
|
+
|
|
980
|
+
// Live updates via SSE (only for today)
|
|
981
|
+
const today = new Date();
|
|
982
|
+
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
|
983
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
984
|
+
if (date === todayStr) {
|
|
985
|
+
const sse = new EventSource(`/api/timeline/stream?after=${encodeURIComponent(new Date().toISOString())}&afterId=`);
|
|
986
|
+
window._timelineSse = sse;
|
|
987
|
+
sse.onmessage = (evt) => {
|
|
988
|
+
try {
|
|
989
|
+
const rows = JSON.parse(evt.data);
|
|
990
|
+
if (!rows.length) return;
|
|
991
|
+
|
|
992
|
+
const fresh = rows.filter(r => !state.seenEventIds.has(r.id));
|
|
993
|
+
if (!fresh.length) return;
|
|
994
|
+
fresh.forEach(r => state.seenEventIds.add(r.id));
|
|
995
|
+
|
|
996
|
+
let wrap = $('#timelineWrap');
|
|
997
|
+
if (!wrap) {
|
|
998
|
+
el.innerHTML = `<div class="timeline-events-wrap" id="timelineWrap"><div class="timeline-line"></div></div>`;
|
|
999
|
+
wrap = $('#timelineWrap');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const empty = $('#timelineEmpty');
|
|
1003
|
+
if (empty) empty.remove();
|
|
1004
|
+
|
|
1005
|
+
const html = fresh.map(renderTimelineEvent).join('');
|
|
1006
|
+
wrap.insertAdjacentHTML('afterbegin', html);
|
|
1007
|
+
state.offset += fresh.length;
|
|
1008
|
+
|
|
1009
|
+
// Flash new events
|
|
1010
|
+
fresh.forEach(r => {
|
|
1011
|
+
const rowEl = wrap.querySelector(`[data-event-id="${r.id}"]`);
|
|
1012
|
+
if (rowEl) { rowEl.classList.add('event-highlight'); setTimeout(() => rowEl.classList.remove('event-highlight'), 2000); }
|
|
1013
|
+
});
|
|
1014
|
+
} catch {}
|
|
1015
|
+
};
|
|
698
1016
|
}
|
|
699
1017
|
|
|
700
|
-
$('#dateInput').addEventListener('change', e =>
|
|
1018
|
+
$('#dateInput').addEventListener('change', e => {
|
|
1019
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
1020
|
+
viewTimeline(e.target.value);
|
|
1021
|
+
});
|
|
701
1022
|
}
|
|
702
1023
|
|
|
703
|
-
async function
|
|
704
|
-
|
|
1024
|
+
async function viewOverview() {
|
|
1025
|
+
clearJumpUi();
|
|
1026
|
+
content.innerHTML = `<div class="page-title">Overview</div><div class="stat-grid">${skeletonRows(5, 'stats')}</div>`;
|
|
1027
|
+
transitionView();
|
|
705
1028
|
const data = await api('/stats');
|
|
706
|
-
|
|
1029
|
+
const sessionsRes = await api('/sessions?limit=30');
|
|
1030
|
+
if (data._error || sessionsRes._error) {
|
|
707
1031
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
708
1032
|
return;
|
|
709
1033
|
}
|
|
710
1034
|
|
|
711
|
-
|
|
1035
|
+
const sessions = sessionsRes.sessions || [];
|
|
1036
|
+
const nowMs = Date.now();
|
|
1037
|
+
const ACTIVE_WINDOW_MIN = 15;
|
|
1038
|
+
const activeNow = sessions
|
|
1039
|
+
.filter(s => {
|
|
1040
|
+
const t = s.end_time || s.start_time;
|
|
1041
|
+
if (!t) return false;
|
|
1042
|
+
const ageMin = (nowMs - new Date(t).getTime()) / 60000;
|
|
1043
|
+
return ageMin >= 0 && ageMin <= ACTIVE_WINDOW_MIN;
|
|
1044
|
+
})
|
|
1045
|
+
.slice(0, 4);
|
|
1046
|
+
const activeIds = new Set(activeNow.map(s => s.id));
|
|
1047
|
+
const recentSessions = sessions.filter(s => !activeIds.has(s.id)).slice(0, 6);
|
|
1048
|
+
const uniqueTools = new Set((data.tools || []).filter(t => t).map(t => fmtToolGroup(t)));
|
|
1049
|
+
|
|
1050
|
+
let html = `<div class="page-title">Overview</div>
|
|
1051
|
+
|
|
1052
|
+
<div class="section-label">Key Metrics</div>
|
|
712
1053
|
<div class="stat-grid">
|
|
713
1054
|
<div class="stat-card accent-blue"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
|
|
714
1055
|
<div class="stat-card accent-green"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
|
|
715
1056
|
<div class="stat-card accent-amber"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
|
|
716
|
-
<div class="stat-card accent-purple"><div class="label">Unique Tools</div><div class="value">${data.uniqueTools}</div></div>
|
|
717
1057
|
<div class="stat-card accent-teal"><div class="label">Total Tokens</div><div class="value">${(data.totalTokens || 0).toLocaleString()}</div></div>
|
|
718
1058
|
</div>
|
|
719
1059
|
|
|
720
|
-
<div class="section-label">
|
|
721
|
-
|
|
1060
|
+
<div class="section-label">Active Now (${activeNow.length})</div>
|
|
1061
|
+
${activeNow.length ? activeNow.map(renderSessionItem).join('') : `<div class="empty" style="margin-bottom:var(--space-xl)"><p>No active sessions right now</p></div>`}
|
|
1062
|
+
|
|
1063
|
+
<div class="section-label">Recent Sessions</div>
|
|
1064
|
+
${recentSessions.map(renderSessionItem).join('')}
|
|
1065
|
+
`;
|
|
1066
|
+
|
|
1067
|
+
content.innerHTML = html;
|
|
1068
|
+
transitionView();
|
|
1069
|
+
$$('.session-item').forEach(item => item.addEventListener('click', () => viewSession(item.dataset.id)));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function viewStats() {
|
|
1073
|
+
clearJumpUi();
|
|
1074
|
+
content.innerHTML = `<div class="page-title">Settings</div>${skeletonRows(4, 'session')}`;
|
|
1075
|
+
transitionView();
|
|
1076
|
+
const data = await api('/stats');
|
|
1077
|
+
if (data._error) {
|
|
1078
|
+
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const uniqueTools = new Set((data.tools||[]).filter(t=>t).map(t=>fmtToolGroup(t)));
|
|
1083
|
+
const themeMode = lsGet(THEME_MODE_KEY) || 'light';
|
|
1084
|
+
const darkVariant = lsGet(THEME_DARK_VARIANT_KEY) || 'default';
|
|
1085
|
+
|
|
1086
|
+
let html = `<div class="settings-page">
|
|
1087
|
+
<div class="page-title">Settings</div>
|
|
1088
|
+
|
|
1089
|
+
<div class="section-label">System</div>
|
|
1090
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:var(--space-md);margin-bottom:var(--space-md)">
|
|
722
1091
|
<div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
|
|
723
|
-
<div class="config-card"><div class="config-label">DB Size</div><div class="config-value">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
|
|
1092
|
+
<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
|
+
</div>
|
|
1094
|
+
<p class="settings-help" style="margin-bottom:var(--space-sm)">Date range: ${fmtDate(data.dateRange?.earliest)} — ${fmtDate(data.dateRange?.latest)}</p>
|
|
1095
|
+
<div class="settings-maintenance">
|
|
1096
|
+
<button class="export-btn" id="optimizeDbBtn">Optimize Database</button>
|
|
1097
|
+
<span id="optimizeDbStatus" class="settings-maintenance-status"></span>
|
|
1098
|
+
</div>
|
|
1099
|
+
<p class="settings-help">Reclaims unused space and merges pending writes. Safe to run anytime, doesn't delete any data.</p>
|
|
1100
|
+
|
|
1101
|
+
<div class="section-label">Appearance</div>
|
|
1102
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:var(--space-md);margin-bottom:var(--space-xl)">
|
|
1103
|
+
<div class="config-card">
|
|
1104
|
+
<div class="config-label">Theme Mode</div>
|
|
1105
|
+
<select id="themeModeSelect" class="settings-select">
|
|
1106
|
+
<option value="system" ${themeMode==='system'?'selected':''}>System</option>
|
|
1107
|
+
<option value="light" ${themeMode==='light'?'selected':''}>Light</option>
|
|
1108
|
+
<option value="dark" ${themeMode==='dark'?'selected':''}>Dark</option>
|
|
1109
|
+
</select>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div class="config-card">
|
|
1112
|
+
<div class="config-label">Dark Variant</div>
|
|
1113
|
+
<select id="darkVariantSelect" class="settings-select">
|
|
1114
|
+
<option value="default" ${darkVariant==='default'?'selected':''}>Default</option>
|
|
1115
|
+
<option value="trueblack" ${darkVariant==='trueblack'?'selected':''}>True Black</option>
|
|
1116
|
+
</select>
|
|
1117
|
+
</div>
|
|
724
1118
|
</div>
|
|
725
1119
|
|
|
726
1120
|
${data.sessionDirs && data.sessionDirs.length ? (() => {
|
|
@@ -752,19 +1146,54 @@ async function viewStats() {
|
|
|
752
1146
|
})() : ''}
|
|
753
1147
|
|
|
754
1148
|
${data.agents && data.agents.length > 1 ? `<div class="section-label">Agents</div><div class="filters" style="margin-bottom:var(--space-xl)">${data.agents.map(a => `<span class="filter-chip">${escHtml(a)}</span>`).join('')}</div>` : ''}
|
|
755
|
-
<div class="section-label">Date Range</div>
|
|
756
|
-
<p style="color:var(--text-secondary);font-size:13px;margin-bottom:var(--space-xl)">${fmtDate(data.dateRange?.earliest)} \u2014 ${fmtDate(data.dateRange?.latest)}</p>
|
|
757
1149
|
<div class="section-label">Tools Used</div>
|
|
758
|
-
<div class="tools-grid">${(data.tools||[]).filter(t => t).sort().map(t => `<span class="tool-chip">${escHtml(t)}</span>`).join('')}</div>
|
|
759
|
-
|
|
1150
|
+
<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>
|
|
1151
|
+
</div>`;
|
|
760
1152
|
|
|
761
1153
|
content.innerHTML = html;
|
|
762
1154
|
transitionView();
|
|
1155
|
+
|
|
1156
|
+
const themeModeSelect = $('#themeModeSelect');
|
|
1157
|
+
const darkVariantSelect = $('#darkVariantSelect');
|
|
1158
|
+
if (themeModeSelect) {
|
|
1159
|
+
themeModeSelect.addEventListener('change', () => {
|
|
1160
|
+
lsSet(THEME_MODE_KEY, themeModeSelect.value);
|
|
1161
|
+
window._themeMode = themeModeSelect.value;
|
|
1162
|
+
applyThemeFromPrefs();
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
if (darkVariantSelect) {
|
|
1166
|
+
darkVariantSelect.addEventListener('change', () => {
|
|
1167
|
+
lsSet(THEME_DARK_VARIANT_KEY, darkVariantSelect.value);
|
|
1168
|
+
window._themeDarkVariant = darkVariantSelect.value;
|
|
1169
|
+
applyThemeFromPrefs();
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const optimizeBtn = $('#optimizeDbBtn');
|
|
1174
|
+
const optimizeStatus = $('#optimizeDbStatus');
|
|
1175
|
+
if (optimizeBtn) {
|
|
1176
|
+
optimizeBtn.addEventListener('click', async () => {
|
|
1177
|
+
optimizeBtn.disabled = true;
|
|
1178
|
+
optimizeStatus.textContent = 'Optimizing…';
|
|
1179
|
+
const result = await api('/maintenance', { method: 'POST' });
|
|
1180
|
+
if (result._error || !result.ok) {
|
|
1181
|
+
optimizeStatus.textContent = `Failed: ${result.error || 'Unknown error'}`;
|
|
1182
|
+
} else {
|
|
1183
|
+
optimizeStatus.textContent = `${result.sizeBefore?.display || 'N/A'} → ${result.sizeAfter?.display || 'N/A'}`;
|
|
1184
|
+
const dbSizeValue = $('#dbSizeValue');
|
|
1185
|
+
if (dbSizeValue) dbSizeValue.textContent = result.sizeAfter?.display || 'N/A';
|
|
1186
|
+
}
|
|
1187
|
+
optimizeBtn.disabled = false;
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
763
1190
|
}
|
|
764
1191
|
|
|
765
1192
|
async function viewFiles() {
|
|
1193
|
+
clearJumpUi();
|
|
766
1194
|
window._lastView = 'files';
|
|
767
|
-
content.innerHTML =
|
|
1195
|
+
content.innerHTML = `<div class="page-title">Files</div>${skeletonRows(6, 'file')}`;
|
|
1196
|
+
transitionView();
|
|
768
1197
|
const data = await api('/files?limit=500');
|
|
769
1198
|
if (data._error) {
|
|
770
1199
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
@@ -933,6 +1362,7 @@ function renderFileItem(f) {
|
|
|
933
1362
|
}
|
|
934
1363
|
|
|
935
1364
|
async function viewFileDetail(filePath) {
|
|
1365
|
+
clearJumpUi();
|
|
936
1366
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
937
1367
|
const data = await api(`/files/sessions?path=${encodeURIComponent(filePath)}`);
|
|
938
1368
|
if (data._error) {
|
|
@@ -959,16 +1389,18 @@ async function viewFileDetail(filePath) {
|
|
|
959
1389
|
// --- Navigation ---
|
|
960
1390
|
window._searchType = '';
|
|
961
1391
|
window._searchRole = '';
|
|
962
|
-
window._lastView = '
|
|
1392
|
+
window._lastView = 'overview';
|
|
963
1393
|
|
|
964
1394
|
$$('.nav-item').forEach(item => {
|
|
965
1395
|
item.addEventListener('click', () => {
|
|
966
1396
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
1397
|
+
if (window._timelineScrollHandler) { window.removeEventListener('scroll', window._timelineScrollHandler); window._timelineScrollHandler = null; }
|
|
1398
|
+
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
967
1399
|
const view = item.dataset.view;
|
|
968
1400
|
window._lastView = view;
|
|
969
1401
|
updateNavActive(view);
|
|
970
1402
|
setHash(view);
|
|
971
|
-
if (view === '
|
|
1403
|
+
if (view === 'overview') viewOverview();
|
|
972
1404
|
else if (view === 'sessions') viewSessions();
|
|
973
1405
|
else if (view === 'files') viewFiles();
|
|
974
1406
|
else if (view === 'timeline') viewTimeline();
|
|
@@ -976,6 +1408,259 @@ $$('.nav-item').forEach(item => {
|
|
|
976
1408
|
});
|
|
977
1409
|
});
|
|
978
1410
|
|
|
1411
|
+
// --- Command Palette (Cmd+K) ---
|
|
1412
|
+
window._cmdk = { open: false, index: 0, items: [], scrollY: 0 };
|
|
1413
|
+
|
|
1414
|
+
function closeCmdk() {
|
|
1415
|
+
const el = $('#cmdkOverlay');
|
|
1416
|
+
if (el) el.remove();
|
|
1417
|
+
|
|
1418
|
+
document.documentElement.classList.remove('cmdk-open');
|
|
1419
|
+
document.body.classList.remove('cmdk-open');
|
|
1420
|
+
|
|
1421
|
+
window._cmdk.open = false;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function execCmdkItem(i) {
|
|
1425
|
+
const item = window._cmdk.items[i];
|
|
1426
|
+
if (!item) return;
|
|
1427
|
+
closeCmdk();
|
|
1428
|
+
item.action();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function renderCmdkList() {
|
|
1432
|
+
const list = $('#cmdkList');
|
|
1433
|
+
if (!list) return;
|
|
1434
|
+
const { items, index } = window._cmdk;
|
|
1435
|
+
if (!items.length) {
|
|
1436
|
+
list.innerHTML = '<div class="cmdk-empty"><h4>No results</h4>Try a different search term</div>';
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
let html = '';
|
|
1441
|
+
let lastGroup = '';
|
|
1442
|
+
items.forEach((item, i) => {
|
|
1443
|
+
if (item.group !== lastGroup) {
|
|
1444
|
+
lastGroup = item.group;
|
|
1445
|
+
html += `<div class="cmdk-group-label">${escHtml(lastGroup)}</div>`;
|
|
1446
|
+
}
|
|
1447
|
+
html += `<button type="button" class="cmdk-item ${i === index ? 'active' : ''}" data-i="${i}">
|
|
1448
|
+
<div class="cmdk-item-body">
|
|
1449
|
+
<div class="cmdk-item-title">${escHtml(item.title)}</div>
|
|
1450
|
+
${item.sub ? `<div class="cmdk-item-sub">${escHtml(item.sub)}</div>` : ''}
|
|
1451
|
+
</div>
|
|
1452
|
+
${item.meta ? `<span class="cmdk-item-meta">${escHtml(item.meta)}</span>` : ''}
|
|
1453
|
+
</button>`;
|
|
1454
|
+
});
|
|
1455
|
+
list.innerHTML = html;
|
|
1456
|
+
|
|
1457
|
+
if (!list.dataset.bound) {
|
|
1458
|
+
list.addEventListener('click', (e) => {
|
|
1459
|
+
const btn = e.target.closest('.cmdk-item');
|
|
1460
|
+
if (!btn) return;
|
|
1461
|
+
e.preventDefault();
|
|
1462
|
+
const idx = Number(btn.dataset.i || -1);
|
|
1463
|
+
if (idx >= 0) execCmdkItem(idx);
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
list.addEventListener('pointermove', (e) => {
|
|
1467
|
+
if (e.pointerType && e.pointerType !== 'mouse') return;
|
|
1468
|
+
const btn = e.target.closest('.cmdk-item');
|
|
1469
|
+
if (!btn) return;
|
|
1470
|
+
const idx = Number(btn.dataset.i || -1);
|
|
1471
|
+
if (idx >= 0 && idx !== window._cmdk.index) {
|
|
1472
|
+
window._cmdk.index = idx;
|
|
1473
|
+
renderCmdkList();
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
list.dataset.bound = '1';
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// scroll active into view
|
|
1481
|
+
const active = list.querySelector('.cmdk-item.active');
|
|
1482
|
+
if (active) active.scrollIntoView({ block: 'nearest' });
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function loadCmdkResults(query) {
|
|
1486
|
+
const list = $('#cmdkList');
|
|
1487
|
+
if (!list) return;
|
|
1488
|
+
const q = (query || '').trim();
|
|
1489
|
+
if (!q) { loadCmdkHome(); return; }
|
|
1490
|
+
|
|
1491
|
+
list.innerHTML = '<div class="cmdk-loading">Searching\u2026</div>';
|
|
1492
|
+
|
|
1493
|
+
const [searchRes, sessionsRes, filesRes] = await Promise.all([
|
|
1494
|
+
api(`/search?q=${encodeURIComponent(q)}&limit=6`),
|
|
1495
|
+
api('/sessions?limit=20'),
|
|
1496
|
+
api('/files?limit=20')
|
|
1497
|
+
]);
|
|
1498
|
+
|
|
1499
|
+
const items = [];
|
|
1500
|
+
|
|
1501
|
+
(sessionsRes.sessions || [])
|
|
1502
|
+
.filter(s => cleanSessionSummary(s.summary, s.initial_prompt).toLowerCase().includes(q.toLowerCase()))
|
|
1503
|
+
.slice(0, 3)
|
|
1504
|
+
.forEach(s => items.push({
|
|
1505
|
+
group: 'Sessions',
|
|
1506
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1507
|
+
sub: shortSessionId(s.id),
|
|
1508
|
+
meta: fmtTime(s.start_time),
|
|
1509
|
+
action: () => viewSession(s.id)
|
|
1510
|
+
}));
|
|
1511
|
+
|
|
1512
|
+
(filesRes.files || [])
|
|
1513
|
+
.filter(f => f.file_path.toLowerCase().includes(q.toLowerCase()))
|
|
1514
|
+
.slice(0, 2)
|
|
1515
|
+
.forEach(f => items.push({
|
|
1516
|
+
group: 'Files',
|
|
1517
|
+
title: f.file_path.split('/').pop(),
|
|
1518
|
+
sub: f.file_path,
|
|
1519
|
+
meta: `${f.touch_count} touches`,
|
|
1520
|
+
action: () => viewFileDetail(f.file_path)
|
|
1521
|
+
}));
|
|
1522
|
+
|
|
1523
|
+
(searchRes.results || []).slice(0, 4).forEach(r => items.push({
|
|
1524
|
+
group: 'Search Results',
|
|
1525
|
+
title: truncate(r.content || r.tool_args || r.tool_result || '', 66),
|
|
1526
|
+
sub: shortSessionId(r.session_id),
|
|
1527
|
+
meta: fmtTime(r.timestamp),
|
|
1528
|
+
action: () => viewSession(r.session_id)
|
|
1529
|
+
}));
|
|
1530
|
+
|
|
1531
|
+
window._cmdk.items = items;
|
|
1532
|
+
window._cmdk.index = 0;
|
|
1533
|
+
renderCmdkList();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
async function loadCmdkHome() {
|
|
1537
|
+
const list = $('#cmdkList');
|
|
1538
|
+
if (!list) return;
|
|
1539
|
+
list.innerHTML = '<div class="cmdk-loading">Loading\u2026</div>';
|
|
1540
|
+
|
|
1541
|
+
const items = [
|
|
1542
|
+
{ group: 'Go to', title: 'Sessions', sub: 'Browse all sessions', action: () => { setHash('sessions'); handleRoute(); } },
|
|
1543
|
+
{ group: 'Go to', title: 'Timeline', sub: 'Today and historical events', action: () => { setHash('timeline'); handleRoute(); } },
|
|
1544
|
+
{ group: 'Go to', title: 'Overview', sub: 'Dashboard summary', action: () => { setHash('overview'); handleRoute(); } },
|
|
1545
|
+
{ group: 'Go to', title: 'Files', sub: 'Touched files explorer', action: () => { setHash('files'); handleRoute(); } },
|
|
1546
|
+
{ group: 'Go to', title: 'Settings', sub: 'Configuration and maintenance', action: () => { setHash('stats'); handleRoute(); } },
|
|
1547
|
+
];
|
|
1548
|
+
|
|
1549
|
+
const sessionsRes = await api('/sessions?limit=20');
|
|
1550
|
+
|
|
1551
|
+
const sessionList = sessionsRes.sessions || [];
|
|
1552
|
+
const sessionMap = new Map(sessionList.map(s => [s.id, s]));
|
|
1553
|
+
|
|
1554
|
+
const recentIds = window._recentSessionIds || [];
|
|
1555
|
+
const missingIds = recentIds.filter(id => !sessionMap.has(id));
|
|
1556
|
+
if (missingIds.length) {
|
|
1557
|
+
const fetched = await Promise.all(missingIds.map(id => api(`/sessions/${id}`)));
|
|
1558
|
+
fetched.forEach(r => {
|
|
1559
|
+
if (r && !r._error && r.session) sessionMap.set(r.session.id, r.session);
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
recentIds.forEach(id => {
|
|
1564
|
+
const s = sessionMap.get(id);
|
|
1565
|
+
if (!s) return;
|
|
1566
|
+
items.push({
|
|
1567
|
+
group: 'Recently Opened',
|
|
1568
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1569
|
+
sub: shortSessionId(s.id),
|
|
1570
|
+
meta: fmtTime(s.start_time),
|
|
1571
|
+
action: () => viewSession(s.id)
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
sessionList.forEach(s => items.push({
|
|
1576
|
+
group: 'Recent Sessions',
|
|
1577
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1578
|
+
sub: shortSessionId(s.id),
|
|
1579
|
+
meta: fmtTime(s.start_time),
|
|
1580
|
+
action: () => viewSession(s.id)
|
|
1581
|
+
}));
|
|
1582
|
+
|
|
1583
|
+
window._cmdk.items = items;
|
|
1584
|
+
window._cmdk.index = 0;
|
|
1585
|
+
renderCmdkList();
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function openCmdk() {
|
|
1589
|
+
if ($('#cmdkOverlay')) { $('#cmdkInput')?.focus(); return; }
|
|
1590
|
+
|
|
1591
|
+
const overlay = document.createElement('div');
|
|
1592
|
+
overlay.className = 'cmdk-overlay';
|
|
1593
|
+
overlay.id = 'cmdkOverlay';
|
|
1594
|
+
overlay.innerHTML = `<div class="cmdk-dialog" role="dialog" aria-modal="true">
|
|
1595
|
+
<div class="cmdk-input-wrap">
|
|
1596
|
+
<svg width="16" height="16" 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>
|
|
1597
|
+
<input id="cmdkInput" type="text" placeholder="Search sessions, files, or jump to a view" />
|
|
1598
|
+
<kbd>ESC</kbd>
|
|
1599
|
+
</div>
|
|
1600
|
+
<div class="cmdk-list" id="cmdkList"></div>
|
|
1601
|
+
</div>`;
|
|
1602
|
+
document.body.appendChild(overlay);
|
|
1603
|
+
window._cmdk.open = true;
|
|
1604
|
+
|
|
1605
|
+
const input = $('#cmdkInput');
|
|
1606
|
+
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
1607
|
+
|
|
1608
|
+
document.documentElement.classList.add('cmdk-open');
|
|
1609
|
+
document.body.classList.add('cmdk-open');
|
|
1610
|
+
|
|
1611
|
+
if (isMobile) {
|
|
1612
|
+
// iOS keyboard requires focus in the same user gesture call stack.
|
|
1613
|
+
try {
|
|
1614
|
+
input.focus({ preventScroll: true });
|
|
1615
|
+
} catch {
|
|
1616
|
+
input.focus();
|
|
1617
|
+
}
|
|
1618
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
1619
|
+
} else {
|
|
1620
|
+
input.focus();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
let debounce;
|
|
1624
|
+
input.addEventListener('input', () => {
|
|
1625
|
+
clearTimeout(debounce);
|
|
1626
|
+
debounce = setTimeout(() => loadCmdkResults(input.value), 150);
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeCmdk(); });
|
|
1630
|
+
|
|
1631
|
+
overlay.addEventListener('keydown', e => {
|
|
1632
|
+
if (e.key === 'Escape') { e.preventDefault(); closeCmdk(); return; }
|
|
1633
|
+
if (e.key === 'ArrowDown') {
|
|
1634
|
+
e.preventDefault();
|
|
1635
|
+
window._cmdk.index = Math.min(window._cmdk.index + 1, window._cmdk.items.length - 1);
|
|
1636
|
+
renderCmdkList();
|
|
1637
|
+
} else if (e.key === 'ArrowUp') {
|
|
1638
|
+
e.preventDefault();
|
|
1639
|
+
window._cmdk.index = Math.max(window._cmdk.index - 1, 0);
|
|
1640
|
+
renderCmdkList();
|
|
1641
|
+
} else if (e.key === 'Enter') {
|
|
1642
|
+
e.preventDefault();
|
|
1643
|
+
execCmdkItem(window._cmdk.index);
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
loadCmdkHome();
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
initTheme();
|
|
1651
|
+
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
|
|
1652
|
+
document.getElementById('theme-toggle-mobile')?.addEventListener('click', toggleTheme);
|
|
1653
|
+
document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
|
|
1654
|
+
document.getElementById('mobile-search-btn')?.addEventListener('click', () => openCmdk());
|
|
1655
|
+
document.addEventListener('keydown', e => {
|
|
1656
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
1657
|
+
e.preventDefault();
|
|
1658
|
+
if (window._cmdk.open) closeCmdk();
|
|
1659
|
+
else openCmdk();
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (e.key === 'Escape' && window._cmdk.open) { e.preventDefault(); closeCmdk(); }
|
|
1663
|
+
});
|
|
979
1664
|
handleRoute();
|
|
980
1665
|
|
|
981
1666
|
// Swipe right from left edge to go back
|