aisessions 1.1.0 → 1.1.2
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/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/router.js +4 -0
- package/src/session/detail.js +59 -0
- package/src/storage/backup.js +35 -9
- package/src/storage/trash.js +36 -11
- package/src/ui/css.js +47 -1
- package/src/ui/js.js +76 -11
- package/src/ui/render.js +14 -2
package/bin/cli.js
CHANGED
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getAllSessions, getInstalledAgents } from './agents/index.js'
|
|
|
3
3
|
import { getUsageData } from './usage/index.js'
|
|
4
4
|
import { listTrash, moveToTrash, restoreFromTrash, purgeTrash } from './storage/trash.js'
|
|
5
5
|
import { listBackups, createBackup, restoreBackup, deleteBackup } from './storage/backup.js'
|
|
6
|
+
import { getSessionDetail } from './session/detail.js'
|
|
6
7
|
|
|
7
8
|
function sendJson(res, data, status = 200) {
|
|
8
9
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
@@ -60,6 +61,9 @@ export async function router(req, res) {
|
|
|
60
61
|
const sessions = await getAllSessions()
|
|
61
62
|
return sendJson(res, sessions.map(({ date, ...rest }) => rest))
|
|
62
63
|
}
|
|
64
|
+
if (method === 'GET' && pathname === '/api/session') {
|
|
65
|
+
return sendJson(res, await getSessionDetail(query.path || ''))
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
// ── Usage (supports ?agent=claude) ───────────────────────────────────────
|
|
65
69
|
if (method === 'GET' && pathname === '/api/usage') {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { parseJsonl, readLimited } from '../agents/utils.js'
|
|
3
|
+
|
|
4
|
+
const MAX_DETAIL_BYTES = 2_000_000
|
|
5
|
+
|
|
6
|
+
function cleanText(v) {
|
|
7
|
+
return String(v || '').replace(/\s+/g, ' ').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractText(v) {
|
|
11
|
+
if (typeof v === 'string') return [v]
|
|
12
|
+
if (Array.isArray(v)) return v.flatMap(extractText)
|
|
13
|
+
if (v && typeof v === 'object') {
|
|
14
|
+
const out = []
|
|
15
|
+
if (typeof v.text === 'string') out.push(v.text)
|
|
16
|
+
if (v.content) out.push(...extractText(v.content))
|
|
17
|
+
if (v.message) out.push(...extractText(v.message))
|
|
18
|
+
if (v.parts) out.push(...extractText(v.parts))
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function roleOf(obj) {
|
|
25
|
+
const role = cleanText(obj?.role || obj?.message?.role || obj?.payload?.role).toLowerCase()
|
|
26
|
+
const type = cleanText(obj?.type || obj?.item_type).toLowerCase()
|
|
27
|
+
if (role) return role
|
|
28
|
+
if (type.includes('user')) return 'user'
|
|
29
|
+
if (type.includes('assistant') || type.includes('model')) return 'assistant'
|
|
30
|
+
if (type.includes('system')) return 'system'
|
|
31
|
+
return type || 'entry'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function messagesFromObjs(objs) {
|
|
35
|
+
return objs.map((obj, idx) => {
|
|
36
|
+
const text = extractText(obj).map(cleanText).filter(Boolean).join('\n\n')
|
|
37
|
+
return {
|
|
38
|
+
index: idx + 1,
|
|
39
|
+
role: roleOf(obj),
|
|
40
|
+
text: text || JSON.stringify(obj).slice(0, 4000),
|
|
41
|
+
}
|
|
42
|
+
}).filter(m => m.text)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getSessionDetail(path) {
|
|
46
|
+
if (!path || !existsSync(path)) return { ok: false, error: 'session file not found' }
|
|
47
|
+
const st = statSync(path)
|
|
48
|
+
if (!st.isFile()) return { ok: false, error: 'session path is not a file' }
|
|
49
|
+
const text = readLimited(path, MAX_DETAIL_BYTES)
|
|
50
|
+
const objs = parseJsonl(text)
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
path,
|
|
54
|
+
size: st.size,
|
|
55
|
+
truncated: st.size > MAX_DETAIL_BYTES,
|
|
56
|
+
messages: messagesFromObjs(objs),
|
|
57
|
+
raw: text,
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/storage/backup.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
-
import { join, basename } from 'node:path'
|
|
3
|
+
import { dirname, join, basename } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const HOME = homedir()
|
|
6
6
|
const BACKUP_DIR = join(HOME, '.aisessions', 'backups')
|
|
7
7
|
|
|
8
8
|
function ensureBackup() { mkdirSync(BACKUP_DIR, { recursive: true }) }
|
|
9
9
|
|
|
10
|
+
function safePart(v, fallback = 'item') {
|
|
11
|
+
return String(v || fallback).replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80) || fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findMetaFiles(dir) {
|
|
15
|
+
const out = []
|
|
16
|
+
try {
|
|
17
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const full = join(dir, e.name)
|
|
19
|
+
if (e.isDirectory()) out.push(...findMetaFiles(full))
|
|
20
|
+
else if (e.name.endsWith('.meta.json') || e.name === 'meta.json') out.push(full)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeStoredItem(metaPath, payloadPath) {
|
|
27
|
+
if (basename(metaPath) === 'meta.json') {
|
|
28
|
+
rmSync(dirname(metaPath), { recursive: true, force: true })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (payloadPath && existsSync(payloadPath)) rmSync(payloadPath, { recursive: true, force: true })
|
|
32
|
+
if (existsSync(metaPath)) rmSync(metaPath, { force: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
function normaliseItems(raw) {
|
|
11
36
|
return (Array.isArray(raw) ? raw : [raw]).map(i =>
|
|
12
37
|
typeof i === 'string' ? { path: i } : i
|
|
@@ -17,9 +42,7 @@ export async function listBackups(agent) {
|
|
|
17
42
|
ensureBackup()
|
|
18
43
|
const items = []
|
|
19
44
|
try {
|
|
20
|
-
for (const
|
|
21
|
-
if (!f.endsWith('.meta.json')) continue
|
|
22
|
-
const mp = join(BACKUP_DIR, f)
|
|
45
|
+
for (const mp of findMetaFiles(BACKUP_DIR)) {
|
|
23
46
|
try {
|
|
24
47
|
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
25
48
|
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
@@ -39,10 +62,13 @@ export async function createBackup(rawItems, note = '') {
|
|
|
39
62
|
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
40
63
|
const ts = Date.now()
|
|
41
64
|
const name = basename(p)
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
65
|
+
const agentId = safePart(item.agent || 'unknown')
|
|
66
|
+
const backupName = ts + '_' + safePart(item.title || name)
|
|
67
|
+
const itemDir = join(BACKUP_DIR, agentId, backupName)
|
|
68
|
+
const backupPath = join(itemDir, 'payload')
|
|
69
|
+
const metaPath = join(itemDir, 'meta.json')
|
|
45
70
|
const st = statSync(p)
|
|
71
|
+
mkdirSync(itemDir, { recursive: true })
|
|
46
72
|
if (st.isDirectory()) cpSync(p, backupPath, { recursive: true })
|
|
47
73
|
else cpSync(p, backupPath)
|
|
48
74
|
const meta = {
|
|
@@ -72,6 +98,7 @@ export async function restoreBackup(metaPath) {
|
|
|
72
98
|
? meta.originalPath.replace(/(\.[^.]+)?$/, '_restored_' + Date.now() + '$1')
|
|
73
99
|
: meta.originalPath
|
|
74
100
|
const st = statSync(meta.backupPath)
|
|
101
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
75
102
|
if (st.isDirectory()) cpSync(meta.backupPath, dest, { recursive: true })
|
|
76
103
|
else cpSync(meta.backupPath, dest)
|
|
77
104
|
return { ok: true, restoredTo: dest }
|
|
@@ -84,8 +111,7 @@ export async function deleteBackup(metaPaths) {
|
|
|
84
111
|
try {
|
|
85
112
|
let backupPath = mp.replace(/\.meta\.json$/, '')
|
|
86
113
|
try { const m = JSON.parse(readFileSync(mp, 'utf8')); backupPath = m.backupPath || backupPath } catch {}
|
|
87
|
-
|
|
88
|
-
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
114
|
+
removeStoredItem(mp, backupPath)
|
|
89
115
|
results.push({ metaPath: mp, ok: true })
|
|
90
116
|
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
117
|
}
|
package/src/storage/trash.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const HOME = homedir()
|
|
6
6
|
const TRASH_DIR = join(HOME, '.aisessions', 'trash')
|
|
7
7
|
|
|
8
8
|
function ensureTrash() { mkdirSync(TRASH_DIR, { recursive: true }) }
|
|
9
9
|
|
|
10
|
+
function safePart(v, fallback = 'item') {
|
|
11
|
+
return String(v || fallback).replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80) || fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findMetaFiles(dir) {
|
|
15
|
+
const out = []
|
|
16
|
+
try {
|
|
17
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const full = join(dir, e.name)
|
|
19
|
+
if (e.isDirectory()) out.push(...findMetaFiles(full))
|
|
20
|
+
else if (e.name.endsWith('.meta.json') || e.name === 'meta.json') out.push(full)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeStoredItem(metaPath, payloadPath) {
|
|
27
|
+
if (basename(metaPath) === 'meta.json') {
|
|
28
|
+
rmSync(dirname(metaPath), { recursive: true, force: true })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (payloadPath && existsSync(payloadPath)) rmSync(payloadPath, { recursive: true, force: true })
|
|
32
|
+
if (existsSync(metaPath)) rmSync(metaPath, { force: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
// items: array of { path, agent?, agentLabel?, project?, title? }
|
|
11
36
|
// OR plain strings (backwards compat)
|
|
12
37
|
function normaliseItems(raw) {
|
|
@@ -19,9 +44,7 @@ export async function listTrash(agent) {
|
|
|
19
44
|
ensureTrash()
|
|
20
45
|
const items = []
|
|
21
46
|
try {
|
|
22
|
-
for (const
|
|
23
|
-
if (!f.endsWith('.meta.json')) continue
|
|
24
|
-
const mp = join(TRASH_DIR, f)
|
|
47
|
+
for (const mp of findMetaFiles(TRASH_DIR)) {
|
|
25
48
|
try {
|
|
26
49
|
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
27
50
|
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
@@ -40,10 +63,13 @@ export async function moveToTrash(rawItems) {
|
|
|
40
63
|
try {
|
|
41
64
|
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
42
65
|
const ts = Date.now()
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
66
|
+
const agentId = safePart(item.agent || 'unknown')
|
|
67
|
+
const trashName = ts + '_' + safePart(item.title || p.split('/').pop())
|
|
68
|
+
const itemDir = join(TRASH_DIR, agentId, trashName)
|
|
69
|
+
const trashPath = join(itemDir, 'payload')
|
|
70
|
+
const metaPath = join(itemDir, 'meta.json')
|
|
46
71
|
const st = statSync(p)
|
|
72
|
+
mkdirSync(itemDir, { recursive: true })
|
|
47
73
|
if (st.isDirectory()) cpSync(p, trashPath, { recursive: true })
|
|
48
74
|
else cpSync(p, trashPath)
|
|
49
75
|
rmSync(p, { recursive: true, force: true })
|
|
@@ -70,10 +96,10 @@ export async function restoreFromTrash(metaPath) {
|
|
|
70
96
|
if (!existsSync(meta.trashPath)) return { ok: false, error: 'trash file missing' }
|
|
71
97
|
if (existsSync(meta.originalPath)) return { ok: false, error: 'destination already exists' }
|
|
72
98
|
const st = statSync(meta.trashPath)
|
|
99
|
+
mkdirSync(dirname(meta.originalPath), { recursive: true })
|
|
73
100
|
if (st.isDirectory()) cpSync(meta.trashPath, meta.originalPath, { recursive: true })
|
|
74
101
|
else cpSync(meta.trashPath, meta.originalPath)
|
|
75
|
-
|
|
76
|
-
rmSync(metaPath, { force: true })
|
|
102
|
+
removeStoredItem(metaPath, meta.trashPath)
|
|
77
103
|
return { ok: true, restoredTo: meta.originalPath }
|
|
78
104
|
} catch (e) { return { ok: false, error: e.message } }
|
|
79
105
|
}
|
|
@@ -84,8 +110,7 @@ export async function purgeTrash(metaPaths) {
|
|
|
84
110
|
try {
|
|
85
111
|
let trashPath = mp.replace(/\.meta\.json$/, '')
|
|
86
112
|
try { const m = JSON.parse(readFileSync(mp, 'utf8')); trashPath = m.trashPath || trashPath } catch {}
|
|
87
|
-
|
|
88
|
-
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
113
|
+
removeStoredItem(mp, trashPath)
|
|
89
114
|
results.push({ metaPath: mp, ok: true })
|
|
90
115
|
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
116
|
}
|
package/src/ui/css.js
CHANGED
|
@@ -278,7 +278,7 @@ html[data-theme="light"] .credit-pkg{color:rgba(24,24,20,.32)}
|
|
|
278
278
|
|
|
279
279
|
.tbl-hdr,.tbl-row{
|
|
280
280
|
display:grid;
|
|
281
|
-
grid-template-columns:40px 80px 1fr 80px 80px 150px
|
|
281
|
+
grid-template-columns:40px 80px 1fr 80px 80px 150px 126px;
|
|
282
282
|
align-items:center;gap:0 10px;padding:0 16px;
|
|
283
283
|
}
|
|
284
284
|
.tbl-hdr{
|
|
@@ -440,6 +440,52 @@ html[data-theme="light"] .credit-pkg{color:rgba(24,24,20,.32)}
|
|
|
440
440
|
#toast.ok{border-color:rgba(51,204,51,.6);color:var(--success)}
|
|
441
441
|
#toast.err{border-color:rgba(255,51,51,.6);color:var(--danger)}
|
|
442
442
|
|
|
443
|
+
/* ── MODAL / SESSION VIEWER ────────────────────────────────────────────────── */
|
|
444
|
+
.modal{
|
|
445
|
+
position:fixed;inset:0;z-index:100;
|
|
446
|
+
display:flex;align-items:center;justify-content:center;
|
|
447
|
+
background:rgba(0,0,0,.72);
|
|
448
|
+
padding:28px;
|
|
449
|
+
}
|
|
450
|
+
.modal[hidden]{display:none}
|
|
451
|
+
.modal-box{
|
|
452
|
+
width:min(980px,calc(100vw - 56px));
|
|
453
|
+
height:min(760px,calc(100vh - 56px));
|
|
454
|
+
display:flex;flex-direction:column;
|
|
455
|
+
border:2px solid var(--border2);
|
|
456
|
+
background:var(--surf);
|
|
457
|
+
}
|
|
458
|
+
.modal-head{
|
|
459
|
+
display:flex;align-items:center;justify-content:space-between;gap:16px;
|
|
460
|
+
padding:14px 16px;border-bottom:1px solid var(--border);
|
|
461
|
+
flex-shrink:0;
|
|
462
|
+
}
|
|
463
|
+
.modal-title{font-family:var(--logo);font-size:10px;color:var(--accent);letter-spacing:1px}
|
|
464
|
+
.modal-sub{font-size:15px;color:var(--muted);margin-top:5px;word-break:break-all}
|
|
465
|
+
.modal-body{flex:1;overflow:auto;padding:14px;background:var(--bg)}
|
|
466
|
+
.chat-msg{
|
|
467
|
+
border:1px solid var(--border);
|
|
468
|
+
background:var(--surf);
|
|
469
|
+
margin-bottom:10px;
|
|
470
|
+
}
|
|
471
|
+
.chat-role{
|
|
472
|
+
padding:7px 10px;border-bottom:1px solid var(--border);
|
|
473
|
+
color:var(--muted);font-size:14px;letter-spacing:1px;text-transform:uppercase;
|
|
474
|
+
}
|
|
475
|
+
.chat-text{
|
|
476
|
+
padding:10px;
|
|
477
|
+
white-space:pre-wrap;
|
|
478
|
+
word-break:break-word;
|
|
479
|
+
font-size:17px;
|
|
480
|
+
line-height:1.35;
|
|
481
|
+
}
|
|
482
|
+
.chat-raw{
|
|
483
|
+
white-space:pre-wrap;
|
|
484
|
+
word-break:break-word;
|
|
485
|
+
font-size:15px;
|
|
486
|
+
line-height:1.35;
|
|
487
|
+
}
|
|
488
|
+
|
|
443
489
|
/* ── SCROLLBAR ───────────────────────────────────────────────────────────────── */
|
|
444
490
|
::-webkit-scrollbar{width:8px;height:8px}
|
|
445
491
|
::-webkit-scrollbar-track{background:var(--bg)}
|
package/src/ui/js.js
CHANGED
|
@@ -65,7 +65,7 @@ document.addEventListener('click', function (e) {
|
|
|
65
65
|
var nav = btn.dataset.nav || '';
|
|
66
66
|
if (a === 'filter-agent') { setAgentFilter(ag); return; }
|
|
67
67
|
if (a === 'switch-tab') { switchTab(nav); return; }
|
|
68
|
-
if (a === 'agent-tab') {
|
|
68
|
+
if (a === 'agent-tab') { switchAgentTab(ag, nav); return; }
|
|
69
69
|
if (a === 'switch-tab-global') { setAgentFilter('all'); switchTab(nav); return; }
|
|
70
70
|
if (a === 'toggle-theme') { toggleTheme(); return; }
|
|
71
71
|
if (a === 'trash-one') { trashOne(p); return; }
|
|
@@ -74,6 +74,8 @@ document.addEventListener('click', function (e) {
|
|
|
74
74
|
if (a === 'purge-trash') { purgeTrash(meta); return; }
|
|
75
75
|
if (a === 'restore-backup') { restoreBackup(meta); return; }
|
|
76
76
|
if (a === 'delete-backup') { deleteBackup(meta); return; }
|
|
77
|
+
if (a === 'view-session') { viewSession(p); return; }
|
|
78
|
+
if (a === 'close-modal') { closeModal(); return; }
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
79
81
|
var row = e.target.closest('.tbl-row');
|
|
@@ -156,10 +158,10 @@ function buildSidebar(agents, sessions) {
|
|
|
156
158
|
'<span class="agent-dot" style="background:' + esc(a.color) + '"></span>' +
|
|
157
159
|
esc(a.label) + '<span class="sb-count">' + (counts[a.id] || 0) + '</span></button>' +
|
|
158
160
|
'<div class="sb-sub" id="sub-' + aid + '">' +
|
|
159
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="sessions">[=] SESSIONS</button>' +
|
|
160
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="usage">[$] USAGE</button>' +
|
|
161
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="trash">[T] TRASH</button>' +
|
|
162
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="backups">[B] BACKUPS</button>' +
|
|
161
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="sessions">[=] SESSIONS</button>' +
|
|
162
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="usage">[$] USAGE</button>' +
|
|
163
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="trash">[T] TRASH</button>' +
|
|
164
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="backups">[B] BACKUPS</button>' +
|
|
163
165
|
'</div>';
|
|
164
166
|
}).join('');
|
|
165
167
|
}
|
|
@@ -187,6 +189,18 @@ function setAgentFilter(id) {
|
|
|
187
189
|
switchTab('sessions');
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
function switchAgentTab(id, tab) {
|
|
193
|
+
if (id && id !== agentFilter) {
|
|
194
|
+
agentFilter = id; PAGE = 0;
|
|
195
|
+
setSidebarActive(id);
|
|
196
|
+
document.querySelectorAll('.sb-sub').forEach(function(el) {
|
|
197
|
+
el.classList.toggle('show', el.id === 'sub-' + id);
|
|
198
|
+
});
|
|
199
|
+
updateContextLabel();
|
|
200
|
+
}
|
|
201
|
+
switchTab(tab || 'sessions');
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
// ── CONTEXT LABEL (shows current filter scope in each panel) ──────────────────
|
|
191
205
|
function updateContextLabel() {
|
|
192
206
|
var label = agentFilter === 'all' ? 'ALL AGENTS' : agentLabel(agentFilter);
|
|
@@ -284,6 +298,7 @@ function rowHtml(s) {
|
|
|
284
298
|
'<span class="col-size">' + esc(s.sizeLabel || '') + '</span>' +
|
|
285
299
|
'<span class="col-date">' + esc(s.dateLabel || '') + '</span>' +
|
|
286
300
|
'<span class="row-actions">' +
|
|
301
|
+
'<button class="act-btn" data-action="view-session" data-path="' + p + '" title="View chat">VIEW</button>' +
|
|
287
302
|
'<button class="act-btn" data-action="backup-one" data-path="' + p + '" title="Backup">BAK</button>' +
|
|
288
303
|
'<button class="act-btn del" data-action="trash-one" data-path="' + p + '" title="Trash">DEL</button>' +
|
|
289
304
|
'</span></div>';
|
|
@@ -309,12 +324,15 @@ wireBtn('btn-trash', async function() {
|
|
|
309
324
|
var ok = res.filter(function(r) { return r.ok; }).map(function(r) { return r.path; });
|
|
310
325
|
ok.forEach(function(p) { ALL = ALL.filter(function(s) { return s.path !== p; }); selected.delete(p); });
|
|
311
326
|
applyFilter(); toast(ok.length + ' MOVED TO TRASH', 'ok');
|
|
327
|
+
if (ok.length) switchTab('trash');
|
|
312
328
|
});
|
|
313
329
|
wireBtn('btn-backup', async function() {
|
|
314
330
|
if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
|
|
315
331
|
var items = buildItems(Array.from(selected));
|
|
316
332
|
var res = await post('/api/backup/create', { items: items, note: '' });
|
|
317
|
-
|
|
333
|
+
var ok = res.filter(function(r) { return r.ok; }).length;
|
|
334
|
+
toast(ok + ' BACKUP(S) CREATED', 'ok');
|
|
335
|
+
if (ok) switchTab('backups');
|
|
318
336
|
});
|
|
319
337
|
|
|
320
338
|
// Enrich paths with session metadata for storage
|
|
@@ -332,12 +350,13 @@ function switchTab(tab) {
|
|
|
332
350
|
document.querySelectorAll('.panel').forEach(function(p) { p.classList.toggle('active', p.id === tab + '-panel'); });
|
|
333
351
|
// Mark active sub-item (agent-specific nav)
|
|
334
352
|
document.querySelectorAll('.sb-sub-item').forEach(function(el) {
|
|
335
|
-
el.classList.toggle('active', el.dataset.nav === tab);
|
|
353
|
+
el.classList.toggle('active', el.dataset.nav === tab && el.dataset.agent === agentFilter);
|
|
336
354
|
});
|
|
337
355
|
// Mark global tools active only when agentFilter is all
|
|
338
356
|
document.querySelectorAll('.sb-item[data-action="switch-tab-global"]').forEach(function(el) {
|
|
339
357
|
el.classList.toggle('active', el.dataset.nav === tab && agentFilter === 'all');
|
|
340
358
|
});
|
|
359
|
+
if (tab === 'sessions') applyFilter();
|
|
341
360
|
if (tab === 'usage') loadUsage(false);
|
|
342
361
|
if (tab === 'trash') loadTrash(false);
|
|
343
362
|
if (tab === 'backups') loadBackups(false);
|
|
@@ -353,17 +372,55 @@ async function trashOne(path) {
|
|
|
353
372
|
var res = await post('/api/trash/move', { items: items });
|
|
354
373
|
if (res[0] && res[0].ok) {
|
|
355
374
|
ALL = ALL.filter(function(s) { return s.path !== path; }); selected.delete(path);
|
|
356
|
-
applyFilter(); toast('MOVED TO TRASH', 'ok');
|
|
375
|
+
applyFilter(); toast('MOVED TO TRASH', 'ok'); switchTab('trash');
|
|
357
376
|
} else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
358
377
|
}
|
|
359
378
|
async function backupOne(path) {
|
|
360
379
|
if (!path) return;
|
|
361
380
|
var items = buildItems([path]);
|
|
362
381
|
var res = await post('/api/backup/create', { items: items, note: '' });
|
|
363
|
-
if (res[0] && res[0].ok) toast('BACKUP CREATED', 'ok');
|
|
382
|
+
if (res[0] && res[0].ok) { toast('BACKUP CREATED', 'ok'); switchTab('backups'); }
|
|
364
383
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
365
384
|
}
|
|
366
385
|
|
|
386
|
+
// ── SESSION VIEWER ───────────────────────────────────────────────────────────
|
|
387
|
+
function closeModal() {
|
|
388
|
+
var m = $('modal');
|
|
389
|
+
if (m) m.hidden = true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function viewSession(path) {
|
|
393
|
+
if (!path) return;
|
|
394
|
+
var modal = $('modal'), title = $('modal-title'), sub = $('modal-sub'), body = $('modal-body');
|
|
395
|
+
if (!modal || !body) return;
|
|
396
|
+
modal.hidden = false;
|
|
397
|
+
if (title) title.textContent = 'LOADING SESSION';
|
|
398
|
+
if (sub) sub.textContent = path;
|
|
399
|
+
body.innerHTML = '<div class="loading"><span class="loading-dots">LOADING CHAT</span></div>';
|
|
400
|
+
try {
|
|
401
|
+
var data = await fetch('/api/session?path=' + encodeURIComponent(path)).then(function(r) { return r.json(); });
|
|
402
|
+
if (!data.ok) {
|
|
403
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(data.error || 'Could not load session') + '</div></div>';
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (title) title.textContent = 'SESSION HISTORY';
|
|
407
|
+
if (sub) sub.textContent = data.path + (data.truncated ? ' / preview truncated' : '');
|
|
408
|
+
var messages = data.messages || [];
|
|
409
|
+
if (messages.length) {
|
|
410
|
+
body.innerHTML = messages.slice(0, 200).map(function(m) {
|
|
411
|
+
return '<div class="chat-msg">' +
|
|
412
|
+
'<div class="chat-role">' + esc(m.index + ' / ' + m.role) + '</div>' +
|
|
413
|
+
'<div class="chat-text">' + esc(m.text) + '</div>' +
|
|
414
|
+
'</div>';
|
|
415
|
+
}).join('') + (messages.length > 200 ? '<div class="empty"><div>Showing first 200 entries</div></div>' : '');
|
|
416
|
+
} else {
|
|
417
|
+
body.innerHTML = '<pre class="chat-raw">' + esc(data.raw || '') + '</pre>';
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
367
424
|
// ── USAGE TAB ─────────────────────────────────────────────────────────────────
|
|
368
425
|
async function loadUsage(force) {
|
|
369
426
|
var key = agentFilter;
|
|
@@ -496,7 +553,7 @@ async function loadTrash(force) {
|
|
|
496
553
|
|
|
497
554
|
async function restoreTrash(mp) {
|
|
498
555
|
var res = await post('/api/trash/restore', { metaPath: mp });
|
|
499
|
-
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); }
|
|
556
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); reloadSessions(); }
|
|
500
557
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
501
558
|
}
|
|
502
559
|
async function purgeTrash(mp) {
|
|
@@ -539,7 +596,7 @@ async function loadBackups(force) {
|
|
|
539
596
|
|
|
540
597
|
async function restoreBackup(mp) {
|
|
541
598
|
var res = await post('/api/backup/restore', { metaPath: mp });
|
|
542
|
-
if (res.ok) toast('RESTORED TO ' + res.restoredTo, 'ok');
|
|
599
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); reloadSessions(); }
|
|
543
600
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
544
601
|
}
|
|
545
602
|
async function deleteBackup(mp) {
|
|
@@ -549,6 +606,14 @@ async function deleteBackup(mp) {
|
|
|
549
606
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
550
607
|
}
|
|
551
608
|
|
|
609
|
+
async function reloadSessions() {
|
|
610
|
+
try {
|
|
611
|
+
var sessions = await fetch('/api/sessions').then(function(r) { return r.json(); });
|
|
612
|
+
ALL = sessions;
|
|
613
|
+
applyFilter();
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
|
|
552
617
|
// ── BOOT ──────────────────────────────────────────────────────────────────────
|
|
553
618
|
init().catch(function(e) { console.error('[AM] fatal:', e); });
|
|
554
619
|
|
package/src/ui/render.js
CHANGED
|
@@ -65,7 +65,7 @@ export function renderPage() {
|
|
|
65
65
|
<span>X.com</span>
|
|
66
66
|
</a>
|
|
67
67
|
</div>
|
|
68
|
-
<div class="credit-pkg">aisessions v1.1</div>
|
|
68
|
+
<div class="credit-pkg">aisessions v1.1.2</div>
|
|
69
69
|
</div>
|
|
70
70
|
</nav>
|
|
71
71
|
|
|
@@ -125,7 +125,7 @@ export function renderPage() {
|
|
|
125
125
|
|
|
126
126
|
<!-- FOOTER -->
|
|
127
127
|
<footer id="foot">
|
|
128
|
-
<span>AISESSIONS v1.1</span>
|
|
128
|
+
<span>AISESSIONS v1.1.2</span>
|
|
129
129
|
<span> | </span>
|
|
130
130
|
<span id="foot-info">LOADING...</span>
|
|
131
131
|
</footer>
|
|
@@ -133,6 +133,18 @@ export function renderPage() {
|
|
|
133
133
|
</div><!-- /shell -->
|
|
134
134
|
|
|
135
135
|
<div id="toast"></div>
|
|
136
|
+
<div id="modal" class="modal" hidden>
|
|
137
|
+
<div class="modal-box">
|
|
138
|
+
<div class="modal-head">
|
|
139
|
+
<div>
|
|
140
|
+
<div id="modal-title" class="modal-title">SESSION</div>
|
|
141
|
+
<div id="modal-sub" class="modal-sub"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<button class="btn" data-action="close-modal">CLOSE</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div id="modal-body" class="modal-body"></div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
136
148
|
<script>${PAGE_JS}</script>
|
|
137
149
|
</body>
|
|
138
150
|
</html>`
|