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 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 = db.prepare('SELECT DISTINCT agent FROM sessions WHERE agent IS NOT NULL').all().map(r => r.agent);
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
- 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(q);
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
- let sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
284
- FROM events_fts fts
285
- JOIN events e ON e.rowid = fts.rowid
286
- JOIN sessions s ON s.id = e.session_id
287
- WHERE events_fts MATCH ?`;
288
- const params = [q];
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: `claude-${proj}` });
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: `claude-${proj}` });
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 indexFile(db, filePath, agentName, stmts, archiveMode) {
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
- stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Audit trail and search engine for AI agent sessions",
5
5
  "main": "index.js",
6
6
  "bin": {
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
- ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
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
- ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
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 ? `<div class="section-label">Sessions Paths</div>
401
- <div style="font-size:13px;color:var(--text2);font-family:var(--mono)">
402
- ${data.sessionDirs.map(d => {
403
- const display = d.path.replace(/^\/home\/[^/]+/, '~').replace(/^\/Users\/[^/]+/, '~');
404
- return `<div style="margin-bottom:4px">๐Ÿ“‚ ${escHtml(display)} <span style="color:var(--accent)">(${escHtml(d.agent)})</span></div>`;
405
- }).join('')}
406
- </div>` : ''}
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
  }