agentacta 1.1.4 → 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/README.md +3 -1
- package/config.js +10 -1
- package/index.js +56 -8
- package/indexer.js +20 -14
- package/package.json +1 -1
- package/public/app.js +212 -99
- package/public/index.html +27 -20
- package/public/style.css +1057 -269
package/public/app.js
CHANGED
|
@@ -54,10 +54,13 @@ 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
|
+
if (!id) return '';
|
|
62
|
+
return id.length > 24 ? `${id.slice(0, 8)}\u2026${id.slice(-8)}` : id;
|
|
63
|
+
}
|
|
61
64
|
|
|
62
65
|
function badgeClass(type, role) {
|
|
63
66
|
if (type === 'tool_call') return 'badge-tool_call';
|
|
@@ -67,6 +70,12 @@ function badgeClass(type, role) {
|
|
|
67
70
|
return 'badge-message';
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
function transitionView() {
|
|
74
|
+
content.classList.remove('view-enter');
|
|
75
|
+
void content.offsetWidth;
|
|
76
|
+
content.classList.add('view-enter');
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
function renderEvent(ev) {
|
|
71
80
|
const badge = `<span class="event-badge ${badgeClass(ev.type, ev.role)}">${ev.type === 'tool_call' ? 'tool' : ev.role || ev.type}</span>`;
|
|
72
81
|
let body = '';
|
|
@@ -82,7 +91,7 @@ function renderEvent(ev) {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
} else if (ev.type === 'tool_result') {
|
|
85
|
-
body = `<span class="tool-name"
|
|
94
|
+
body = `<span class="tool-name">\u2192 ${escHtml(ev.tool_name)}</span>`;
|
|
86
95
|
if (ev.content) {
|
|
87
96
|
body += `<div class="tool-args">${escHtml(truncate(ev.content, 500))}</div>`;
|
|
88
97
|
}
|
|
@@ -97,6 +106,40 @@ function renderEvent(ev) {
|
|
|
97
106
|
</div>`;
|
|
98
107
|
}
|
|
99
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
|
+
|
|
100
143
|
function fmtDuration(start, end) {
|
|
101
144
|
if (!start) return '';
|
|
102
145
|
const ms = (end ? new Date(end) : new Date()) - new Date(start);
|
|
@@ -113,6 +156,13 @@ function fmtTimeOnly(ts) {
|
|
|
113
156
|
return new Date(ts).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
114
157
|
}
|
|
115
158
|
|
|
159
|
+
function normalizeAgentLabel(a) {
|
|
160
|
+
if (!a) return a;
|
|
161
|
+
if (a === 'main') return 'openclaw-main';
|
|
162
|
+
if (a.startsWith('claude-') || a.startsWith('claude--')) return 'claude-code';
|
|
163
|
+
return a;
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
function renderProjectTags(s) {
|
|
117
167
|
let projects = [];
|
|
118
168
|
if (s.projects) {
|
|
@@ -122,7 +172,6 @@ function renderProjectTags(s) {
|
|
|
122
172
|
}
|
|
123
173
|
|
|
124
174
|
function renderModelTags(s) {
|
|
125
|
-
// Prefer models array if present, fall back to single model
|
|
126
175
|
let models = [];
|
|
127
176
|
if (s.models) {
|
|
128
177
|
try { models = JSON.parse(s.models); } catch {}
|
|
@@ -133,23 +182,23 @@ function renderModelTags(s) {
|
|
|
133
182
|
|
|
134
183
|
function renderSessionItem(s) {
|
|
135
184
|
const duration = fmtDuration(s.start_time, s.end_time);
|
|
136
|
-
const timeRange = `${fmtTime(s.start_time)}
|
|
185
|
+
const timeRange = `${fmtTime(s.start_time)} \u2192 ${s.end_time ? fmtTimeOnly(s.end_time) : 'now'}`;
|
|
137
186
|
|
|
138
187
|
return `
|
|
139
188
|
<div class="session-item" data-id="${s.id}">
|
|
140
189
|
<div class="session-header">
|
|
141
|
-
<span class="session-time">${timeRange}
|
|
190
|
+
<span class="session-time">${timeRange} \u00b7 ${duration}</span>
|
|
142
191
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
143
192
|
${renderProjectTags(s)}
|
|
144
|
-
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
|
|
193
|
+
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
145
194
|
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
146
195
|
${renderModelTags(s)}
|
|
147
196
|
</span>
|
|
148
197
|
</div>
|
|
149
198
|
<div class="session-summary">${escHtml(truncate(s.summary || 'No summary', 120))}</div>
|
|
150
199
|
<div class="session-meta">
|
|
151
|
-
<span
|
|
152
|
-
<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>
|
|
153
202
|
</div>
|
|
154
203
|
</div>
|
|
155
204
|
`;
|
|
@@ -163,8 +212,8 @@ async function viewSearch(query = '') {
|
|
|
163
212
|
|
|
164
213
|
let html = `<div class="page-title">Search</div>
|
|
165
214
|
<div class="search-bar">
|
|
166
|
-
<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>
|
|
167
|
-
<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)}">
|
|
168
217
|
</div>
|
|
169
218
|
<div class="filters">
|
|
170
219
|
<span class="filter-chip ${typeFilter===''?'active':''}" data-filter="type" data-val="">All</span>
|
|
@@ -177,6 +226,7 @@ async function viewSearch(query = '') {
|
|
|
177
226
|
<div id="results"></div>`;
|
|
178
227
|
|
|
179
228
|
content.innerHTML = html;
|
|
229
|
+
transitionView();
|
|
180
230
|
|
|
181
231
|
const input = $('#searchInput');
|
|
182
232
|
input.focus();
|
|
@@ -202,7 +252,7 @@ async function viewSearch(query = '') {
|
|
|
202
252
|
|
|
203
253
|
async function showSearchHome() {
|
|
204
254
|
const el = $('#results');
|
|
205
|
-
el.innerHTML = '<div class="loading">Loading
|
|
255
|
+
el.innerHTML = '<div class="loading">Loading</div>';
|
|
206
256
|
|
|
207
257
|
const stats = await api('/stats');
|
|
208
258
|
const sessions = await api('/sessions?limit=5');
|
|
@@ -211,17 +261,17 @@ async function showSearchHome() {
|
|
|
211
261
|
try { const r = await fetch('/api/suggestions'); const d = await r.json(); suggestions = d.suggestions || []; } catch(e) { suggestions = []; }
|
|
212
262
|
|
|
213
263
|
let html = `
|
|
214
|
-
<div class="
|
|
215
|
-
<div class="stat
|
|
216
|
-
<div class="stat
|
|
217
|
-
<div class="stat
|
|
218
|
-
<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>
|
|
219
269
|
</div>
|
|
220
270
|
|
|
221
|
-
|
|
271
|
+
${suggestions.length ? `<div class="section-label">Quick Search</div>
|
|
222
272
|
<div class="filters" id="suggestions">
|
|
223
|
-
${suggestions.map(s => `<span class="
|
|
224
|
-
</div
|
|
273
|
+
${suggestions.map(s => `<span class="suggestion-chip" data-q="${s}">${s}</span>`).join('')}
|
|
274
|
+
</div>` : ''}
|
|
225
275
|
|
|
226
276
|
<div class="section-label">Recent Sessions</div>
|
|
227
277
|
${sessions.sessions.map(renderSessionItem).join('')}
|
|
@@ -229,7 +279,7 @@ async function showSearchHome() {
|
|
|
229
279
|
|
|
230
280
|
el.innerHTML = html;
|
|
231
281
|
|
|
232
|
-
$$('.suggestion', el).forEach(chip => {
|
|
282
|
+
$$('.suggestion-chip', el).forEach(chip => {
|
|
233
283
|
chip.addEventListener('click', () => {
|
|
234
284
|
$('#searchInput').value = chip.dataset.q;
|
|
235
285
|
doSearch(chip.dataset.q);
|
|
@@ -249,7 +299,7 @@ async function doSearch(q) {
|
|
|
249
299
|
const el = $('#results');
|
|
250
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; }
|
|
251
301
|
|
|
252
|
-
el.innerHTML = '<div class="loading">Searching
|
|
302
|
+
el.innerHTML = '<div class="loading">Searching</div>';
|
|
253
303
|
|
|
254
304
|
const type = window._searchType || '';
|
|
255
305
|
const role = window._searchRole || '';
|
|
@@ -260,13 +310,13 @@ async function doSearch(q) {
|
|
|
260
310
|
const data = await api(url);
|
|
261
311
|
|
|
262
312
|
if (data.error) { el.innerHTML = `<div class="empty"><p>${escHtml(data.error)}</p></div>`; return; }
|
|
263
|
-
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; }
|
|
264
314
|
|
|
265
|
-
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)">
|
|
266
316
|
<span class="section-label" style="margin:0">${data.results.length} results</span>
|
|
267
317
|
<div style="display:flex;gap:8px">
|
|
268
|
-
<a class="export-btn" href="#" onclick="dlExport('/api/export/search?q=${encodeURIComponent(q)}&format=md','search.md');return false"
|
|
269
|
-
<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>
|
|
270
320
|
</div>
|
|
271
321
|
</div>`;
|
|
272
322
|
|
|
@@ -276,7 +326,7 @@ async function doSearch(q) {
|
|
|
276
326
|
<span class="event-badge ${badgeClass(r.type, r.role)}">${r.type === 'tool_call' ? 'tool' : r.role || r.type}</span>
|
|
277
327
|
<span class="session-time">${fmtTime(r.timestamp)}</span>
|
|
278
328
|
${r.tool_name ? `<span class="tool-name">${escHtml(r.tool_name)}</span>` : ''}
|
|
279
|
-
<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>
|
|
280
330
|
</div>
|
|
281
331
|
<div class="result-content">${escHtml(truncate(r.content || r.tool_args || r.tool_result || '', 400))}</div>
|
|
282
332
|
</div>
|
|
@@ -293,12 +343,13 @@ async function doSearch(q) {
|
|
|
293
343
|
|
|
294
344
|
async function viewSessions() {
|
|
295
345
|
window._currentSessionId = null;
|
|
296
|
-
content.innerHTML = '<div class="loading">Loading
|
|
346
|
+
content.innerHTML = '<div class="loading">Loading</div>';
|
|
297
347
|
const data = await api('/sessions?limit=200');
|
|
298
348
|
|
|
299
349
|
let html = `<div class="page-title">Sessions</div>`;
|
|
300
350
|
html += data.sessions.map(renderSessionItem).join('');
|
|
301
351
|
content.innerHTML = html;
|
|
352
|
+
transitionView();
|
|
302
353
|
|
|
303
354
|
$$('.session-item').forEach(item => {
|
|
304
355
|
item.addEventListener('click', () => viewSession(item.dataset.id));
|
|
@@ -307,7 +358,7 @@ async function viewSessions() {
|
|
|
307
358
|
|
|
308
359
|
async function viewSession(id) {
|
|
309
360
|
window._currentSessionId = id;
|
|
310
|
-
content.innerHTML = '<div class="loading">Loading
|
|
361
|
+
content.innerHTML = '<div class="loading">Loading</div>';
|
|
311
362
|
const data = await api(`/sessions/${id}`);
|
|
312
363
|
|
|
313
364
|
if (data.error) { content.innerHTML = `<div class="empty"><h2>${data.error}</h2></div>`; return; }
|
|
@@ -315,28 +366,34 @@ async function viewSession(id) {
|
|
|
315
366
|
const s = data.session;
|
|
316
367
|
const cost = fmtCost(s.total_cost);
|
|
317
368
|
let html = `
|
|
318
|
-
<div class="back-btn" id="backBtn"
|
|
369
|
+
<div class="back-btn" id="backBtn">\u2190 Back</div>
|
|
319
370
|
<div class="page-title">Session</div>
|
|
320
|
-
<div
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
<
|
|
371
|
+
<div class="session-id-row">
|
|
372
|
+
<span class="session-id-label">ID</span>
|
|
373
|
+
<span class="session-id-value" title="${escHtml(id)}">${escHtml(id)}</span>
|
|
374
|
+
<button class="session-id-copy" id="copySessionId" title="Copy session ID">\u29c9</button>
|
|
375
|
+
<span class="session-id-copied" id="copyConfirm">Copied!</span>
|
|
325
376
|
</div>
|
|
326
|
-
<div
|
|
327
|
-
|
|
328
|
-
|
|
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>
|
|
382
|
+
</div>
|
|
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>
|
|
329
386
|
<span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
330
387
|
${renderProjectTags(s)}
|
|
331
|
-
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
|
|
388
|
+
${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(normalizeAgentLabel(s.agent))}</span>` : ''}
|
|
332
389
|
${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
|
|
333
390
|
${renderModelTags(s)}
|
|
334
391
|
</span>
|
|
335
392
|
</div>
|
|
336
|
-
<div class="session-
|
|
337
|
-
<span
|
|
338
|
-
<span
|
|
339
|
-
${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>'}
|
|
340
397
|
</div>
|
|
341
398
|
</div>
|
|
342
399
|
<div class="section-label">Events</div>
|
|
@@ -344,6 +401,7 @@ async function viewSession(id) {
|
|
|
344
401
|
|
|
345
402
|
html += data.events.map(renderEvent).join('');
|
|
346
403
|
content.innerHTML = html;
|
|
404
|
+
transitionView();
|
|
347
405
|
|
|
348
406
|
$('#backBtn').addEventListener('click', () => {
|
|
349
407
|
if (window._lastView === 'timeline') viewTimeline();
|
|
@@ -352,15 +410,54 @@ async function viewSession(id) {
|
|
|
352
410
|
else viewSessions();
|
|
353
411
|
});
|
|
354
412
|
|
|
413
|
+
$('#copySessionId').addEventListener('click', async () => {
|
|
414
|
+
const conf = $('#copyConfirm');
|
|
415
|
+
const showCopied = () => {
|
|
416
|
+
conf.textContent = 'Copied!';
|
|
417
|
+
conf.classList.add('show');
|
|
418
|
+
setTimeout(() => conf.classList.remove('show'), 1500);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
423
|
+
await navigator.clipboard.writeText(id);
|
|
424
|
+
showCopied();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const ta = document.createElement('textarea');
|
|
429
|
+
ta.value = id;
|
|
430
|
+
ta.setAttribute('readonly', '');
|
|
431
|
+
ta.style.position = 'fixed';
|
|
432
|
+
ta.style.opacity = '0';
|
|
433
|
+
ta.style.pointerEvents = 'none';
|
|
434
|
+
document.body.appendChild(ta);
|
|
435
|
+
ta.focus();
|
|
436
|
+
ta.select();
|
|
437
|
+
const ok = document.execCommand('copy');
|
|
438
|
+
document.body.removeChild(ta);
|
|
439
|
+
|
|
440
|
+
if (ok) showCopied();
|
|
441
|
+
else throw new Error('Copy failed');
|
|
442
|
+
} catch {
|
|
443
|
+
conf.textContent = 'Press Ctrl/Cmd+C';
|
|
444
|
+
conf.classList.add('show');
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
conf.classList.remove('show');
|
|
447
|
+
conf.textContent = 'Copied!';
|
|
448
|
+
}, 1800);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
355
452
|
const jumpBtn = $('#jumpToStartBtn');
|
|
356
453
|
if (jumpBtn) {
|
|
357
454
|
jumpBtn.addEventListener('click', () => {
|
|
358
455
|
const firstMessage = document.querySelector(`[data-event-id="${s.first_message_id}"]`);
|
|
359
456
|
if (firstMessage) {
|
|
360
457
|
firstMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
361
|
-
firstMessage.
|
|
458
|
+
firstMessage.classList.add('event-highlight');
|
|
362
459
|
setTimeout(() => {
|
|
363
|
-
firstMessage.
|
|
460
|
+
firstMessage.classList.remove('event-highlight');
|
|
364
461
|
}, 2000);
|
|
365
462
|
}
|
|
366
463
|
});
|
|
@@ -368,13 +465,17 @@ async function viewSession(id) {
|
|
|
368
465
|
}
|
|
369
466
|
|
|
370
467
|
async function viewTimeline(date) {
|
|
371
|
-
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
|
+
}
|
|
372
472
|
window._lastView = 'timeline';
|
|
373
473
|
|
|
374
474
|
let html = `<div class="page-title">Timeline</div>
|
|
375
475
|
<input type="date" class="date-input" id="dateInput" value="${date}">
|
|
376
|
-
<div id="timelineContent"><div class="loading">Loading
|
|
476
|
+
<div id="timelineContent"><div class="loading">Loading</div></div>`;
|
|
377
477
|
content.innerHTML = html;
|
|
478
|
+
transitionView();
|
|
378
479
|
|
|
379
480
|
const data = await api(`/timeline?date=${date}`);
|
|
380
481
|
const el = $('#timelineContent');
|
|
@@ -382,52 +483,76 @@ async function viewTimeline(date) {
|
|
|
382
483
|
if (!data.events.length) {
|
|
383
484
|
el.innerHTML = '<div class="empty"><h2>No activity</h2><p>Nothing recorded on this day</p></div>';
|
|
384
485
|
} else {
|
|
385
|
-
el.innerHTML =
|
|
486
|
+
el.innerHTML = `<div class="timeline-events-wrap">
|
|
487
|
+
<div class="timeline-line"></div>
|
|
488
|
+
${data.events.map(renderTimelineEvent).join('')}
|
|
489
|
+
</div>`;
|
|
386
490
|
}
|
|
387
491
|
|
|
388
492
|
$('#dateInput').addEventListener('change', e => viewTimeline(e.target.value));
|
|
389
493
|
}
|
|
390
494
|
|
|
391
495
|
async function viewStats() {
|
|
392
|
-
content.innerHTML = '<div class="loading">Loading
|
|
496
|
+
content.innerHTML = '<div class="loading">Loading</div>';
|
|
393
497
|
const data = await api('/stats');
|
|
394
498
|
|
|
395
499
|
let html = `<div class="page-title">Stats</div>
|
|
396
500
|
<div class="stat-grid">
|
|
397
|
-
<div class="stat-card"><div class="label">Sessions</div><div class="value">${data.sessions}</div></div>
|
|
398
|
-
<div class="stat-card"><div class="label">Messages</div><div class="value">${data.messages.toLocaleString()}</div></div>
|
|
399
|
-
<div class="stat-card"><div class="label">Tool Calls</div><div class="value">${data.toolCalls.toLocaleString()}</div></div>
|
|
400
|
-
<div class="stat-card"><div class="label">Unique Tools</div><div class="value">${data.uniqueTools}</div></div>
|
|
401
|
-
<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>
|
|
402
506
|
</div>
|
|
403
507
|
|
|
404
508
|
<div class="section-label">Configuration</div>
|
|
405
|
-
<div
|
|
406
|
-
<div class="
|
|
407
|
-
<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>
|
|
408
512
|
</div>
|
|
409
513
|
|
|
410
|
-
${data.sessionDirs && data.sessionDirs.length ?
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
514
|
+
${data.sessionDirs && data.sessionDirs.length ? (() => {
|
|
515
|
+
const dirs = data.sessionDirs || [];
|
|
516
|
+
const claudeDirs = dirs.filter(d => d.agent === 'claude-code' || /^claude-/.test(d.agent || ''));
|
|
517
|
+
const otherDirs = dirs.filter(d => !(d.agent === 'claude-code' || /^claude-/.test(d.agent || '')));
|
|
518
|
+
|
|
519
|
+
const lines = [];
|
|
520
|
+
|
|
521
|
+
if (claudeDirs.length) {
|
|
522
|
+
const projects = new Set();
|
|
523
|
+
for (const d of claudeDirs) {
|
|
524
|
+
const m = (d.path || '').match(/[\\/]\\.claude[\\/]projects[\\/]([^\\/]+)$/);
|
|
525
|
+
if (m && m[1]) projects.add(m[1]);
|
|
526
|
+
}
|
|
527
|
+
const projectCount = projects.size || claudeDirs.length;
|
|
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>`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
for (const d of otherDirs) {
|
|
532
|
+
const display = (d.path || '').replace(/^\/home\/[^/]+/, '~').replace(/^\/Users\/[^/]+/, '~');
|
|
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>`);
|
|
534
|
+
}
|
|
535
|
+
|
|
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>`;
|
|
540
|
+
})() : ''}
|
|
417
541
|
|
|
418
|
-
${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>` : ''}
|
|
419
543
|
<div class="section-label">Date Range</div>
|
|
420
|
-
<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>
|
|
421
545
|
<div class="section-label">Tools Used</div>
|
|
422
|
-
<div class="
|
|
546
|
+
<div class="tools-grid">${(data.tools||[]).filter(t => t).sort().map(t => `<span class="tool-chip">${escHtml(t)}</span>`).join('')}</div>
|
|
423
547
|
`;
|
|
424
548
|
|
|
425
549
|
content.innerHTML = html;
|
|
550
|
+
transitionView();
|
|
426
551
|
}
|
|
427
552
|
|
|
428
553
|
async function viewFiles() {
|
|
429
554
|
window._lastView = 'files';
|
|
430
|
-
content.innerHTML = '<div class="loading">Loading
|
|
555
|
+
content.innerHTML = '<div class="loading">Loading</div>';
|
|
431
556
|
const data = await api('/files?limit=500');
|
|
432
557
|
window._allFiles = data.files || [];
|
|
433
558
|
window._fileSort = window._fileSort || 'touches';
|
|
@@ -443,8 +568,6 @@ function getFileExt(p) {
|
|
|
443
568
|
}
|
|
444
569
|
|
|
445
570
|
function getFileDir(p) {
|
|
446
|
-
// Group by project-level directory
|
|
447
|
-
// Strip common home dir prefixes
|
|
448
571
|
let rel = p.replace(/^\/home\/[^/]+\//, '~/').replace(/^\/Users\/[^/]+\//, '~/');
|
|
449
572
|
if (rel.startsWith('~/')) rel = rel.slice(2);
|
|
450
573
|
const parts = rel.split('/');
|
|
@@ -455,53 +578,48 @@ function getFileDir(p) {
|
|
|
455
578
|
function renderFiles() {
|
|
456
579
|
let files = [...window._allFiles];
|
|
457
580
|
|
|
458
|
-
// Search filter
|
|
459
581
|
const q = window._fileSearch.toLowerCase();
|
|
460
582
|
if (q) files = files.filter(f => f.file_path.toLowerCase().includes(q));
|
|
461
583
|
|
|
462
|
-
// Extension filter
|
|
463
584
|
if (window._fileFilter) {
|
|
464
585
|
files = files.filter(f => getFileExt(f.file_path) === window._fileFilter);
|
|
465
586
|
}
|
|
466
587
|
|
|
467
|
-
// Sort
|
|
468
588
|
const sort = window._fileSort;
|
|
469
589
|
if (sort === 'touches') files.sort((a, b) => b.touch_count - a.touch_count);
|
|
470
590
|
else if (sort === 'recent') files.sort((a, b) => new Date(b.last_touched) - new Date(a.last_touched));
|
|
471
591
|
else if (sort === 'name') files.sort((a, b) => a.file_path.localeCompare(b.file_path));
|
|
472
592
|
else if (sort === 'sessions') files.sort((a, b) => b.session_count - a.session_count);
|
|
473
593
|
|
|
474
|
-
// Get unique extensions for filter chips
|
|
475
594
|
const exts = [...new Set(window._allFiles.map(f => getFileExt(f.file_path)))].sort();
|
|
476
595
|
|
|
477
596
|
let html = `<div class="page-title">Files</div>
|
|
478
597
|
<div class="search-bar" style="margin-bottom:12px">
|
|
479
|
-
<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>
|
|
480
|
-
<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)}">
|
|
481
600
|
</div>
|
|
482
601
|
<div class="filters" style="margin-bottom:8px">
|
|
483
602
|
<span class="filter-chip ${sort==='touches'?'active':''}" data-sort="touches">Most touched</span>
|
|
484
603
|
<span class="filter-chip ${sort==='recent'?'active':''}" data-sort="recent">Recent</span>
|
|
485
604
|
<span class="filter-chip ${sort==='sessions'?'active':''}" data-sort="sessions">Most sessions</span>
|
|
486
|
-
<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>
|
|
487
606
|
</div>
|
|
488
607
|
<div class="filters" style="margin-bottom:12px">
|
|
489
608
|
<span class="filter-chip ext-chip ${!window._fileFilter?'active':''}" data-ext="">All</span>
|
|
490
609
|
${exts.map(e => `<span class="filter-chip ext-chip ${window._fileFilter===e?'active':''}" data-ext="${e}">${e}</span>`).join('')}
|
|
491
610
|
</div>
|
|
492
611
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
493
|
-
<span style="color:var(--
|
|
494
|
-
<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>
|
|
495
614
|
</div>
|
|
496
615
|
<div id="filesList"></div>`;
|
|
497
616
|
|
|
498
617
|
content.innerHTML = html;
|
|
618
|
+
transitionView();
|
|
499
619
|
|
|
500
|
-
// Render file list
|
|
501
620
|
const listEl = $('#filesList');
|
|
502
621
|
|
|
503
622
|
if (window._fileGrouped && !q) {
|
|
504
|
-
// Group by directory
|
|
505
623
|
const groups = {};
|
|
506
624
|
files.forEach(f => {
|
|
507
625
|
const dir = getFileDir(f.file_path);
|
|
@@ -509,7 +627,6 @@ function renderFiles() {
|
|
|
509
627
|
groups[dir].push(f);
|
|
510
628
|
});
|
|
511
629
|
|
|
512
|
-
// Sort groups by active sort criteria
|
|
513
630
|
const groupMetric = (files) => {
|
|
514
631
|
if (sort === 'touches') return files.reduce((s, f) => s + f.touch_count, 0);
|
|
515
632
|
if (sort === 'sessions') return files.reduce((s, f) => s + f.session_count, 0);
|
|
@@ -528,9 +645,9 @@ function renderFiles() {
|
|
|
528
645
|
return `
|
|
529
646
|
<div class="file-group">
|
|
530
647
|
<div class="file-group-header" data-dir="${escHtml(dir)}">
|
|
531
|
-
<span class="file-group-arrow"
|
|
648
|
+
<span class="file-group-arrow">\u25b6</span>
|
|
532
649
|
<span class="file-group-name">~/${escHtml(dir)}</span>
|
|
533
|
-
<span style="color:var(--
|
|
650
|
+
<span style="color:var(--text-tertiary);font-size:12px;margin-left:auto">${dirFiles.length} files \u00b7 ${groupStat}</span>
|
|
534
651
|
</div>
|
|
535
652
|
<div class="file-group-items" style="display:none">
|
|
536
653
|
${dirFiles.map(f => renderFileItem(f)).join('')}
|
|
@@ -544,10 +661,10 @@ function renderFiles() {
|
|
|
544
661
|
const arrow = h.querySelector('.file-group-arrow');
|
|
545
662
|
if (items.style.display === 'none') {
|
|
546
663
|
items.style.display = 'block';
|
|
547
|
-
arrow.textContent = '
|
|
664
|
+
arrow.textContent = '\u25bc';
|
|
548
665
|
} else {
|
|
549
666
|
items.style.display = 'none';
|
|
550
|
-
arrow.textContent = '
|
|
667
|
+
arrow.textContent = '\u25b6';
|
|
551
668
|
}
|
|
552
669
|
});
|
|
553
670
|
});
|
|
@@ -555,7 +672,6 @@ function renderFiles() {
|
|
|
555
672
|
listEl.innerHTML = files.map(f => renderFileItem(f)).join('');
|
|
556
673
|
}
|
|
557
674
|
|
|
558
|
-
// Event listeners — must re-attach every render since innerHTML replaces DOM
|
|
559
675
|
let debounce;
|
|
560
676
|
const searchInput = $('#fileSearchInput');
|
|
561
677
|
searchInput.addEventListener('input', e => {
|
|
@@ -563,7 +679,6 @@ function renderFiles() {
|
|
|
563
679
|
debounce = setTimeout(() => { window._fileSearch = e.target.value; renderFiles(); }, 200);
|
|
564
680
|
});
|
|
565
681
|
|
|
566
|
-
// Preserve cursor position after re-render
|
|
567
682
|
const cursorPos = window._fileCursorPos || 0;
|
|
568
683
|
searchInput.setSelectionRange(cursorPos, cursorPos);
|
|
569
684
|
if (window._fileSearch) searchInput.focus();
|
|
@@ -582,7 +697,6 @@ function renderFiles() {
|
|
|
582
697
|
item.addEventListener('click', () => viewFileDetail(item.dataset.path));
|
|
583
698
|
});
|
|
584
699
|
|
|
585
|
-
// Track cursor for re-renders
|
|
586
700
|
searchInput.addEventListener('keyup', () => { window._fileCursorPos = searchInput.selectionStart; });
|
|
587
701
|
}
|
|
588
702
|
|
|
@@ -591,11 +705,11 @@ function renderFileItem(f) {
|
|
|
591
705
|
const dir = f.file_path.split('/').slice(0, -1).join('/');
|
|
592
706
|
return `
|
|
593
707
|
<div class="file-item" data-path="${escHtml(f.file_path)}">
|
|
594
|
-
<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>
|
|
595
709
|
<div class="file-meta">
|
|
596
710
|
<span>${f.touch_count} touches</span>
|
|
597
711
|
<span>${f.session_count} sessions</span>
|
|
598
|
-
<span style="color:var(--
|
|
712
|
+
<span style="color:var(--amber)">${escHtml(f.operations)}</span>
|
|
599
713
|
<span class="session-time">${fmtTime(f.last_touched)}</span>
|
|
600
714
|
</div>
|
|
601
715
|
</div>
|
|
@@ -603,17 +717,18 @@ function renderFileItem(f) {
|
|
|
603
717
|
}
|
|
604
718
|
|
|
605
719
|
async function viewFileDetail(filePath) {
|
|
606
|
-
content.innerHTML = '<div class="loading">Loading
|
|
720
|
+
content.innerHTML = '<div class="loading">Loading</div>';
|
|
607
721
|
const data = await api(`/files/sessions?path=${encodeURIComponent(filePath)}`);
|
|
608
722
|
|
|
609
723
|
let html = `
|
|
610
|
-
<div class="back-btn" id="backBtn"
|
|
724
|
+
<div class="back-btn" id="backBtn">\u2190 Back</div>
|
|
611
725
|
<div class="page-title" style="word-break:break-all;font-size:16px">${escHtml(filePath)}</div>
|
|
612
726
|
<div class="section-label">${data.sessions.length} sessions touched this file</div>
|
|
613
727
|
`;
|
|
614
728
|
|
|
615
729
|
html += data.sessions.map(s => renderSessionItem(s)).join('');
|
|
616
730
|
content.innerHTML = html;
|
|
731
|
+
transitionView();
|
|
617
732
|
|
|
618
733
|
$('#backBtn').addEventListener('click', () => viewFiles());
|
|
619
734
|
$$('.session-item').forEach(item => {
|
|
@@ -645,7 +760,7 @@ viewSearch();
|
|
|
645
760
|
// Swipe right from left edge to go back
|
|
646
761
|
(function initSwipeBack() {
|
|
647
762
|
let startX = 0, startY = 0, swiping = false;
|
|
648
|
-
const edgeWidth = 30;
|
|
763
|
+
const edgeWidth = 30;
|
|
649
764
|
const threshold = 80;
|
|
650
765
|
|
|
651
766
|
document.addEventListener('touchstart', e => {
|
|
@@ -661,7 +776,6 @@ viewSearch();
|
|
|
661
776
|
if (!swiping) return;
|
|
662
777
|
const dx = e.touches[0].clientX - startX;
|
|
663
778
|
const dy = Math.abs(e.touches[0].clientY - startY);
|
|
664
|
-
// Cancel if vertical movement exceeds horizontal (it's a scroll)
|
|
665
779
|
if (dy > dx) { swiping = false; }
|
|
666
780
|
}, { passive: true });
|
|
667
781
|
|
|
@@ -685,7 +799,7 @@ viewSearch();
|
|
|
685
799
|
const indicator = document.createElement('div');
|
|
686
800
|
indicator.className = 'ptr-indicator';
|
|
687
801
|
indicator.id = 'ptr';
|
|
688
|
-
indicator.textContent = '
|
|
802
|
+
indicator.textContent = '\u2193 Pull to refresh';
|
|
689
803
|
document.body.appendChild(indicator);
|
|
690
804
|
|
|
691
805
|
document.addEventListener('touchstart', e => {
|
|
@@ -700,7 +814,7 @@ viewSearch();
|
|
|
700
814
|
const diff = e.touches[0].clientY - startY;
|
|
701
815
|
if (diff > 20 && window.scrollY <= 0) {
|
|
702
816
|
indicator.classList.add('visible');
|
|
703
|
-
indicator.textContent = diff > threshold ? '
|
|
817
|
+
indicator.textContent = diff > threshold ? '\u2191 Release to refresh' : '\u2193 Pull to refresh';
|
|
704
818
|
} else {
|
|
705
819
|
indicator.classList.remove('visible');
|
|
706
820
|
}
|
|
@@ -711,11 +825,10 @@ viewSearch();
|
|
|
711
825
|
pulling = false;
|
|
712
826
|
const diff = e.changedTouches[0].clientY - startY;
|
|
713
827
|
if (diff > threshold && indicator.classList.contains('visible')) {
|
|
714
|
-
indicator.textContent = 'Refreshing
|
|
828
|
+
indicator.textContent = 'Refreshing\u2026';
|
|
715
829
|
indicator.classList.add('refreshing');
|
|
716
830
|
try {
|
|
717
831
|
await api('/reindex');
|
|
718
|
-
// If viewing a session detail, refresh it in place
|
|
719
832
|
const backBtn = $('#backBtn');
|
|
720
833
|
if (backBtn && window._currentSessionId) {
|
|
721
834
|
await viewSession(window._currentSessionId);
|