agentacta 1.1.4 โ†’ 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/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,13 +107,19 @@ function extractFilePaths(toolName, toolArgs) {
107
107
  return paths;
108
108
  }
109
109
 
110
- function extractProjectFromPath(filePath) {
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) {
111
117
  if (!filePath || typeof filePath !== 'string') return null;
112
118
 
113
119
  const normalized = filePath.replace(/\\/g, '/');
114
120
 
115
121
  // Relative paths are usually from workspace cwd -> treat as workspace activity
116
- if (!normalized.startsWith('/') && !normalized.startsWith('~')) return 'workspace';
122
+ if (!normalized.startsWith('/') && !normalized.startsWith('~')) return aliasProject('workspace', config);
117
123
 
118
124
  let rel = normalized
119
125
  .replace(/^\/home\/[^/]+\//, '')
@@ -124,22 +130,22 @@ function extractProjectFromPath(filePath) {
124
130
  if (!parts.length) return null;
125
131
 
126
132
  // Common repo location: ~/Developer/<repo>/...
127
- if (parts[0] === 'Developer' && parts[1]) return parts[1];
133
+ if (parts[0] === 'Developer' && parts[1]) return aliasProject(parts[1], config);
128
134
 
129
135
  // OpenClaw workspace and agent stores
130
- if (parts[0] === '.openclaw' && parts[1] === 'workspace') return 'workspace';
131
- if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2]) return `agent:${parts[2]}`;
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);
132
138
 
133
139
  // Claude Code projects
134
- if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return `claude:${parts[2]}`;
140
+ if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return aliasProject(`claude:${parts[2]}`, config);
135
141
 
136
142
  // Shared files area
137
- if (parts[0] === 'Shared') return 'shared';
143
+ if (parts[0] === 'Shared') return aliasProject('shared', config);
138
144
 
139
145
  return null;
140
146
  }
141
147
 
142
- function indexFile(db, filePath, agentName, stmts, archiveMode) {
148
+ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
143
149
  const stat = fs.statSync(filePath);
144
150
  const mtime = stat.mtime.toISOString();
145
151
 
@@ -312,7 +318,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
312
318
  : 'read';
313
319
  fileActivities.push([sessionId, fp, op, ts]);
314
320
 
315
- const project = extractProjectFromPath(fp);
321
+ const project = extractProjectFromPath(fp, config);
316
322
  if (project) projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
317
323
  }
318
324
  }
@@ -391,7 +397,7 @@ function run() {
391
397
  const indexMany = db.transaction(() => {
392
398
  let indexed = 0;
393
399
  for (const f of allFiles) {
394
- const result = indexFile(db, f.path, f.agent, stmts, archiveMode);
400
+ const result = indexFile(db, f.path, f.agent, stmts, archiveMode, config);
395
401
  if (!result.skipped) {
396
402
  indexed++;
397
403
  if (indexed % 10 === 0) process.stdout.write('.');
@@ -416,7 +422,7 @@ function run() {
416
422
  if (!fs.existsSync(filePath)) return;
417
423
  setTimeout(() => {
418
424
  try {
419
- const result = indexFile(db, filePath, dir.agent, stmts, archiveMode);
425
+ const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
420
426
  if (!result.skipped) console.log(`Re-indexed: ${filename} (${dir.agent})`);
421
427
  } catch (err) {
422
428
  console.error(`Error re-indexing ${filename}:`, err.message);
@@ -437,7 +443,7 @@ function indexAll(db, config) {
437
443
  for (const dir of sessionDirs) {
438
444
  const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
439
445
  for (const file of files) {
440
- 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);
441
447
  if (!result.skipped) totalSessions++;
442
448
  }
443
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "1.1.4",
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,13 @@ 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
+
116
128
  function renderProjectTags(s) {
117
129
  let projects = [];
118
130
  if (s.projects) {
@@ -141,7 +153,7 @@ function renderSessionItem(s) {
141
153
  <span class="session-time">${timeRange} ยท ${duration}</span>
142
154
  <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
143
155
  ${renderProjectTags(s)}
144
- ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
156
+ ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
145
157
  ${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
146
158
  ${renderModelTags(s)}
147
159
  </span>
@@ -317,6 +329,12 @@ async function viewSession(id) {
317
329
  let html = `
318
330
  <div class="back-btn" id="backBtn">โ† Back</div>
319
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>
320
338
  <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
321
339
  ${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">โ†—๏ธ Initial Prompt</button>` : ''}
322
340
  ${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">๐Ÿ“ฆ JSONL</a>` : ''}
@@ -328,7 +346,7 @@ async function viewSession(id) {
328
346
  <span class="session-time">${fmtDate(s.start_time)} ยท ${fmtTimeShort(s.start_time)} โ€“ ${fmtTimeShort(s.end_time)}</span>
329
347
  <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
330
348
  ${renderProjectTags(s)}
331
- ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
349
+ ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
332
350
  ${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
333
351
  ${renderModelTags(s)}
334
352
  </span>
@@ -352,6 +370,46 @@ async function viewSession(id) {
352
370
  else viewSessions();
353
371
  });
354
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
+
355
413
  const jumpBtn = $('#jumpToStartBtn');
356
414
  if (jumpBtn) {
357
415
  jumpBtn.addEventListener('click', () => {
@@ -407,13 +465,31 @@ async function viewStats() {
407
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>
408
466
  </div>
409
467
 
410
- ${data.sessionDirs && data.sessionDirs.length ? `<div class="section-label">Sessions Paths</div>
411
- <div style="font-size:13px;color:var(--text2);font-family:var(--mono)">
412
- ${data.sessionDirs.map(d => {
413
- const display = d.path.replace(/^\/home\/[^/]+/, '~').replace(/^\/Users\/[^/]+/, '~');
414
- return `<div style="margin-bottom:4px">๐Ÿ“‚ ${escHtml(display)} <span style="color:var(--accent)">(${escHtml(d.agent)})</span></div>`;
415
- }).join('')}
416
- </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
+ })() : ''}
417
493
 
418
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>` : ''}
419
495
  <div class="section-label">Date Range</div>
package/public/style.css CHANGED
@@ -459,6 +459,64 @@ mark { background: rgba(210,153,34,0.3); color: var(--text); border-radius: 2px;
459
459
  border-color: var(--accent2);
460
460
  }
461
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
+
462
520
  /* File activity */
463
521
  .file-item {
464
522
  background: var(--bg2);
@@ -560,4 +618,6 @@ mark { background: rgba(210,153,34,0.3); color: var(--text); border-radius: 2px;
560
618
 
561
619
  .result-meta { flex-wrap: wrap; }
562
620
  .tool-args { font-size: 11px; }
621
+
622
+ .session-id-value { font-size: 11px; }
563
623
  }