aisessions 1.0.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 +24 -0
- package/bin/cli.js +65 -0
- package/package.json +29 -0
- package/src/agents/amp.js +51 -0
- package/src/agents/claude.js +102 -0
- package/src/agents/codebuff.js +38 -0
- package/src/agents/codex.js +118 -0
- package/src/agents/copilot.js +43 -0
- package/src/agents/cursor.js +53 -0
- package/src/agents/droid.js +38 -0
- package/src/agents/gemini.js +50 -0
- package/src/agents/goose.js +52 -0
- package/src/agents/hermes.js +38 -0
- package/src/agents/index.js +39 -0
- package/src/agents/kilo.js +38 -0
- package/src/agents/kimi.js +38 -0
- package/src/agents/openclaw.js +38 -0
- package/src/agents/opencode.js +50 -0
- package/src/agents/pi.js +41 -0
- package/src/agents/qwen.js +38 -0
- package/src/agents/utils.js +283 -0
- package/src/router.js +83 -0
- package/src/server.js +12 -0
- package/src/storage/backup.js +93 -0
- package/src/storage/trash.js +93 -0
- package/src/ui/css.js +392 -0
- package/src/ui/js.js +528 -0
- package/src/ui/render.js +138 -0
- package/src/usage/index.js +111 -0
package/src/router.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { renderPage } from './ui/render.js'
|
|
2
|
+
import { getAllSessions, getInstalledAgents } from './agents/index.js'
|
|
3
|
+
import { getUsageData } from './usage/index.js'
|
|
4
|
+
import { listTrash, moveToTrash, restoreFromTrash, purgeTrash } from './storage/trash.js'
|
|
5
|
+
import { listBackups, createBackup, restoreBackup, deleteBackup } from './storage/backup.js'
|
|
6
|
+
|
|
7
|
+
function sendJson(res, data, status = 200) {
|
|
8
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
9
|
+
res.end(JSON.stringify(data))
|
|
10
|
+
}
|
|
11
|
+
function sendHtml(res, content) {
|
|
12
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
13
|
+
res.end(content)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readBody(req) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const chunks = []
|
|
19
|
+
req.on('data', c => chunks.push(c))
|
|
20
|
+
req.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
23
|
+
if ((req.headers['content-type'] || '').includes('application/json')) {
|
|
24
|
+
resolve(JSON.parse(text || '{}'))
|
|
25
|
+
} else {
|
|
26
|
+
const p = new URLSearchParams(text)
|
|
27
|
+
const obj = {}
|
|
28
|
+
for (const [k, v] of p) obj[k] = obj[k] !== undefined ? [].concat(obj[k], v) : v
|
|
29
|
+
resolve(obj)
|
|
30
|
+
}
|
|
31
|
+
} catch { resolve({}) }
|
|
32
|
+
})
|
|
33
|
+
req.on('error', reject)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function arr(v) { return Array.isArray(v) ? v : [v].filter(Boolean) }
|
|
38
|
+
|
|
39
|
+
// Normalise body items: accept either `items:[{path,agent,...}]` or `paths:[...]`
|
|
40
|
+
function bodyItems(b) {
|
|
41
|
+
if (Array.isArray(b.items)) return b.items
|
|
42
|
+
return arr(b.paths).map(p => (typeof p === 'string' ? { path: p } : p))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function router(req, res) {
|
|
46
|
+
const u = new URL(req.url, 'http://localhost')
|
|
47
|
+
const pathname = u.pathname
|
|
48
|
+
const query = Object.fromEntries(u.searchParams)
|
|
49
|
+
const method = req.method.toUpperCase()
|
|
50
|
+
const agent = query.agent || 'all'
|
|
51
|
+
|
|
52
|
+
// ── UI shell ──────────────────────────────────────────────────────────────
|
|
53
|
+
if (method === 'GET' && pathname === '/') return sendHtml(res, renderPage())
|
|
54
|
+
|
|
55
|
+
// ── Agents ────────────────────────────────────────────────────────────────
|
|
56
|
+
if (method === 'GET' && pathname === '/api/agents') return sendJson(res, getInstalledAgents())
|
|
57
|
+
|
|
58
|
+
// ── Sessions ──────────────────────────────────────────────────────────────
|
|
59
|
+
if (method === 'GET' && pathname === '/api/sessions') {
|
|
60
|
+
const sessions = await getAllSessions()
|
|
61
|
+
return sendJson(res, sessions.map(({ date, ...rest }) => rest))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Usage (supports ?agent=claude) ───────────────────────────────────────
|
|
65
|
+
if (method === 'GET' && pathname === '/api/usage') {
|
|
66
|
+
return sendJson(res, await getUsageData({ since: query.since, until: query.until, agent }))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Trash (supports ?agent=claude) ───────────────────────────────────────
|
|
70
|
+
if (method === 'GET' && pathname === '/api/trash') return sendJson(res, await listTrash(agent))
|
|
71
|
+
if (method === 'POST' && pathname === '/api/trash/move') { const b = await readBody(req); return sendJson(res, await moveToTrash(bodyItems(b))) }
|
|
72
|
+
if (method === 'POST' && pathname === '/api/trash/restore') { const b = await readBody(req); return sendJson(res, await restoreFromTrash(b.metaPath)) }
|
|
73
|
+
if (method === 'POST' && pathname === '/api/trash/purge') { const b = await readBody(req); return sendJson(res, await purgeTrash(arr(b.metaPaths))) }
|
|
74
|
+
|
|
75
|
+
// ── Backups (supports ?agent=claude) ─────────────────────────────────────
|
|
76
|
+
if (method === 'GET' && pathname === '/api/backups') return sendJson(res, await listBackups(agent))
|
|
77
|
+
if (method === 'POST' && pathname === '/api/backup/create') { const b = await readBody(req); return sendJson(res, await createBackup(bodyItems(b), b.note || '')) }
|
|
78
|
+
if (method === 'POST' && pathname === '/api/backup/restore') { const b = await readBody(req); return sendJson(res, await restoreBackup(b.metaPath)) }
|
|
79
|
+
if (method === 'POST' && pathname === '/api/backup/delete') { const b = await readBody(req); return sendJson(res, await deleteBackup(arr(b.metaPaths))) }
|
|
80
|
+
|
|
81
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
82
|
+
res.end('Not found')
|
|
83
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createServer as _http } from 'node:http'
|
|
2
|
+
import { router } from './router.js'
|
|
3
|
+
|
|
4
|
+
export function createServer() {
|
|
5
|
+
return _http((req, res) => {
|
|
6
|
+
router(req, res).catch(err => {
|
|
7
|
+
console.error('[router]', err.message)
|
|
8
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
9
|
+
res.end('Internal server error')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const HOME = homedir()
|
|
6
|
+
const BACKUP_DIR = join(HOME, '.aisessions', 'backups')
|
|
7
|
+
|
|
8
|
+
function ensureBackup() { mkdirSync(BACKUP_DIR, { recursive: true }) }
|
|
9
|
+
|
|
10
|
+
function normaliseItems(raw) {
|
|
11
|
+
return (Array.isArray(raw) ? raw : [raw]).map(i =>
|
|
12
|
+
typeof i === 'string' ? { path: i } : i
|
|
13
|
+
).filter(i => i && i.path)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function listBackups(agent) {
|
|
17
|
+
ensureBackup()
|
|
18
|
+
const items = []
|
|
19
|
+
try {
|
|
20
|
+
for (const f of readdirSync(BACKUP_DIR)) {
|
|
21
|
+
if (!f.endsWith('.meta.json')) continue
|
|
22
|
+
const mp = join(BACKUP_DIR, f)
|
|
23
|
+
try {
|
|
24
|
+
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
25
|
+
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
26
|
+
items.push({ ...meta, metaPath: mp, exists: existsSync(meta.backupPath) })
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return items.sort((a, b) => new Date(b.backedUpAt) - new Date(a.backedUpAt))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function createBackup(rawItems, note = '') {
|
|
34
|
+
ensureBackup()
|
|
35
|
+
const results = []
|
|
36
|
+
for (const item of normaliseItems(rawItems)) {
|
|
37
|
+
const p = item.path
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
40
|
+
const ts = Date.now()
|
|
41
|
+
const name = basename(p)
|
|
42
|
+
const backupName = ts + '_' + name
|
|
43
|
+
const backupPath = join(BACKUP_DIR, backupName)
|
|
44
|
+
const metaPath = join(BACKUP_DIR, backupName + '.meta.json')
|
|
45
|
+
const st = statSync(p)
|
|
46
|
+
if (st.isDirectory()) cpSync(p, backupPath, { recursive: true })
|
|
47
|
+
else cpSync(p, backupPath)
|
|
48
|
+
const meta = {
|
|
49
|
+
originalPath: p,
|
|
50
|
+
filename: name,
|
|
51
|
+
agent: item.agent || '',
|
|
52
|
+
agentLabel: item.agentLabel || '',
|
|
53
|
+
project: item.project || '',
|
|
54
|
+
title: item.title || '',
|
|
55
|
+
backedUpAt: new Date().toISOString(),
|
|
56
|
+
backupPath,
|
|
57
|
+
metaPath,
|
|
58
|
+
note,
|
|
59
|
+
}
|
|
60
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2))
|
|
61
|
+
results.push({ path: p, ok: true, backupPath, metaPath })
|
|
62
|
+
} catch (e) { results.push({ path: p, ok: false, error: e.message }) }
|
|
63
|
+
}
|
|
64
|
+
return results
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function restoreBackup(metaPath) {
|
|
68
|
+
try {
|
|
69
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
70
|
+
if (!existsSync(meta.backupPath)) return { ok: false, error: 'backup file missing' }
|
|
71
|
+
const dest = existsSync(meta.originalPath)
|
|
72
|
+
? meta.originalPath.replace(/(\.[^.]+)?$/, '_restored_' + Date.now() + '$1')
|
|
73
|
+
: meta.originalPath
|
|
74
|
+
const st = statSync(meta.backupPath)
|
|
75
|
+
if (st.isDirectory()) cpSync(meta.backupPath, dest, { recursive: true })
|
|
76
|
+
else cpSync(meta.backupPath, dest)
|
|
77
|
+
return { ok: true, restoredTo: dest }
|
|
78
|
+
} catch (e) { return { ok: false, error: e.message } }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function deleteBackup(metaPaths) {
|
|
82
|
+
const results = []
|
|
83
|
+
for (const mp of metaPaths) {
|
|
84
|
+
try {
|
|
85
|
+
let backupPath = mp.replace(/\.meta\.json$/, '')
|
|
86
|
+
try { const m = JSON.parse(readFileSync(mp, 'utf8')); backupPath = m.backupPath || backupPath } catch {}
|
|
87
|
+
if (existsSync(backupPath)) rmSync(backupPath, { recursive: true, force: true })
|
|
88
|
+
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
89
|
+
results.push({ metaPath: mp, ok: true })
|
|
90
|
+
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
|
+
}
|
|
92
|
+
return results
|
|
93
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const HOME = homedir()
|
|
6
|
+
const TRASH_DIR = join(HOME, '.aisessions', 'trash')
|
|
7
|
+
|
|
8
|
+
function ensureTrash() { mkdirSync(TRASH_DIR, { recursive: true }) }
|
|
9
|
+
|
|
10
|
+
// items: array of { path, agent?, agentLabel?, project?, title? }
|
|
11
|
+
// OR plain strings (backwards compat)
|
|
12
|
+
function normaliseItems(raw) {
|
|
13
|
+
return (Array.isArray(raw) ? raw : [raw]).map(i =>
|
|
14
|
+
typeof i === 'string' ? { path: i } : i
|
|
15
|
+
).filter(i => i && i.path)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function listTrash(agent) {
|
|
19
|
+
ensureTrash()
|
|
20
|
+
const items = []
|
|
21
|
+
try {
|
|
22
|
+
for (const f of readdirSync(TRASH_DIR)) {
|
|
23
|
+
if (!f.endsWith('.meta.json')) continue
|
|
24
|
+
const mp = join(TRASH_DIR, f)
|
|
25
|
+
try {
|
|
26
|
+
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
27
|
+
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
28
|
+
items.push({ ...meta, metaPath: mp, exists: existsSync(meta.trashPath) })
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
return items.sort((a, b) => new Date(b.deletedAt) - new Date(a.deletedAt))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function moveToTrash(rawItems) {
|
|
36
|
+
ensureTrash()
|
|
37
|
+
const results = []
|
|
38
|
+
for (const item of normaliseItems(rawItems)) {
|
|
39
|
+
const p = item.path
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
42
|
+
const ts = Date.now()
|
|
43
|
+
const trashName = ts + '_' + p.replace(/\//g, '_').replace(/^_+/, '').slice(-80)
|
|
44
|
+
const trashPath = join(TRASH_DIR, trashName)
|
|
45
|
+
const metaPath = join(TRASH_DIR, trashName + '.meta.json')
|
|
46
|
+
const st = statSync(p)
|
|
47
|
+
if (st.isDirectory()) cpSync(p, trashPath, { recursive: true })
|
|
48
|
+
else cpSync(p, trashPath)
|
|
49
|
+
rmSync(p, { recursive: true, force: true })
|
|
50
|
+
const meta = {
|
|
51
|
+
originalPath: p,
|
|
52
|
+
agent: item.agent || '',
|
|
53
|
+
agentLabel: item.agentLabel || '',
|
|
54
|
+
project: item.project || '',
|
|
55
|
+
title: item.title || '',
|
|
56
|
+
deletedAt: new Date().toISOString(),
|
|
57
|
+
trashPath,
|
|
58
|
+
metaPath,
|
|
59
|
+
}
|
|
60
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2))
|
|
61
|
+
results.push({ path: p, ok: true, trashPath, metaPath })
|
|
62
|
+
} catch (e) { results.push({ path: p, ok: false, error: e.message }) }
|
|
63
|
+
}
|
|
64
|
+
return results
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function restoreFromTrash(metaPath) {
|
|
68
|
+
try {
|
|
69
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
70
|
+
if (!existsSync(meta.trashPath)) return { ok: false, error: 'trash file missing' }
|
|
71
|
+
if (existsSync(meta.originalPath)) return { ok: false, error: 'destination already exists' }
|
|
72
|
+
const st = statSync(meta.trashPath)
|
|
73
|
+
if (st.isDirectory()) cpSync(meta.trashPath, meta.originalPath, { recursive: true })
|
|
74
|
+
else cpSync(meta.trashPath, meta.originalPath)
|
|
75
|
+
rmSync(meta.trashPath, { recursive: true, force: true })
|
|
76
|
+
rmSync(metaPath, { force: true })
|
|
77
|
+
return { ok: true, restoredTo: meta.originalPath }
|
|
78
|
+
} catch (e) { return { ok: false, error: e.message } }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function purgeTrash(metaPaths) {
|
|
82
|
+
const results = []
|
|
83
|
+
for (const mp of metaPaths) {
|
|
84
|
+
try {
|
|
85
|
+
let trashPath = mp.replace(/\.meta\.json$/, '')
|
|
86
|
+
try { const m = JSON.parse(readFileSync(mp, 'utf8')); trashPath = m.trashPath || trashPath } catch {}
|
|
87
|
+
if (existsSync(trashPath)) rmSync(trashPath, { recursive: true, force: true })
|
|
88
|
+
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
89
|
+
results.push({ metaPath: mp, ok: true })
|
|
90
|
+
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
|
+
}
|
|
92
|
+
return results
|
|
93
|
+
}
|
package/src/ui/css.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
export const PAGE_CSS = `
|
|
2
|
+
@import url('https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap');
|
|
3
|
+
|
|
4
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
5
|
+
|
|
6
|
+
:root{
|
|
7
|
+
--bg:#060606;
|
|
8
|
+
--sb:#0a0a0a;
|
|
9
|
+
--surf:#0f0f0f;
|
|
10
|
+
--surf2:#151515;
|
|
11
|
+
--surf3:#1a1a1a;
|
|
12
|
+
--border:rgba(255,255,255,0.09);
|
|
13
|
+
--border2:rgba(255,255,255,0.18);
|
|
14
|
+
--text:#e8e8e8;
|
|
15
|
+
--muted:#666;
|
|
16
|
+
--dim:#3a3a3a;
|
|
17
|
+
--accent:#ffffff;
|
|
18
|
+
--danger:#ff3333;
|
|
19
|
+
--success:#33cc33;
|
|
20
|
+
--warn:#ccaa00;
|
|
21
|
+
--sb-w:230px;
|
|
22
|
+
--hdr-h:58px;
|
|
23
|
+
--tab-h:46px;
|
|
24
|
+
--toolbar-h:52px;
|
|
25
|
+
--foot-h:38px;
|
|
26
|
+
--row-h:52px;
|
|
27
|
+
--pixel: 'VT323', 'Courier New', monospace;
|
|
28
|
+
--logo: 'Press Start 2P', monospace;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
32
|
+
font-family:var(--pixel);font-size:20px;line-height:1.2;
|
|
33
|
+
-webkit-font-smoothing:none;font-smooth:never;image-rendering:pixelated}
|
|
34
|
+
|
|
35
|
+
/* ── LAYOUT ──────────────────────────────────────────────────────────────────── */
|
|
36
|
+
#shell{display:flex;flex-direction:column;height:100vh;width:100vw}
|
|
37
|
+
|
|
38
|
+
#hdr{
|
|
39
|
+
height:var(--hdr-h);
|
|
40
|
+
display:flex;align-items:center;gap:14px;padding:0 20px;
|
|
41
|
+
background:var(--sb);
|
|
42
|
+
border-bottom:2px solid var(--border2);
|
|
43
|
+
flex-shrink:0;z-index:20;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#body{display:flex;flex:1;min-height:0;overflow:hidden}
|
|
47
|
+
|
|
48
|
+
#sidebar{
|
|
49
|
+
width:var(--sb-w);background:var(--sb);
|
|
50
|
+
border-right:2px solid var(--border);
|
|
51
|
+
display:flex;flex-direction:column;
|
|
52
|
+
flex-shrink:0;overflow-y:auto;overflow-x:hidden;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
|
|
56
|
+
|
|
57
|
+
#foot{
|
|
58
|
+
height:var(--foot-h);
|
|
59
|
+
display:flex;align-items:center;gap:12px;padding:0 16px;
|
|
60
|
+
background:var(--sb);border-top:2px solid var(--border);
|
|
61
|
+
flex-shrink:0;font-size:16px;color:var(--muted);
|
|
62
|
+
letter-spacing:.5px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── HEADER ──────────────────────────────────────────────────────────────────── */
|
|
66
|
+
#logo{
|
|
67
|
+
font-family:var(--logo);font-size:11px;
|
|
68
|
+
color:var(--accent);letter-spacing:1px;
|
|
69
|
+
text-shadow:0 0 10px rgba(255,255,255,.25);
|
|
70
|
+
white-space:nowrap;
|
|
71
|
+
}
|
|
72
|
+
#hdr-sub{font-size:16px;color:var(--muted);letter-spacing:.5px}
|
|
73
|
+
#hdr-right{margin-left:auto;display:flex;align-items:center;gap:10px}
|
|
74
|
+
#search{
|
|
75
|
+
background:var(--surf);border:2px solid var(--border);
|
|
76
|
+
color:var(--text);padding:8px 14px;border-radius:2px;
|
|
77
|
+
font-family:var(--pixel);font-size:18px;width:260px;outline:none;
|
|
78
|
+
-webkit-font-smoothing:none;
|
|
79
|
+
}
|
|
80
|
+
#search:focus{border-color:var(--border2);background:var(--surf2)}
|
|
81
|
+
#search::placeholder{color:var(--dim)}
|
|
82
|
+
|
|
83
|
+
/* ── SIDEBAR ─────────────────────────────────────────────────────────────────── */
|
|
84
|
+
.sb-section{padding:10px 0}
|
|
85
|
+
.sb-section+.sb-section{border-top:1px solid var(--border)}
|
|
86
|
+
.sb-label{
|
|
87
|
+
padding:6px 16px 4px;
|
|
88
|
+
font-size:13px;letter-spacing:1.5px;
|
|
89
|
+
color:var(--dim);text-transform:uppercase;
|
|
90
|
+
}
|
|
91
|
+
.sb-item{
|
|
92
|
+
display:flex;align-items:center;gap:10px;
|
|
93
|
+
padding:10px 16px;cursor:pointer;
|
|
94
|
+
color:var(--muted);font-family:var(--pixel);font-size:18px;
|
|
95
|
+
border:none;background:none;width:100%;text-align:left;
|
|
96
|
+
transition:color .1s,background .1s;
|
|
97
|
+
-webkit-font-smoothing:none;
|
|
98
|
+
}
|
|
99
|
+
.sb-item:hover{color:var(--text);background:rgba(255,255,255,0.04)}
|
|
100
|
+
.sb-item.active{
|
|
101
|
+
color:var(--accent);background:rgba(255,255,255,0.07);
|
|
102
|
+
border-left:3px solid var(--accent);padding-left:13px;
|
|
103
|
+
}
|
|
104
|
+
.agent-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;border:1px solid rgba(255,255,255,.2)}
|
|
105
|
+
.sb-count{
|
|
106
|
+
margin-left:auto;font-size:15px;color:var(--dim);
|
|
107
|
+
background:var(--surf2);padding:1px 7px;border:1px solid var(--border);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ── DEV CREDIT ──────────────────────────────────────────────────────────────── */
|
|
111
|
+
#sb-credit{
|
|
112
|
+
margin-top:auto;
|
|
113
|
+
padding:18px 14px 20px;
|
|
114
|
+
border-top:1px solid var(--border);
|
|
115
|
+
text-align:center;
|
|
116
|
+
flex-shrink:0;
|
|
117
|
+
background:linear-gradient(180deg,rgba(255,255,255,.025),rgba(255,255,255,0) 48%);
|
|
118
|
+
}
|
|
119
|
+
.credit-kicker{
|
|
120
|
+
font-family:var(--logo);font-size:7px;
|
|
121
|
+
color:rgba(255,255,255,.42);letter-spacing:1.4px;
|
|
122
|
+
margin-bottom:9px;
|
|
123
|
+
}
|
|
124
|
+
.credit-frame{
|
|
125
|
+
position:relative;
|
|
126
|
+
margin:0 auto 12px;
|
|
127
|
+
padding:12px 8px 11px;
|
|
128
|
+
border:1px solid rgba(255,255,255,.18);
|
|
129
|
+
background:rgba(255,255,255,.035);
|
|
130
|
+
}
|
|
131
|
+
.credit-frame::before,.credit-frame::after{
|
|
132
|
+
content:"";
|
|
133
|
+
position:absolute;
|
|
134
|
+
top:-1px;
|
|
135
|
+
width:18px;height:1px;
|
|
136
|
+
background:var(--accent);
|
|
137
|
+
opacity:.65;
|
|
138
|
+
}
|
|
139
|
+
.credit-frame::before{left:10px}
|
|
140
|
+
.credit-frame::after{right:10px}
|
|
141
|
+
.credit-handle{
|
|
142
|
+
font-family:var(--logo);font-size:9px;
|
|
143
|
+
color:var(--accent);letter-spacing:1.5px;
|
|
144
|
+
}
|
|
145
|
+
.credit-links{
|
|
146
|
+
display:grid;grid-template-columns:1fr 1fr;gap:8px;
|
|
147
|
+
margin-bottom:11px;
|
|
148
|
+
}
|
|
149
|
+
.credit-link{
|
|
150
|
+
display:flex;align-items:center;justify-content:center;gap:6px;
|
|
151
|
+
min-width:0;
|
|
152
|
+
font-size:14px;color:rgba(255,255,255,.58);letter-spacing:.4px;
|
|
153
|
+
text-decoration:none;padding:6px 7px;
|
|
154
|
+
border:1px solid var(--border);
|
|
155
|
+
background:rgba(255,255,255,.025);
|
|
156
|
+
transition:color .1s,border-color .1s,background .1s;
|
|
157
|
+
}
|
|
158
|
+
.credit-link:hover{
|
|
159
|
+
color:var(--accent);
|
|
160
|
+
border-color:rgba(255,255,255,.35);
|
|
161
|
+
background:rgba(255,255,255,.06);
|
|
162
|
+
}
|
|
163
|
+
.credit-link-icon{
|
|
164
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
165
|
+
width:18px;height:18px;
|
|
166
|
+
border:1px solid rgba(255,255,255,.18);
|
|
167
|
+
color:rgba(255,255,255,.78);
|
|
168
|
+
font-size:10px;line-height:1;
|
|
169
|
+
}
|
|
170
|
+
.credit-pkg{
|
|
171
|
+
font-size:12px;color:rgba(255,255,255,.26);letter-spacing:1px;
|
|
172
|
+
text-transform:uppercase;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ── TAB BAR — hidden; sidebar drives navigation ────────────────────────────── */
|
|
176
|
+
#tab-bar{display:none}
|
|
177
|
+
|
|
178
|
+
/* ── SIDEBAR SUB-ITEMS (per-agent nav) ───────────────────────────────────────── */
|
|
179
|
+
.sb-sub{display:none;border-left:2px solid var(--border2);margin-left:16px}
|
|
180
|
+
.sb-sub.show{display:block}
|
|
181
|
+
.sb-sub-item{
|
|
182
|
+
display:flex;align-items:center;gap:8px;
|
|
183
|
+
padding:8px 12px 8px 14px;cursor:pointer;
|
|
184
|
+
color:var(--dim);font-family:var(--pixel);font-size:16px;
|
|
185
|
+
border:none;background:none;width:100%;text-align:left;
|
|
186
|
+
transition:color .1s,background .1s;-webkit-font-smoothing:none;
|
|
187
|
+
}
|
|
188
|
+
.sb-sub-item:hover{color:var(--text);background:rgba(255,255,255,.03)}
|
|
189
|
+
.sb-sub-item.active{color:var(--accent)}
|
|
190
|
+
|
|
191
|
+
/* ── TOOLBAR ─────────────────────────────────────────────────────────────────── */
|
|
192
|
+
#toolbar{
|
|
193
|
+
display:flex;align-items:center;gap:8px;
|
|
194
|
+
padding:0 16px;height:var(--toolbar-h);flex-shrink:0;
|
|
195
|
+
background:var(--surf);border-bottom:1px solid var(--border);
|
|
196
|
+
}
|
|
197
|
+
.btn{
|
|
198
|
+
padding:6px 14px;border:2px solid var(--border2);
|
|
199
|
+
background:var(--surf2);color:var(--text);
|
|
200
|
+
font-family:var(--pixel);font-size:16px;cursor:pointer;
|
|
201
|
+
transition:background .1s,border-color .1s;letter-spacing:.3px;
|
|
202
|
+
-webkit-font-smoothing:none;
|
|
203
|
+
}
|
|
204
|
+
.btn:hover{background:var(--surf3);border-color:rgba(255,255,255,.3)}
|
|
205
|
+
.btn:active{background:#222}
|
|
206
|
+
.btn.danger{border-color:rgba(255,51,51,.5);color:var(--danger)}
|
|
207
|
+
.btn.danger:hover{background:rgba(255,51,51,.1);border-color:var(--danger)}
|
|
208
|
+
.btn.success{border-color:rgba(51,204,51,.5);color:var(--success)}
|
|
209
|
+
.btn.success:hover{background:rgba(51,204,51,.1);border-color:var(--success)}
|
|
210
|
+
.btn.active-btn{border-color:rgba(255,255,255,.5);color:var(--accent)}
|
|
211
|
+
#sel-info{font-size:17px;color:var(--muted);padding:0 6px}
|
|
212
|
+
.spacer{flex:1}
|
|
213
|
+
|
|
214
|
+
/* ── CONTENT / PANELS ────────────────────────────────────────────────────────── */
|
|
215
|
+
#content{flex:1;overflow:hidden;position:relative}
|
|
216
|
+
.panel{display:none;height:100%;overflow:hidden;flex-direction:column}
|
|
217
|
+
.panel.active{display:flex}
|
|
218
|
+
|
|
219
|
+
/* ── SESSIONS TABLE ──────────────────────────────────────────────────────────── */
|
|
220
|
+
#tbl-wrap{flex:1;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column}
|
|
221
|
+
#tbl-body{flex:1;display:flex;flex-direction:column}
|
|
222
|
+
|
|
223
|
+
.tbl-hdr,.tbl-row{
|
|
224
|
+
display:grid;
|
|
225
|
+
grid-template-columns:40px 80px 1fr 80px 80px 150px 82px;
|
|
226
|
+
align-items:center;gap:0 10px;padding:0 16px;
|
|
227
|
+
}
|
|
228
|
+
.tbl-hdr{
|
|
229
|
+
height:36px;position:sticky;top:0;z-index:5;
|
|
230
|
+
background:var(--surf2);border-bottom:2px solid var(--border2);
|
|
231
|
+
font-size:14px;letter-spacing:1.5px;color:var(--dim);
|
|
232
|
+
text-transform:uppercase;flex-shrink:0;
|
|
233
|
+
}
|
|
234
|
+
.tbl-row{
|
|
235
|
+
height:var(--row-h);border-bottom:1px solid rgba(255,255,255,.04);
|
|
236
|
+
cursor:pointer;transition:background .08s;
|
|
237
|
+
}
|
|
238
|
+
.tbl-row:hover{background:rgba(255,255,255,.04)}
|
|
239
|
+
.tbl-row.selected{background:rgba(255,255,255,.08)}
|
|
240
|
+
.tbl-row input[type=checkbox]{
|
|
241
|
+
width:18px;height:18px;cursor:pointer;accent-color:#fff;
|
|
242
|
+
border:2px solid var(--dim);
|
|
243
|
+
}
|
|
244
|
+
.tbl-row .col-title{
|
|
245
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
246
|
+
font-size:19px;color:var(--text);
|
|
247
|
+
}
|
|
248
|
+
.tbl-row .col-agent,.tbl-row .col-msgs,.tbl-row .col-size,.tbl-row .col-date{
|
|
249
|
+
font-size:16px;color:var(--muted);
|
|
250
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
251
|
+
}
|
|
252
|
+
.agent-badge{
|
|
253
|
+
font-size:13px;padding:2px 6px;
|
|
254
|
+
border:1px solid var(--border2);background:var(--surf3);color:var(--text);
|
|
255
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
256
|
+
letter-spacing:.3px;
|
|
257
|
+
}
|
|
258
|
+
.row-actions{
|
|
259
|
+
display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .1s;
|
|
260
|
+
}
|
|
261
|
+
.tbl-row:hover .row-actions{opacity:1}
|
|
262
|
+
.act-btn{
|
|
263
|
+
background:var(--surf3);border:1px solid var(--border);
|
|
264
|
+
color:var(--muted);cursor:pointer;padding:3px 7px;
|
|
265
|
+
font-family:var(--pixel);font-size:15px;line-height:1;
|
|
266
|
+
-webkit-font-smoothing:none;
|
|
267
|
+
}
|
|
268
|
+
.act-btn:hover{color:var(--accent);border-color:var(--border2);background:var(--surf2)}
|
|
269
|
+
.act-btn.del:hover{color:var(--danger);border-color:rgba(255,51,51,.4)}
|
|
270
|
+
|
|
271
|
+
/* ── GROUP HEADERS ───────────────────────────────────────────────────────────── */
|
|
272
|
+
.group-hdr{
|
|
273
|
+
display:flex;align-items:center;gap:10px;
|
|
274
|
+
padding:8px 16px;
|
|
275
|
+
background:var(--surf2);
|
|
276
|
+
border-bottom:1px solid var(--border);
|
|
277
|
+
border-top:2px solid var(--border);
|
|
278
|
+
font-size:18px;cursor:pointer;user-select:none;flex-shrink:0;
|
|
279
|
+
}
|
|
280
|
+
.group-hdr .g-proj{color:var(--text);font-size:19px}
|
|
281
|
+
.group-hdr .g-agent{font-size:14px;color:var(--muted)}
|
|
282
|
+
.group-hdr .g-count{font-size:15px;color:var(--dim)}
|
|
283
|
+
.group-hdr .g-chev{margin-left:auto;font-size:14px;transition:transform .15s;color:var(--muted)}
|
|
284
|
+
.group-hdr.collapsed .g-chev{transform:rotate(-90deg)}
|
|
285
|
+
|
|
286
|
+
/* ── PAGINATION ──────────────────────────────────────────────────────────────── */
|
|
287
|
+
#pagination{
|
|
288
|
+
display:flex;align-items:center;justify-content:center;gap:12px;
|
|
289
|
+
padding:12px 16px;border-top:1px solid var(--border);
|
|
290
|
+
flex-shrink:0;font-size:17px;color:var(--muted);
|
|
291
|
+
}
|
|
292
|
+
#pagination .btn{font-size:16px}
|
|
293
|
+
|
|
294
|
+
/* ── EMPTY / LOADING ─────────────────────────────────────────────────────────── */
|
|
295
|
+
.empty{
|
|
296
|
+
display:flex;flex-direction:column;align-items:center;
|
|
297
|
+
justify-content:center;flex:1;gap:16px;
|
|
298
|
+
color:var(--muted);font-size:22px;
|
|
299
|
+
}
|
|
300
|
+
.empty .e-icon{font-size:40px;opacity:.25}
|
|
301
|
+
.loading{
|
|
302
|
+
display:flex;flex-direction:column;align-items:center;
|
|
303
|
+
justify-content:center;flex:1;gap:16px;
|
|
304
|
+
color:var(--muted);font-size:20px;
|
|
305
|
+
}
|
|
306
|
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:.2}}
|
|
307
|
+
.loading-dots::after{
|
|
308
|
+
content:'...';animation:blink 1.2s infinite;
|
|
309
|
+
display:inline-block;width:2em;text-align:left;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ── CONTEXT BANNER ──────────────────────────────────────────────────────────── */
|
|
313
|
+
.ctx-banner{
|
|
314
|
+
width:100%;padding:8px 16px;font-size:15px;color:var(--muted);
|
|
315
|
+
border-bottom:1px solid var(--border);letter-spacing:.5px;flex-shrink:0;
|
|
316
|
+
}
|
|
317
|
+
.ctx-banner .ctx-label{color:var(--accent);letter-spacing:1px}
|
|
318
|
+
|
|
319
|
+
/* ── USAGE PANEL ─────────────────────────────────────────────────────────────── */
|
|
320
|
+
#usage-panel{overflow-y:auto;display:block;padding:0}
|
|
321
|
+
|
|
322
|
+
.stat-grid{
|
|
323
|
+
display:grid;
|
|
324
|
+
grid-template-columns:repeat(auto-fill,minmax(170px,1fr));
|
|
325
|
+
gap:1px;background:var(--border);
|
|
326
|
+
border-bottom:2px solid var(--border2);
|
|
327
|
+
}
|
|
328
|
+
.stat-card{
|
|
329
|
+
background:var(--surf);padding:14px 18px;
|
|
330
|
+
}
|
|
331
|
+
.stat-card .s-label{
|
|
332
|
+
font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);
|
|
333
|
+
}
|
|
334
|
+
.stat-card .s-value{
|
|
335
|
+
font-size:26px;font-weight:400;margin-top:4px;color:var(--accent);
|
|
336
|
+
}
|
|
337
|
+
.stat-card .s-sub{font-size:14px;color:var(--muted);margin-top:1px}
|
|
338
|
+
|
|
339
|
+
.u-section{
|
|
340
|
+
border-bottom:1px solid var(--border);
|
|
341
|
+
}
|
|
342
|
+
.u-section h3{
|
|
343
|
+
font-size:14px;color:var(--muted);
|
|
344
|
+
text-transform:uppercase;letter-spacing:1px;
|
|
345
|
+
padding:10px 16px;border-bottom:1px solid var(--border);
|
|
346
|
+
position:sticky;top:0;background:var(--surf2);z-index:2;
|
|
347
|
+
}
|
|
348
|
+
.u-table{width:100%;border-collapse:collapse;font-size:18px}
|
|
349
|
+
.u-table th{
|
|
350
|
+
text-align:left;padding:8px 16px;
|
|
351
|
+
border-bottom:1px solid var(--border);
|
|
352
|
+
font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px;
|
|
353
|
+
position:sticky;top:39px;background:var(--surf);z-index:1;
|
|
354
|
+
}
|
|
355
|
+
.u-table td{padding:10px 16px;border-bottom:1px solid rgba(255,255,255,.03)}
|
|
356
|
+
.u-table tr:last-child td{border-bottom:none}
|
|
357
|
+
.u-table tr:hover td{background:rgba(255,255,255,.03)}
|
|
358
|
+
|
|
359
|
+
/* ── TRASH / BACKUPS ─────────────────────────────────────────────────────────── */
|
|
360
|
+
.list-panel-wrap{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}
|
|
361
|
+
.meta-card{
|
|
362
|
+
background:var(--surf);border:1px solid var(--border);
|
|
363
|
+
padding:12px 16px;display:flex;align-items:center;gap:12px;
|
|
364
|
+
}
|
|
365
|
+
.meta-card:hover{border-color:var(--border2);background:var(--surf2)}
|
|
366
|
+
.meta-body{flex:1;min-width:0}
|
|
367
|
+
.meta-title{
|
|
368
|
+
font-size:19px;color:var(--text);
|
|
369
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
370
|
+
}
|
|
371
|
+
.meta-sub{font-size:15px;color:var(--muted);margin-top:3px}
|
|
372
|
+
.meta-actions{display:flex;gap:8px;flex-shrink:0}
|
|
373
|
+
|
|
374
|
+
/* ── TOAST ───────────────────────────────────────────────────────────────────── */
|
|
375
|
+
#toast{
|
|
376
|
+
position:fixed;bottom:50px;left:50%;transform:translateX(-50%);
|
|
377
|
+
background:var(--surf2);border:2px solid var(--border2);
|
|
378
|
+
color:var(--text);padding:10px 20px;
|
|
379
|
+
font-family:var(--pixel);font-size:18px;z-index:9999;
|
|
380
|
+
opacity:0;transition:opacity .15s;pointer-events:none;
|
|
381
|
+
-webkit-font-smoothing:none;white-space:nowrap;
|
|
382
|
+
}
|
|
383
|
+
#toast.show{opacity:1}
|
|
384
|
+
#toast.ok{border-color:rgba(51,204,51,.6);color:var(--success)}
|
|
385
|
+
#toast.err{border-color:rgba(255,51,51,.6);color:var(--danger)}
|
|
386
|
+
|
|
387
|
+
/* ── SCROLLBAR ───────────────────────────────────────────────────────────────── */
|
|
388
|
+
::-webkit-scrollbar{width:8px;height:8px}
|
|
389
|
+
::-webkit-scrollbar-track{background:var(--bg)}
|
|
390
|
+
::-webkit-scrollbar-thumb{background:var(--surf3);border:1px solid var(--border)}
|
|
391
|
+
::-webkit-scrollbar-thumb:hover{background:#222}
|
|
392
|
+
`
|