agentacta 1.1.3 โ 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/config.js +10 -1
- package/db.js +2 -1
- package/index.js +56 -8
- package/indexer.js +60 -7
- package/package.json +1 -1
- package/public/app.js +95 -9
- package/public/style.css +61 -0
package/README.md
CHANGED
|
@@ -108,7 +108,8 @@ On first run, AgentActa creates a config file with sensible defaults at `~/.conf
|
|
|
108
108
|
"port": 4003,
|
|
109
109
|
"storage": "reference",
|
|
110
110
|
"sessionsPath": null,
|
|
111
|
-
"dbPath": "./agentacta.db"
|
|
111
|
+
"dbPath": "./agentacta.db",
|
|
112
|
+
"projectAliases": {}
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -126,6 +127,7 @@ On first run, AgentActa creates a config file with sensible defaults at `~/.conf
|
|
|
126
127
|
| `AGENTACTA_SESSIONS_PATH` | Auto-detected | Custom sessions directory |
|
|
127
128
|
| `AGENTACTA_DB_PATH` | `./agentacta.db` | Database file location |
|
|
128
129
|
| `AGENTACTA_STORAGE` | `reference` | Storage mode (`reference` or `archive`) |
|
|
130
|
+
| `AGENTACTA_PROJECT_ALIASES_JSON` | unset | JSON object mapping inferred project names (e.g. `{"old-name":"new-name"}`) |
|
|
129
131
|
|
|
130
132
|
## API
|
|
131
133
|
|
package/config.js
CHANGED
|
@@ -18,7 +18,8 @@ const DEFAULTS = {
|
|
|
18
18
|
port: 4003,
|
|
19
19
|
storage: 'reference',
|
|
20
20
|
sessionsPath: null,
|
|
21
|
-
dbPath: './agentacta.db'
|
|
21
|
+
dbPath: './agentacta.db',
|
|
22
|
+
projectAliases: {}
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
function loadConfig() {
|
|
@@ -45,9 +46,17 @@ function loadConfig() {
|
|
|
45
46
|
if (process.env.AGENTACTA_STORAGE) config.storage = process.env.AGENTACTA_STORAGE;
|
|
46
47
|
if (process.env.AGENTACTA_SESSIONS_PATH) config.sessionsPath = process.env.AGENTACTA_SESSIONS_PATH;
|
|
47
48
|
if (process.env.AGENTACTA_DB_PATH) config.dbPath = process.env.AGENTACTA_DB_PATH;
|
|
49
|
+
if (process.env.AGENTACTA_PROJECT_ALIASES_JSON) {
|
|
50
|
+
try {
|
|
51
|
+
config.projectAliases = JSON.parse(process.env.AGENTACTA_PROJECT_ALIASES_JSON);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Warning: Could not parse AGENTACTA_PROJECT_ALIASES_JSON:', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
// Resolve dbPath relative to cwd
|
|
50
58
|
config.dbPath = path.resolve(config.dbPath);
|
|
59
|
+
if (!config.projectAliases || typeof config.projectAliases !== 'object') config.projectAliases = {};
|
|
51
60
|
|
|
52
61
|
return config;
|
|
53
62
|
}
|
package/db.js
CHANGED
|
@@ -115,6 +115,7 @@ function init(dbPath) {
|
|
|
115
115
|
if (!cols.includes('cache_read_tokens')) db.exec("ALTER TABLE sessions ADD COLUMN cache_read_tokens INTEGER DEFAULT 0");
|
|
116
116
|
if (!cols.includes('cache_write_tokens')) db.exec("ALTER TABLE sessions ADD COLUMN cache_write_tokens INTEGER DEFAULT 0");
|
|
117
117
|
if (!cols.includes('models')) db.exec("ALTER TABLE sessions ADD COLUMN models TEXT");
|
|
118
|
+
if (!cols.includes('projects')) db.exec("ALTER TABLE sessions ADD COLUMN projects TEXT");
|
|
118
119
|
|
|
119
120
|
db.close();
|
|
120
121
|
}
|
|
@@ -127,7 +128,7 @@ function createStmts(db) {
|
|
|
127
128
|
deleteSession: db.prepare('DELETE FROM sessions WHERE id = ?'),
|
|
128
129
|
deleteFileActivity: db.prepare('DELETE FROM file_activity WHERE session_id = ?'),
|
|
129
130
|
insertEvent: db.prepare(`INSERT OR REPLACE INTO events (id, session_id, timestamp, type, role, content, tool_name, tool_args, tool_result) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
130
|
-
upsertSession: db.prepare(`INSERT OR REPLACE INTO sessions (id, start_time, end_time, message_count, tool_count, model, summary, agent, session_type, total_cost, total_tokens, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, initial_prompt, first_message_id, first_message_timestamp, models) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
131
|
+
upsertSession: db.prepare(`INSERT OR REPLACE INTO sessions (id, start_time, end_time, message_count, tool_count, model, summary, agent, session_type, total_cost, total_tokens, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, initial_prompt, first_message_id, first_message_timestamp, models, projects) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
131
132
|
upsertState: db.prepare(`INSERT OR REPLACE INTO index_state (file_path, last_offset, last_modified) VALUES (?, ?, ?)`),
|
|
132
133
|
insertFileActivity: db.prepare(`INSERT INTO file_activity (session_id, file_path, operation, timestamp) VALUES (?, ?, ?, ?)`),
|
|
133
134
|
deleteArchive: db.prepare('DELETE FROM archive WHERE session_id = ?'),
|
package/index.js
CHANGED
|
@@ -97,6 +97,31 @@ function getDbSize() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function normalizeAgentLabel(agent) {
|
|
101
|
+
if (!agent) return agent;
|
|
102
|
+
if (agent === 'main') return 'openclaw-main';
|
|
103
|
+
if (agent.startsWith('claude-') || agent.startsWith('claude--')) return 'claude-code';
|
|
104
|
+
return agent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function looksLikeSessionId(q) {
|
|
108
|
+
const s = (q || '').trim();
|
|
109
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toFtsQuery(q) {
|
|
113
|
+
const s = (q || '').trim();
|
|
114
|
+
if (!s) return '';
|
|
115
|
+
// Quote each token so dashes and punctuation don't break FTS parsing.
|
|
116
|
+
// Example: abc-def -> "abc-def"
|
|
117
|
+
const tokens = s.match(/"[^"]+"|\S+/g) || [];
|
|
118
|
+
return tokens
|
|
119
|
+
.map(t => t.replace(/^"|"$/g, '').replace(/"/g, '""'))
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map(t => `"${t}"`)
|
|
122
|
+
.join(' AND ');
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
// Init DB and start watcher
|
|
101
126
|
init();
|
|
102
127
|
const db = open();
|
|
@@ -197,7 +222,11 @@ const server = http.createServer((req, res) => {
|
|
|
197
222
|
const tools = db.prepare("SELECT DISTINCT tool_name FROM events WHERE tool_name IS NOT NULL").all().map(r => r.tool_name);
|
|
198
223
|
const dateRange = db.prepare('SELECT MIN(start_time) as earliest, MAX(start_time) as latest FROM sessions').get();
|
|
199
224
|
const costData = db.prepare('SELECT SUM(total_cost) as cost, SUM(total_tokens) as tokens FROM sessions').get();
|
|
200
|
-
const agents =
|
|
225
|
+
const agents = [...new Set(
|
|
226
|
+
db.prepare('SELECT DISTINCT agent FROM sessions WHERE agent IS NOT NULL').all()
|
|
227
|
+
.map(r => normalizeAgentLabel(r.agent))
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
)];
|
|
201
230
|
const dbSize = getDbSize();
|
|
202
231
|
json(res, { sessions, events, messages, toolCalls, uniqueTools: tools.length, tools, dateRange, totalCost: costData.cost || 0, totalTokens: costData.tokens || 0, agents, storageMode: config.storage, dbSize, sessionDirs: sessionDirs.map(d => ({ path: d.path, agent: d.agent })) });
|
|
203
232
|
}
|
|
@@ -256,7 +285,12 @@ const server = http.createServer((req, res) => {
|
|
|
256
285
|
if (!q) { json(res, { error: 'No query' }, 400); return; }
|
|
257
286
|
let results;
|
|
258
287
|
try {
|
|
259
|
-
|
|
288
|
+
if (looksLikeSessionId(q)) {
|
|
289
|
+
results = db.prepare(`SELECT e.*, s.start_time as session_start, s.summary as session_summary FROM events e JOIN sessions s ON s.id = e.session_id WHERE e.session_id = ? ORDER BY e.timestamp DESC LIMIT 200`).all(q.trim());
|
|
290
|
+
} else {
|
|
291
|
+
const ftsQuery = toFtsQuery(q);
|
|
292
|
+
results = db.prepare(`SELECT e.*, s.start_time as session_start, s.summary as session_summary FROM events_fts fts JOIN events e ON e.rowid = fts.rowid JOIN sessions s ON s.id = e.session_id WHERE events_fts MATCH ? ORDER BY e.timestamp DESC LIMIT 200`).all(ftsQuery);
|
|
293
|
+
}
|
|
260
294
|
} catch (err) { json(res, { error: 'Invalid search query' }, 400); return; }
|
|
261
295
|
if (format === 'md') {
|
|
262
296
|
let md = `# Search Results: "${q}"\n\n${results.length} results\n\n`;
|
|
@@ -280,18 +314,32 @@ const server = http.createServer((req, res) => {
|
|
|
280
314
|
|
|
281
315
|
if (!q) { json(res, { results: [], total: 0 }); }
|
|
282
316
|
else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
const isSessionLookup = looksLikeSessionId(q);
|
|
318
|
+
let sql;
|
|
319
|
+
const params = [];
|
|
320
|
+
|
|
321
|
+
if (isSessionLookup) {
|
|
322
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
323
|
+
FROM events e
|
|
324
|
+
JOIN sessions s ON s.id = e.session_id
|
|
325
|
+
WHERE e.session_id = ?`;
|
|
326
|
+
params.push(q.trim());
|
|
327
|
+
} else {
|
|
328
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
329
|
+
FROM events_fts fts
|
|
330
|
+
JOIN events e ON e.rowid = fts.rowid
|
|
331
|
+
JOIN sessions s ON s.id = e.session_id
|
|
332
|
+
WHERE events_fts MATCH ?`;
|
|
333
|
+
params.push(toFtsQuery(q));
|
|
334
|
+
}
|
|
335
|
+
|
|
289
336
|
if (type) { sql += ` AND e.type = ?`; params.push(type); }
|
|
290
337
|
if (role) { sql += ` AND e.role = ?`; params.push(role); }
|
|
291
338
|
if (from) { sql += ` AND e.timestamp >= ?`; params.push(from); }
|
|
292
339
|
if (to) { sql += ` AND e.timestamp <= ?`; params.push(to); }
|
|
293
340
|
sql += ` ORDER BY e.timestamp DESC LIMIT ?`;
|
|
294
341
|
params.push(limit);
|
|
342
|
+
|
|
295
343
|
try {
|
|
296
344
|
const results = db.prepare(sql).all(...params);
|
|
297
345
|
json(res, { results, total: results.length });
|
package/indexer.js
CHANGED
|
@@ -38,12 +38,12 @@ function discoverSessionDirs(config) {
|
|
|
38
38
|
// Claude Code: JSONL files directly in project dir
|
|
39
39
|
if (fs.existsSync(projDir) && fs.statSync(projDir).isDirectory()) {
|
|
40
40
|
const hasJsonl = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
41
|
-
if (hasJsonl) dirs.push({ path: projDir, agent:
|
|
41
|
+
if (hasJsonl) dirs.push({ path: projDir, agent: 'claude-code' });
|
|
42
42
|
}
|
|
43
43
|
// Also check sessions/ subdirectory (future-proofing)
|
|
44
44
|
const sp = path.join(projDir, 'sessions');
|
|
45
45
|
if (fs.existsSync(sp) && fs.statSync(sp).isDirectory()) {
|
|
46
|
-
dirs.push({ path: sp, agent:
|
|
46
|
+
dirs.push({ path: sp, agent: 'claude-code' });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -107,7 +107,45 @@ function extractFilePaths(toolName, toolArgs) {
|
|
|
107
107
|
return paths;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function
|
|
110
|
+
function aliasProject(project, config) {
|
|
111
|
+
if (!project) return project;
|
|
112
|
+
const aliases = (config && config.projectAliases && typeof config.projectAliases === 'object') ? config.projectAliases : {};
|
|
113
|
+
return aliases[project] || project;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractProjectFromPath(filePath, config) {
|
|
117
|
+
if (!filePath || typeof filePath !== 'string') return null;
|
|
118
|
+
|
|
119
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
120
|
+
|
|
121
|
+
// Relative paths are usually from workspace cwd -> treat as workspace activity
|
|
122
|
+
if (!normalized.startsWith('/') && !normalized.startsWith('~')) return aliasProject('workspace', config);
|
|
123
|
+
|
|
124
|
+
let rel = normalized
|
|
125
|
+
.replace(/^\/home\/[^/]+\//, '')
|
|
126
|
+
.replace(/^\/Users\/[^/]+\//, '')
|
|
127
|
+
.replace(/^~\//, '');
|
|
128
|
+
|
|
129
|
+
const parts = rel.split('/').filter(Boolean);
|
|
130
|
+
if (!parts.length) return null;
|
|
131
|
+
|
|
132
|
+
// Common repo location: ~/Developer/<repo>/...
|
|
133
|
+
if (parts[0] === 'Developer' && parts[1]) return aliasProject(parts[1], config);
|
|
134
|
+
|
|
135
|
+
// OpenClaw workspace and agent stores
|
|
136
|
+
if (parts[0] === '.openclaw' && parts[1] === 'workspace') return aliasProject('workspace', config);
|
|
137
|
+
if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2]) return aliasProject(`agent:${parts[2]}`, config);
|
|
138
|
+
|
|
139
|
+
// Claude Code projects
|
|
140
|
+
if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return aliasProject(`claude:${parts[2]}`, config);
|
|
141
|
+
|
|
142
|
+
// Shared files area
|
|
143
|
+
if (parts[0] === 'Shared') return aliasProject('shared', config);
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
111
149
|
const stat = fs.statSync(filePath);
|
|
112
150
|
const mtime = stat.mtime.toISOString();
|
|
113
151
|
|
|
@@ -177,6 +215,13 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
|
177
215
|
|
|
178
216
|
const pendingEvents = [];
|
|
179
217
|
const fileActivities = [];
|
|
218
|
+
const projectCounts = new Map();
|
|
219
|
+
|
|
220
|
+
// Seed project from session cwd when available (helps chat-only sessions)
|
|
221
|
+
if (firstLine && firstLine.cwd) {
|
|
222
|
+
const p = extractProjectFromPath(firstLine.cwd);
|
|
223
|
+
if (p) projectCounts.set(p, 1);
|
|
224
|
+
}
|
|
180
225
|
|
|
181
226
|
for (const line of lines) {
|
|
182
227
|
let obj;
|
|
@@ -272,6 +317,9 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
|
272
317
|
: tool.name.includes('edit') || tool.name === 'Edit' ? 'edit'
|
|
273
318
|
: 'read';
|
|
274
319
|
fileActivities.push([sessionId, fp, op, ts]);
|
|
320
|
+
|
|
321
|
+
const project = extractProjectFromPath(fp, config);
|
|
322
|
+
if (project) projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
|
|
275
323
|
}
|
|
276
324
|
}
|
|
277
325
|
}
|
|
@@ -301,7 +349,12 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
|
301
349
|
}
|
|
302
350
|
|
|
303
351
|
const modelsJson = modelsSet.size > 0 ? JSON.stringify([...modelsSet]) : null;
|
|
304
|
-
|
|
352
|
+
const projects = [...projectCounts.entries()]
|
|
353
|
+
.sort((a, b) => (b[1] - a[1]) || a[0].localeCompare(b[0]))
|
|
354
|
+
.map(([name]) => name);
|
|
355
|
+
const projectsJson = projects.length > 0 ? JSON.stringify(projects) : null;
|
|
356
|
+
|
|
357
|
+
stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson, projectsJson);
|
|
305
358
|
for (const ev of pendingEvents) stmts.insertEvent.run(...ev);
|
|
306
359
|
for (const fa of fileActivities) stmts.insertFileActivity.run(...fa);
|
|
307
360
|
|
|
@@ -344,7 +397,7 @@ function run() {
|
|
|
344
397
|
const indexMany = db.transaction(() => {
|
|
345
398
|
let indexed = 0;
|
|
346
399
|
for (const f of allFiles) {
|
|
347
|
-
const result = indexFile(db, f.path, f.agent, stmts, archiveMode);
|
|
400
|
+
const result = indexFile(db, f.path, f.agent, stmts, archiveMode, config);
|
|
348
401
|
if (!result.skipped) {
|
|
349
402
|
indexed++;
|
|
350
403
|
if (indexed % 10 === 0) process.stdout.write('.');
|
|
@@ -369,7 +422,7 @@ function run() {
|
|
|
369
422
|
if (!fs.existsSync(filePath)) return;
|
|
370
423
|
setTimeout(() => {
|
|
371
424
|
try {
|
|
372
|
-
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode);
|
|
425
|
+
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
373
426
|
if (!result.skipped) console.log(`Re-indexed: ${filename} (${dir.agent})`);
|
|
374
427
|
} catch (err) {
|
|
375
428
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
@@ -390,7 +443,7 @@ function indexAll(db, config) {
|
|
|
390
443
|
for (const dir of sessionDirs) {
|
|
391
444
|
const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
|
|
392
445
|
for (const file of files) {
|
|
393
|
-
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode);
|
|
446
|
+
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode, config);
|
|
394
447
|
if (!result.skipped) totalSessions++;
|
|
395
448
|
}
|
|
396
449
|
}
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -57,6 +57,11 @@ function truncate(s, n = 200) {
|
|
|
57
57
|
return s.length > n ? s.slice(0, n) + 'โฆ' : s;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function shortSessionId(id) {
|
|
61
|
+
if (!id) return '';
|
|
62
|
+
return id.length > 24 ? `${id.slice(0, 8)}โฆ${id.slice(-8)}` : id;
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
// Removed jumpToInitialPrompt - now handled within session view
|
|
61
66
|
|
|
62
67
|
function badgeClass(type, role) {
|
|
@@ -113,6 +118,21 @@ function fmtTimeOnly(ts) {
|
|
|
113
118
|
return new Date(ts).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
function normalizeAgentLabel(a) {
|
|
122
|
+
if (!a) return a;
|
|
123
|
+
if (a === 'main') return 'openclaw-main';
|
|
124
|
+
if (a.startsWith('claude-') || a.startsWith('claude--')) return 'claude-code';
|
|
125
|
+
return a;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderProjectTags(s) {
|
|
129
|
+
let projects = [];
|
|
130
|
+
if (s.projects) {
|
|
131
|
+
try { projects = JSON.parse(s.projects); } catch {}
|
|
132
|
+
}
|
|
133
|
+
return projects.map(p => `<span class="session-project">${escHtml(p)}</span>`).join('');
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
function renderModelTags(s) {
|
|
117
137
|
// Prefer models array if present, fall back to single model
|
|
118
138
|
let models = [];
|
|
@@ -132,7 +152,8 @@ function renderSessionItem(s) {
|
|
|
132
152
|
<div class="session-header">
|
|
133
153
|
<span class="session-time">${timeRange} ยท ${duration}</span>
|
|
134
154
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
135
|
-
${
|
|
155
|
+
${renderProjectTags(s)}
|
|
156
|
+
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
136
157
|
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
137
158
|
${renderModelTags(s)}
|
|
138
159
|
</span>
|
|
@@ -308,6 +329,12 @@ async function viewSession(id) {
|
|
|
308
329
|
let html = `
|
|
309
330
|
<div class="back-btn" id="backBtn">โ Back</div>
|
|
310
331
|
<div class="page-title">Session</div>
|
|
332
|
+
<div class="session-id-row">
|
|
333
|
+
<span class="session-id-label">ID</span>
|
|
334
|
+
<span class="session-id-value" title="${escHtml(id)}">${escHtml(id)}</span>
|
|
335
|
+
<button class="session-id-copy" id="copySessionId" title="Copy session ID">โง</button>
|
|
336
|
+
<span class="session-id-copied" id="copyConfirm">Copied!</span>
|
|
337
|
+
</div>
|
|
311
338
|
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
|
|
312
339
|
${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">โ๏ธ Initial Prompt</button>` : ''}
|
|
313
340
|
${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">๐ฆ JSONL</a>` : ''}
|
|
@@ -318,7 +345,8 @@ async function viewSession(id) {
|
|
|
318
345
|
<div class="session-header">
|
|
319
346
|
<span class="session-time">${fmtDate(s.start_time)} ยท ${fmtTimeShort(s.start_time)} โ ${fmtTimeShort(s.end_time)}</span>
|
|
320
347
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
321
|
-
${
|
|
348
|
+
${renderProjectTags(s)}
|
|
349
|
+
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
322
350
|
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
323
351
|
${renderModelTags(s)}
|
|
324
352
|
</span>
|
|
@@ -342,6 +370,46 @@ async function viewSession(id) {
|
|
|
342
370
|
else viewSessions();
|
|
343
371
|
});
|
|
344
372
|
|
|
373
|
+
$('#copySessionId').addEventListener('click', async () => {
|
|
374
|
+
const conf = $('#copyConfirm');
|
|
375
|
+
const showCopied = () => {
|
|
376
|
+
conf.textContent = 'Copied!';
|
|
377
|
+
conf.classList.add('show');
|
|
378
|
+
setTimeout(() => conf.classList.remove('show'), 1500);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
383
|
+
await navigator.clipboard.writeText(id);
|
|
384
|
+
showCopied();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Fallback for non-secure contexts (http/local)
|
|
389
|
+
const ta = document.createElement('textarea');
|
|
390
|
+
ta.value = id;
|
|
391
|
+
ta.setAttribute('readonly', '');
|
|
392
|
+
ta.style.position = 'fixed';
|
|
393
|
+
ta.style.opacity = '0';
|
|
394
|
+
ta.style.pointerEvents = 'none';
|
|
395
|
+
document.body.appendChild(ta);
|
|
396
|
+
ta.focus();
|
|
397
|
+
ta.select();
|
|
398
|
+
const ok = document.execCommand('copy');
|
|
399
|
+
document.body.removeChild(ta);
|
|
400
|
+
|
|
401
|
+
if (ok) showCopied();
|
|
402
|
+
else throw new Error('Copy failed');
|
|
403
|
+
} catch {
|
|
404
|
+
conf.textContent = 'Press Ctrl/Cmd+C';
|
|
405
|
+
conf.classList.add('show');
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
conf.classList.remove('show');
|
|
408
|
+
conf.textContent = 'Copied!';
|
|
409
|
+
}, 1800);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
345
413
|
const jumpBtn = $('#jumpToStartBtn');
|
|
346
414
|
if (jumpBtn) {
|
|
347
415
|
jumpBtn.addEventListener('click', () => {
|
|
@@ -397,13 +465,31 @@ async function viewStats() {
|
|
|
397
465
|
<div class="stat-card"><div class="label">DB Size</div><div class="value" style="font-size:18px">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
|
|
398
466
|
</div>
|
|
399
467
|
|
|
400
|
-
${data.sessionDirs && data.sessionDirs.length ?
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
468
|
+
${data.sessionDirs && data.sessionDirs.length ? (() => {
|
|
469
|
+
const dirs = data.sessionDirs || [];
|
|
470
|
+
const claudeDirs = dirs.filter(d => d.agent === 'claude-code' || /^claude-/.test(d.agent || ''));
|
|
471
|
+
const otherDirs = dirs.filter(d => !(d.agent === 'claude-code' || /^claude-/.test(d.agent || '')));
|
|
472
|
+
|
|
473
|
+
const lines = [];
|
|
474
|
+
|
|
475
|
+
if (claudeDirs.length) {
|
|
476
|
+
const projects = new Set();
|
|
477
|
+
for (const d of claudeDirs) {
|
|
478
|
+
const m = (d.path || '').match(/[\\/]\.claude[\\/]projects[\\/]([^\\/]+)$/);
|
|
479
|
+
if (m && m[1]) projects.add(m[1]);
|
|
480
|
+
}
|
|
481
|
+
const projectCount = projects.size || claudeDirs.length;
|
|
482
|
+
lines.push(`<div style="margin-bottom:4px">๐ ~/.claude/projects/* <span style="color:var(--accent)">(claude-code ยท ${projectCount} workspace${projectCount === 1 ? '' : 's'})</span></div>`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const d of otherDirs) {
|
|
486
|
+
const display = (d.path || '').replace(/^\/home\/[^/]+/, '~').replace(/^\/Users\/[^/]+/, '~');
|
|
487
|
+
lines.push(`<div style="margin-bottom:4px">๐ ${escHtml(display)} <span style="color:var(--accent)">(${escHtml(normalizeAgentLabel(d.agent))})</span></div>`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `<div class="section-label">Sessions Paths</div>
|
|
491
|
+
<div style="font-size:13px;color:var(--text2);font-family:var(--mono)">${lines.join('')}</div>`;
|
|
492
|
+
})() : ''}
|
|
407
493
|
|
|
408
494
|
${data.agents && data.agents.length > 1 ? `<div class="section-label">Agents</div><div class="filters">${data.agents.map(a => `<span class="filter-chip">${escHtml(a)}</span>`).join('')}</div>` : ''}
|
|
409
495
|
<div class="section-label">Date Range</div>
|
package/public/style.css
CHANGED
|
@@ -180,6 +180,7 @@ body {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
.session-time { font-size: 13px; color: var(--text2); font-family: var(--mono); }
|
|
183
|
+
.session-project { font-size: 11px; color: var(--accent2); background: rgba(63,185,80,0.14); padding: 2px 8px; border-radius: 10px; }
|
|
183
184
|
.session-model { font-size: 11px; color: var(--purple); background: rgba(188,140,255,0.1); padding: 2px 8px; border-radius: 10px; }
|
|
184
185
|
.session-summary { font-size: 14px; color: var(--text); line-height: 1.4; }
|
|
185
186
|
.session-meta { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--text2); }
|
|
@@ -458,6 +459,64 @@ mark { background: rgba(210,153,34,0.3); color: var(--text); border-radius: 2px;
|
|
|
458
459
|
border-color: var(--accent2);
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
/* Session ID (copyable) */
|
|
463
|
+
.session-id-row {
|
|
464
|
+
display: flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
gap: 6px;
|
|
467
|
+
margin-bottom: 8px;
|
|
468
|
+
width: 100%;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.session-id-label {
|
|
472
|
+
font-size: 11px;
|
|
473
|
+
color: var(--text2);
|
|
474
|
+
text-transform: uppercase;
|
|
475
|
+
letter-spacing: 0.5px;
|
|
476
|
+
flex-shrink: 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.session-id-value {
|
|
480
|
+
font-family: var(--mono);
|
|
481
|
+
font-size: 12px;
|
|
482
|
+
color: var(--text2);
|
|
483
|
+
background: var(--bg3);
|
|
484
|
+
padding: 2px 8px;
|
|
485
|
+
border-radius: 4px;
|
|
486
|
+
flex: 1 1 auto;
|
|
487
|
+
min-width: 0;
|
|
488
|
+
max-width: none;
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
text-overflow: ellipsis;
|
|
491
|
+
white-space: nowrap;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.session-id-copy {
|
|
495
|
+
background: none;
|
|
496
|
+
border: 1px solid var(--border);
|
|
497
|
+
border-radius: 4px;
|
|
498
|
+
color: var(--text2);
|
|
499
|
+
cursor: pointer;
|
|
500
|
+
padding: 2px 6px;
|
|
501
|
+
font-size: 12px;
|
|
502
|
+
line-height: 1;
|
|
503
|
+
transition: all 0.15s;
|
|
504
|
+
font-family: inherit;
|
|
505
|
+
display: inline-flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.session-id-copy:hover { color: var(--text); border-color: var(--accent); }
|
|
510
|
+
|
|
511
|
+
.session-id-copied {
|
|
512
|
+
font-size: 11px;
|
|
513
|
+
color: var(--accent2);
|
|
514
|
+
opacity: 0;
|
|
515
|
+
transition: opacity 0.2s;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.session-id-copied.show { opacity: 1; }
|
|
519
|
+
|
|
461
520
|
/* File activity */
|
|
462
521
|
.file-item {
|
|
463
522
|
background: var(--bg2);
|
|
@@ -559,4 +618,6 @@ mark { background: rgba(210,153,34,0.3); color: var(--text); border-radius: 2px;
|
|
|
559
618
|
|
|
560
619
|
.result-meta { flex-wrap: wrap; }
|
|
561
620
|
.tool-args { font-size: 11px; }
|
|
621
|
+
|
|
622
|
+
.session-id-value { font-size: 11px; }
|
|
562
623
|
}
|