aisessions 1.1.0 → 1.1.4
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 +47 -2
- package/package.json +3 -2
- 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 +48 -2
- package/src/ui/js.js +127 -26
- package/src/ui/render.js +19 -7
package/bin/cli.js
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
import { createServer } from '../src/server.js'
|
|
3
3
|
import { parseArgs } from 'node:util'
|
|
4
4
|
import { exec } from 'node:child_process'
|
|
5
|
+
import pc from 'picocolors'
|
|
5
6
|
|
|
6
|
-
const VERSION = '1.1.
|
|
7
|
+
const VERSION = '1.1.4'
|
|
8
|
+
const DEV_HANDLE = 's41r4j'
|
|
9
|
+
const GITHUB_URL = 'https://github.com/s41r4j/aisessions'
|
|
10
|
+
const ISSUE_URL = 'https://github.com/s41r4j/aisessions/issues'
|
|
11
|
+
const X_URL = 'https://x.com/s41r4j'
|
|
7
12
|
|
|
8
13
|
const { values } = parseArgs({
|
|
9
14
|
options: {
|
|
@@ -44,9 +49,49 @@ if (values.help) {
|
|
|
44
49
|
const PORT = parseInt(values.port, 10) || 7878
|
|
45
50
|
const server = createServer()
|
|
46
51
|
|
|
52
|
+
function printBanner(url) {
|
|
53
|
+
const art = [
|
|
54
|
+
' _ _ ',
|
|
55
|
+
' __ _ (_) ___ ___ ___ ___ ___(_) ___ _ __ ___ ',
|
|
56
|
+
' / _` || |/ __|/ _ \\/ __|/ _ \\/ __| |/ _ \\| `_ \\/ __|',
|
|
57
|
+
'| (_| || |\\__ \\ __/\\__ \\ __/\\__ \\ | (_) | | | \\__ \\',
|
|
58
|
+
' \\__,_||_||___/\\___||___/\\___||___/_|\\___/|_| |_|___/',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
console.log('')
|
|
62
|
+
art.forEach(line => console.log(pc.cyan(line)))
|
|
63
|
+
console.log('')
|
|
64
|
+
console.log(`${pc.bold(' aisessions')} ${pc.dim('v' + VERSION)} ${pc.green('ready')}`)
|
|
65
|
+
console.log(` ${pc.dim('Local UI:')} ${pc.underline(url)}`)
|
|
66
|
+
console.log(` ${pc.dim('Data store:')} ~/.aisessions/trash ~/.aisessions/backups`)
|
|
67
|
+
console.log(` ${pc.dim('Developer:')} @${DEV_HANDLE}`)
|
|
68
|
+
console.log(` ${pc.dim('GitHub:')} ${GITHUB_URL}`)
|
|
69
|
+
console.log(` ${pc.dim('X:')} ${X_URL}`)
|
|
70
|
+
console.log(` ${pc.dim('Report issue:')} ${ISSUE_URL}`)
|
|
71
|
+
console.log('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function startStatusLine(url) {
|
|
75
|
+
if (!process.stdout.isTTY) return
|
|
76
|
+
const frames = ['-', '\\\\', '|', '/']
|
|
77
|
+
let i = 0
|
|
78
|
+
const timer = setInterval(() => {
|
|
79
|
+
const frame = frames[i++ % frames.length]
|
|
80
|
+
process.stdout.write(`\\r ${pc.cyan(frame)} ${pc.dim('running at')} ${url} ${pc.dim('Ctrl+C to stop')} `)
|
|
81
|
+
}, 1200)
|
|
82
|
+
process.once('SIGINT', () => {
|
|
83
|
+
clearInterval(timer)
|
|
84
|
+
process.stdout.clearLine?.(0)
|
|
85
|
+
process.stdout.cursorTo?.(0)
|
|
86
|
+
console.log(pc.dim(' stopped aisessions'))
|
|
87
|
+
process.exit(0)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
47
91
|
server.listen(PORT, '127.0.0.1', () => {
|
|
48
92
|
const url = `http://127.0.0.1:${PORT}`
|
|
49
|
-
|
|
93
|
+
printBanner(url)
|
|
94
|
+
startStatusLine(url)
|
|
50
95
|
|
|
51
96
|
if (!values['no-open']) {
|
|
52
97
|
const cmd = process.platform === 'darwin' ? `open "${url}"` :
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aisessions",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Browse, manage and analyze your AI coding agent sessions — Claude Code, Codex, and more",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aisessions": "bin/cli.js"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"ccusage": "^20.0.6"
|
|
30
|
+
"ccusage": "^20.0.6",
|
|
31
|
+
"picocolors": "^1.1.1"
|
|
31
32
|
}
|
|
32
33
|
}
|
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{
|
|
@@ -373,7 +373,7 @@ html[data-theme="light"] .credit-pkg{color:rgba(24,24,20,.32)}
|
|
|
373
373
|
.ctx-banner .ctx-label{color:var(--accent);letter-spacing:1px}
|
|
374
374
|
|
|
375
375
|
/* ── USAGE PANEL ─────────────────────────────────────────────────────────────── */
|
|
376
|
-
#usage-panel{overflow-y:auto;
|
|
376
|
+
#usage-panel{overflow-y:auto;padding:0}
|
|
377
377
|
|
|
378
378
|
.stat-grid{
|
|
379
379
|
display:grid;
|
|
@@ -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,13 @@ 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; }
|
|
79
|
+
if (a === 'select-all') { selectAllSessions(); return; }
|
|
80
|
+
if (a === 'select-none') { selectNoSessions(); return; }
|
|
81
|
+
if (a === 'toggle-group') { toggleGrouping(); return; }
|
|
82
|
+
if (a === 'trash-selected') { trashSelected(); return; }
|
|
83
|
+
if (a === 'backup-selected'){ backupSelected(); return; }
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
86
|
var row = e.target.closest('.tbl-row');
|
|
@@ -156,10 +163,10 @@ function buildSidebar(agents, sessions) {
|
|
|
156
163
|
'<span class="agent-dot" style="background:' + esc(a.color) + '"></span>' +
|
|
157
164
|
esc(a.label) + '<span class="sb-count">' + (counts[a.id] || 0) + '</span></button>' +
|
|
158
165
|
'<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>' +
|
|
166
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="sessions">[=] SESSIONS</button>' +
|
|
167
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="usage">[$] USAGE</button>' +
|
|
168
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="trash">[T] TRASH</button>' +
|
|
169
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="backups">[B] BACKUPS</button>' +
|
|
163
170
|
'</div>';
|
|
164
171
|
}).join('');
|
|
165
172
|
}
|
|
@@ -187,6 +194,18 @@ function setAgentFilter(id) {
|
|
|
187
194
|
switchTab('sessions');
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
function switchAgentTab(id, tab) {
|
|
198
|
+
if (id && id !== agentFilter) {
|
|
199
|
+
agentFilter = id; PAGE = 0;
|
|
200
|
+
setSidebarActive(id);
|
|
201
|
+
document.querySelectorAll('.sb-sub').forEach(function(el) {
|
|
202
|
+
el.classList.toggle('show', el.id === 'sub-' + id);
|
|
203
|
+
});
|
|
204
|
+
updateContextLabel();
|
|
205
|
+
}
|
|
206
|
+
switchTab(tab || 'sessions');
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
// ── CONTEXT LABEL (shows current filter scope in each panel) ──────────────────
|
|
191
210
|
function updateContextLabel() {
|
|
192
211
|
var label = agentFilter === 'all' ? 'ALL AGENTS' : agentLabel(agentFilter);
|
|
@@ -284,38 +303,67 @@ function rowHtml(s) {
|
|
|
284
303
|
'<span class="col-size">' + esc(s.sizeLabel || '') + '</span>' +
|
|
285
304
|
'<span class="col-date">' + esc(s.dateLabel || '') + '</span>' +
|
|
286
305
|
'<span class="row-actions">' +
|
|
306
|
+
'<button class="act-btn" data-action="view-session" data-path="' + p + '" title="View chat">VIEW</button>' +
|
|
287
307
|
'<button class="act-btn" data-action="backup-one" data-path="' + p + '" title="Backup">BAK</button>' +
|
|
288
308
|
'<button class="act-btn del" data-action="trash-one" data-path="' + p + '" title="Trash">DEL</button>' +
|
|
289
309
|
'</span></div>';
|
|
290
310
|
}
|
|
291
311
|
|
|
292
312
|
// ── TOOLBAR ───────────────────────────────────────────────────────────────────
|
|
293
|
-
function
|
|
313
|
+
function selectAllSessions() {
|
|
314
|
+
filtered.forEach(function(s) { selected.add(s.path); });
|
|
315
|
+
renderSessions();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function selectNoSessions() {
|
|
319
|
+
selected.clear();
|
|
320
|
+
renderSessions();
|
|
321
|
+
}
|
|
294
322
|
|
|
295
|
-
|
|
296
|
-
wireBtn('btn-sel-none', function() { selected.clear(); renderSessions(); });
|
|
297
|
-
wireBtn('btn-group', function() {
|
|
323
|
+
function toggleGrouping() {
|
|
298
324
|
grouped = !grouped;
|
|
299
325
|
var el = $('btn-group');
|
|
300
326
|
if (el) { el.textContent = grouped ? 'FLAT VIEW' : 'GROUP VIEW'; el.classList.toggle('active-btn', !grouped); }
|
|
301
|
-
PAGE = 0;
|
|
302
|
-
|
|
327
|
+
PAGE = 0;
|
|
328
|
+
renderSessions();
|
|
329
|
+
}
|
|
303
330
|
|
|
304
|
-
|
|
331
|
+
async function trashSelected() {
|
|
305
332
|
if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
|
|
306
333
|
if (!confirm('Move ' + selected.size + ' session(s) to trash?')) return;
|
|
307
334
|
var items = buildItems(Array.from(selected));
|
|
308
|
-
var res
|
|
335
|
+
var res;
|
|
336
|
+
try { res = await post('/api/trash/move', { items: items }); }
|
|
337
|
+
catch (e) { toast('TRASH FAILED: ' + e.message, 'err', 5000); return; }
|
|
338
|
+
if (!Array.isArray(res)) { toast('TRASH FAILED: INVALID SERVER RESPONSE', 'err', 5000); return; }
|
|
309
339
|
var ok = res.filter(function(r) { return r.ok; }).map(function(r) { return r.path; });
|
|
340
|
+
var fail = res.filter(function(r) { return !r.ok; });
|
|
310
341
|
ok.forEach(function(p) { ALL = ALL.filter(function(s) { return s.path !== p; }); selected.delete(p); });
|
|
311
|
-
applyFilter();
|
|
312
|
-
|
|
313
|
-
|
|
342
|
+
applyFilter();
|
|
343
|
+
if (ok.length) {
|
|
344
|
+
toast(ok.length + ' MOVED TO TRASH' + (fail.length ? ' / ' + fail.length + ' FAILED' : ''), fail.length ? 'err' : 'ok');
|
|
345
|
+
switchTab('trash');
|
|
346
|
+
} else {
|
|
347
|
+
toast('TRASH FAILED: ' + ((fail[0] && fail[0].error) || 'NO ITEMS MOVED'), 'err', 5000);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function backupSelected() {
|
|
314
352
|
if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
|
|
315
353
|
var items = buildItems(Array.from(selected));
|
|
316
|
-
var res
|
|
317
|
-
|
|
318
|
-
|
|
354
|
+
var res;
|
|
355
|
+
try { res = await post('/api/backup/create', { items: items, note: '' }); }
|
|
356
|
+
catch (e) { toast('BACKUP FAILED: ' + e.message, 'err', 5000); return; }
|
|
357
|
+
if (!Array.isArray(res)) { toast('BACKUP FAILED: INVALID SERVER RESPONSE', 'err', 5000); return; }
|
|
358
|
+
var ok = res.filter(function(r) { return r.ok; });
|
|
359
|
+
var fail = res.filter(function(r) { return !r.ok; });
|
|
360
|
+
if (ok.length) {
|
|
361
|
+
toast(ok.length + ' BACKUP(S) CREATED' + (fail.length ? ' / ' + fail.length + ' FAILED' : ''), fail.length ? 'err' : 'ok');
|
|
362
|
+
switchTab('backups');
|
|
363
|
+
} else {
|
|
364
|
+
toast('BACKUP FAILED: ' + ((fail[0] && fail[0].error) || 'NO BACKUPS CREATED'), 'err', 5000);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
319
367
|
|
|
320
368
|
// Enrich paths with session metadata for storage
|
|
321
369
|
function buildItems(paths) {
|
|
@@ -332,12 +380,13 @@ function switchTab(tab) {
|
|
|
332
380
|
document.querySelectorAll('.panel').forEach(function(p) { p.classList.toggle('active', p.id === tab + '-panel'); });
|
|
333
381
|
// Mark active sub-item (agent-specific nav)
|
|
334
382
|
document.querySelectorAll('.sb-sub-item').forEach(function(el) {
|
|
335
|
-
el.classList.toggle('active', el.dataset.nav === tab);
|
|
383
|
+
el.classList.toggle('active', el.dataset.nav === tab && el.dataset.agent === agentFilter);
|
|
336
384
|
});
|
|
337
385
|
// Mark global tools active only when agentFilter is all
|
|
338
386
|
document.querySelectorAll('.sb-item[data-action="switch-tab-global"]').forEach(function(el) {
|
|
339
387
|
el.classList.toggle('active', el.dataset.nav === tab && agentFilter === 'all');
|
|
340
388
|
});
|
|
389
|
+
if (tab === 'sessions') applyFilter();
|
|
341
390
|
if (tab === 'usage') loadUsage(false);
|
|
342
391
|
if (tab === 'trash') loadTrash(false);
|
|
343
392
|
if (tab === 'backups') loadBackups(false);
|
|
@@ -350,20 +399,64 @@ document.querySelectorAll('.tab').forEach(function(t) {
|
|
|
350
399
|
async function trashOne(path) {
|
|
351
400
|
if (!path || !confirm('Move to trash?')) return;
|
|
352
401
|
var items = buildItems([path]);
|
|
353
|
-
var res
|
|
402
|
+
var res;
|
|
403
|
+
try { res = await post('/api/trash/move', { items: items }); }
|
|
404
|
+
catch (e) { toast('ERROR: ' + e.message, 'err', 5000); return; }
|
|
405
|
+
if (!Array.isArray(res)) { toast('ERROR: INVALID SERVER RESPONSE', 'err', 5000); return; }
|
|
354
406
|
if (res[0] && res[0].ok) {
|
|
355
407
|
ALL = ALL.filter(function(s) { return s.path !== path; }); selected.delete(path);
|
|
356
|
-
applyFilter(); toast('MOVED TO TRASH', 'ok');
|
|
408
|
+
applyFilter(); toast('MOVED TO TRASH', 'ok'); switchTab('trash');
|
|
357
409
|
} else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
358
410
|
}
|
|
359
411
|
async function backupOne(path) {
|
|
360
412
|
if (!path) return;
|
|
361
413
|
var items = buildItems([path]);
|
|
362
|
-
var res
|
|
363
|
-
|
|
414
|
+
var res;
|
|
415
|
+
try { res = await post('/api/backup/create', { items: items, note: '' }); }
|
|
416
|
+
catch (e) { toast('ERROR: ' + e.message, 'err', 5000); return; }
|
|
417
|
+
if (!Array.isArray(res)) { toast('ERROR: INVALID SERVER RESPONSE', 'err', 5000); return; }
|
|
418
|
+
if (res[0] && res[0].ok) { toast('BACKUP CREATED', 'ok'); switchTab('backups'); }
|
|
364
419
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
365
420
|
}
|
|
366
421
|
|
|
422
|
+
// ── SESSION VIEWER ───────────────────────────────────────────────────────────
|
|
423
|
+
function closeModal() {
|
|
424
|
+
var m = $('modal');
|
|
425
|
+
if (m) m.hidden = true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function viewSession(path) {
|
|
429
|
+
if (!path) return;
|
|
430
|
+
var modal = $('modal'), title = $('modal-title'), sub = $('modal-sub'), body = $('modal-body');
|
|
431
|
+
if (!modal || !body) return;
|
|
432
|
+
modal.hidden = false;
|
|
433
|
+
if (title) title.textContent = 'LOADING SESSION';
|
|
434
|
+
if (sub) sub.textContent = path;
|
|
435
|
+
body.innerHTML = '<div class="loading"><span class="loading-dots">LOADING CHAT</span></div>';
|
|
436
|
+
try {
|
|
437
|
+
var data = await fetch('/api/session?path=' + encodeURIComponent(path)).then(function(r) { return r.json(); });
|
|
438
|
+
if (!data.ok) {
|
|
439
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(data.error || 'Could not load session') + '</div></div>';
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (title) title.textContent = 'SESSION HISTORY';
|
|
443
|
+
if (sub) sub.textContent = data.path + (data.truncated ? ' / preview truncated' : '');
|
|
444
|
+
var messages = data.messages || [];
|
|
445
|
+
if (messages.length) {
|
|
446
|
+
body.innerHTML = messages.slice(0, 200).map(function(m) {
|
|
447
|
+
return '<div class="chat-msg">' +
|
|
448
|
+
'<div class="chat-role">' + esc(m.index + ' / ' + m.role) + '</div>' +
|
|
449
|
+
'<div class="chat-text">' + esc(m.text) + '</div>' +
|
|
450
|
+
'</div>';
|
|
451
|
+
}).join('') + (messages.length > 200 ? '<div class="empty"><div>Showing first 200 entries</div></div>' : '');
|
|
452
|
+
} else {
|
|
453
|
+
body.innerHTML = '<pre class="chat-raw">' + esc(data.raw || '') + '</pre>';
|
|
454
|
+
}
|
|
455
|
+
} catch (e) {
|
|
456
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
367
460
|
// ── USAGE TAB ─────────────────────────────────────────────────────────────────
|
|
368
461
|
async function loadUsage(force) {
|
|
369
462
|
var key = agentFilter;
|
|
@@ -496,7 +589,7 @@ async function loadTrash(force) {
|
|
|
496
589
|
|
|
497
590
|
async function restoreTrash(mp) {
|
|
498
591
|
var res = await post('/api/trash/restore', { metaPath: mp });
|
|
499
|
-
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); }
|
|
592
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); reloadSessions(); }
|
|
500
593
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
501
594
|
}
|
|
502
595
|
async function purgeTrash(mp) {
|
|
@@ -539,7 +632,7 @@ async function loadBackups(force) {
|
|
|
539
632
|
|
|
540
633
|
async function restoreBackup(mp) {
|
|
541
634
|
var res = await post('/api/backup/restore', { metaPath: mp });
|
|
542
|
-
if (res.ok) toast('RESTORED TO ' + res.restoredTo, 'ok');
|
|
635
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); reloadSessions(); }
|
|
543
636
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
544
637
|
}
|
|
545
638
|
async function deleteBackup(mp) {
|
|
@@ -549,6 +642,14 @@ async function deleteBackup(mp) {
|
|
|
549
642
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
550
643
|
}
|
|
551
644
|
|
|
645
|
+
async function reloadSessions() {
|
|
646
|
+
try {
|
|
647
|
+
var sessions = await fetch('/api/sessions').then(function(r) { return r.json(); });
|
|
648
|
+
ALL = sessions;
|
|
649
|
+
applyFilter();
|
|
650
|
+
} catch {}
|
|
651
|
+
}
|
|
652
|
+
|
|
552
653
|
// ── BOOT ──────────────────────────────────────────────────────────────────────
|
|
553
654
|
init().catch(function(e) { console.error('[AM] fatal:', e); });
|
|
554
655
|
|
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.4</div>
|
|
69
69
|
</div>
|
|
70
70
|
</nav>
|
|
71
71
|
|
|
@@ -86,14 +86,14 @@ export function renderPage() {
|
|
|
86
86
|
<!-- ── Sessions ── -->
|
|
87
87
|
<div id="sessions-panel" class="panel active">
|
|
88
88
|
<div id="toolbar">
|
|
89
|
-
<button class="btn" id="btn-sel-all">SELECT ALL</button>
|
|
90
|
-
<button class="btn" id="btn-sel-none">DESELECT</button>
|
|
91
|
-
<button class="btn" id="btn-group">FLAT VIEW</button>
|
|
89
|
+
<button class="btn" id="btn-sel-all" data-action="select-all">SELECT ALL</button>
|
|
90
|
+
<button class="btn" id="btn-sel-none" data-action="select-none">DESELECT</button>
|
|
91
|
+
<button class="btn" id="btn-group" data-action="toggle-group">FLAT VIEW</button>
|
|
92
92
|
<span class="spacer"></span>
|
|
93
93
|
<span id="sel-info">...</span>
|
|
94
94
|
<span class="spacer"></span>
|
|
95
|
-
<button class="btn success" id="btn-backup">BACKUP SEL</button>
|
|
96
|
-
<button class="btn danger" id="btn-trash">TRASH SEL</button>
|
|
95
|
+
<button class="btn success" id="btn-backup" data-action="backup-selected">BACKUP SEL</button>
|
|
96
|
+
<button class="btn danger" id="btn-trash" data-action="trash-selected">TRASH SEL</button>
|
|
97
97
|
</div>
|
|
98
98
|
<div id="tbl-wrap">
|
|
99
99
|
<div class="tbl-hdr">
|
|
@@ -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.4</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>`
|