agentacta 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -54,16 +54,14 @@ function escHtml(s) {
54
54
 
55
55
  function truncate(s, n = 200) {
56
56
  if (!s) return '';
57
- return s.length > n ? s.slice(0, n) + '' : s;
57
+ return s.length > n ? s.slice(0, n) + '\u2026' : s;
58
58
  }
59
59
 
60
60
  function shortSessionId(id) {
61
61
  if (!id) return '';
62
- return id.length > 24 ? `${id.slice(0, 8)}…${id.slice(-8)}` : id;
62
+ return id.length > 24 ? `${id.slice(0, 8)}\u2026${id.slice(-8)}` : id;
63
63
  }
64
64
 
65
- // Removed jumpToInitialPrompt - now handled within session view
66
-
67
65
  function badgeClass(type, role) {
68
66
  if (type === 'tool_call') return 'badge-tool_call';
69
67
  if (type === 'tool_result') return 'badge-tool_result';
@@ -72,6 +70,12 @@ function badgeClass(type, role) {
72
70
  return 'badge-message';
73
71
  }
74
72
 
73
+ function transitionView() {
74
+ content.classList.remove('view-enter');
75
+ void content.offsetWidth;
76
+ content.classList.add('view-enter');
77
+ }
78
+
75
79
  function renderEvent(ev) {
76
80
  const badge = `<span class="event-badge ${badgeClass(ev.type, ev.role)}">${ev.type === 'tool_call' ? 'tool' : ev.role || ev.type}</span>`;
77
81
  let body = '';
@@ -87,7 +91,7 @@ function renderEvent(ev) {
87
91
  }
88
92
  }
89
93
  } else if (ev.type === 'tool_result') {
90
- body = `<span class="tool-name">→ ${escHtml(ev.tool_name)}</span>`;
94
+ body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
91
95
  if (ev.content) {
92
96
  body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
93
97
  }
@@ -102,6 +106,40 @@ function renderEvent(ev) {
102
106
  </div>`;
103
107
  }
104
108
 
109
+ function renderTimelineEvent(ev) {
110
+ const typeClass = ev.type === 'tool_call' || ev.type === 'tool_result' ? 'type-tool' :
111
+ ev.role === 'user' ? 'type-user' :
112
+ ev.role === 'assistant' ? 'type-assistant' : '';
113
+
114
+ const badge = `<span class="event-badge ${badgeClass(ev.type, ev.role)}">${ev.type === 'tool_call' ? 'tool' : ev.role || ev.type}</span>`;
115
+ let body = '';
116
+
117
+ if (ev.type === 'tool_call') {
118
+ body = `<span class="tool-name">${escHtml(ev.tool_name)}</span>`;
119
+ if (ev.tool_args) {
120
+ try {
121
+ const args = JSON.parse(ev.tool_args);
122
+ body += `<div class="tool-args">${escHtml(JSON.stringify(args, null, 2))}</div>`;
123
+ } catch {
124
+ body += `<div class="tool-args">${escHtml(ev.tool_args)}</div>`;
125
+ }
126
+ }
127
+ } else if (ev.type === 'tool_result') {
128
+ body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
129
+ if (ev.content) {
130
+ body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
131
+ }
132
+ } else {
133
+ body = `<div class="event-content">${escHtml(ev.content || '')}</div>`;
134
+ }
135
+
136
+ return `<div class="timeline-event ${typeClass}" data-event-id="${ev.id}">
137
+ <div class="event-time">${fmtTimeShort(ev.timestamp)}</div>
138
+ ${badge}
139
+ <div class="event-body">${body}</div>
140
+ </div>`;
141
+ }
142
+
105
143
  function fmtDuration(start, end) {
106
144
  if (!start) return '';
107
145
  const ms = (end ? new Date(end) : new Date()) - new Date(start);
@@ -134,7 +172,6 @@ function renderProjectTags(s) {
134
172
  }
135
173
 
136
174
  function renderModelTags(s) {
137
- // Prefer models array if present, fall back to single model
138
175
  let models = [];
139
176
  if (s.models) {
140
177
  try { models = JSON.parse(s.models); } catch {}
@@ -145,12 +182,12 @@ function renderModelTags(s) {
145
182
 
146
183
  function renderSessionItem(s) {
147
184
  const duration = fmtDuration(s.start_time, s.end_time);
148
- const timeRange = `${fmtTime(s.start_time)} ${s.end_time ? fmtTimeOnly(s.end_time) : 'now'}`;
185
+ const timeRange = `${fmtTime(s.start_time)} \u2192 ${s.end_time ? fmtTimeOnly(s.end_time) : 'now'}`;
149
186
 
150
187
  return `
151
188
  <div class="session-item" data-id="${s.id}">
152
189
  <div class="session-header">
153
- <span class="session-time">${timeRange} · ${duration}</span>
190
+ <span class="session-time">${timeRange} \u00b7 ${duration}</span>
154
191
  <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
155
192
  ${renderProjectTags(s)}
156
193
  ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
@@ -160,8 +197,8 @@ function renderSessionItem(s) {
160
197
  </div>
161
198
  <div class="session-summary">${escHtml(truncate(s.summary || 'No summary', 120))}</div>
162
199
  <div class="session-meta">
163
- <span>💬 ${s.message_count}</span>
164
- <span>🔧 ${s.tool_count}</span>
200
+ <span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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> ${s.message_count}</span>
201
+ <span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> ${s.tool_count}</span>
165
202
  </div>
166
203
  </div>
167
204
  `;
@@ -175,8 +212,8 @@ async function viewSearch(query = '') {
175
212
 
176
213
  let html = `<div class="page-title">Search</div>
177
214
  <div class="search-bar">
178
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
179
- <input type="text" id="searchInput" placeholder="Search messages, tool calls, files" value="${escHtml(query)}">
215
+ <svg 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>
216
+ <input type="text" id="searchInput" placeholder="Search messages, tool calls, files\u2026" value="${escHtml(query)}">
180
217
  </div>
181
218
  <div class="filters">
182
219
  <span class="filter-chip ${typeFilter===''?'active':''}" data-filter="type" data-val="">All</span>
@@ -189,6 +226,7 @@ async function viewSearch(query = '') {
189
226
  <div id="results"></div>`;
190
227
 
191
228
  content.innerHTML = html;
229
+ transitionView();
192
230
 
193
231
  const input = $('#searchInput');
194
232
  input.focus();
@@ -214,7 +252,7 @@ async function viewSearch(query = '') {
214
252
 
215
253
  async function showSearchHome() {
216
254
  const el = $('#results');
217
- el.innerHTML = '<div class="loading">Loading…</div>';
255
+ el.innerHTML = '<div class="loading">Loading</div>';
218
256
 
219
257
  const stats = await api('/stats');
220
258
  const sessions = await api('/sessions?limit=5');
@@ -223,17 +261,17 @@ async function showSearchHome() {
223
261
  try { const r = await fetch('/api/suggestions'); const d = await r.json(); suggestions = d.suggestions || []; } catch(e) { suggestions = []; }
224
262
 
225
263
  let html = `
226
- <div class="stat-grid" style="margin-top:8px">
227
- <div class="stat-card"><div class="label">Sessions</div><div class="value">${stats.sessions}</div></div>
228
- <div class="stat-card"><div class="label">Messages</div><div class="value">${stats.messages.toLocaleString()}</div></div>
229
- <div class="stat-card"><div class="label">Tool Calls</div><div class="value">${stats.toolCalls.toLocaleString()}</div></div>
230
- <div class="stat-card"><div class="label">Tokens</div><div class="value">${(stats.totalTokens || 0).toLocaleString()}</div></div>
264
+ <div class="search-stats" style="margin-top:8px">
265
+ <div class="search-stat"><div class="num">${stats.sessions}</div><div class="lbl">Sessions</div></div>
266
+ <div class="search-stat"><div class="num">${stats.messages.toLocaleString()}</div><div class="lbl">Messages</div></div>
267
+ <div class="search-stat"><div class="num">${stats.toolCalls.toLocaleString()}</div><div class="lbl">Tool Calls</div></div>
268
+ <div class="search-stat"><div class="num">${fmtTokens(stats.totalTokens || 0)}</div><div class="lbl">Tokens</div></div>
231
269
  </div>
232
270
 
233
- <div class="section-label">Quick Search</div>
271
+ ${suggestions.length ? `<div class="section-label">Quick Search</div>
234
272
  <div class="filters" id="suggestions">
235
- ${suggestions.map(s => `<span class="filter-chip suggestion" data-q="${s}">${s}</span>`).join('')}
236
- </div>
273
+ ${suggestions.map(s => `<span class="suggestion-chip" data-q="${s}">${s}</span>`).join('')}
274
+ </div>` : ''}
237
275
 
238
276
  <div class="section-label">Recent Sessions</div>
239
277
  ${sessions.sessions.map(renderSessionItem).join('')}
@@ -241,7 +279,7 @@ async function showSearchHome() {
241
279
 
242
280
  el.innerHTML = html;
243
281
 
244
- $$('.suggestion', el).forEach(chip => {
282
+ $$('.suggestion-chip', el).forEach(chip => {
245
283
  chip.addEventListener('click', () => {
246
284
  $('#searchInput').value = chip.dataset.q;
247
285
  doSearch(chip.dataset.q);
@@ -261,7 +299,7 @@ async function doSearch(q) {
261
299
  const el = $('#results');
262
300
  if (!q.trim()) { el.innerHTML = '<div class="empty"><h2>Type to search</h2><p>Search across all sessions, messages, and tool calls</p></div>'; return; }
263
301
 
264
- el.innerHTML = '<div class="loading">Searching…</div>';
302
+ el.innerHTML = '<div class="loading">Searching</div>';
265
303
 
266
304
  const type = window._searchType || '';
267
305
  const role = window._searchRole || '';
@@ -272,13 +310,13 @@ async function doSearch(q) {
272
310
  const data = await api(url);
273
311
 
274
312
  if (data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error)}</p></div>`; return; }
275
- if (!data.results.length) { el.innerHTML = '<div class="empty"><h2>No results</h2></div>'; return; }
313
+ if (!data.results.length) { el.innerHTML = '<div class="empty"><h2>No results</h2><p>Try a different search term or adjust filters</p></div>'; return; }
276
314
 
277
- let header = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
315
+ let header = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-md)">
278
316
  <span class="section-label" style="margin:0">${data.results.length} results</span>
279
317
  <div style="display:flex;gap:8px">
280
- <a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=md','search.md');return false">📄 MD</a>
281
- <a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=json','search.json');return false">📋 JSON</a>
318
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=md','search.md');return false">MD</a>
319
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=json','search.json');return false">JSON</a>
282
320
  </div>
283
321
  </div>`;
284
322
 
@@ -288,7 +326,7 @@ async function doSearch(q) {
288
326
  <span class="event-badge ${badgeClass(r.type, r.role)}">${r.type === 'tool_call' ? 'tool' : r.role || r.type}</span>
289
327
  <span class="session-time">${fmtTime(r.timestamp)}</span>
290
328
  ${r.tool_name ? `<span class="tool-name">${escHtml(r.tool_name)}</span>` : ''}
291
- <span class="session-link" data-session="${r.session_id}">view session →</span>
329
+ <span class="session-link" data-session="${r.session_id}">view session \u2192</span>
292
330
  </div>
293
331
  <div class="result-content">${escHtml(truncate(r.content || r.tool_args || r.tool_result || '', 400))}</div>
294
332
  </div>
@@ -305,12 +343,13 @@ async function doSearch(q) {
305
343
 
306
344
  async function viewSessions() {
307
345
  window._currentSessionId = null;
308
- content.innerHTML = '<div class="loading">Loading…</div>';
346
+ content.innerHTML = '<div class="loading">Loading</div>';
309
347
  const data = await api('/sessions?limit=200');
310
348
 
311
349
  let html = `<div class="page-title">Sessions</div>`;
312
350
  html += data.sessions.map(renderSessionItem).join('');
313
351
  content.innerHTML = html;
352
+ transitionView();
314
353
 
315
354
  $$('.session-item').forEach(item => {
316
355
  item.addEventListener('click', () => viewSession(item.dataset.id));
@@ -319,7 +358,7 @@ async function viewSessions() {
319
358
 
320
359
  async function viewSession(id) {
321
360
  window._currentSessionId = id;
322
- content.innerHTML = '<div class="loading">Loading…</div>';
361
+ content.innerHTML = '<div class="loading">Loading</div>';
323
362
  const data = await api(`/sessions/${id}`);
324
363
 
325
364
  if (data.error) { content.innerHTML = `<div class="empty"><h2>${data.error}</h2></div>`; return; }
@@ -327,23 +366,23 @@ async function viewSession(id) {
327
366
  const s = data.session;
328
367
  const cost = fmtCost(s.total_cost);
329
368
  let html = `
330
- <div class="back-btn" id="backBtn">← Back</div>
369
+ <div class="back-btn" id="backBtn">\u2190 Back</div>
331
370
  <div class="page-title">Session</div>
332
371
  <div class="session-id-row">
333
372
  <span class="session-id-label">ID</span>
334
373
  <span class="session-id-value" title="${escHtml(id)}">${escHtml(id)}</span>
335
- <button class="session-id-copy" id="copySessionId" title="Copy session ID">⧉</button>
374
+ <button class="session-id-copy" id="copySessionId" title="Copy session ID">\u29c9</button>
336
375
  <span class="session-id-copied" id="copyConfirm">Copied!</span>
337
376
  </div>
338
- <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
339
- ${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">↗️ Initial Prompt</button>` : ''}
340
- ${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">📦 JSONL</a>` : ''}
341
- <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=md','session.md');return false">📄 MD</a>
342
- <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=json','session.json');return false">📋 JSON</a>
377
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:12px">
378
+ ${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">Initial Prompt</button>` : ''}
379
+ ${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">JSONL</a>` : ''}
380
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=md','session.md');return false">MD</a>
381
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=json','session.json');return false">JSON</a>
343
382
  </div>
344
- <div class="session-item" style="cursor:default">
345
- <div class="session-header">
346
- <span class="session-time">${fmtDate(s.start_time)} · ${fmtTimeShort(s.start_time)} ${fmtTimeShort(s.end_time)}</span>
383
+ <div class="session-detail-card">
384
+ <div class="session-header" style="margin-bottom:12px">
385
+ <span class="session-time">${fmtDate(s.start_time)} \u00b7 ${fmtTimeShort(s.start_time)} \u2013 ${fmtTimeShort(s.end_time)}</span>
347
386
  <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
348
387
  ${renderProjectTags(s)}
349
388
  ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
@@ -351,10 +390,10 @@ async function viewSession(id) {
351
390
  ${renderModelTags(s)}
352
391
  </span>
353
392
  </div>
354
- <div class="session-meta" style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px 16px">
355
- <span>💬 ${s.message_count} messages</span>
356
- <span>🔧 ${s.tool_count} tools</span>
357
- ${s.output_tokens ? `<span>📤 ${fmtTokens(s.output_tokens)} output</span><span>📥 ${fmtTokens(s.input_tokens + s.cache_read_tokens)} input</span>` : s.total_tokens ? `<span>🔤 ${fmtTokens(s.total_tokens)} tokens</span><span></span>` : '<span></span><span></span>'}
393
+ <div class="session-detail-grid">
394
+ <span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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></span> ${s.message_count} messages</span>
395
+ <span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span> ${s.tool_count} tools</span>
396
+ ${s.output_tokens ? `<span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/></svg></span> ${fmtTokens(s.output_tokens)} output</span><span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></span> ${fmtTokens(s.input_tokens + s.cache_read_tokens)} input</span>` : s.total_tokens ? `<span><span class="detail-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg></span> ${fmtTokens(s.total_tokens)} tokens</span><span></span>` : '<span></span><span></span>'}
358
397
  </div>
359
398
  </div>
360
399
  <div class="section-label">Events</div>
@@ -362,6 +401,7 @@ async function viewSession(id) {
362
401
 
363
402
  html += data.events.map(renderEvent).join('');
364
403
  content.innerHTML = html;
404
+ transitionView();
365
405
 
366
406
  $('#backBtn').addEventListener('click', () => {
367
407
  if (window._lastView === 'timeline') viewTimeline();
@@ -385,7 +425,6 @@ async function viewSession(id) {
385
425
  return;
386
426
  }
387
427
 
388
- // Fallback for non-secure contexts (http/local)
389
428
  const ta = document.createElement('textarea');
390
429
  ta.value = id;
391
430
  ta.setAttribute('readonly', '');
@@ -416,9 +455,9 @@ async function viewSession(id) {
416
455
  const firstMessage = document.querySelector(`[data-event-id="${s.first_message_id}"]`);
417
456
  if (firstMessage) {
418
457
  firstMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
419
- firstMessage.style.background = 'var(--accent-bg)';
458
+ firstMessage.classList.add('event-highlight');
420
459
  setTimeout(() => {
421
- firstMessage.style.background = '';
460
+ firstMessage.classList.remove('event-highlight');
422
461
  }, 2000);
423
462
  }
424
463
  });
@@ -426,13 +465,17 @@ async function viewSession(id) {
426
465
  }
427
466
 
428
467
  async function viewTimeline(date) {
429
- if (!date) date = new Date().toISOString().slice(0, 10);
468
+ if (!date) {
469
+ const now = new Date();
470
+ date = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
471
+ }
430
472
  window._lastView = 'timeline';
431
473
 
432
474
  let html = `<div class="page-title">Timeline</div>
433
475
  <input type="date" class="date-input" id="dateInput" value="${date}">
434
- <div id="timelineContent"><div class="loading">Loading…</div></div>`;
476
+ <div id="timelineContent"><div class="loading">Loading</div></div>`;
435
477
  content.innerHTML = html;
478
+ transitionView();
436
479
 
437
480
  const data = await api(`/timeline?date=${date}`);
438
481
  const el = $('#timelineContent');
@@ -440,29 +483,32 @@ async function viewTimeline(date) {
440
483
  if (!data.events.length) {
441
484
  el.innerHTML = '<div class="empty"><h2>No activity</h2><p>Nothing recorded on this day</p></div>';
442
485
  } else {
443
- el.innerHTML = data.events.map(renderEvent).join('');
486
+ el.innerHTML = `<div class="timeline-events-wrap">
487
+ <div class="timeline-line"></div>
488
+ ${data.events.map(renderTimelineEvent).join('')}
489
+ </div>`;
444
490
  }
445
491
 
446
492
  $('#dateInput').addEventListener('change', e => viewTimeline(e.target.value));
447
493
  }
448
494
 
449
495
  async function viewStats() {
450
- content.innerHTML = '<div class="loading">Loading…</div>';
496
+ content.innerHTML = '<div class="loading">Loading</div>';
451
497
  const data = await api('/stats');
452
498
 
453
499
  let html = `<div class="page-title">Stats</div>
454
500
  <div class="stat-grid">
455
- <div class="stat-card"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
456
- <div class="stat-card"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
457
- <div class="stat-card"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
458
- <div class="stat-card"><div class="label">Unique Tools</div><div class="value">${data.uniqueTools}</div></div>
459
- <div class="stat-card"><div class="label">Total Tokens</div><div class="value">${(data.totalTokens || 0).toLocaleString()}</div></div>
501
+ <div class="stat-card accent-blue"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
502
+ <div class="stat-card accent-green"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
503
+ <div class="stat-card accent-amber"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
504
+ <div class="stat-card accent-purple"><div class="label">Unique Tools</div><div class="value">${data.uniqueTools}</div></div>
505
+ <div class="stat-card accent-teal"><div class="label">Total Tokens</div><div class="value">${(data.totalTokens || 0).toLocaleString()}</div></div>
460
506
  </div>
461
507
 
462
508
  <div class="section-label">Configuration</div>
463
- <div class="stat-grid">
464
- <div class="stat-card"><div class="label">Storage Mode</div><div class="value" style="font-size:18px">${escHtml(data.storageMode || 'reference')}</div></div>
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>
509
+ <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:var(--space-md);margin-bottom:var(--space-xl)">
510
+ <div class="config-card"><div class="config-label">Storage Mode</div><div class="config-value">${escHtml(data.storageMode || 'reference')}</div></div>
511
+ <div class="config-card"><div class="config-label">DB Size</div><div class="config-value">${escHtml(data.dbSize?.display || 'N/A')}</div></div>
466
512
  </div>
467
513
 
468
514
  ${data.sessionDirs && data.sessionDirs.length ? (() => {
@@ -475,35 +521,38 @@ async function viewStats() {
475
521
  if (claudeDirs.length) {
476
522
  const projects = new Set();
477
523
  for (const d of claudeDirs) {
478
- const m = (d.path || '').match(/[\\/]\.claude[\\/]projects[\\/]([^\\/]+)$/);
524
+ const m = (d.path || '').match(/[\\/]\\.claude[\\/]projects[\\/]([^\\/]+)$/);
479
525
  if (m && m[1]) projects.add(m[1]);
480
526
  }
481
527
  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>`);
528
+ lines.push(`<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border-subtle)"><span style="color:var(--text-tertiary)">~/.claude/projects/*</span> <span style="color:var(--accent);font-size:12px">claude-code \u00b7 ${projectCount} workspace${projectCount === 1 ? '' : 's'}</span></div>`);
483
529
  }
484
530
 
485
531
  for (const d of otherDirs) {
486
532
  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>`);
533
+ lines.push(`<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border-subtle)"><span style="color:var(--text-tertiary)">${escHtml(display)}</span> <span style="color:var(--accent);font-size:12px">${escHtml(normalizeAgentLabel(d.agent))}</span></div>`);
488
534
  }
489
535
 
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>`;
536
+ return `<div class="section-label">Session Paths</div>
537
+ <div class="config-card" style="margin-bottom:var(--space-xl)">
538
+ <div style="font-size:12.5px;font-family:var(--font-mono)">${lines.join('')}</div>
539
+ </div>`;
492
540
  })() : ''}
493
541
 
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>` : ''}
542
+ ${data.agents && data.agents.length > 1 ? `<div class="section-label">Agents</div><div class="filters" style="margin-bottom:var(--space-xl)">${data.agents.map(a => `<span class="filter-chip">${escHtml(a)}</span>`).join('')}</div>` : ''}
495
543
  <div class="section-label">Date Range</div>
496
- <p style="color:var(--text2);font-size:14px">${fmtDate(data.dateRange?.earliest)} ${fmtDate(data.dateRange?.latest)}</p>
544
+ <p style="color:var(--text-secondary);font-size:13px;margin-bottom:var(--space-xl)">${fmtDate(data.dateRange?.earliest)} \u2014 ${fmtDate(data.dateRange?.latest)}</p>
497
545
  <div class="section-label">Tools Used</div>
498
- <div class="filters">${(data.tools||[]).filter(t => t).sort().map(t => `<span class="filter-chip">${escHtml(t)}</span>`).join('')}</div>
546
+ <div class="tools-grid">${(data.tools||[]).filter(t => t).sort().map(t => `<span class="tool-chip">${escHtml(t)}</span>`).join('')}</div>
499
547
  `;
500
548
 
501
549
  content.innerHTML = html;
550
+ transitionView();
502
551
  }
503
552
 
504
553
  async function viewFiles() {
505
554
  window._lastView = 'files';
506
- content.innerHTML = '<div class="loading">Loading…</div>';
555
+ content.innerHTML = '<div class="loading">Loading</div>';
507
556
  const data = await api('/files?limit=500');
508
557
  window._allFiles = data.files || [];
509
558
  window._fileSort = window._fileSort || 'touches';
@@ -519,8 +568,6 @@ function getFileExt(p) {
519
568
  }
520
569
 
521
570
  function getFileDir(p) {
522
- // Group by project-level directory
523
- // Strip common home dir prefixes
524
571
  let rel = p.replace(/^\/home\/[^/]+\//, '~/').replace(/^\/Users\/[^/]+\//, '~/');
525
572
  if (rel.startsWith('~/')) rel = rel.slice(2);
526
573
  const parts = rel.split('/');
@@ -531,53 +578,48 @@ function getFileDir(p) {
531
578
  function renderFiles() {
532
579
  let files = [...window._allFiles];
533
580
 
534
- // Search filter
535
581
  const q = window._fileSearch.toLowerCase();
536
582
  if (q) files = files.filter(f => f.file_path.toLowerCase().includes(q));
537
583
 
538
- // Extension filter
539
584
  if (window._fileFilter) {
540
585
  files = files.filter(f => getFileExt(f.file_path) === window._fileFilter);
541
586
  }
542
587
 
543
- // Sort
544
588
  const sort = window._fileSort;
545
589
  if (sort === 'touches') files.sort((a, b) => b.touch_count - a.touch_count);
546
590
  else if (sort === 'recent') files.sort((a, b) => new Date(b.last_touched) - new Date(a.last_touched));
547
591
  else if (sort === 'name') files.sort((a, b) => a.file_path.localeCompare(b.file_path));
548
592
  else if (sort === 'sessions') files.sort((a, b) => b.session_count - a.session_count);
549
593
 
550
- // Get unique extensions for filter chips
551
594
  const exts = [...new Set(window._allFiles.map(f => getFileExt(f.file_path)))].sort();
552
595
 
553
596
  let html = `<div class="page-title">Files</div>
554
597
  <div class="search-bar" style="margin-bottom:12px">
555
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
556
- <input type="text" id="fileSearchInput" placeholder="Filter by filename or path" value="${escHtml(window._fileSearch)}">
598
+ <svg 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>
599
+ <input type="text" id="fileSearchInput" placeholder="Filter by filename or path\u2026" value="${escHtml(window._fileSearch)}">
557
600
  </div>
558
601
  <div class="filters" style="margin-bottom:8px">
559
602
  <span class="filter-chip ${sort==='touches'?'active':''}" data-sort="touches">Most touched</span>
560
603
  <span class="filter-chip ${sort==='recent'?'active':''}" data-sort="recent">Recent</span>
561
604
  <span class="filter-chip ${sort==='sessions'?'active':''}" data-sort="sessions">Most sessions</span>
562
- <span class="filter-chip ${sort==='name'?'active':''}" data-sort="name">A-Z</span>
605
+ <span class="filter-chip ${sort==='name'?'active':''}" data-sort="name">A\u2013Z</span>
563
606
  </div>
564
607
  <div class="filters" style="margin-bottom:12px">
565
608
  <span class="filter-chip ext-chip ${!window._fileFilter?'active':''}" data-ext="">All</span>
566
609
  ${exts.map(e => `<span class="filter-chip ext-chip ${window._fileFilter===e?'active':''}" data-ext="${e}">${e}</span>`).join('')}
567
610
  </div>
568
611
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
569
- <span style="color:var(--text2);font-size:13px">${files.length} files</span>
570
- <span class="filter-chip ${window._fileGrouped?'active':''}" id="groupToggle" style="cursor:pointer">📂 Group by directory</span>
612
+ <span style="color:var(--text-tertiary);font-size:12px;font-weight:500">${files.length} files</span>
613
+ <span class="filter-chip ${window._fileGrouped?'active':''}" id="groupToggle" style="cursor:pointer">Group by directory</span>
571
614
  </div>
572
615
  <div id="filesList"></div>`;
573
616
 
574
617
  content.innerHTML = html;
618
+ transitionView();
575
619
 
576
- // Render file list
577
620
  const listEl = $('#filesList');
578
621
 
579
622
  if (window._fileGrouped && !q) {
580
- // Group by directory
581
623
  const groups = {};
582
624
  files.forEach(f => {
583
625
  const dir = getFileDir(f.file_path);
@@ -585,7 +627,6 @@ function renderFiles() {
585
627
  groups[dir].push(f);
586
628
  });
587
629
 
588
- // Sort groups by active sort criteria
589
630
  const groupMetric = (files) => {
590
631
  if (sort === 'touches') return files.reduce((s, f) => s + f.touch_count, 0);
591
632
  if (sort === 'sessions') return files.reduce((s, f) => s + f.session_count, 0);
@@ -604,9 +645,9 @@ function renderFiles() {
604
645
  return `
605
646
  <div class="file-group">
606
647
  <div class="file-group-header" data-dir="${escHtml(dir)}">
607
- <span class="file-group-arrow">▶</span>
648
+ <span class="file-group-arrow">\u25b6</span>
608
649
  <span class="file-group-name">~/${escHtml(dir)}</span>
609
- <span style="color:var(--text2);font-size:12px;margin-left:auto">${dirFiles.length} files · ${groupStat}</span>
650
+ <span style="color:var(--text-tertiary);font-size:12px;margin-left:auto">${dirFiles.length} files \u00b7 ${groupStat}</span>
610
651
  </div>
611
652
  <div class="file-group-items" style="display:none">
612
653
  ${dirFiles.map(f => renderFileItem(f)).join('')}
@@ -620,10 +661,10 @@ function renderFiles() {
620
661
  const arrow = h.querySelector('.file-group-arrow');
621
662
  if (items.style.display === 'none') {
622
663
  items.style.display = 'block';
623
- arrow.textContent = '';
664
+ arrow.textContent = '\u25bc';
624
665
  } else {
625
666
  items.style.display = 'none';
626
- arrow.textContent = '';
667
+ arrow.textContent = '\u25b6';
627
668
  }
628
669
  });
629
670
  });
@@ -631,7 +672,6 @@ function renderFiles() {
631
672
  listEl.innerHTML = files.map(f => renderFileItem(f)).join('');
632
673
  }
633
674
 
634
- // Event listeners — must re-attach every render since innerHTML replaces DOM
635
675
  let debounce;
636
676
  const searchInput = $('#fileSearchInput');
637
677
  searchInput.addEventListener('input', e => {
@@ -639,7 +679,6 @@ function renderFiles() {
639
679
  debounce = setTimeout(() => { window._fileSearch = e.target.value; renderFiles(); }, 200);
640
680
  });
641
681
 
642
- // Preserve cursor position after re-render
643
682
  const cursorPos = window._fileCursorPos || 0;
644
683
  searchInput.setSelectionRange(cursorPos, cursorPos);
645
684
  if (window._fileSearch) searchInput.focus();
@@ -658,7 +697,6 @@ function renderFiles() {
658
697
  item.addEventListener('click', () => viewFileDetail(item.dataset.path));
659
698
  });
660
699
 
661
- // Track cursor for re-renders
662
700
  searchInput.addEventListener('keyup', () => { window._fileCursorPos = searchInput.selectionStart; });
663
701
  }
664
702
 
@@ -667,11 +705,11 @@ function renderFileItem(f) {
667
705
  const dir = f.file_path.split('/').slice(0, -1).join('/');
668
706
  return `
669
707
  <div class="file-item" data-path="${escHtml(f.file_path)}">
670
- <div class="file-path"><span style="color:var(--text)">${escHtml(fname)}</span> <span style="color:var(--text2);font-size:12px">${escHtml(dir)}/</span></div>
708
+ <div class="file-path"><span style="color:var(--text-primary);font-weight:500">${escHtml(fname)}</span> <span style="color:var(--text-tertiary);font-size:12px">${escHtml(dir)}/</span></div>
671
709
  <div class="file-meta">
672
710
  <span>${f.touch_count} touches</span>
673
711
  <span>${f.session_count} sessions</span>
674
- <span style="color:var(--orange)">${escHtml(f.operations)}</span>
712
+ <span style="color:var(--amber)">${escHtml(f.operations)}</span>
675
713
  <span class="session-time">${fmtTime(f.last_touched)}</span>
676
714
  </div>
677
715
  </div>
@@ -679,17 +717,18 @@ function renderFileItem(f) {
679
717
  }
680
718
 
681
719
  async function viewFileDetail(filePath) {
682
- content.innerHTML = '<div class="loading">Loading…</div>';
720
+ content.innerHTML = '<div class="loading">Loading</div>';
683
721
  const data = await api(`/files/sessions?path=${encodeURIComponent(filePath)}`);
684
722
 
685
723
  let html = `
686
- <div class="back-btn" id="backBtn">← Back</div>
724
+ <div class="back-btn" id="backBtn">\u2190 Back</div>
687
725
  <div class="page-title" style="word-break:break-all;font-size:16px">${escHtml(filePath)}</div>
688
726
  <div class="section-label">${data.sessions.length} sessions touched this file</div>
689
727
  `;
690
728
 
691
729
  html += data.sessions.map(s => renderSessionItem(s)).join('');
692
730
  content.innerHTML = html;
731
+ transitionView();
693
732
 
694
733
  $('#backBtn').addEventListener('click', () => viewFiles());
695
734
  $$('.session-item').forEach(item => {
@@ -721,7 +760,7 @@ viewSearch();
721
760
  // Swipe right from left edge to go back
722
761
  (function initSwipeBack() {
723
762
  let startX = 0, startY = 0, swiping = false;
724
- const edgeWidth = 30; // px from left edge
763
+ const edgeWidth = 30;
725
764
  const threshold = 80;
726
765
 
727
766
  document.addEventListener('touchstart', e => {
@@ -737,7 +776,6 @@ viewSearch();
737
776
  if (!swiping) return;
738
777
  const dx = e.touches[0].clientX - startX;
739
778
  const dy = Math.abs(e.touches[0].clientY - startY);
740
- // Cancel if vertical movement exceeds horizontal (it's a scroll)
741
779
  if (dy > dx) { swiping = false; }
742
780
  }, { passive: true });
743
781
 
@@ -761,7 +799,7 @@ viewSearch();
761
799
  const indicator = document.createElement('div');
762
800
  indicator.className = 'ptr-indicator';
763
801
  indicator.id = 'ptr';
764
- indicator.textContent = ' Pull to refresh';
802
+ indicator.textContent = '\u2193 Pull to refresh';
765
803
  document.body.appendChild(indicator);
766
804
 
767
805
  document.addEventListener('touchstart', e => {
@@ -776,7 +814,7 @@ viewSearch();
776
814
  const diff = e.touches[0].clientY - startY;
777
815
  if (diff > 20 && window.scrollY <= 0) {
778
816
  indicator.classList.add('visible');
779
- indicator.textContent = diff > threshold ? ' Release to refresh' : ' Pull to refresh';
817
+ indicator.textContent = diff > threshold ? '\u2191 Release to refresh' : '\u2193 Pull to refresh';
780
818
  } else {
781
819
  indicator.classList.remove('visible');
782
820
  }
@@ -787,11 +825,10 @@ viewSearch();
787
825
  pulling = false;
788
826
  const diff = e.changedTouches[0].clientY - startY;
789
827
  if (diff > threshold && indicator.classList.contains('visible')) {
790
- indicator.textContent = 'Refreshing';
828
+ indicator.textContent = 'Refreshing\u2026';
791
829
  indicator.classList.add('refreshing');
792
830
  try {
793
831
  await api('/reindex');
794
- // If viewing a session detail, refresh it in place
795
832
  const backBtn = $('#backBtn');
796
833
  if (backBtn && window._currentSessionId) {
797
834
  await viewSession(window._currentSessionId);