agentacta 1.4.0 → 1.5.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 +4 -26
- package/index.js +38 -5
- package/package.json +1 -1
- package/public/app.js +583 -49
- package/public/index.html +14 -6
- package/public/style.css +383 -7
package/public/app.js
CHANGED
|
@@ -3,25 +3,68 @@ const $$ = (s, p = document) => [...p.querySelectorAll(s)];
|
|
|
3
3
|
const content = $('#content');
|
|
4
4
|
const API = '/api';
|
|
5
5
|
|
|
6
|
-
const THEME_KEY = 'agentacta-theme';
|
|
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
|
+
}
|
|
7
24
|
|
|
8
25
|
function applyTheme(theme) {
|
|
9
26
|
document.documentElement.setAttribute('data-theme', theme);
|
|
10
27
|
const meta = document.querySelector('meta[name="theme-color"]');
|
|
11
|
-
if (meta)
|
|
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));
|
|
12
40
|
}
|
|
13
41
|
|
|
14
42
|
function initTheme() {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
+
}
|
|
18
60
|
}
|
|
19
61
|
|
|
20
62
|
function toggleTheme() {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
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();
|
|
25
68
|
}
|
|
26
69
|
|
|
27
70
|
async function api(path, options = {}) {
|
|
@@ -199,7 +242,8 @@ function updateNavActive(view) {
|
|
|
199
242
|
}
|
|
200
243
|
|
|
201
244
|
function handleRoute() {
|
|
202
|
-
|
|
245
|
+
clearJumpUi();
|
|
246
|
+
const raw = (window.location.hash || '').slice(1) || 'overview';
|
|
203
247
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
204
248
|
if (window._timelineScrollHandler) { window.removeEventListener('scroll', window._timelineScrollHandler); window._timelineScrollHandler = null; }
|
|
205
249
|
|
|
@@ -208,14 +252,15 @@ function handleRoute() {
|
|
|
208
252
|
if (id) { viewSession(id); return; }
|
|
209
253
|
}
|
|
210
254
|
|
|
211
|
-
const
|
|
255
|
+
const normalized = raw === 'search' ? 'overview' : raw;
|
|
256
|
+
const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' ? normalized : 'overview';
|
|
212
257
|
window._lastView = view;
|
|
213
258
|
updateNavActive(view);
|
|
214
|
-
if (view === '
|
|
259
|
+
if (view === 'overview') viewOverview();
|
|
260
|
+
else if (view === 'sessions') viewSessions();
|
|
215
261
|
else if (view === 'files') viewFiles();
|
|
216
262
|
else if (view === 'timeline') viewTimeline();
|
|
217
|
-
else
|
|
218
|
-
else viewSearch(window._lastSearchQuery || '');
|
|
263
|
+
else viewStats();
|
|
219
264
|
}
|
|
220
265
|
|
|
221
266
|
window.addEventListener('popstate', () => {
|
|
@@ -310,6 +355,56 @@ function normalizeAgentLabel(a) {
|
|
|
310
355
|
return a;
|
|
311
356
|
}
|
|
312
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
|
+
|
|
313
408
|
function isInternalProjectTag(tag) {
|
|
314
409
|
if (!tag) return true;
|
|
315
410
|
if (tag.startsWith('agent:')) return true;
|
|
@@ -352,7 +447,7 @@ function renderSessionItem(s) {
|
|
|
352
447
|
${renderModelTags(s)}
|
|
353
448
|
</span>
|
|
354
449
|
</div>
|
|
355
|
-
<div class="session-summary">${escHtml(truncate(s.summary
|
|
450
|
+
<div class="session-summary">${escHtml(truncate(cleanSessionSummary(s.summary, s.initial_prompt), 120))}</div>
|
|
356
451
|
<div class="session-meta">
|
|
357
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>
|
|
358
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>
|
|
@@ -364,6 +459,7 @@ function renderSessionItem(s) {
|
|
|
364
459
|
// --- Views ---
|
|
365
460
|
|
|
366
461
|
async function viewSearch(query = '') {
|
|
462
|
+
clearJumpUi();
|
|
367
463
|
const typeFilter = window._searchType || '';
|
|
368
464
|
const roleFilter = window._searchRole || '';
|
|
369
465
|
|
|
@@ -414,10 +510,12 @@ async function viewSearch(query = '') {
|
|
|
414
510
|
|
|
415
511
|
async function showSearchHome() {
|
|
416
512
|
const el = $('#results');
|
|
513
|
+
const reqId = (window._searchReqSeq = (window._searchReqSeq || 0) + 1);
|
|
417
514
|
el.innerHTML = `${skeletonRows(4, 'session')}`;
|
|
418
515
|
|
|
419
516
|
const stats = await api('/stats');
|
|
420
517
|
const sessions = await api('/sessions?limit=5');
|
|
518
|
+
if (reqId !== window._searchReqSeq) return;
|
|
421
519
|
if (stats._error || sessions._error) {
|
|
422
520
|
el.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
423
521
|
return;
|
|
@@ -425,6 +523,7 @@ async function showSearchHome() {
|
|
|
425
523
|
|
|
426
524
|
let suggestions = [];
|
|
427
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;
|
|
428
527
|
|
|
429
528
|
let html = `
|
|
430
529
|
<div class="search-stats" style="margin-top:8px">
|
|
@@ -463,6 +562,7 @@ async function showSearchHome() {
|
|
|
463
562
|
|
|
464
563
|
async function doSearch(q) {
|
|
465
564
|
const el = $('#results');
|
|
565
|
+
const reqId = (window._searchReqSeq = (window._searchReqSeq || 0) + 1);
|
|
466
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; }
|
|
467
567
|
|
|
468
568
|
el.innerHTML = `${skeletonRows(6, 'event')}`;
|
|
@@ -474,6 +574,7 @@ async function doSearch(q) {
|
|
|
474
574
|
if (role) url += `&role=${role}`;
|
|
475
575
|
|
|
476
576
|
const data = await api(url);
|
|
577
|
+
if (reqId !== window._searchReqSeq) return;
|
|
477
578
|
|
|
478
579
|
if (data._error || data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error || 'Server error')}</p></div>`; return; }
|
|
479
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; }
|
|
@@ -508,6 +609,7 @@ async function doSearch(q) {
|
|
|
508
609
|
}
|
|
509
610
|
|
|
510
611
|
async function viewSessions() {
|
|
612
|
+
clearJumpUi();
|
|
511
613
|
window._currentSessionId = null;
|
|
512
614
|
content.innerHTML = `<div class="page-title">Sessions</div>${skeletonRows(4, 'session')}`;
|
|
513
615
|
transitionView();
|
|
@@ -528,6 +630,8 @@ async function viewSessions() {
|
|
|
528
630
|
}
|
|
529
631
|
|
|
530
632
|
async function viewSession(id) {
|
|
633
|
+
clearJumpUi();
|
|
634
|
+
window._recentSessionIds = [id, ...(window._recentSessionIds || []).filter(x => x !== id)].slice(0, 5);
|
|
531
635
|
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
532
636
|
window._currentSessionId = id;
|
|
533
637
|
setHash('session/' + encodeURIComponent(id));
|
|
@@ -621,6 +725,7 @@ async function viewSession(id) {
|
|
|
621
725
|
}
|
|
622
726
|
|
|
623
727
|
$('#backBtn').addEventListener('click', () => {
|
|
728
|
+
clearJumpUi();
|
|
624
729
|
if (window._navDepth > 0) {
|
|
625
730
|
history.back();
|
|
626
731
|
} else {
|
|
@@ -671,19 +776,65 @@ async function viewSession(id) {
|
|
|
671
776
|
|
|
672
777
|
const jumpBtn = $('#jumpToStartBtn');
|
|
673
778
|
if (jumpBtn) {
|
|
674
|
-
jumpBtn.addEventListener('click', () => {
|
|
675
|
-
|
|
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;
|
|
676
790
|
while (rendered < allEvents.length) {
|
|
677
791
|
renderBatch();
|
|
792
|
+
loops += 1;
|
|
793
|
+
if (loops % 2 === 0) await new Promise(requestAnimationFrame);
|
|
678
794
|
}
|
|
795
|
+
|
|
679
796
|
const firstMessage = document.querySelector(`[data-event-id="${s.first_message_id}"]`);
|
|
680
|
-
if (firstMessage) {
|
|
681
|
-
|
|
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(() => {
|
|
682
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
|
+
|
|
683
834
|
setTimeout(() => {
|
|
684
|
-
|
|
685
|
-
},
|
|
686
|
-
}
|
|
835
|
+
if (returnBtn) returnBtn.classList.remove('show');
|
|
836
|
+
}, 9000);
|
|
837
|
+
}, doneDelay);
|
|
687
838
|
});
|
|
688
839
|
}
|
|
689
840
|
|
|
@@ -765,12 +916,13 @@ async function viewSession(id) {
|
|
|
765
916
|
}
|
|
766
917
|
|
|
767
918
|
async function viewTimeline(date) {
|
|
919
|
+
clearJumpUi();
|
|
768
920
|
if (!date) {
|
|
769
921
|
const now = new Date();
|
|
770
922
|
date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
|
|
771
923
|
}
|
|
772
924
|
window._lastView = 'timeline';
|
|
773
|
-
window._timelineState = { date, limit: 100, offset: 0, hasMore: true, loading: false };
|
|
925
|
+
window._timelineState = { date, limit: 100, offset: 0, hasMore: true, loading: false, seenEventIds: new Set() };
|
|
774
926
|
|
|
775
927
|
content.innerHTML = `<div class="page-title">Timeline</div>
|
|
776
928
|
<input type="date" class="date-input" id="dateInput" value="${date}">
|
|
@@ -797,10 +949,11 @@ async function viewTimeline(date) {
|
|
|
797
949
|
|
|
798
950
|
state.hasMore = !!data.hasMore;
|
|
799
951
|
state.offset += (data.events || []).length;
|
|
952
|
+
(data.events || []).forEach(ev => state.seenEventIds.add(ev.id));
|
|
800
953
|
|
|
801
954
|
if (!append) {
|
|
802
955
|
if (!data.events.length) {
|
|
803
|
-
el.innerHTML =
|
|
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>`;
|
|
804
957
|
return;
|
|
805
958
|
}
|
|
806
959
|
el.innerHTML = `<div class="timeline-events-wrap" id="timelineWrap"><div class="timeline-line"></div>${data.events.map(renderTimelineEvent).join('')}</div>`;
|
|
@@ -829,21 +982,35 @@ async function viewTimeline(date) {
|
|
|
829
982
|
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
|
830
983
|
if (window._timelineSse) { window._timelineSse.close(); window._timelineSse = null; }
|
|
831
984
|
if (date === todayStr) {
|
|
832
|
-
const sse = new EventSource(`/api/timeline/stream?after=${encodeURIComponent(new Date().toISOString())}
|
|
985
|
+
const sse = new EventSource(`/api/timeline/stream?after=${encodeURIComponent(new Date().toISOString())}&afterId=`);
|
|
833
986
|
window._timelineSse = sse;
|
|
834
987
|
sse.onmessage = (evt) => {
|
|
835
988
|
try {
|
|
836
989
|
const rows = JSON.parse(evt.data);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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');
|
|
846
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
|
+
});
|
|
847
1014
|
} catch {}
|
|
848
1015
|
};
|
|
849
1016
|
}
|
|
@@ -854,33 +1021,100 @@ async function viewTimeline(date) {
|
|
|
854
1021
|
});
|
|
855
1022
|
}
|
|
856
1023
|
|
|
857
|
-
async function
|
|
858
|
-
|
|
1024
|
+
async function viewOverview() {
|
|
1025
|
+
clearJumpUi();
|
|
1026
|
+
content.innerHTML = `<div class="page-title">Overview</div><div class="stat-grid">${skeletonRows(5, 'stats')}</div>`;
|
|
859
1027
|
transitionView();
|
|
860
1028
|
const data = await api('/stats');
|
|
861
|
-
|
|
1029
|
+
const sessionsRes = await api('/sessions?limit=30');
|
|
1030
|
+
if (data._error || sessionsRes._error) {
|
|
862
1031
|
content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
|
|
863
1032
|
return;
|
|
864
1033
|
}
|
|
865
1034
|
|
|
866
|
-
|
|
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>
|
|
867
1053
|
<div class="stat-grid">
|
|
868
1054
|
<div class="stat-card accent-blue"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
|
|
869
1055
|
<div class="stat-card accent-green"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
|
|
870
1056
|
<div class="stat-card accent-amber"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
|
|
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>
|
|
872
1057
|
<div class="stat-card accent-teal"><div class="label">Total Tokens</div><div class="value">${(data.totalTokens || 0).toLocaleString()}</div></div>
|
|
873
1058
|
</div>
|
|
874
1059
|
|
|
875
|
-
<div class="section-label">
|
|
876
|
-
|
|
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)">
|
|
877
1091
|
<div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
|
|
878
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>
|
|
879
1093
|
</div>
|
|
880
|
-
<
|
|
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">
|
|
881
1096
|
<button class="export-btn" id="optimizeDbBtn">Optimize Database</button>
|
|
882
|
-
<span
|
|
883
|
-
|
|
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>
|
|
884
1118
|
</div>
|
|
885
1119
|
|
|
886
1120
|
${data.sessionDirs && data.sessionDirs.length ? (() => {
|
|
@@ -912,15 +1146,38 @@ async function viewStats() {
|
|
|
912
1146
|
})() : ''}
|
|
913
1147
|
|
|
914
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>` : ''}
|
|
915
|
-
<div class="section-label">Date Range</div>
|
|
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>
|
|
917
1149
|
<div class="section-label">Tools Used</div>
|
|
918
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>
|
|
919
|
-
|
|
1151
|
+
|
|
1152
|
+
<div class="section-label">System Info</div>
|
|
1153
|
+
<div id="systemInfoContainer" style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:var(--space-md);margin-bottom:var(--space-md)">
|
|
1154
|
+
<div class="config-card"><div class="config-label">Version</div><div class="config-value" id="sysVersion">…</div></div>
|
|
1155
|
+
<div class="config-card"><div class="config-label">Uptime</div><div class="config-value" id="sysUptime">…</div></div>
|
|
1156
|
+
<div class="config-card"><div class="config-label">Indexed Sessions</div><div class="config-value" id="sysSessions">…</div></div>
|
|
1157
|
+
<div class="config-card"><div class="config-label">Node.js</div><div class="config-value" id="sysNode">…</div></div>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>`;
|
|
920
1160
|
|
|
921
1161
|
content.innerHTML = html;
|
|
922
1162
|
transitionView();
|
|
923
1163
|
|
|
1164
|
+
const themeModeSelect = $('#themeModeSelect');
|
|
1165
|
+
const darkVariantSelect = $('#darkVariantSelect');
|
|
1166
|
+
if (themeModeSelect) {
|
|
1167
|
+
themeModeSelect.addEventListener('change', () => {
|
|
1168
|
+
lsSet(THEME_MODE_KEY, themeModeSelect.value);
|
|
1169
|
+
window._themeMode = themeModeSelect.value;
|
|
1170
|
+
applyThemeFromPrefs();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (darkVariantSelect) {
|
|
1174
|
+
darkVariantSelect.addEventListener('change', () => {
|
|
1175
|
+
lsSet(THEME_DARK_VARIANT_KEY, darkVariantSelect.value);
|
|
1176
|
+
window._themeDarkVariant = darkVariantSelect.value;
|
|
1177
|
+
applyThemeFromPrefs();
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
924
1181
|
const optimizeBtn = $('#optimizeDbBtn');
|
|
925
1182
|
const optimizeStatus = $('#optimizeDbStatus');
|
|
926
1183
|
if (optimizeBtn) {
|
|
@@ -938,9 +1195,35 @@ async function viewStats() {
|
|
|
938
1195
|
optimizeBtn.disabled = false;
|
|
939
1196
|
});
|
|
940
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
// Fetch system info
|
|
1200
|
+
api('/health').then(h => {
|
|
1201
|
+
if (h._error) return;
|
|
1202
|
+
const fmtUptime = (s) => {
|
|
1203
|
+
const d = Math.floor(s / 86400);
|
|
1204
|
+
const hr = Math.floor((s % 86400) / 3600);
|
|
1205
|
+
const m = Math.floor((s % 3600) / 60);
|
|
1206
|
+
if (d > 0) return `${d}d ${hr}h`;
|
|
1207
|
+
if (hr > 0) return `${hr}h ${m}m`;
|
|
1208
|
+
return `${m}m`;
|
|
1209
|
+
};
|
|
1210
|
+
const fmtBytes = (b) => {
|
|
1211
|
+
if (b >= 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
|
1212
|
+
return `${(b / 1024).toFixed(1)} KB`;
|
|
1213
|
+
};
|
|
1214
|
+
const v = $('#sysVersion');
|
|
1215
|
+
const u = $('#sysUptime');
|
|
1216
|
+
const sc = $('#sysSessions');
|
|
1217
|
+
const n = $('#sysNode');
|
|
1218
|
+
if (v) v.textContent = `v${h.version}`;
|
|
1219
|
+
if (u) u.textContent = fmtUptime(h.uptime);
|
|
1220
|
+
if (sc) sc.textContent = String(h.sessions);
|
|
1221
|
+
if (n) n.textContent = h.node;
|
|
1222
|
+
});
|
|
941
1223
|
}
|
|
942
1224
|
|
|
943
1225
|
async function viewFiles() {
|
|
1226
|
+
clearJumpUi();
|
|
944
1227
|
window._lastView = 'files';
|
|
945
1228
|
content.innerHTML = `<div class="page-title">Files</div>${skeletonRows(6, 'file')}`;
|
|
946
1229
|
transitionView();
|
|
@@ -1112,6 +1395,7 @@ function renderFileItem(f) {
|
|
|
1112
1395
|
}
|
|
1113
1396
|
|
|
1114
1397
|
async function viewFileDetail(filePath) {
|
|
1398
|
+
clearJumpUi();
|
|
1115
1399
|
content.innerHTML = '<div class="loading">Loading</div>';
|
|
1116
1400
|
const data = await api(`/files/sessions?path=${encodeURIComponent(filePath)}`);
|
|
1117
1401
|
if (data._error) {
|
|
@@ -1138,7 +1422,7 @@ async function viewFileDetail(filePath) {
|
|
|
1138
1422
|
// --- Navigation ---
|
|
1139
1423
|
window._searchType = '';
|
|
1140
1424
|
window._searchRole = '';
|
|
1141
|
-
window._lastView = '
|
|
1425
|
+
window._lastView = 'overview';
|
|
1142
1426
|
|
|
1143
1427
|
$$('.nav-item').forEach(item => {
|
|
1144
1428
|
item.addEventListener('click', () => {
|
|
@@ -1149,7 +1433,7 @@ $$('.nav-item').forEach(item => {
|
|
|
1149
1433
|
window._lastView = view;
|
|
1150
1434
|
updateNavActive(view);
|
|
1151
1435
|
setHash(view);
|
|
1152
|
-
if (view === '
|
|
1436
|
+
if (view === 'overview') viewOverview();
|
|
1153
1437
|
else if (view === 'sessions') viewSessions();
|
|
1154
1438
|
else if (view === 'files') viewFiles();
|
|
1155
1439
|
else if (view === 'timeline') viewTimeline();
|
|
@@ -1157,9 +1441,259 @@ $$('.nav-item').forEach(item => {
|
|
|
1157
1441
|
});
|
|
1158
1442
|
});
|
|
1159
1443
|
|
|
1444
|
+
// --- Command Palette (Cmd+K) ---
|
|
1445
|
+
window._cmdk = { open: false, index: 0, items: [], scrollY: 0 };
|
|
1446
|
+
|
|
1447
|
+
function closeCmdk() {
|
|
1448
|
+
const el = $('#cmdkOverlay');
|
|
1449
|
+
if (el) el.remove();
|
|
1450
|
+
|
|
1451
|
+
document.documentElement.classList.remove('cmdk-open');
|
|
1452
|
+
document.body.classList.remove('cmdk-open');
|
|
1453
|
+
|
|
1454
|
+
window._cmdk.open = false;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function execCmdkItem(i) {
|
|
1458
|
+
const item = window._cmdk.items[i];
|
|
1459
|
+
if (!item) return;
|
|
1460
|
+
closeCmdk();
|
|
1461
|
+
item.action();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function renderCmdkList() {
|
|
1465
|
+
const list = $('#cmdkList');
|
|
1466
|
+
if (!list) return;
|
|
1467
|
+
const { items, index } = window._cmdk;
|
|
1468
|
+
if (!items.length) {
|
|
1469
|
+
list.innerHTML = '<div class="cmdk-empty"><h4>No results</h4>Try a different search term</div>';
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
let html = '';
|
|
1474
|
+
let lastGroup = '';
|
|
1475
|
+
items.forEach((item, i) => {
|
|
1476
|
+
if (item.group !== lastGroup) {
|
|
1477
|
+
lastGroup = item.group;
|
|
1478
|
+
html += `<div class="cmdk-group-label">${escHtml(lastGroup)}</div>`;
|
|
1479
|
+
}
|
|
1480
|
+
html += `<button type="button" class="cmdk-item ${i === index ? 'active' : ''}" data-i="${i}">
|
|
1481
|
+
<div class="cmdk-item-body">
|
|
1482
|
+
<div class="cmdk-item-title">${escHtml(item.title)}</div>
|
|
1483
|
+
${item.sub ? `<div class="cmdk-item-sub">${escHtml(item.sub)}</div>` : ''}
|
|
1484
|
+
</div>
|
|
1485
|
+
${item.meta ? `<span class="cmdk-item-meta">${escHtml(item.meta)}</span>` : ''}
|
|
1486
|
+
</button>`;
|
|
1487
|
+
});
|
|
1488
|
+
list.innerHTML = html;
|
|
1489
|
+
|
|
1490
|
+
if (!list.dataset.bound) {
|
|
1491
|
+
list.addEventListener('click', (e) => {
|
|
1492
|
+
const btn = e.target.closest('.cmdk-item');
|
|
1493
|
+
if (!btn) return;
|
|
1494
|
+
e.preventDefault();
|
|
1495
|
+
const idx = Number(btn.dataset.i || -1);
|
|
1496
|
+
if (idx >= 0) execCmdkItem(idx);
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
list.addEventListener('pointermove', (e) => {
|
|
1500
|
+
if (e.pointerType && e.pointerType !== 'mouse') return;
|
|
1501
|
+
const btn = e.target.closest('.cmdk-item');
|
|
1502
|
+
if (!btn) return;
|
|
1503
|
+
const idx = Number(btn.dataset.i || -1);
|
|
1504
|
+
if (idx >= 0 && idx !== window._cmdk.index) {
|
|
1505
|
+
window._cmdk.index = idx;
|
|
1506
|
+
renderCmdkList();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
list.dataset.bound = '1';
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// scroll active into view
|
|
1514
|
+
const active = list.querySelector('.cmdk-item.active');
|
|
1515
|
+
if (active) active.scrollIntoView({ block: 'nearest' });
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function loadCmdkResults(query) {
|
|
1519
|
+
const list = $('#cmdkList');
|
|
1520
|
+
if (!list) return;
|
|
1521
|
+
const q = (query || '').trim();
|
|
1522
|
+
if (!q) { loadCmdkHome(); return; }
|
|
1523
|
+
|
|
1524
|
+
list.innerHTML = '<div class="cmdk-loading">Searching\u2026</div>';
|
|
1525
|
+
|
|
1526
|
+
const [searchRes, sessionsRes, filesRes] = await Promise.all([
|
|
1527
|
+
api(`/search?q=${encodeURIComponent(q)}&limit=6`),
|
|
1528
|
+
api('/sessions?limit=20'),
|
|
1529
|
+
api('/files?limit=20')
|
|
1530
|
+
]);
|
|
1531
|
+
|
|
1532
|
+
const items = [];
|
|
1533
|
+
|
|
1534
|
+
(sessionsRes.sessions || [])
|
|
1535
|
+
.filter(s => cleanSessionSummary(s.summary, s.initial_prompt).toLowerCase().includes(q.toLowerCase()))
|
|
1536
|
+
.slice(0, 3)
|
|
1537
|
+
.forEach(s => items.push({
|
|
1538
|
+
group: 'Sessions',
|
|
1539
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1540
|
+
sub: shortSessionId(s.id),
|
|
1541
|
+
meta: fmtTime(s.start_time),
|
|
1542
|
+
action: () => viewSession(s.id)
|
|
1543
|
+
}));
|
|
1544
|
+
|
|
1545
|
+
(filesRes.files || [])
|
|
1546
|
+
.filter(f => f.file_path.toLowerCase().includes(q.toLowerCase()))
|
|
1547
|
+
.slice(0, 2)
|
|
1548
|
+
.forEach(f => items.push({
|
|
1549
|
+
group: 'Files',
|
|
1550
|
+
title: f.file_path.split('/').pop(),
|
|
1551
|
+
sub: f.file_path,
|
|
1552
|
+
meta: `${f.touch_count} touches`,
|
|
1553
|
+
action: () => viewFileDetail(f.file_path)
|
|
1554
|
+
}));
|
|
1555
|
+
|
|
1556
|
+
(searchRes.results || []).slice(0, 4).forEach(r => items.push({
|
|
1557
|
+
group: 'Search Results',
|
|
1558
|
+
title: truncate(r.content || r.tool_args || r.tool_result || '', 66),
|
|
1559
|
+
sub: shortSessionId(r.session_id),
|
|
1560
|
+
meta: fmtTime(r.timestamp),
|
|
1561
|
+
action: () => viewSession(r.session_id)
|
|
1562
|
+
}));
|
|
1563
|
+
|
|
1564
|
+
window._cmdk.items = items;
|
|
1565
|
+
window._cmdk.index = 0;
|
|
1566
|
+
renderCmdkList();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
async function loadCmdkHome() {
|
|
1570
|
+
const list = $('#cmdkList');
|
|
1571
|
+
if (!list) return;
|
|
1572
|
+
list.innerHTML = '<div class="cmdk-loading">Loading\u2026</div>';
|
|
1573
|
+
|
|
1574
|
+
const items = [
|
|
1575
|
+
{ group: 'Go to', title: 'Sessions', sub: 'Browse all sessions', action: () => { setHash('sessions'); handleRoute(); } },
|
|
1576
|
+
{ group: 'Go to', title: 'Timeline', sub: 'Today and historical events', action: () => { setHash('timeline'); handleRoute(); } },
|
|
1577
|
+
{ group: 'Go to', title: 'Overview', sub: 'Dashboard summary', action: () => { setHash('overview'); handleRoute(); } },
|
|
1578
|
+
{ group: 'Go to', title: 'Files', sub: 'Touched files explorer', action: () => { setHash('files'); handleRoute(); } },
|
|
1579
|
+
{ group: 'Go to', title: 'Settings', sub: 'Configuration and maintenance', action: () => { setHash('stats'); handleRoute(); } },
|
|
1580
|
+
];
|
|
1581
|
+
|
|
1582
|
+
const sessionsRes = await api('/sessions?limit=20');
|
|
1583
|
+
|
|
1584
|
+
const sessionList = sessionsRes.sessions || [];
|
|
1585
|
+
const sessionMap = new Map(sessionList.map(s => [s.id, s]));
|
|
1586
|
+
|
|
1587
|
+
const recentIds = window._recentSessionIds || [];
|
|
1588
|
+
const missingIds = recentIds.filter(id => !sessionMap.has(id));
|
|
1589
|
+
if (missingIds.length) {
|
|
1590
|
+
const fetched = await Promise.all(missingIds.map(id => api(`/sessions/${id}`)));
|
|
1591
|
+
fetched.forEach(r => {
|
|
1592
|
+
if (r && !r._error && r.session) sessionMap.set(r.session.id, r.session);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
recentIds.forEach(id => {
|
|
1597
|
+
const s = sessionMap.get(id);
|
|
1598
|
+
if (!s) return;
|
|
1599
|
+
items.push({
|
|
1600
|
+
group: 'Recently Opened',
|
|
1601
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1602
|
+
sub: shortSessionId(s.id),
|
|
1603
|
+
meta: fmtTime(s.start_time),
|
|
1604
|
+
action: () => viewSession(s.id)
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
sessionList.forEach(s => items.push({
|
|
1609
|
+
group: 'Recent Sessions',
|
|
1610
|
+
title: truncate(cleanSessionSummary(s.summary, s.initial_prompt), 64),
|
|
1611
|
+
sub: shortSessionId(s.id),
|
|
1612
|
+
meta: fmtTime(s.start_time),
|
|
1613
|
+
action: () => viewSession(s.id)
|
|
1614
|
+
}));
|
|
1615
|
+
|
|
1616
|
+
window._cmdk.items = items;
|
|
1617
|
+
window._cmdk.index = 0;
|
|
1618
|
+
renderCmdkList();
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function openCmdk() {
|
|
1622
|
+
if ($('#cmdkOverlay')) { $('#cmdkInput')?.focus(); return; }
|
|
1623
|
+
|
|
1624
|
+
const overlay = document.createElement('div');
|
|
1625
|
+
overlay.className = 'cmdk-overlay';
|
|
1626
|
+
overlay.id = 'cmdkOverlay';
|
|
1627
|
+
overlay.innerHTML = `<div class="cmdk-dialog" role="dialog" aria-modal="true">
|
|
1628
|
+
<div class="cmdk-input-wrap">
|
|
1629
|
+
<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>
|
|
1630
|
+
<input id="cmdkInput" type="text" placeholder="Search sessions, files, or jump to a view" />
|
|
1631
|
+
<kbd>ESC</kbd>
|
|
1632
|
+
</div>
|
|
1633
|
+
<div class="cmdk-list" id="cmdkList"></div>
|
|
1634
|
+
</div>`;
|
|
1635
|
+
document.body.appendChild(overlay);
|
|
1636
|
+
window._cmdk.open = true;
|
|
1637
|
+
|
|
1638
|
+
const input = $('#cmdkInput');
|
|
1639
|
+
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
1640
|
+
|
|
1641
|
+
document.documentElement.classList.add('cmdk-open');
|
|
1642
|
+
document.body.classList.add('cmdk-open');
|
|
1643
|
+
|
|
1644
|
+
if (isMobile) {
|
|
1645
|
+
// iOS keyboard requires focus in the same user gesture call stack.
|
|
1646
|
+
try {
|
|
1647
|
+
input.focus({ preventScroll: true });
|
|
1648
|
+
} catch {
|
|
1649
|
+
input.focus();
|
|
1650
|
+
}
|
|
1651
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
1652
|
+
} else {
|
|
1653
|
+
input.focus();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
let debounce;
|
|
1657
|
+
input.addEventListener('input', () => {
|
|
1658
|
+
clearTimeout(debounce);
|
|
1659
|
+
debounce = setTimeout(() => loadCmdkResults(input.value), 150);
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) closeCmdk(); });
|
|
1663
|
+
|
|
1664
|
+
overlay.addEventListener('keydown', e => {
|
|
1665
|
+
if (e.key === 'Escape') { e.preventDefault(); closeCmdk(); return; }
|
|
1666
|
+
if (e.key === 'ArrowDown') {
|
|
1667
|
+
e.preventDefault();
|
|
1668
|
+
window._cmdk.index = Math.min(window._cmdk.index + 1, window._cmdk.items.length - 1);
|
|
1669
|
+
renderCmdkList();
|
|
1670
|
+
} else if (e.key === 'ArrowUp') {
|
|
1671
|
+
e.preventDefault();
|
|
1672
|
+
window._cmdk.index = Math.max(window._cmdk.index - 1, 0);
|
|
1673
|
+
renderCmdkList();
|
|
1674
|
+
} else if (e.key === 'Enter') {
|
|
1675
|
+
e.preventDefault();
|
|
1676
|
+
execCmdkItem(window._cmdk.index);
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
loadCmdkHome();
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1160
1683
|
initTheme();
|
|
1161
1684
|
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
|
|
1162
1685
|
document.getElementById('theme-toggle-mobile')?.addEventListener('click', toggleTheme);
|
|
1686
|
+
document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
|
|
1687
|
+
document.getElementById('mobile-search-btn')?.addEventListener('click', () => openCmdk());
|
|
1688
|
+
document.addEventListener('keydown', e => {
|
|
1689
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
1690
|
+
e.preventDefault();
|
|
1691
|
+
if (window._cmdk.open) closeCmdk();
|
|
1692
|
+
else openCmdk();
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (e.key === 'Escape' && window._cmdk.open) { e.preventDefault(); closeCmdk(); }
|
|
1696
|
+
});
|
|
1163
1697
|
handleRoute();
|
|
1164
1698
|
|
|
1165
1699
|
// Swipe right from left edge to go back
|