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/package.json +1 -1
- package/public/app.js +136 -99
- package/public/index.html +27 -20
- package/public/style.css +1031 -303
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) + '
|
|
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)}
|
|
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"
|
|
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)}
|
|
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}
|
|
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
|
|
164
|
-
<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
|
|
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
|
|
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="
|
|
227
|
-
<div class="stat
|
|
228
|
-
<div class="stat
|
|
229
|
-
<div class="stat
|
|
230
|
-
<div class="stat
|
|
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
|
-
|
|
271
|
+
${suggestions.length ? `<div class="section-label">Quick Search</div>
|
|
234
272
|
<div class="filters" id="suggestions">
|
|
235
|
-
${suggestions.map(s => `<span class="
|
|
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
|
|
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:
|
|
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"
|
|
281
|
-
<a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=json','search.json');return false"
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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:
|
|
339
|
-
${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt"
|
|
340
|
-
${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false"
|
|
341
|
-
<a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=md','session.md');return false"
|
|
342
|
-
<a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=json','session.json');return false"
|
|
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-
|
|
345
|
-
<div class="session-header">
|
|
346
|
-
<span class="session-time">${fmtDate(s.start_time)}
|
|
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-
|
|
355
|
-
<span
|
|
356
|
-
<span
|
|
357
|
-
${s.output_tokens ? `<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.
|
|
458
|
+
firstMessage.classList.add('event-highlight');
|
|
420
459
|
setTimeout(() => {
|
|
421
|
-
firstMessage.
|
|
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)
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
464
|
-
<div class="
|
|
465
|
-
<div class="
|
|
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(/[\\/]
|
|
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="
|
|
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="
|
|
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">
|
|
491
|
-
<div style="
|
|
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(--
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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(--
|
|
570
|
-
<span class="filter-chip ${window._fileGrouped?'active':''}" id="groupToggle" style="cursor:pointer"
|
|
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"
|
|
648
|
+
<span class="file-group-arrow">\u25b6</span>
|
|
608
649
|
<span class="file-group-name">~/${escHtml(dir)}</span>
|
|
609
|
-
<span style="color:var(--
|
|
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(--
|
|
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(--
|
|
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
|
|
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"
|
|
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;
|
|
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 = '
|
|
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 ? '
|
|
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);
|