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 +182 -0
- package/lib/claude-runner.js +40 -1
- package/package.json +1 -1
- package/static/js/event-processor.js +24 -79
- package/static/js/streaming-renderer.js +79 -1
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
|
+
}
|
package/lib/claude-runner.js
CHANGED
|
@@ -603,7 +603,46 @@ registry.register({
|
|
|
603
603
|
|
|
604
604
|
parseOutput(line) {
|
|
605
605
|
try {
|
|
606
|
-
|
|
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
|
@@ -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
|
*/
|