agentacta 2026.3.12-r2 → 2026.3.26

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
@@ -39,7 +39,8 @@ AgentActa gives you one place to inspect the full trail.
39
39
  - 🌗 Light and dark themes
40
40
  - 📊 Stats for sessions, messages, tools, and tokens
41
41
  - ⚡ Live indexing via file watching
42
- - 📱 Mobile-friendly UI
42
+ - 📱 Mobile-optimized UI with floating navigation
43
+ - 🏥 Session health scoring — reliability scores, issue detection, and per-signal breakdowns
43
44
  - 💡 Search suggestions based on real data
44
45
  - ⌨️ Command palette (⌘K / Ctrl+K) for quick navigation
45
46
  - 🎨 Theme settings (system, light, dark, OLED)
@@ -90,6 +91,14 @@ Session types get tagged so noisy categories are easier to spot (cron, sub-agent
90
91
 
91
92
  Pick a date, see everything that happened, newest first. Today's view updates live as new events come in.
92
93
 
94
+ ### Insights
95
+
96
+ The Insights tab surfaces session health across your entire history.
97
+
98
+ It tracks five issue signals — repeated tool loops, sessions that produced no output, high error rates, vague instructions, and incomplete sessions. Each signal is severity-scaled so scores reflect how bad the problem actually was, not just whether it occurred.
99
+
100
+ The reliability score (0–100) is the inverse of the confusion score: higher means the agent completed work cleanly. The issue rate shows what percentage of possible signal types were detected in a session.
101
+
93
102
  ### File Activity
94
103
 
95
104
  See what files were touched, how often, and by which sessions.
@@ -167,6 +176,7 @@ Default config (auto-generated on first run — session directories are detected
167
176
  | `GET /api/timeline/stream?after=<ts>` | SSE stream for live timeline updates |
168
177
  | `POST /api/maintenance` | VACUUM + WAL checkpoint (returns size before/after) |
169
178
  | `GET /api/health` | Server status, version, uptime, session count |
179
+ | `GET /api/insights` | Session health summary — reliability scores, issue counts, top flagged sessions |
170
180
  | `GET /api/export/search?q=<query>&format=md` | Export search results |
171
181
 
172
182
  ### Context API
package/config.js CHANGED
@@ -57,6 +57,8 @@ function loadConfig() {
57
57
  }
58
58
  }
59
59
 
60
+ // In demo mode, ignore file-based sessionsPath so live data doesn't bleed in
61
+ if (process.env.AGENTACTA_DEMO_MODE) delete fileConfig.sessionsPath;
60
62
  const config = { ...DEFAULTS, ...fileConfig };
61
63
 
62
64
  // Env var overrides (highest priority)
package/db.js CHANGED
@@ -103,6 +103,18 @@ function init(dbPath) {
103
103
  );
104
104
 
105
105
  CREATE INDEX IF NOT EXISTS idx_archive_session ON archive(session_id);
106
+
107
+ CREATE TABLE IF NOT EXISTS session_insights (
108
+ session_id TEXT PRIMARY KEY,
109
+ signals TEXT,
110
+ confusion_score INTEGER DEFAULT 0,
111
+ flagged INTEGER DEFAULT 0,
112
+ computed_at TEXT,
113
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
114
+ );
115
+
116
+ CREATE INDEX IF NOT EXISTS idx_insights_flagged ON session_insights(flagged);
117
+ CREATE INDEX IF NOT EXISTS idx_insights_score ON session_insights(confusion_score DESC);
106
118
  `);
107
119
 
108
120
  // Add columns if missing (migration)
package/index.js CHANGED
@@ -20,6 +20,7 @@ if (process.argv.includes('--demo')) {
20
20
  }
21
21
  process.env.AGENTACTA_SESSIONS_PATH = demoDir;
22
22
  process.env.AGENTACTA_DB_PATH = path.join(demoDir, 'demo.db');
23
+ process.env.AGENTACTA_DEMO_MODE = '1'; // signal to config.js to skip file-based sessionsPath
23
24
  console.log(`Demo mode: using sessions from ${demoDir}`);
24
25
  }
25
26
 
@@ -28,6 +29,7 @@ const { open, init, createStmts } = require('./db');
28
29
  const { discoverSessionDirs, listJsonlFiles, indexFile } = require('./indexer');
29
30
  const { attributeSessionEvents, attributeEventDelta } = require('./project-attribution');
30
31
  const { loadDeltaAttributionContext } = require('./delta-attribution-context');
32
+ const { analyzeSession, analyzeAll, getInsightsSummary } = require('./insights');
31
33
 
32
34
  const config = loadConfig();
33
35
  const PORT = config.port;
@@ -167,6 +169,14 @@ for (const dir of sessionDirs) {
167
169
  }
168
170
  }
169
171
 
172
+ // Compute insights for all indexed sessions
173
+ try {
174
+ analyzeAll(db);
175
+ console.log('Insights computed for all sessions');
176
+ } catch (err) {
177
+ console.error('Error computing insights:', err.message);
178
+ }
179
+
170
180
  console.log(`Watching ${sessionDirs.length} session directories`);
171
181
 
172
182
  // Debounce map: filePath -> timeout handle
@@ -178,11 +188,18 @@ function reindexRecursiveDir(dir) {
178
188
  try {
179
189
  const files = listJsonlFiles(dir.path, true);
180
190
  let changed = 0;
191
+ const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)');
181
192
  for (const filePath of files) {
182
193
  const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
183
194
  if (!result.skipped) {
184
195
  changed++;
185
- if (result.sessionId) sseEmitter.emit('session-update', result.sessionId);
196
+ if (result.sessionId) {
197
+ try {
198
+ const insight = analyzeSession(db, result.sessionId);
199
+ if (insight) upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at);
200
+ } catch {}
201
+ sseEmitter.emit('session-update', result.sessionId);
202
+ }
186
203
  }
187
204
  }
188
205
  if (changed > 0) console.log(`Live re-indexed ${changed} files (${dir.agent})`);
@@ -215,7 +232,10 @@ for (const dir of sessionDirs) {
215
232
  const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
216
233
  if (!result.skipped) {
217
234
  console.log(`Live re-indexed: ${filename} (${dir.agent})`);
218
- if (result.sessionId) sseEmitter.emit('session-update', result.sessionId);
235
+ if (result.sessionId) {
236
+ try { analyzeSession(db, result.sessionId); const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)'); const insight = analyzeSession(db, result.sessionId); if (insight) upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at); } catch {}
237
+ sseEmitter.emit('session-update', result.sessionId);
238
+ }
219
239
  }
220
240
  } catch (err) {
221
241
  console.error(`Error re-indexing ${filename}:`, err.message);
@@ -239,6 +259,7 @@ const server = http.createServer((req, res) => {
239
259
  if (pathname === '/api/reindex') {
240
260
  const { indexAll } = require('./indexer');
241
261
  const result = indexAll(db, config);
262
+ try { analyzeAll(db); } catch (e) { console.error('Insights recompute error:', e.message); }
242
263
  return json(res, { ok: true, sessions: result.sessions, events: result.events });
243
264
  }
244
265
 
@@ -764,6 +785,27 @@ const server = http.createServer((req, res) => {
764
785
  `).all(fp);
765
786
  json(res, { file: fp, sessions: rows });
766
787
  }
788
+ else if (pathname === '/api/insights') {
789
+ const summary = getInsightsSummary(db);
790
+ return json(res, summary);
791
+ }
792
+ else if (pathname.match(/^\/api\/insights\/session\/[^/]+$/)) {
793
+ const id = pathname.split('/')[4];
794
+ const row = db.prepare('SELECT * FROM session_insights WHERE session_id = ?').get(id);
795
+ if (!row) {
796
+ // Compute on-the-fly if not yet analyzed
797
+ const result = analyzeSession(db, id);
798
+ if (!result) return json(res, { error: 'Session not found' }, 404);
799
+ return json(res, result);
800
+ }
801
+ return json(res, {
802
+ session_id: row.session_id,
803
+ signals: JSON.parse(row.signals || '[]'),
804
+ confusion_score: row.confusion_score,
805
+ flagged: !!row.flagged,
806
+ computed_at: row.computed_at
807
+ });
808
+ }
767
809
  else if (!serveStatic(req, res)) {
768
810
  const index = path.join(PUBLIC, 'index.html');
769
811
  if (fs.existsSync(index)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "2026.3.12-r2",
3
+ "version": "2026.3.26",
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
@@ -239,6 +239,16 @@ function updateNavActive(view) {
239
239
  $$('.nav-item').forEach(i => i.classList.remove('active'));
240
240
  const navItem = $(`.nav-item[data-view="${view}"]`);
241
241
  if (navItem) navItem.classList.add('active');
242
+ // Settings gear buttons
243
+ const isSettings = view === 'stats';
244
+ document.getElementById('settings-btn')?.classList.toggle('active', isSettings);
245
+ document.getElementById('settings-btn-mobile')?.classList.toggle('active', isSettings);
246
+ }
247
+
248
+ function updateMobileNavActive(view) {
249
+ $$('.mobile-nav-btn').forEach(b => b.classList.remove('active'));
250
+ const btn = $(`.mobile-nav-btn[data-view="${view}"]`);
251
+ if (btn) btn.classList.add('active');
242
252
  }
243
253
 
244
254
  function handleRoute() {
@@ -253,13 +263,15 @@ function handleRoute() {
253
263
  }
254
264
 
255
265
  const normalized = raw === 'search' ? 'overview' : raw;
256
- const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' ? normalized : 'overview';
266
+ const view = normalized === 'overview' || normalized === 'sessions' || normalized === 'timeline' || normalized === 'files' || normalized === 'stats' || normalized === 'insights' ? normalized : 'overview';
257
267
  window._lastView = view;
258
268
  updateNavActive(view);
269
+ updateMobileNavActive(view);
259
270
  if (view === 'overview') viewOverview();
260
271
  else if (view === 'sessions') viewSessions();
261
272
  else if (view === 'files') viewFiles();
262
273
  else if (view === 'timeline') viewTimeline();
274
+ else if (view === 'insights') viewInsights();
263
275
  else viewStats();
264
276
  }
265
277
 
@@ -690,6 +702,7 @@ async function viewSession(id) {
690
702
  <div class="section-label" id="sessionEventsLabel">Events</div>
691
703
  <div id="eventsContainer"></div>
692
704
  <div class="empty" id="sessionEventsEmpty" style="display:none"><h2>No events</h2><p>This session has no events to display.</p></div>
705
+ <div id="sessionInsightsPanel" class="loading" style="margin-top:var(--space-xl)">Loading insights...</div>
693
706
  `;
694
707
 
695
708
  const PAGE_SIZE = 50;
@@ -1018,6 +1031,12 @@ async function viewSession(id) {
1018
1031
  const ind = document.getElementById('newEventsIndicator');
1019
1032
  if (ind) ind.remove();
1020
1033
  };
1034
+
1035
+ // Load insights panel
1036
+ api(`/insights/session/${id}`).then(insights => {
1037
+ const panel = document.getElementById('sessionInsightsPanel');
1038
+ if (panel) panel.outerHTML = renderInsightsPanel(insights._error ? null : insights);
1039
+ });
1021
1040
  }
1022
1041
 
1023
1042
  async function viewTimeline(date) {
@@ -1196,7 +1215,7 @@ async function viewStats() {
1196
1215
  <div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
1197
1216
  <div class="config-card"><div class="config-label">DB Size</div><div class="config-value" id="dbSizeValue">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
1198
1217
  </div>
1199
- <p class="settings-help" style="margin-bottom:var(--space-sm)">Date range: ${fmtDate(data.dateRange?.earliest)} ${fmtDate(data.dateRange?.latest)}</p>
1218
+ <p class="settings-help" style="margin-bottom:var(--space-sm)">Date range: ${fmtDate(data.dateRange?.earliest)} to ${fmtDate(data.dateRange?.latest)}</p>
1200
1219
  <div class="settings-maintenance">
1201
1220
  <button class="export-btn" id="optimizeDbBtn">Optimize Database</button>
1202
1221
  <span id="optimizeDbStatus" class="settings-maintenance-status"></span>
@@ -1524,6 +1543,166 @@ async function viewFileDetail(filePath) {
1524
1543
  });
1525
1544
  }
1526
1545
 
1546
+ // --- Insights helpers ---
1547
+ const SIGNAL_LABELS = {
1548
+ tool_retry_loop: 'Repeated Actions',
1549
+ session_bail: 'No Output Produced',
1550
+ high_error_rate: 'Frequent Errors',
1551
+ long_prompt_short_session: 'Vague Instructions',
1552
+ no_completion: 'Incomplete Session'
1553
+ };
1554
+
1555
+ const SIGNAL_DESCRIPTIONS = {
1556
+ tool_retry_loop: 'The agent called the same tool many times in a row, suggesting it was stuck in a retry loop',
1557
+ session_bail: 'The agent ran many actions but never wrote or edited any files',
1558
+ high_error_rate: 'More than 30% of tool calls returned errors',
1559
+ long_prompt_short_session: 'A very short prompt led to a long session, suggesting the agent lacked sufficient context',
1560
+ no_completion: 'The session ended mid-action instead of finishing with a response'
1561
+ };
1562
+
1563
+ const SIGNAL_COLORS = {
1564
+ tool_retry_loop: 'amber',
1565
+ high_error_rate: 'red',
1566
+ no_completion: 'purple',
1567
+ session_bail: 'teal',
1568
+ long_prompt_short_session: 'accent'
1569
+ };
1570
+
1571
+ function renderSignalTag(sig) {
1572
+ const label = SIGNAL_LABELS[sig.type] || sig.type;
1573
+ const color = SIGNAL_COLORS[sig.type] || 'muted';
1574
+ const desc = SIGNAL_DESCRIPTIONS[sig.type] || '';
1575
+ return `<span class="signal-tag signal-${color}"${desc ? ` title="${escHtml(desc)}"` : ''}><span class="signal-dot"></span>${escHtml(label)}</span>`;
1576
+ }
1577
+
1578
+ function renderReliabilityBadge(score) {
1579
+ const reliability = 100 - score;
1580
+ return `<span class="insight-score-value" title="Reliability score: higher means fewer errors">${reliability}</span>`;
1581
+ }
1582
+
1583
+ function renderIssueRateBadge(signals) {
1584
+ const TOTAL_SIGNAL_TYPES = 5;
1585
+ const uniqueTypes = new Set((signals || []).map(s => s.type)).size;
1586
+ const rate = Math.round((uniqueTypes / TOTAL_SIGNAL_TYPES) * 100);
1587
+ return `<span class="insight-score-value" title="${uniqueTypes} of ${TOTAL_SIGNAL_TYPES} issue types detected">${rate}%</span>`;
1588
+ }
1589
+
1590
+ function renderInsightsPanel(insights) {
1591
+ if (!insights || !insights.signals || !insights.signals.length) {
1592
+ return `<div class="insights-panel insights-clean"><span style="color:var(--text-tertiary);font-size:13px">No issues detected</span></div>`;
1593
+ }
1594
+ return `<div class="insights-panel">
1595
+ <div class="section-label" style="margin-top:0">Session Health ${renderReliabilityBadge(insights.confusion_score)}</div>
1596
+ <div class="insights-signals">
1597
+ ${insights.signals.map(sig => {
1598
+ let detail = '';
1599
+ if (sig.type === 'tool_retry_loop') detail = `${escHtml(sig.tool)} called ${sig.count}x consecutively`;
1600
+ else if (sig.type === 'session_bail') detail = `${sig.tool_calls} tool calls with no file writes`;
1601
+ else if (sig.type === 'high_error_rate') detail = `${sig.rate}% error rate (${sig.error_count}/${sig.total})`;
1602
+ else if (sig.type === 'long_prompt_short_session') detail = `${sig.prompt_words} word prompt, ${sig.tool_calls} tool calls`;
1603
+ else if (sig.type === 'no_completion') detail = `Ended on ${escHtml(sig.last_event_type)}${sig.last_tool ? ': ' + escHtml(sig.last_tool) : ''}`;
1604
+ return `<div class="insight-callout">
1605
+ ${renderSignalTag(sig)}
1606
+ <span class="insight-detail">${detail}</span>
1607
+ </div>`;
1608
+ }).join('')}
1609
+ </div>
1610
+ </div>`;
1611
+ }
1612
+
1613
+ async function viewInsights() {
1614
+ clearJumpUi();
1615
+ window._lastView = 'insights';
1616
+ content.innerHTML = `<div class="page-title">Insights</div><div class="stat-grid">${skeletonRows(3, 'stats')}</div>`;
1617
+ transitionView();
1618
+
1619
+ const data = await api('/insights');
1620
+ if (data._error) {
1621
+ content.innerHTML = '<div class="empty"><h2>Unable to load</h2><p>Server unavailable. Pull to refresh or try again.</p></div>';
1622
+ return;
1623
+ }
1624
+
1625
+ // Signal breakdown data — sorted by count descending
1626
+ const signalTypes = Object.keys(SIGNAL_LABELS).sort((a, b) => (data.signal_counts[b] || 0) - (data.signal_counts[a] || 0));
1627
+ const maxSignalCount = Math.max(1, ...signalTypes.map(t => data.signal_counts[t] || 0));
1628
+
1629
+ // Issue rate
1630
+ const issueRate = data.total_sessions > 0 ? Math.round((data.flagged_count / data.total_sessions) * 100) : 0;
1631
+
1632
+ // Reliability score (inverted struggle score)
1633
+ const reliabilityScore = 100 - (data.avg_confusion_score || 0);
1634
+
1635
+ let html = `<div class="page-title">Insights</div>
1636
+
1637
+ <div class="stat-grid">
1638
+ <div class="stat-card accent-amber"><div class="label">Issue Rate</div><div class="value">${issueRate}%</div><div class="stat-desc">${data.flagged_count} of ${data.total_sessions} sessions had at least one detected issue</div></div>
1639
+ <div class="stat-card accent-purple"><div class="label">Reliability Score</div><div class="value">${reliabilityScore}</div><div class="stat-desc">How often the agent completed tasks cleanly, out of 100</div></div>
1640
+ </div>
1641
+
1642
+ <div class="section-label">Issue Types</div>
1643
+ <div class="signal-chart">
1644
+ ${signalTypes.map(type => {
1645
+ const count = data.signal_counts[type] || 0;
1646
+ const pct = Math.round((count / maxSignalCount) * 100);
1647
+ const color = SIGNAL_COLORS[type] || 'muted';
1648
+ const desc = SIGNAL_DESCRIPTIONS[type] || '';
1649
+ const barColor = color === 'muted' ? 'var(--text-tertiary)' : `var(--${color})`;
1650
+ return `<div class="signal-lollipop-row${desc ? ' signal-lollipop-expandable' : ''}" data-desc="${escHtml(desc)}">
1651
+ <div class="signal-lollipop-main">
1652
+ <span class="signal-bar-label">${SIGNAL_LABELS[type]}</span>
1653
+ <div class="signal-lollipop-track">
1654
+ <svg width="100%" height="20" class="signal-lollipop-svg">
1655
+ <line x1="0" y1="10" x2="${pct}%" y2="10" stroke="${barColor}" stroke-width="2"/>
1656
+ <circle cx="${pct}%" cy="10" r="5" fill="${barColor}"/>
1657
+ </svg>
1658
+ </div>
1659
+ <span class="signal-bar-count">${count}</span>
1660
+ </div>
1661
+ ${desc ? `<div class="signal-lollipop-desc">${escHtml(desc)}</div>` : ''}
1662
+ </div>`;
1663
+ }).join('')}
1664
+ </div>
1665
+
1666
+ <div class="section-label">Sessions with Issues (${data.flagged_count})</div>
1667
+ <div id="insightsList">
1668
+ ${data.top_flagged.length ? [...data.top_flagged].sort((a, b) => {
1669
+ const aRate = new Set((a.signals||[]).map(s=>s.type)).size;
1670
+ const bRate = new Set((b.signals||[]).map(s=>s.type)).size;
1671
+ return bRate - aRate || b.confusion_score - a.confusion_score;
1672
+ }).map(s => {
1673
+ const summary = cleanSessionSummary(s.summary, '');
1674
+ return `<div class="session-item insight-row" data-id="${escHtml(s.session_id)}">
1675
+ <div class="session-header">
1676
+ <span class="session-time">${fmtTime(s.start_time)}</span>
1677
+ <span class="insight-scores">issue rate ${renderIssueRateBadge(s.signals)} · reliability ${renderReliabilityBadge(s.confusion_score)}</span>
1678
+ </div>
1679
+ <div class="session-summary">${escHtml(truncate(summary, 120))}</div>
1680
+ <div class="session-meta">
1681
+ <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1682
+ ${s.agent ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
1683
+ ${s.model ? `<span class="session-model">${escHtml(s.model)}</span>` : ''}
1684
+ </span>
1685
+ <span class="insight-signal-tags">${s.signals.map(sig => renderSignalTag(sig)).join('')}</span>
1686
+ </div>
1687
+ </div>`;
1688
+ }).join('') : '<div class="empty"><p>No flagged sessions found</p></div>'}
1689
+ </div>
1690
+ `;
1691
+
1692
+ content.innerHTML = html;
1693
+ transitionView();
1694
+
1695
+ $$('.session-item', content).forEach(item => {
1696
+ item.addEventListener('click', () => viewSession(item.dataset.id));
1697
+ });
1698
+
1699
+ $$('.signal-lollipop-expandable', content).forEach(row => {
1700
+ row.addEventListener('click', () => {
1701
+ row.classList.toggle('signal-lollipop-open');
1702
+ });
1703
+ });
1704
+ }
1705
+
1527
1706
  // --- Navigation ---
1528
1707
  window._searchType = '';
1529
1708
  window._searchRole = '';
@@ -1542,6 +1721,7 @@ $$('.nav-item').forEach(item => {
1542
1721
  else if (view === 'sessions') viewSessions();
1543
1722
  else if (view === 'files') viewFiles();
1544
1723
  else if (view === 'timeline') viewTimeline();
1724
+ else if (view === 'insights') viewInsights();
1545
1725
  else if (view === 'stats') viewStats();
1546
1726
  });
1547
1727
  });
@@ -1788,8 +1968,14 @@ function openCmdk() {
1788
1968
  initTheme();
1789
1969
  document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
1790
1970
  document.getElementById('theme-toggle-mobile')?.addEventListener('click', toggleTheme);
1791
- document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
1792
1971
  document.getElementById('mobile-search-btn')?.addEventListener('click', () => openCmdk());
1972
+ document.getElementById('settings-btn')?.addEventListener('click', () => { window._lastView = 'stats'; updateNavActive('stats'); setHash('stats'); viewStats(); });
1973
+ document.getElementById('settings-btn-mobile')?.addEventListener('click', () => { window._lastView = 'stats'; updateNavActive('stats'); setHash('stats'); viewStats(); });
1974
+ document.getElementById('cmdkBtn')?.addEventListener('click', () => openCmdk());
1975
+
1976
+
1977
+
1978
+
1793
1979
  document.addEventListener('keydown', e => {
1794
1980
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
1795
1981
  e.preventDefault();
package/public/index.html CHANGED
@@ -21,7 +21,10 @@
21
21
  <nav class="sidebar">
22
22
  <div class="sidebar-header">
23
23
  <h1>Agent<span>Acta</span></h1>
24
- <button class="theme-toggle" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme"><svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg><svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
24
+ <div class="sidebar-header-actions">
25
+ <button class="theme-toggle" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme"><svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg><svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
26
+ <button class="theme-toggle" id="settings-btn" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg></button>
27
+ </div>
25
28
  </div>
26
29
  <button class="cmdk-trigger" id="cmdkBtn" aria-label="Search" title="Search (⌘K)">
27
30
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
@@ -29,26 +32,26 @@
29
32
  <kbd>⌘K</kbd>
30
33
  </button>
31
34
  <div class="nav-section">
35
+ <div class="nav-item active" data-view="overview">
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h7V3H3z"/><path d="M14 21h7v-7h-7z"/><path d="M14 10h7V3h-7z"/><path d="M3 21h7v-5H3z"/></svg>
37
+ <span>Overview</span>
38
+ </div>
32
39
  <div class="nav-item" data-view="sessions">
33
40
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
34
41
  <span>Sessions</span>
35
42
  </div>
43
+ <div class="nav-item" data-view="insights">
44
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
45
+ <span>Insights</span>
46
+ </div>
36
47
  <div class="nav-item" data-view="timeline">
37
48
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
38
49
  <span>Timeline</span>
39
50
  </div>
40
- <div class="nav-item active" data-view="overview">
41
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h7V3H3z"/><path d="M14 21h7v-7h-7z"/><path d="M14 10h7V3h-7z"/><path d="M3 21h7v-5H3z"/></svg>
42
- <span>Overview</span>
43
- </div>
44
51
  <div class="nav-item" data-view="files">
45
52
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
46
53
  <span>Files</span>
47
54
  </div>
48
- <div class="nav-item" data-view="stats">
49
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>
50
- <span>Settings</span>
51
- </div>
52
55
 
53
56
  </div>
54
57
  </nav>
@@ -56,8 +59,8 @@
56
59
  <div class="mobile-toolbar">
57
60
  <button class="mobile-search-btn" id="mobile-search-btn" title="Search" aria-label="Search"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></button>
58
61
  <button class="theme-toggle-mobile" id="theme-toggle-mobile" title="Toggle theme" aria-label="Toggle theme"><svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg><svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
62
+ <button class="theme-toggle-mobile" id="settings-btn-mobile" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.325 4.317a1.724 1.724 0 0 1 3.35 0 1.724 1.724 0 0 0 2.573 1.066 1.724 1.724 0 0 1 2.28 2.28 1.724 1.724 0 0 0 1.065 2.573 1.724 1.724 0 0 1 0 3.35 1.724 1.724 0 0 0-1.066 2.573 1.724 1.724 0 0 1-2.28 2.28 1.724 1.724 0 0 0-2.573 1.065 1.724 1.724 0 0 1-3.35 0 1.724 1.724 0 0 0-2.573-1.066 1.724 1.724 0 0 1-2.28-2.28 1.724 1.724 0 0 0-1.065-2.573 1.724 1.724 0 0 1 0-3.35 1.724 1.724 0 0 0 1.066-2.573 1.724 1.724 0 0 1 2.28-2.28 1.724 1.724 0 0 0 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg></button>
59
63
  </div>
60
-
61
64
  </div>
62
65
  <script src="/app.js"></script>
63
66
  <script>
package/public/style.css CHANGED
@@ -197,7 +197,12 @@ body.cmdk-open {
197
197
  display: flex;
198
198
  align-items: center;
199
199
  justify-content: space-between;
200
- gap: var(--space-sm);
200
+ }
201
+
202
+ .sidebar-header-actions {
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 6px;
201
206
  }
202
207
 
203
208
  .sidebar h1 {
@@ -233,6 +238,13 @@ body.cmdk-open {
233
238
  color: var(--text-primary);
234
239
  }
235
240
 
241
+ .theme-toggle.active,
242
+ .theme-toggle-mobile.active {
243
+ color: var(--accent);
244
+ border-color: color-mix(in srgb, var(--accent) 40%, transparent);
245
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg-elevated));
246
+ }
247
+
236
248
  .theme-toggle svg,
237
249
  .theme-toggle-mobile svg {
238
250
  width: 16px;
@@ -605,6 +617,28 @@ body.cmdk-open {
605
617
  border-radius: 10px;
606
618
  }
607
619
 
620
+ .signal-tag {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ gap: 6px;
624
+ font-size: 11px;
625
+ font-weight: 500;
626
+ color: var(--text-secondary);
627
+ white-space: nowrap;
628
+ }
629
+ .signal-dot {
630
+ width: 7px;
631
+ height: 7px;
632
+ border-radius: 50%;
633
+ flex-shrink: 0;
634
+ }
635
+ .signal-amber .signal-dot { background: var(--amber); }
636
+ .signal-red .signal-dot { background: var(--red); }
637
+ .signal-purple .signal-dot { background: var(--purple); }
638
+ .signal-teal .signal-dot { background: var(--teal); }
639
+ .signal-accent .signal-dot { background: var(--accent); }
640
+ .signal-muted .signal-dot { background: var(--text-tertiary); }
641
+
608
642
  .session-type {
609
643
  font-size: 11px;
610
644
  font-weight: 500;
@@ -623,6 +657,8 @@ body.cmdk-open {
623
657
 
624
658
  .session-meta {
625
659
  display: flex;
660
+ align-items: center;
661
+ justify-content: space-between;
626
662
  gap: var(--space-lg);
627
663
  margin-top: var(--space-sm);
628
664
  font-size: 12px;
@@ -1511,22 +1547,25 @@ mark {
1511
1547
  .sidebar {
1512
1548
  position: fixed;
1513
1549
  top: auto;
1514
- bottom: 0;
1515
- left: 0;
1516
- right: 0;
1517
- width: 100%;
1550
+ bottom: calc(24px + env(safe-area-inset-bottom, 0px));
1551
+ left: 50%;
1552
+ right: auto;
1553
+ transform: translateX(-50%);
1554
+ width: auto;
1518
1555
  height: auto;
1519
1556
  border-right: none;
1520
- border-top: 1px solid var(--border-subtle);
1521
- padding: 0;
1522
- padding-bottom: max(env(safe-area-inset-bottom, 0px), 16px);
1557
+ border: 1px solid var(--border-default);
1558
+ border-radius: 999px;
1559
+ padding: 8px 12px;
1523
1560
  display: flex;
1524
1561
  flex-direction: row;
1525
- overflow: hidden;
1562
+ align-items: center;
1563
+ overflow: visible;
1526
1564
  z-index: 100;
1527
1565
  background: var(--bg-surface);
1528
1566
  backdrop-filter: blur(20px);
1529
1567
  -webkit-backdrop-filter: blur(20px);
1568
+ box-shadow: 0 8px 32px rgba(0,0,0,0.28);
1530
1569
  }
1531
1570
 
1532
1571
  .sidebar-header { display: none; }
@@ -1535,29 +1574,30 @@ mark {
1535
1574
  display: flex;
1536
1575
  }
1537
1576
 
1577
+ .sidebar-header, .cmdk-trigger { display: none; }
1578
+
1538
1579
  .nav-section {
1539
1580
  display: flex;
1540
1581
  flex-direction: row;
1541
- width: 100%;
1542
1582
  padding: 0;
1543
- gap: 0;
1583
+ gap: 2px;
1544
1584
  }
1545
1585
 
1546
1586
  .nav-item {
1547
- flex: 1;
1587
+ flex: none;
1548
1588
  flex-direction: column;
1549
1589
  align-items: center;
1550
1590
  justify-content: center;
1551
- padding: 10px 0 4px;
1591
+ padding: 7px 14px 5px;
1552
1592
  font-size: 10px;
1553
1593
  gap: 3px;
1554
- border-radius: 0;
1594
+ border-radius: 999px;
1555
1595
  letter-spacing: 0.02em;
1556
1596
  }
1557
1597
 
1558
1598
  .nav-item::before { display: none; }
1559
1599
 
1560
- .nav-item svg { width: 22px; height: 22px; }
1600
+ .nav-item svg { width: 20px; height: 20px; }
1561
1601
 
1562
1602
  .nav-item.active {
1563
1603
  color: var(--accent);
@@ -1568,7 +1608,7 @@ mark {
1568
1608
  margin-left: 0;
1569
1609
  padding: var(--space-xl) var(--space-lg);
1570
1610
  padding-top: calc(var(--space-xl) + env(safe-area-inset-top, 0px));
1571
- padding-bottom: calc(80px + max(env(safe-area-inset-bottom, 0px), 20px));
1611
+ padding-bottom: calc(100px + max(env(safe-area-inset-bottom, 0px), 20px));
1572
1612
  max-width: 100%;
1573
1613
  }
1574
1614
 
@@ -1911,3 +1951,144 @@ mark {
1911
1951
  .cmdk-item-sub { display: none; }
1912
1952
  .cmdk-item-meta { font-size: 10px; }
1913
1953
  }
1954
+
1955
+ /* ── Insights ── */
1956
+ .settings-gear {
1957
+ background: none;
1958
+ border: none;
1959
+ cursor: pointer;
1960
+ padding: 4px;
1961
+ border-radius: var(--radius-sm);
1962
+ color: var(--text-muted);
1963
+ display: flex;
1964
+ align-items: center;
1965
+ justify-content: center;
1966
+ transition: color 0.15s, background 0.15s;
1967
+ }
1968
+ .settings-gear svg { width: 16px; height: 16px; }
1969
+ .settings-gear:hover { color: var(--text-primary); background: var(--hover-bg); }
1970
+
1971
+ .signal-lollipop-row {
1972
+ display: flex;
1973
+ flex-direction: column;
1974
+ margin-bottom: 8px;
1975
+ }
1976
+ .signal-lollipop-expandable {
1977
+ cursor: pointer;
1978
+ }
1979
+ .signal-lollipop-main {
1980
+ display: flex;
1981
+ align-items: center;
1982
+ gap: 10px;
1983
+ }
1984
+ .signal-bar-label {
1985
+ font-size: 13px;
1986
+ color: var(--text-secondary);
1987
+ width: 130px;
1988
+ flex-shrink: 0;
1989
+ }
1990
+ .signal-expand-hint {
1991
+ font-size: 11px;
1992
+ color: var(--text-tertiary);
1993
+ flex-shrink: 0;
1994
+ }
1995
+ .signal-lollipop-desc {
1996
+ display: none;
1997
+ font-size: 12px;
1998
+ color: var(--text-tertiary);
1999
+ padding: 5px 0 3px 0;
2000
+ line-height: 1.4;
2001
+ }
2002
+ .signal-lollipop-open .signal-lollipop-desc {
2003
+ display: block;
2004
+ }
2005
+ .signal-lollipop-open .signal-expand-hint {
2006
+ color: var(--text-secondary);
2007
+ }
2008
+ .signal-lollipop-track {
2009
+ flex: 1;
2010
+ height: 20px;
2011
+ }
2012
+ .signal-lollipop-svg {
2013
+ display: block;
2014
+ overflow: visible;
2015
+ }
2016
+ .signal-bar-count {
2017
+ font-size: 12px;
2018
+ font-variant-numeric: tabular-nums;
2019
+ color: var(--text-muted);
2020
+ width: 36px;
2021
+ text-align: right;
2022
+ flex-shrink: 0;
2023
+ }
2024
+ .stat-desc {
2025
+ font-size: 11px;
2026
+ color: var(--text-tertiary);
2027
+ margin-top: 4px;
2028
+ line-height: 1.3;
2029
+ }
2030
+
2031
+ .insight-row { cursor: pointer; }
2032
+ .insight-row:hover { background: var(--hover-bg); }
2033
+ .insight-callout {
2034
+ display: flex;
2035
+ align-items: flex-start;
2036
+ gap: 8px;
2037
+ padding: 8px 0;
2038
+ border-bottom: 1px solid var(--border-color);
2039
+ font-size: 13px;
2040
+ }
2041
+ .insight-callout:last-child { border-bottom: none; }
2042
+ .insight-detail { color: var(--text-secondary); }
2043
+ .insights-signals { margin-top: 8px; }
2044
+
2045
+ .confusion-badge {
2046
+ display: inline-flex;
2047
+ align-items: center;
2048
+ gap: 4px;
2049
+ padding: 1px 7px;
2050
+ border-radius: 999px;
2051
+ font-size: 11px;
2052
+ font-weight: 600;
2053
+ letter-spacing: 0.02em;
2054
+ }
2055
+
2056
+ .confusion-label {
2057
+ font-weight: 400;
2058
+ font-size: 10px;
2059
+ opacity: 0.7;
2060
+ text-transform: uppercase;
2061
+ letter-spacing: 0.04em;
2062
+ }
2063
+
2064
+ .confusion-issue-count {
2065
+ background: color-mix(in srgb, var(--text-tertiary) 15%, transparent);
2066
+ color: var(--text-secondary);
2067
+ }
2068
+
2069
+ .insight-scores {
2070
+ font-size: 11px;
2071
+ color: var(--text-tertiary);
2072
+ white-space: nowrap;
2073
+ text-transform: uppercase;
2074
+ letter-spacing: 0.05em;
2075
+ }
2076
+
2077
+ .insight-score-value {
2078
+ font-weight: 600;
2079
+ color: var(--text-secondary);
2080
+ }
2081
+
2082
+ .insight-signal-tags {
2083
+ display: flex;
2084
+ flex-wrap: wrap;
2085
+ gap: 5px;
2086
+ align-items: center;
2087
+ }
2088
+ .confusion-green { background: #dcfce7; color: #166534; }
2089
+ .confusion-yellow { background: #fef9c3; color: #854d0e; }
2090
+ .confusion-red { background: #fee2e2; color: #991b1b; }
2091
+ [data-theme="dark"] .confusion-green { background: #14532d; color: #86efac; }
2092
+ [data-theme="dark"] .confusion-yellow { background: #713f12; color: #fde68a; }
2093
+ [data-theme="dark"] .confusion-red { background: #7f1d1d; color: #fca5a5; }
2094
+