agentgui 1.0.705 → 1.0.707

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/acp-queries.js ADDED
@@ -0,0 +1,182 @@
1
+ import { randomUUID } from 'crypto';
2
+ const gid = (p) => `${p}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
3
+ const uuid = () => randomUUID();
4
+ const iso = (t) => new Date(t).toISOString();
5
+ const j = (o) => JSON.stringify(o);
6
+ const jp = (s) => { try { return JSON.parse(s); } catch { return {}; } };
7
+
8
+ export function createACPQueries(db, prep) {
9
+ return {
10
+ createThread(metadata = {}) {
11
+ const id = uuid(), now = Date.now();
12
+ prep('INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)').run(id, 'unknown', null, now, now, 'idle', j(metadata));
13
+ return { thread_id: id, created_at: iso(now), updated_at: iso(now), metadata, status: 'idle' };
14
+ },
15
+ getThread(tid) {
16
+ const r = prep('SELECT * FROM conversations WHERE id = ?').get(tid);
17
+ if (!r) return null;
18
+ return { thread_id: r.id, created_at: iso(r.created_at), updated_at: iso(r.updated_at), metadata: jp(r.metadata), status: r.status || 'idle' };
19
+ },
20
+ patchThread(tid, upd) {
21
+ const t = this.getThread(tid);
22
+ if (!t) throw new Error('Thread not found');
23
+ const now = Date.now(), meta = upd.metadata !== undefined ? upd.metadata : t.metadata, stat = upd.status !== undefined ? upd.status : t.status;
24
+ prep('UPDATE conversations SET metadata = ?, status = ?, updated_at = ? WHERE id = ?').run(j(meta), stat, now, tid);
25
+ return { thread_id: tid, created_at: t.created_at, updated_at: iso(now), metadata: meta, status: stat };
26
+ },
27
+ deleteThread(tid) {
28
+ const pr = prep('SELECT COUNT(*) as count FROM run_metadata WHERE thread_id = ? AND status = ?').get(tid, 'pending');
29
+ if (pr && pr.count > 0) throw new Error('Cannot delete thread with pending runs');
30
+ db.transaction(() => {
31
+ prep('DELETE FROM thread_states WHERE thread_id = ?').run(tid);
32
+ prep('DELETE FROM checkpoints WHERE thread_id = ?').run(tid);
33
+ prep('DELETE FROM run_metadata WHERE thread_id = ?').run(tid);
34
+ prep('DELETE FROM sessions WHERE conversationId = ?').run(tid);
35
+ prep('DELETE FROM messages WHERE conversationId = ?').run(tid);
36
+ prep('DELETE FROM chunks WHERE conversationId = ?').run(tid);
37
+ prep('DELETE FROM events WHERE conversationId = ?').run(tid);
38
+ prep('DELETE FROM conversations WHERE id = ?').run(tid);
39
+ })();
40
+ return true;
41
+ },
42
+ saveThreadState(tid, cid, sd) {
43
+ const id = gid('state'), now = Date.now();
44
+ prep('INSERT INTO thread_states (id, thread_id, checkpoint_id, state_data, created_at) VALUES (?, ?, ?, ?, ?)').run(id, tid, cid, j(sd), now);
45
+ return { id, thread_id: tid, checkpoint_id: cid, created_at: iso(now) };
46
+ },
47
+ getThreadState(tid, cid = null) {
48
+ const r = cid ? prep('SELECT * FROM thread_states WHERE thread_id = ? AND checkpoint_id = ? ORDER BY created_at DESC LIMIT 1').get(tid, cid) : prep('SELECT * FROM thread_states WHERE thread_id = ? ORDER BY created_at DESC LIMIT 1').get(tid);
49
+ if (!r) return null;
50
+ const sd = jp(r.state_data);
51
+ return { checkpoint: { checkpoint_id: r.checkpoint_id }, values: sd.values || {}, messages: sd.messages || [], metadata: sd.metadata || {} };
52
+ },
53
+ getThreadHistory(tid, lim = 50, off = 0) {
54
+ const tot = prep('SELECT COUNT(*) as count FROM thread_states WHERE thread_id = ?').get(tid).count;
55
+ const rows = prep('SELECT * FROM thread_states WHERE thread_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(tid, lim, off);
56
+ const states = rows.map(r => { const sd = jp(r.state_data); return { checkpoint: { checkpoint_id: r.checkpoint_id }, values: sd.values || {}, messages: sd.messages || [], metadata: sd.metadata || {} }; });
57
+ return { states, total: tot, limit: lim, offset: off, hasMore: off + lim < tot };
58
+ },
59
+ copyThread(stid) {
60
+ const st = this.getThread(stid);
61
+ if (!st) throw new Error('Source thread not found');
62
+ const ntid = uuid(), now = Date.now();
63
+ db.transaction(() => {
64
+ prep('INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, metadata, workingDirectory) SELECT ?, agentId, title || \' (copy)\', ?, ?, status, metadata, workingDirectory FROM conversations WHERE id = ?').run(ntid, now, now, stid);
65
+ const cps = prep('SELECT * FROM checkpoints WHERE thread_id = ? ORDER BY sequence ASC').all(stid);
66
+ cps.forEach(cp => prep('INSERT INTO checkpoints (id, thread_id, checkpoint_name, sequence, created_at) VALUES (?, ?, ?, ?, ?)').run(uuid(), ntid, cp.checkpoint_name, cp.sequence, now));
67
+ const sts = prep('SELECT * FROM thread_states WHERE thread_id = ? ORDER BY created_at ASC').all(stid);
68
+ sts.forEach(s => prep('INSERT INTO thread_states (id, thread_id, checkpoint_id, state_data, created_at) VALUES (?, ?, ?, ?, ?)').run(gid('state'), ntid, s.checkpoint_id, s.state_data, now));
69
+ const msgs = prep('SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC').all(stid);
70
+ msgs.forEach(m => prep('INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)').run(gid('msg'), ntid, m.role, m.content, now));
71
+ })();
72
+ return this.getThread(ntid);
73
+ },
74
+ createCheckpoint(tid, name = null) {
75
+ const id = uuid(), now = Date.now();
76
+ const ms = prep('SELECT MAX(sequence) as max FROM checkpoints WHERE thread_id = ?').get(tid);
77
+ const seq = (ms?.max ?? -1) + 1;
78
+ prep('INSERT INTO checkpoints (id, thread_id, checkpoint_name, sequence, created_at) VALUES (?, ?, ?, ?, ?)').run(id, tid, name, seq, now);
79
+ return { checkpoint_id: id, thread_id: tid, checkpoint_name: name, sequence: seq, created_at: iso(now) };
80
+ },
81
+ getCheckpoint(cid) {
82
+ const r = prep('SELECT * FROM checkpoints WHERE id = ?').get(cid);
83
+ if (!r) return null;
84
+ return { checkpoint_id: r.id, thread_id: r.thread_id, checkpoint_name: r.checkpoint_name, sequence: r.sequence, created_at: iso(r.created_at) };
85
+ },
86
+ listCheckpoints(tid, lim = 50, off = 0) {
87
+ const tot = prep('SELECT COUNT(*) as count FROM checkpoints WHERE thread_id = ?').get(tid).count;
88
+ const rows = prep('SELECT * FROM checkpoints WHERE thread_id = ? ORDER BY sequence DESC LIMIT ? OFFSET ?').all(tid, lim, off);
89
+ const cps = rows.map(r => ({ checkpoint_id: r.id, thread_id: r.thread_id, checkpoint_name: r.checkpoint_name, sequence: r.sequence, created_at: iso(r.created_at) }));
90
+ return { checkpoints: cps, total: tot, limit: lim, offset: off, hasMore: off + lim < tot };
91
+ },
92
+ createRun(aid, tid = null, inp = null, cfg = null, wh = null) {
93
+ const rid = uuid(), now = Date.now(), mid = gid('runmeta');
94
+ let atid = tid;
95
+ if (!tid) {
96
+ atid = uuid();
97
+ prep('INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)').run(atid, aid, 'Stateless Run', now, now, 'idle', '{"stateless":true}');
98
+ }
99
+ prep('INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)').run(rid, atid, 'pending', now, null, null, null);
100
+ prep('INSERT INTO run_metadata (id, run_id, thread_id, agent_id, status, input, config, webhook_url, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(mid, rid, tid, aid, 'pending', inp ? j(inp) : null, cfg ? j(cfg) : null, wh, now, now);
101
+ return { run_id: rid, thread_id: tid, agent_id: aid, status: 'pending', created_at: iso(now), updated_at: iso(now) };
102
+ },
103
+ getRun(rid) {
104
+ const r = prep('SELECT * FROM run_metadata WHERE run_id = ?').get(rid);
105
+ if (!r) return null;
106
+ return { run_id: r.run_id, thread_id: r.thread_id, agent_id: r.agent_id, status: r.status, created_at: iso(r.created_at), updated_at: iso(r.updated_at) };
107
+ },
108
+ updateRunStatus(rid, stat) {
109
+ const now = Date.now();
110
+ prep('UPDATE run_metadata SET status = ?, updated_at = ? WHERE run_id = ?').run(stat, now, rid);
111
+ prep('UPDATE sessions SET status = ? WHERE id = ?').run(stat, rid);
112
+ return this.getRun(rid);
113
+ },
114
+ cancelRun(rid) {
115
+ const r = this.getRun(rid);
116
+ if (!r) throw new Error('Run not found');
117
+ if (['success', 'error', 'cancelled'].includes(r.status)) throw new Error('Run already completed or cancelled');
118
+ return this.updateRunStatus(rid, 'cancelled');
119
+ },
120
+ deleteRun(rid) {
121
+ db.transaction(() => {
122
+ prep('DELETE FROM chunks WHERE sessionId = ?').run(rid);
123
+ prep('DELETE FROM events WHERE sessionId = ?').run(rid);
124
+ prep('DELETE FROM run_metadata WHERE run_id = ?').run(rid);
125
+ prep('DELETE FROM sessions WHERE id = ?').run(rid);
126
+ })();
127
+ return true;
128
+ },
129
+ getThreadRuns(tid, lim = 50, off = 0) {
130
+ const tot = prep('SELECT COUNT(*) as count FROM run_metadata WHERE thread_id = ?').get(tid).count;
131
+ const rows = prep('SELECT * FROM run_metadata WHERE thread_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(tid, lim, off);
132
+ const runs = rows.map(r => ({ run_id: r.run_id, thread_id: r.thread_id, agent_id: r.agent_id, status: r.status, created_at: iso(r.created_at), updated_at: iso(r.updated_at) }));
133
+ return { runs, total: tot, limit: lim, offset: off, hasMore: off + lim < tot };
134
+ },
135
+ searchThreads(flt = {}) {
136
+ const { metadata, status, dateRange, limit = 50, offset = 0 } = flt;
137
+ let wh = "status != 'deleted'", prm = [];
138
+ if (status) { wh += ' AND status = ?'; prm.push(status); }
139
+ if (dateRange?.start) { wh += ' AND created_at >= ?'; prm.push(new Date(dateRange.start).getTime()); }
140
+ if (dateRange?.end) { wh += ' AND created_at <= ?'; prm.push(new Date(dateRange.end).getTime()); }
141
+ if (metadata) { for (const [k, v] of Object.entries(metadata)) { wh += ' AND metadata LIKE ?'; prm.push(`%"${k}":"${v}"%`); } }
142
+ const tot = prep(`SELECT COUNT(*) as count FROM conversations WHERE ${wh}`).get(...prm).count;
143
+ const rows = prep(`SELECT * FROM conversations WHERE ${wh} ORDER BY updated_at DESC LIMIT ? OFFSET ?`).all(...prm, limit, offset);
144
+ const ths = rows.map(r => ({ thread_id: r.id, created_at: iso(r.created_at), updated_at: iso(r.updated_at), metadata: jp(r.metadata), status: r.status || 'idle' }));
145
+ return { threads: ths, total: tot, limit, offset, hasMore: offset + limit < tot };
146
+ },
147
+ searchAgents(agents, flt = {}) {
148
+ const { name, version, capabilities, limit = 50, offset = 0 } = flt;
149
+ let results = agents;
150
+ if (name) {
151
+ const n = name.toLowerCase();
152
+ results = results.filter(a => a.name.toLowerCase().includes(n) || a.id.toLowerCase().includes(n));
153
+ }
154
+ if (capabilities) {
155
+ results = results.filter(a => {
156
+ const desc = this.getAgentDescriptor ? this.getAgentDescriptor(a.id) : null;
157
+ if (!desc) return false;
158
+ const caps = desc.specs?.capabilities || {};
159
+ if (capabilities.streaming !== undefined && !caps.streaming) return false;
160
+ if (capabilities.threads !== undefined && caps.threads !== capabilities.threads) return false;
161
+ if (capabilities.interrupts !== undefined && caps.interrupts !== capabilities.interrupts) return false;
162
+ return true;
163
+ });
164
+ }
165
+ const total = results.length;
166
+ const paginated = results.slice(offset, offset + limit);
167
+ const agents_list = paginated.map(a => ({ agent_id: a.id, name: a.name, version: version || '1.0.0', path: a.path }));
168
+ return { agents: agents_list, total, limit, offset, hasMore: offset + limit < total };
169
+ },
170
+ searchRuns(flt = {}) {
171
+ const { agent_id, thread_id, status, limit = 50, offset = 0 } = flt;
172
+ let wh = '1=1', prm = [];
173
+ if (agent_id) { wh += ' AND agent_id = ?'; prm.push(agent_id); }
174
+ if (thread_id) { wh += ' AND thread_id = ?'; prm.push(thread_id); }
175
+ if (status) { wh += ' AND status = ?'; prm.push(status); }
176
+ const tot = prep(`SELECT COUNT(*) as count FROM run_metadata WHERE ${wh}`).get(...prm).count;
177
+ const rows = prep(`SELECT * FROM run_metadata WHERE ${wh} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...prm, limit, offset);
178
+ const runs = rows.map(r => ({ run_id: r.run_id, thread_id: r.thread_id, agent_id: r.agent_id, status: r.status, created_at: iso(r.created_at), updated_at: iso(r.updated_at) }));
179
+ return { runs, total: tot, limit, offset, hasMore: offset + limit < tot };
180
+ }
181
+ };
182
+ }
@@ -603,7 +603,46 @@ registry.register({
603
603
 
604
604
  parseOutput(line) {
605
605
  try {
606
- return JSON.parse(line);
606
+ const entry = JSON.parse(line);
607
+ if (!entry || typeof entry !== 'object') return null;
608
+
609
+ // Filter isMeta user entries (local command caveats, not real conversation turns)
610
+ if (entry.type === 'user' && entry.isMeta === true) return null;
611
+
612
+ // Mark isCompactSummary entries so renderer can display them specially
613
+ // (already passes through as-is, renderer checks this flag)
614
+
615
+ // Detect rate limit via isApiErrorMessage + error field
616
+ if (entry.isApiErrorMessage === true && entry.error === 'rate_limit') {
617
+ entry._rateLimitDetected = true;
618
+ }
619
+
620
+ // Annotate streaming fragments vs final consolidated response
621
+ // assistant entries with stop_reason: null are fragments; non-null stop_reason is final
622
+ if (entry.type === 'assistant' && entry.message) {
623
+ entry._isFragment = entry.message.stop_reason === null || entry.message.stop_reason === undefined;
624
+ }
625
+
626
+ // Extract turn duration from system/turn_duration entries
627
+ if (entry.type === 'system' && entry.subtype === 'turn_duration' && entry.durationMs) {
628
+ entry._turnDurationMs = entry.durationMs;
629
+ }
630
+
631
+ // Extract compact boundary metadata
632
+ if (entry.type === 'system' && entry.subtype === 'compact_boundary' && entry.compactMetadata) {
633
+ entry._preTokens = entry.compactMetadata.preTokens;
634
+ }
635
+
636
+ // Normalize cache usage fields into a flat structure for cost tracking
637
+ if (entry.message?.usage) {
638
+ const u = entry.message.usage;
639
+ entry._cacheUsage = {
640
+ cache_creation: u.cache_creation_input_tokens || u['cache_creation.ephemeral_1h_input_tokens'] || u['cache_creation.ephemeral_5m_input_tokens'] || 0,
641
+ cache_read: u.cache_read_input_tokens || 0
642
+ };
643
+ }
644
+
645
+ return entry;
607
646
  } catch {
608
647
  return null;
609
648
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.705",
3
+ "version": "1.0.707",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -23,6 +23,11 @@ class EventProcessor {
23
23
  return null;
24
24
  }
25
25
 
26
+ // Filter isMeta user entries (local command caveats injected by Claude Code CLI)
27
+ if (event.type === 'user' && event.isMeta === true) {
28
+ return null;
29
+ }
30
+
26
31
  const startTime = performance.now();
27
32
  this.stats.totalEvents++;
28
33
 
@@ -42,6 +47,25 @@ class EventProcessor {
42
47
  processTime: 0
43
48
  };
44
49
 
50
+ // Route isCompactSummary entries to special display type
51
+ if (event.isCompactSummary === true) {
52
+ processed.type = 'compact_summary';
53
+ this.stats.transformedEvents++;
54
+ }
55
+
56
+ // Extract and flatten cache usage for cost tracking
57
+ if (event.message?.usage) {
58
+ const u = event.message.usage;
59
+ processed._cacheCreation = u.cache_creation_input_tokens
60
+ || u['cache_creation.ephemeral_1h_input_tokens']
61
+ || u['cache_creation.ephemeral_5m_input_tokens']
62
+ || 0;
63
+ processed._cacheRead = u.cache_read_input_tokens || 0;
64
+ if (processed._cacheCreation || processed._cacheRead) {
65
+ this.stats.transformedEvents++;
66
+ }
67
+ }
68
+
45
69
  if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
46
70
  processed.isImage = true;
47
71
  processed.imagePath = event.path;
@@ -101,90 +125,11 @@ class EventProcessor {
101
125
  return true;
102
126
  }
103
127
 
104
- /**
105
- * Detect language from content or hint
106
- */
107
- detectLanguage(content, hint = null) {
108
- if (hint) {
109
- return hint.toLowerCase();
110
- }
111
-
112
- // Simple language detection based on shebang or content patterns
113
- if (content.startsWith('#!/')) {
114
- if (content.includes('python')) return 'python';
115
- if (content.includes('node') || content.includes('javascript')) return 'javascript';
116
- if (content.includes('bash') || content.includes('sh')) return 'bash';
117
- if (content.includes('ruby')) return 'ruby';
118
- }
119
-
120
- // Pattern detection
121
- if (content.includes('def ') && content.includes(':')) return 'python';
122
- if (content.includes('function') || content.includes('=>')) return 'javascript';
123
- if (content.includes('fn ') && content.includes('->')) return 'rust';
124
- if (content.includes('public static void') || content.includes('class ')) return 'java';
125
-
126
- return 'plaintext';
127
- }
128
-
129
- /**
130
- * Parse JSON safely
131
- */
132
- parseJSON(jsonStr) {
133
- try {
134
- return JSON.parse(jsonStr);
135
- } catch (e) {
136
- console.error('JSON parse error:', e);
137
- return null;
138
- }
139
- }
140
-
141
- /**
142
- * Format JSON for display
143
- */
144
- formatJSON(obj, indent = 2) {
145
- try {
146
- return JSON.stringify(obj, null, indent);
147
- } catch (e) {
148
- return String(obj);
149
- }
150
- }
151
-
152
- /**
153
- * Extract file extension
154
- */
155
128
  getFileExtension(filePath) {
156
129
  const match = filePath.match(/\.([^.]+)$/);
157
130
  return match ? match[1].toLowerCase() : null;
158
131
  }
159
132
 
160
- /**
161
- * Truncate text with ellipsis
162
- */
163
- truncateText(text, maxLength = 200) {
164
- if (text.length <= maxLength) {
165
- return text;
166
- }
167
- return text.substring(0, maxLength) + '...';
168
- }
169
-
170
- /**
171
- * HTML escape utility
172
- */
173
- escapeHtml(text) {
174
- return window._escHtml(text);
175
- }
176
-
177
- /**
178
- * Format timestamp
179
- */
180
- formatTimestamp(timestamp) {
181
- const date = new Date(timestamp);
182
- return date.toLocaleTimeString();
183
- }
184
-
185
- /**
186
- * Get statistics
187
- */
188
133
  getStats() {
189
134
  return { ...this.stats };
190
135
  }
@@ -350,6 +350,16 @@ class StreamingRenderer {
350
350
  return null;
351
351
  case 'tool_use':
352
352
  return this.renderToolUse(event);
353
+ case 'compact_boundary':
354
+ return this.renderCompactBoundary(event);
355
+ case 'compact_summary':
356
+ return this.renderCompactBoundary(event);
357
+ case 'turn_duration':
358
+ return this.renderTurnDuration(event);
359
+ case 'agent_progress':
360
+ return this.renderAgentProgress(event);
361
+ case 'mcp_progress':
362
+ return this.renderMcpProgress(event);
353
363
  default:
354
364
  return this.renderGeneric(event);
355
365
  }
@@ -527,7 +537,7 @@ class StreamingRenderer {
527
537
  }
528
538
 
529
539
  /**
530
- * Render thinking block (expandable)
540
+ * Render thinking block (expandable), signature-aware
531
541
  */
532
542
  renderBlockThinking(block, context) {
533
543
  const div = document.createElement('div');
@@ -535,11 +545,13 @@ class StreamingRenderer {
535
545
  div.classList.add(this._getBlockTypeClass('thinking'));
536
546
 
537
547
  const thinking = block.thinking || '';
548
+ const hasSignature = !!block.signature;
538
549
  div.innerHTML = `
539
550
  <details>
540
551
  <summary>
541
552
  <svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
542
553
  <span>Thinking Process</span>
554
+ ${hasSignature ? '<span style="margin-left:0.5rem;font-size:0.65rem;opacity:0.5;font-weight:400">verified</span>' : ''}
543
555
  </summary>
544
556
  <div class="thinking-content">${this.escapeHtml(thinking)}</div>
545
557
  </details>
@@ -1333,6 +1345,8 @@ class StreamingRenderer {
1333
1345
  * Render system event
1334
1346
  */
1335
1347
  renderBlockSystem(block, context) {
1348
+ if (block.subtype === 'compact_boundary') return this.renderCompactBoundary(block);
1349
+ if (block.subtype === 'turn_duration') return this.renderTurnDuration(block);
1336
1350
  if (!block.model && !block.cwd && !block.tools) return null;
1337
1351
  const details = document.createElement('details');
1338
1352
  details.className = 'folded-tool folded-tool-info permanently-expanded';
@@ -2030,6 +2044,70 @@ class StreamingRenderer {
2030
2044
 
2031
2045
 
2032
2046
 
2047
+ renderCompactBoundary(event) {
2048
+ const preTokens = event._preTokens || event.compactMetadata?.preTokens;
2049
+ const div = document.createElement('div');
2050
+ div.className = 'event-compact-boundary';
2051
+ div.dataset.eventType = 'compact_boundary';
2052
+ div.innerHTML = `
2053
+ <div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0.75rem;margin:0.5rem 0;background:var(--color-bg-secondary);border-radius:0.375rem;font-size:0.75rem;color:var(--color-text-secondary)">
2054
+ <svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;flex-shrink:0;opacity:0.6"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h6a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>
2055
+ <span>Conversation compacted${preTokens ? ` (${preTokens.toLocaleString()} tokens before)` : ''}</span>
2056
+ </div>
2057
+ `;
2058
+ return div;
2059
+ }
2060
+
2061
+ renderTurnDuration(event) {
2062
+ const ms = event._turnDurationMs || event.durationMs;
2063
+ if (!ms) return null;
2064
+ const sec = (ms / 1000).toFixed(1);
2065
+ const div = document.createElement('div');
2066
+ div.className = 'event-turn-duration';
2067
+ div.dataset.eventType = 'turn_duration';
2068
+ div.innerHTML = `
2069
+ <div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.75rem;font-size:0.7rem;color:var(--color-text-secondary);opacity:0.7">
2070
+ <svg viewBox="0 0 20 20" fill="currentColor" style="width:0.75rem;height:0.75rem;flex-shrink:0"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
2071
+ <span>${sec}s</span>
2072
+ </div>
2073
+ `;
2074
+ return div;
2075
+ }
2076
+
2077
+ renderAgentProgress(event) {
2078
+ const msg = event.data?.message || event.message || '';
2079
+ if (!msg) return null;
2080
+ const div = document.createElement('div');
2081
+ div.className = 'event-agent-progress';
2082
+ div.dataset.eventType = 'agent_progress';
2083
+ div.innerHTML = `
2084
+ <div style="display:flex;align-items:flex-start;gap:0.5rem;padding:0.375rem 0.75rem;margin:0.25rem 0;background:var(--color-bg-secondary);border-left:2px solid #7c3aed;border-radius:0 0.25rem 0.25rem 0;font-size:0.75rem;color:var(--color-text-secondary)">
2085
+ <svg viewBox="0 0 20 20" fill="currentColor" style="width:0.875rem;height:0.875rem;flex-shrink:0;margin-top:0.125rem;color:#7c3aed"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5z" clip-rule="evenodd"/></svg>
2086
+ <span>${this.escapeHtml(msg)}</span>
2087
+ </div>
2088
+ `;
2089
+ return div;
2090
+ }
2091
+
2092
+ renderMcpProgress(event) {
2093
+ const status = event.status || 'running';
2094
+ const name = event.tool || event.name || 'MCP tool';
2095
+ const statusColors = { running: '#0891b2', completed: '#059669', failed: '#dc2626' };
2096
+ const color = statusColors[status] || statusColors.running;
2097
+ const div = document.createElement('div');
2098
+ div.className = 'event-mcp-progress';
2099
+ div.dataset.eventType = 'mcp_progress';
2100
+ div.innerHTML = `
2101
+ <div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.75rem;font-size:0.75rem;color:var(--color-text-secondary)">
2102
+ <span style="width:0.5rem;height:0.5rem;border-radius:50%;background:${color};flex-shrink:0"></span>
2103
+ <span style="color:${color};font-weight:600">MCP</span>
2104
+ <span>${this.escapeHtml(name)}</span>
2105
+ <span style="opacity:0.6">${this.escapeHtml(status)}</span>
2106
+ </div>
2107
+ `;
2108
+ return div;
2109
+ }
2110
+
2033
2111
  /**
2034
2112
  * Render generic event with formatted key-value pairs
2035
2113
  */