agentacta 1.1.0 → 1.1.2

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/db.js CHANGED
@@ -114,6 +114,7 @@ function init(dbPath) {
114
114
  if (!cols.includes('output_tokens')) db.exec("ALTER TABLE sessions ADD COLUMN output_tokens INTEGER DEFAULT 0");
115
115
  if (!cols.includes('cache_read_tokens')) db.exec("ALTER TABLE sessions ADD COLUMN cache_read_tokens INTEGER DEFAULT 0");
116
116
  if (!cols.includes('cache_write_tokens')) db.exec("ALTER TABLE sessions ADD COLUMN cache_write_tokens INTEGER DEFAULT 0");
117
+ if (!cols.includes('models')) db.exec("ALTER TABLE sessions ADD COLUMN models TEXT");
117
118
 
118
119
  db.close();
119
120
  }
@@ -126,7 +127,7 @@ function createStmts(db) {
126
127
  deleteSession: db.prepare('DELETE FROM sessions WHERE id = ?'),
127
128
  deleteFileActivity: db.prepare('DELETE FROM file_activity WHERE session_id = ?'),
128
129
  insertEvent: db.prepare(`INSERT OR REPLACE INTO events (id, session_id, timestamp, type, role, content, tool_name, tool_args, tool_result) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
129
- upsertSession: db.prepare(`INSERT OR REPLACE INTO sessions (id, start_time, end_time, message_count, tool_count, model, summary, agent, session_type, total_cost, total_tokens, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, initial_prompt, first_message_id, first_message_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
130
+ upsertSession: db.prepare(`INSERT OR REPLACE INTO sessions (id, start_time, end_time, message_count, tool_count, model, summary, agent, session_type, total_cost, total_tokens, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, initial_prompt, first_message_id, first_message_timestamp, models) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
130
131
  upsertState: db.prepare(`INSERT OR REPLACE INTO index_state (file_path, last_offset, last_modified) VALUES (?, ?, ?)`),
131
132
  insertFileActivity: db.prepare(`INSERT INTO file_activity (session_id, file_path, operation, timestamp) VALUES (?, ?, ?, ?)`),
132
133
  deleteArchive: db.prepare('DELETE FROM archive WHERE session_id = ?'),
package/indexer.js CHANGED
@@ -126,6 +126,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
126
126
  let msgCount = 0;
127
127
  let toolCount = 0;
128
128
  let model = null;
129
+ const modelsSet = new Set();
129
130
  let summary = '';
130
131
  let sessionType = null;
131
132
  let agent = agentName;
@@ -182,7 +183,10 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
182
183
  try { obj = JSON.parse(line); } catch { continue; }
183
184
 
184
185
  if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom' || obj.type === 'file-history-snapshot') {
185
- if (obj.type === 'model_change') model = obj.modelId || model;
186
+ if (obj.type === 'model_change' && obj.modelId) {
187
+ if (!model) model = obj.modelId; // First model for backwards compat
188
+ modelsSet.add(obj.modelId); // Collect all unique models
189
+ }
186
190
  continue;
187
191
  }
188
192
 
@@ -204,9 +208,10 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
204
208
  if (msg) {
205
209
  sessionEnd = ts;
206
210
 
207
- // Extract model from assistant messages as fallback
208
- if (!model && msg.role === 'assistant' && msg.model && msg.model !== 'delivery-mirror' && !msg.model.startsWith('<')) {
209
- model = msg.model;
211
+ // Extract model from assistant messages
212
+ if (msg.role === 'assistant' && msg.model && msg.model !== 'delivery-mirror' && !msg.model.startsWith('<')) {
213
+ if (!model) model = msg.model; // Keep first model for backwards compat
214
+ modelsSet.add(msg.model); // Collect all unique models
210
215
  }
211
216
 
212
217
  // Cost tracking
@@ -292,7 +297,8 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
292
297
  if (/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-/.test(p)) sessionType = 'subagent';
293
298
  }
294
299
 
295
- stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp);
300
+ const modelsJson = modelsSet.size > 0 ? JSON.stringify([...modelsSet]) : null;
301
+ stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson);
296
302
  for (const ev of pendingEvents) stmts.insertEvent.run(...ev);
297
303
  for (const fa of fileActivities) stmts.insertFileActivity.run(...fa);
298
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Audit trail and search engine for AI agent sessions",
5
5
  "main": "index.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -113,6 +113,16 @@ function fmtTimeOnly(ts) {
113
113
  return new Date(ts).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
114
114
  }
115
115
 
116
+ function renderModelTags(s) {
117
+ // Prefer models array if present, fall back to single model
118
+ let models = [];
119
+ if (s.models) {
120
+ try { models = JSON.parse(s.models); } catch {}
121
+ }
122
+ if (!models.length && s.model) models = [s.model];
123
+ return models.map(m => `<span class="session-model">${escHtml(m)}</span>`).join('');
124
+ }
125
+
116
126
  function renderSessionItem(s) {
117
127
  const duration = fmtDuration(s.start_time, s.end_time);
118
128
  const timeRange = `${fmtTime(s.start_time)} → ${s.end_time ? fmtTimeOnly(s.end_time) : 'now'}`;
@@ -121,10 +131,10 @@ function renderSessionItem(s) {
121
131
  <div class="session-item" data-id="${s.id}">
122
132
  <div class="session-header">
123
133
  <span class="session-time">${timeRange} · ${duration}</span>
124
- <span style="display:flex;gap:6px;align-items:center">
134
+ <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
125
135
  ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
126
136
  ${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
127
- ${s.model ? `<span class="session-model">${escHtml(s.model)}</span>` : ''}
137
+ ${renderModelTags(s)}
128
138
  </span>
129
139
  </div>
130
140
  <div class="session-summary">${escHtml(truncate(s.summary || 'No summary', 120))}</div>
@@ -218,7 +228,11 @@ async function showSearchHome() {
218
228
  });
219
229
 
220
230
  $$('.session-item', el).forEach(item => {
221
- item.addEventListener('click', () => viewSession(item.dataset.id));
231
+ item.addEventListener('click', () => {
232
+ window._lastView = 'search';
233
+ window._lastSearchQuery = $('#searchInput')?.value || '';
234
+ viewSession(item.dataset.id);
235
+ });
222
236
  });
223
237
  }
224
238
 
@@ -260,11 +274,16 @@ async function doSearch(q) {
260
274
  `).join('');
261
275
 
262
276
  $$('.session-link', el).forEach(link => {
263
- link.addEventListener('click', () => viewSession(link.dataset.session));
277
+ link.addEventListener('click', () => {
278
+ window._lastView = 'search';
279
+ window._lastSearchQuery = q;
280
+ viewSession(link.dataset.session);
281
+ });
264
282
  });
265
283
  }
266
284
 
267
285
  async function viewSessions() {
286
+ window._currentSessionId = null;
268
287
  content.innerHTML = '<div class="loading">Loading…</div>';
269
288
  const data = await api('/sessions?limit=200');
270
289
 
@@ -278,6 +297,7 @@ async function viewSessions() {
278
297
  }
279
298
 
280
299
  async function viewSession(id) {
300
+ window._currentSessionId = id;
281
301
  content.innerHTML = '<div class="loading">Loading…</div>';
282
302
  const data = await api(`/sessions/${id}`);
283
303
 
@@ -287,22 +307,20 @@ async function viewSession(id) {
287
307
  const cost = fmtCost(s.total_cost);
288
308
  let html = `
289
309
  <div class="back-btn" id="backBtn">← Back</div>
290
- <div style="display:flex;justify-content:space-between;align-items:center">
291
- <div class="page-title">Session</div>
292
- <div style="display:flex;gap:8px;align-items:center">
293
- ${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">↗️ Initial Prompt</button>` : ''}
294
- ${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">📦 JSONL</a>` : ''}
295
- <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=md','session.md');return false">📄 MD</a>
296
- <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=json','session.json');return false">📋 JSON</a>
297
- </div>
310
+ <div class="page-title">Session</div>
311
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
312
+ ${s.first_message_id ? `<button class="jump-to-start-btn" id="jumpToStartBtn" title="Jump to initial prompt">↗️ Initial Prompt</button>` : ''}
313
+ ${data.hasArchive ? `<a class="export-btn" href="#" onclick="dlExport('/api/archive/export/${id}','session.jsonl');return false">📦 JSONL</a>` : ''}
314
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=md','session.md');return false">📄 MD</a>
315
+ <a class="export-btn" href="#" onclick="dlExport('/api/export/session/${id}?format=json','session.json');return false">📋 JSON</a>
298
316
  </div>
299
317
  <div class="session-item" style="cursor:default">
300
318
  <div class="session-header">
301
319
  <span class="session-time">${fmtDate(s.start_time)} · ${fmtTimeShort(s.start_time)} – ${fmtTimeShort(s.end_time)}</span>
302
- <span style="display:flex;gap:6px;align-items:center">
320
+ <span style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
303
321
  ${s.agent && s.agent !== 'main' ? `<span class="session-agent">${escHtml(s.agent)}</span>` : ''}
304
322
  ${s.session_type ? `<span class="session-type">${escHtml(s.session_type)}</span>` : ''}
305
- ${s.model ? `<span class="session-model">${escHtml(s.model)}</span>` : ''}
323
+ ${renderModelTags(s)}
306
324
  </span>
307
325
  </div>
308
326
  <div class="session-meta" style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px 16px">
@@ -320,6 +338,7 @@ async function viewSession(id) {
320
338
  $('#backBtn').addEventListener('click', () => {
321
339
  if (window._lastView === 'timeline') viewTimeline();
322
340
  else if (window._lastView === 'files') viewFiles();
341
+ else if (window._lastView === 'search') viewSearch(window._lastSearchQuery || '');
323
342
  else viewSessions();
324
343
  });
325
344
 
@@ -613,6 +632,40 @@ $$('.nav-item').forEach(item => {
613
632
 
614
633
  viewSearch();
615
634
 
635
+ // Swipe right from left edge to go back
636
+ (function initSwipeBack() {
637
+ let startX = 0, startY = 0, swiping = false;
638
+ const edgeWidth = 30; // px from left edge
639
+ const threshold = 80;
640
+
641
+ document.addEventListener('touchstart', e => {
642
+ const x = e.touches[0].clientX;
643
+ if (x <= edgeWidth) {
644
+ startX = x;
645
+ startY = e.touches[0].clientY;
646
+ swiping = true;
647
+ }
648
+ }, { passive: true });
649
+
650
+ document.addEventListener('touchmove', e => {
651
+ if (!swiping) return;
652
+ const dx = e.touches[0].clientX - startX;
653
+ const dy = Math.abs(e.touches[0].clientY - startY);
654
+ // Cancel if vertical movement exceeds horizontal (it's a scroll)
655
+ if (dy > dx) { swiping = false; }
656
+ }, { passive: true });
657
+
658
+ document.addEventListener('touchend', e => {
659
+ if (!swiping) return;
660
+ swiping = false;
661
+ const dx = e.changedTouches[0].clientX - startX;
662
+ if (dx > threshold) {
663
+ const backBtn = $('#backBtn');
664
+ if (backBtn) backBtn.click();
665
+ }
666
+ });
667
+ })();
668
+
616
669
  // Pull to refresh
617
670
  (function initPTR() {
618
671
  let startY = 0;
@@ -652,8 +705,14 @@ viewSearch();
652
705
  indicator.classList.add('refreshing');
653
706
  try {
654
707
  await api('/reindex');
655
- const active = $('.nav-item.active');
656
- if (active) active.click();
708
+ // If viewing a session detail, refresh it in place
709
+ const backBtn = $('#backBtn');
710
+ if (backBtn && window._currentSessionId) {
711
+ await viewSession(window._currentSessionId);
712
+ } else {
713
+ const active = $('.nav-item.active');
714
+ if (active) active.click();
715
+ }
657
716
  } catch(err) {}
658
717
  setTimeout(() => {
659
718
  indicator.classList.remove('visible', 'refreshing');