@moxxy/cli 0.0.12 → 0.1.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.
Files changed (148) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +341 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +767 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/skill.js +125 -0
  18. package/src/commands/template.js +237 -0
  19. package/src/commands/uninstall.js +196 -0
  20. package/src/commands/update.js +406 -0
  21. package/src/commands/vault.js +219 -0
  22. package/src/help.js +368 -0
  23. package/src/lib/plugin-registry.js +98 -0
  24. package/src/platform.js +40 -0
  25. package/src/sse-client.js +79 -0
  26. package/src/tui/action-wizards.js +130 -0
  27. package/src/tui/app.jsx +859 -0
  28. package/src/tui/components/action-picker.jsx +86 -0
  29. package/src/tui/components/chat-panel.jsx +120 -0
  30. package/src/tui/components/footer.jsx +13 -0
  31. package/src/tui/components/header.jsx +45 -0
  32. package/src/tui/components/input-area.jsx +384 -0
  33. package/src/tui/components/messages/ask-message.jsx +13 -0
  34. package/src/tui/components/messages/assistant-message.jsx +165 -0
  35. package/src/tui/components/messages/channel-message.jsx +18 -0
  36. package/src/tui/components/messages/event-message.jsx +22 -0
  37. package/src/tui/components/messages/hive-status.jsx +34 -0
  38. package/src/tui/components/messages/skill-message.jsx +31 -0
  39. package/src/tui/components/messages/system-message.jsx +12 -0
  40. package/src/tui/components/messages/thinking.jsx +25 -0
  41. package/src/tui/components/messages/tool-group.jsx +62 -0
  42. package/src/tui/components/messages/tool-message.jsx +66 -0
  43. package/src/tui/components/messages/user-message.jsx +12 -0
  44. package/src/tui/components/model-picker.jsx +138 -0
  45. package/src/tui/components/multiline-input.jsx +72 -0
  46. package/src/tui/events-handler.js +730 -0
  47. package/src/tui/helpers.js +59 -0
  48. package/src/tui/hooks/use-command-handler.js +451 -0
  49. package/src/tui/index.jsx +55 -0
  50. package/src/tui/input-utils.js +26 -0
  51. package/src/tui/markdown-renderer.js +66 -0
  52. package/src/tui/mcp-wizard.js +136 -0
  53. package/src/tui/model-picker.js +174 -0
  54. package/src/tui/slash-commands.js +26 -0
  55. package/src/tui/store.js +12 -0
  56. package/src/tui/theme.js +17 -0
  57. package/src/ui.js +109 -0
  58. package/bin/moxxy.js +0 -2
  59. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  60. package/dist/chunk-2FZEA3NG.mjs +0 -457
  61. package/dist/chunk-3KDPLS22.mjs +0 -1131
  62. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  63. package/dist/chunk-6DZX6EAA.mjs +0 -37
  64. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  65. package/dist/chunk-C46NSEKG.mjs +0 -211
  66. package/dist/chunk-CAUXONEF.mjs +0 -1131
  67. package/dist/chunk-CPL5V56X.mjs +0 -1131
  68. package/dist/chunk-CTBVTTBG.mjs +0 -440
  69. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  70. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  71. package/dist/chunk-GSNMMI3H.mjs +0 -530
  72. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  73. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  74. package/dist/chunk-J33O35WX.mjs +0 -532
  75. package/dist/chunk-N5JTPB6U.mjs +0 -820
  76. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  77. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  78. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  79. package/dist/chunk-QO2JONHP.mjs +0 -1131
  80. package/dist/chunk-RVAPILHA.mjs +0 -1242
  81. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  82. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  83. package/dist/chunk-SOFST2PV.mjs +0 -1242
  84. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  85. package/dist/chunk-TMZWETMH.mjs +0 -1242
  86. package/dist/chunk-TYD7NMMI.mjs +0 -581
  87. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  88. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  89. package/dist/chunk-UQZKODNW.mjs +0 -1124
  90. package/dist/chunk-USC6R2ON.mjs +0 -1242
  91. package/dist/chunk-W32EQCVC.mjs +0 -823
  92. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  93. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  94. package/dist/cli-2AIWTL6F.mjs +0 -8
  95. package/dist/cli-2QKJ5UUL.mjs +0 -8
  96. package/dist/cli-4RIS6DQX.mjs +0 -8
  97. package/dist/cli-5RH4VBBL.mjs +0 -7
  98. package/dist/cli-7MK4YGOP.mjs +0 -7
  99. package/dist/cli-B4KH6MZI.mjs +0 -8
  100. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  101. package/dist/cli-CVP26EL2.mjs +0 -8
  102. package/dist/cli-DDRVVNAV.mjs +0 -8
  103. package/dist/cli-E7U56QVQ.mjs +0 -8
  104. package/dist/cli-EQNRMLL3.mjs +0 -8
  105. package/dist/cli-F5RUHHH4.mjs +0 -8
  106. package/dist/cli-LX6FFSEF.mjs +0 -8
  107. package/dist/cli-LY74GWKR.mjs +0 -6
  108. package/dist/cli-MAT3ZJHI.mjs +0 -8
  109. package/dist/cli-NJXXTQYF.mjs +0 -8
  110. package/dist/cli-O4ZGFAZG.mjs +0 -8
  111. package/dist/cli-ORVLI3UQ.mjs +0 -8
  112. package/dist/cli-PV43ZVKA.mjs +0 -8
  113. package/dist/cli-REVD6ISM.mjs +0 -8
  114. package/dist/cli-TBX76KQX.mjs +0 -8
  115. package/dist/cli-THCGF7SQ.mjs +0 -8
  116. package/dist/cli-TLX5ENVM.mjs +0 -8
  117. package/dist/cli-TMNI5ZYE.mjs +0 -8
  118. package/dist/cli-TNJHCBQA.mjs +0 -6
  119. package/dist/cli-TUX22CZP.mjs +0 -8
  120. package/dist/cli-XJVH7EEP.mjs +0 -8
  121. package/dist/cli-XXOW4VXJ.mjs +0 -8
  122. package/dist/cli-XZ5RESNB.mjs +0 -6
  123. package/dist/cli-YCBYZ76Q.mjs +0 -8
  124. package/dist/cli-ZLMQCU7X.mjs +0 -8
  125. package/dist/dist-2VGKJRBH.mjs +0 -6820
  126. package/dist/dist-37BNX4QG.mjs +0 -7081
  127. package/dist/dist-7LTHRYKA.mjs +0 -11569
  128. package/dist/dist-7XJPQW5C.mjs +0 -6950
  129. package/dist/dist-AYMVOW7T.mjs +0 -7123
  130. package/dist/dist-BHUWCDRS.mjs +0 -7132
  131. package/dist/dist-FAXRJMEN.mjs +0 -6812
  132. package/dist/dist-HQGANM3P.mjs +0 -6976
  133. package/dist/dist-KATLOZQV.mjs +0 -7054
  134. package/dist/dist-KLSB6YHV.mjs +0 -6964
  135. package/dist/dist-LKIOZQ42.mjs +0 -17
  136. package/dist/dist-UYA4RJUH.mjs +0 -2792
  137. package/dist/dist-ZYHCBILM.mjs +0 -6993
  138. package/dist/index.d.mts +0 -23
  139. package/dist/index.d.ts +0 -23
  140. package/dist/index.js +0 -25531
  141. package/dist/index.mjs +0 -18
  142. package/dist/src-APP5P3UD.mjs +0 -1386
  143. package/dist/src-D5HMDDVE.mjs +0 -1324
  144. package/dist/src-EK3WD4AU.mjs +0 -1327
  145. package/dist/src-LSZFLMFN.mjs +0 -1400
  146. package/dist/src-T77DFTFP.mjs +0 -1407
  147. package/dist/src-WIOCZRAC.mjs +0 -1397
  148. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,730 @@
1
+ import { createSseClient } from '../sse-client.js';
2
+
3
+ // Error events always shown in chat regardless of debug mode
4
+ const ERROR_EVENTS = new Set([
5
+ 'run.failed', 'primitive.failed', 'skill.failed',
6
+ 'security.violation', 'sandbox.denied',
7
+ 'mcp.connection_failed', 'mcp.tool_failed',
8
+ ]);
9
+
10
+ // Error events that should be shown as user-friendly system messages
11
+ // instead of raw event brackets (brackets only in debug mode)
12
+ const FRIENDLY_ERROR_EVENTS = new Set([
13
+ 'run.failed', 'primitive.failed',
14
+ ]);
15
+
16
+ // Tool activity events always shown in chat as compact messages
17
+ const TOOL_ACTIVITY_EVENTS = new Set([
18
+ 'primitive.invoked', 'primitive.completed',
19
+ ]);
20
+
21
+ const DELTA_FLUSH_MS = 50;
22
+ const PARAM_TRUNCATE = 80;
23
+ const HIDDEN_TOOL_PREFIXES = ['agent.'];
24
+
25
+ function isHiddenTool(name) {
26
+ return HIDDEN_TOOL_PREFIXES.some(p => name.startsWith(p));
27
+ }
28
+
29
+ function toTokenNumber(value) {
30
+ const n = Number(value);
31
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
32
+ }
33
+
34
+ function normalizeUsage(rawUsage) {
35
+ if (!rawUsage || typeof rawUsage !== 'object') return null;
36
+
37
+ const promptTokens = toTokenNumber(rawUsage.prompt_tokens || rawUsage.input_tokens);
38
+ const completionTokens = toTokenNumber(rawUsage.completion_tokens || rawUsage.output_tokens);
39
+ const totalTokens = toTokenNumber(rawUsage.total_tokens) || (promptTokens + completionTokens);
40
+
41
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0) {
42
+ return null;
43
+ }
44
+
45
+ return { promptTokens, completionTokens, totalTokens };
46
+ }
47
+
48
+ function extractUsage(payload) {
49
+ if (!payload || typeof payload !== 'object') return null;
50
+
51
+ const nested = normalizeUsage(payload.usage);
52
+ if (nested) return nested;
53
+
54
+ const responseNested = normalizeUsage(payload.response?.usage);
55
+ if (responseNested) return responseNested;
56
+
57
+ const topLevel = normalizeUsage(payload);
58
+ if (topLevel) return topLevel;
59
+
60
+ return null;
61
+ }
62
+
63
+ function formatValue(v) {
64
+ if (v == null) return 'null';
65
+ if (typeof v === 'string') return v.length > PARAM_TRUNCATE ? v.slice(0, PARAM_TRUNCATE) + '…' : v;
66
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
67
+ if (Array.isArray(v)) return v.length === 0 ? '[]' : `[${v.length} items]`;
68
+ return JSON.stringify(v);
69
+ }
70
+
71
+ function formatParams(raw) {
72
+ if (raw == null) return null;
73
+ if (typeof raw === 'string') return raw;
74
+ if (typeof raw !== 'object' || Array.isArray(raw)) return formatValue(raw);
75
+ const keys = Object.keys(raw);
76
+ if (keys.length === 0) return null;
77
+ return keys.map(k => `${k}: ${formatValue(raw[k])}`).join('\n');
78
+ }
79
+
80
+ /**
81
+ * Imperative SSE event handler.
82
+ * Replaces the React useEvents hook.
83
+ */
84
+ export class EventsHandler {
85
+ constructor(client, agentId, { debug = false } = {}) {
86
+ this.client = client;
87
+ this.agentId = agentId;
88
+ this.debug = debug;
89
+ this.messages = [];
90
+ this.stats = {
91
+ eventCount: 0,
92
+ tokenEstimate: 0,
93
+ contextTokens: 0,
94
+ skills: {},
95
+ primitives: {},
96
+ };
97
+ this.connected = false;
98
+ this._assistantBuffer = '';
99
+ this._deltaTimer = null;
100
+ this._active = false;
101
+ this._sse = null;
102
+ this._onChange = null; // callback when messages/stats change
103
+ this.thinking = false;
104
+ this._thinkingTimer = null;
105
+ this.pendingAsk = null; // { questionId, question } = set when agent asks user
106
+ this._subAgents = new Map(); // agentId -> { name, task, buffer, status }
107
+ this._hiveStatus = null; // aggregated hive status tracker
108
+ this._version = 0;
109
+ this.messageVersion = 0;
110
+ this._snapshot = null; // cached for useSyncExternalStore referential stability
111
+ }
112
+
113
+ /** Set callback invoked on any state change. */
114
+ onChange(fn) {
115
+ this._onChange = fn;
116
+ }
117
+
118
+ _notify() {
119
+ this._version++;
120
+ // Rebuild cached snapshot so getSnapshot() returns a new reference
121
+ this._snapshot = {
122
+ messages: this.messages,
123
+ stats: { ...this.stats },
124
+ connected: this.connected,
125
+ thinking: this.thinking,
126
+ pendingAsk: this.pendingAsk,
127
+ version: this._version,
128
+ messageVersion: this.messageVersion,
129
+ };
130
+ if (this._onChange) this._onChange();
131
+ }
132
+
133
+ getSnapshot() {
134
+ if (!this._snapshot) {
135
+ this._snapshot = {
136
+ messages: this.messages,
137
+ stats: { ...this.stats },
138
+ connected: this.connected,
139
+ thinking: this.thinking,
140
+ pendingAsk: this.pendingAsk,
141
+ version: this._version,
142
+ messageVersion: this.messageVersion,
143
+ };
144
+ }
145
+ return this._snapshot;
146
+ }
147
+
148
+ async loadHistory(client, agentId) {
149
+ try {
150
+ const data = await client.getHistory(agentId);
151
+ const msgs = (data.messages || []).map(m => ({
152
+ type: m.role === 'user' ? 'user' : 'assistant',
153
+ content: m.content,
154
+ ts: Date.parse(m.created_at),
155
+ }));
156
+ this.messages = [...msgs, ...this.messages];
157
+ this.messageVersion++;
158
+ this._notify();
159
+ } catch {
160
+ // Silently ignore history load failures
161
+ }
162
+ }
163
+
164
+ async connect() {
165
+ if (!this.agentId) return;
166
+ this._active = true;
167
+ let retryCount = 0;
168
+
169
+ while (this._active && retryCount < 10) {
170
+ try {
171
+ this._sse = createSseClient(this.client.baseUrl, this.client.token, { agent_id: this.agentId });
172
+ retryCount = 0;
173
+ for await (const event of this._sse.stream()) {
174
+ if (!this._active) return;
175
+ if (!this.connected) {
176
+ this.connected = true;
177
+ this._notify();
178
+ }
179
+ this._processEvent(event);
180
+ }
181
+ if (!this._active) return;
182
+ } catch (err) {
183
+ if (err.name === 'AbortError' || !this._active) return;
184
+ retryCount++;
185
+ this.connected = false;
186
+ if (err.isGatewayDown || this._isConnectionError(err)) {
187
+ this.addSystemMessage('Gateway is not running. Start it with: moxxy gateway start');
188
+ this._notify();
189
+ return;
190
+ }
191
+ this._notify();
192
+ const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
193
+ await new Promise(r => setTimeout(r, delay));
194
+ }
195
+ }
196
+ }
197
+
198
+ disconnect() {
199
+ this._active = false;
200
+ this._stopThinking();
201
+ if (this._deltaTimer) {
202
+ clearTimeout(this._deltaTimer);
203
+ this._deltaTimer = null;
204
+ }
205
+ if (this._sse) this._sse.disconnect();
206
+ }
207
+
208
+ _startThinking() {
209
+ this.thinking = true;
210
+ if (this._thinkingTimer) return;
211
+ this._thinkingTimer = setInterval(() => this._notify(), 120);
212
+ }
213
+
214
+ _stopThinking() {
215
+ this.thinking = false;
216
+ if (this._thinkingTimer) {
217
+ clearInterval(this._thinkingTimer);
218
+ this._thinkingTimer = null;
219
+ }
220
+ }
221
+
222
+ addUserMessage(content) {
223
+ this._assistantBuffer = '';
224
+ this.messages.push({ type: 'user', content, ts: Date.now() });
225
+ this._startThinking();
226
+ this.messageVersion++;
227
+ this._notify();
228
+ }
229
+
230
+ addSystemMessage(content) {
231
+ this.messages.push({ type: 'system', content, ts: Date.now() });
232
+ this.messageVersion++;
233
+ this._notify();
234
+ }
235
+
236
+ clearMessages() {
237
+ this.messages = [];
238
+ this._assistantBuffer = '';
239
+ this.messageVersion++;
240
+ this._notify();
241
+ }
242
+
243
+ _isConnectionError(err) {
244
+ const cause = err.cause;
245
+ if (cause && (cause.code === 'ECONNREFUSED' || cause.code === 'ECONNRESET')) return true;
246
+ const msg = (err.message || '').toLowerCase();
247
+ return msg.includes('econnrefused') || msg.includes('fetch failed');
248
+ }
249
+
250
+ _flushDelta() {
251
+ // Flush parent assistant buffer
252
+ const content = this._assistantBuffer;
253
+ if (content) {
254
+ const last = this.messages[this.messages.length - 1];
255
+ if (last && last.type === 'assistant' && last.streaming) {
256
+ last.content = content;
257
+ } else {
258
+ this.messages.push({ type: 'assistant', content, streaming: true, ts: Date.now() });
259
+ }
260
+ this.messageVersion++;
261
+ this._notify();
262
+ }
263
+ }
264
+
265
+ _processSubAgentEvent(event) {
266
+ const type = event.event_type;
267
+ const payload = event.payload || {};
268
+ const agentId = event.agent_id;
269
+ const sub = this._subAgents.get(agentId);
270
+ if (!sub) return;
271
+
272
+ // Track sub-agent text internally but don't render in chat.
273
+ // Hive events (task created/claimed/completed) are shown instead.
274
+ if (type === 'message.delta') {
275
+ sub.buffer += (payload.content || payload.text || '');
276
+ return;
277
+ }
278
+
279
+ if (type === 'message.final') {
280
+ sub.buffer = '';
281
+ return;
282
+ }
283
+
284
+ // Track stats for sub-agent primitives but don't render in chat
285
+ if (type === 'primitive.invoked' && payload.name) {
286
+ this.stats.primitives[payload.name] = (this.stats.primitives[payload.name] || 0) + 1;
287
+ return;
288
+ }
289
+
290
+ if (type === 'primitive.completed' || type === 'primitive.failed') {
291
+ return;
292
+ }
293
+
294
+ // Pass other sub-agent events through in debug mode only
295
+ if (this.debug) {
296
+ this.messages.push({ type: 'event', eventType: type, payload, ts: event.ts });
297
+ this.messageVersion++;
298
+ this._notify();
299
+ }
300
+ }
301
+
302
+ _updateHiveStatus(type, payload, ts) {
303
+ if (!this._hiveStatus) {
304
+ this._hiveStatus = {
305
+ totalTasks: 0, completedTasks: 0, inProgressTasks: 0,
306
+ workers: 0, recentEvents: [],
307
+ };
308
+ }
309
+ const hs = this._hiveStatus;
310
+
311
+ // Build a short description for the recent events log
312
+ let desc = '';
313
+ if (type === 'hive.task_created') {
314
+ hs.totalTasks++;
315
+ desc = `Task "${payload.title || 'untitled'}" created`;
316
+ } else if (type === 'hive.task_claimed') {
317
+ hs.inProgressTasks++;
318
+ desc = `Task claimed by ${payload.agent_id || 'worker'}`;
319
+ } else if (type === 'hive.task_completed') {
320
+ hs.completedTasks++;
321
+ hs.inProgressTasks = Math.max(0, hs.inProgressTasks - 1);
322
+ desc = `Task completed by ${payload.agent_id || 'worker'}`;
323
+ } else if (type === 'hive.member_joined') {
324
+ hs.workers++;
325
+ desc = `${payload.agent_id || 'worker'} joined as ${payload.role || 'worker'}`;
326
+ } else if (type === 'hive.signal_posted') {
327
+ desc = `Signal: ${payload.signal_type || 'info'} from ${payload.author || 'agent'}`;
328
+ } else if (type === 'hive.proposal_created') {
329
+ desc = `Proposal: "${payload.title || 'untitled'}"`;
330
+ } else if (type === 'hive.vote_cast') {
331
+ desc = `Vote: ${payload.vote || '?'} by ${payload.voter || 'agent'}`;
332
+ } else if (type === 'hive.disbanded') {
333
+ desc = 'Hive disbanded';
334
+ } else {
335
+ desc = type.replace('hive.', '');
336
+ }
337
+
338
+ // Keep only last 3 events
339
+ hs.recentEvents.push(desc);
340
+ if (hs.recentEvents.length > 3) hs.recentEvents.shift();
341
+
342
+ // In debug mode, also add individual hive-event messages
343
+ if (this.debug) {
344
+ this.messages.push({ type: 'hive-event', subtype: type.replace('hive.', ''), content: desc, ts });
345
+ }
346
+
347
+ // Find or create the single hive-status message
348
+ const existingIdx = this.messages.findIndex(m => m.type === 'hive-status');
349
+ const statusMsg = {
350
+ type: 'hive-status',
351
+ totalTasks: hs.totalTasks,
352
+ completedTasks: hs.completedTasks,
353
+ inProgressTasks: hs.inProgressTasks,
354
+ workers: hs.workers,
355
+ recentEvents: [...hs.recentEvents],
356
+ ts,
357
+ };
358
+
359
+ if (existingIdx >= 0) {
360
+ this.messages[existingIdx] = statusMsg;
361
+ } else {
362
+ this.messages.push(statusMsg);
363
+ }
364
+ this.messageVersion++;
365
+ this._notify();
366
+ }
367
+
368
+ _processEvent(event) {
369
+ const type = event.event_type;
370
+ const payload = event.payload || {};
371
+
372
+ // Update stats
373
+ this.stats.eventCount++;
374
+ if (type === 'skill.invoked' && payload.name) {
375
+ this.stats.skills[payload.name] = (this.stats.skills[payload.name] || 0) + 1;
376
+ }
377
+ if (type === 'primitive.invoked' && payload.name) {
378
+ this.stats.primitives[payload.name] = (this.stats.primitives[payload.name] || 0) + 1;
379
+ }
380
+ if (type === 'model.response') {
381
+ const usage = extractUsage(payload);
382
+ if (usage) {
383
+ const promptTokens = usage.promptTokens;
384
+ const completionTokens = usage.completionTokens;
385
+ const totalTokens = usage.totalTokens;
386
+
387
+ this.stats.tokenEstimate += totalTokens;
388
+ this.stats.contextTokens = promptTokens || Math.max(0, totalTokens - completionTokens);
389
+ } else {
390
+ // Fallback estimate when provider does not report usage in payload.
391
+ const contentLength = payload.content_length || 0;
392
+ if (contentLength > 0) {
393
+ this.stats.tokenEstimate += Math.ceil(contentLength / 4);
394
+ }
395
+ }
396
+ }
397
+
398
+ // Register new sub-agents and show in chat
399
+ if (type === 'subagent.spawned') {
400
+ const subId = payload.sub_agent_id || payload.child_name;
401
+ const name = payload.name || payload.child_name || subId;
402
+ const task = payload.task || '';
403
+ this._subAgents.set(subId, { name, task, buffer: '', status: 'running' });
404
+ this.messages.push({
405
+ type: 'tool', name: `agent.spawn → ${name}`, status: 'invoked',
406
+ arguments: task ? `task: ${task.length > PARAM_TRUNCATE ? task.slice(0, PARAM_TRUNCATE) + '…' : task}` : null,
407
+ ts: event.ts,
408
+ });
409
+ this.messageVersion++;
410
+ this._notify();
411
+ return;
412
+ }
413
+
414
+ // Handle sub-agent completion
415
+ if (type === 'subagent.completed') {
416
+ const subId = payload.sub_agent_id || payload.child_name;
417
+ const sub = this._subAgents.get(subId);
418
+ if (sub) {
419
+ sub.status = 'completed';
420
+ // Update matching invoked message to completed
421
+ for (let i = this.messages.length - 1; i >= 0; i--) {
422
+ const m = this.messages[i];
423
+ if (m.type === 'tool' && m.name === `agent.spawn → ${sub.name}` && m.status === 'invoked') {
424
+ m.status = 'completed';
425
+ break;
426
+ }
427
+ }
428
+ this.messageVersion++;
429
+ this._notify();
430
+ }
431
+ return;
432
+ }
433
+
434
+ if (type === 'subagent.failed') {
435
+ const subId = payload.sub_agent_id || payload.child_name;
436
+ const sub = this._subAgents.get(subId);
437
+ if (sub) {
438
+ sub.status = 'failed';
439
+ // Update matching invoked message to error
440
+ for (let i = this.messages.length - 1; i >= 0; i--) {
441
+ const m = this.messages[i];
442
+ if (m.type === 'tool' && m.name === `agent.spawn → ${sub.name}` && m.status === 'invoked') {
443
+ m.status = 'error';
444
+ m.error = payload.error || 'sub-agent failed';
445
+ break;
446
+ }
447
+ }
448
+ this.messageVersion++;
449
+ this._notify();
450
+ }
451
+ return;
452
+ }
453
+
454
+ // Handle hive events via aggregated status tracker
455
+ if (type.startsWith('hive.')) {
456
+ this._updateHiveStatus(type, payload, event.ts);
457
+ return;
458
+ }
459
+
460
+ // Handle heartbeat events - show as system notifications in chat
461
+ if (type === 'heartbeat.completed') {
462
+ const msg = payload.message || 'Heartbeat fired';
463
+ const hbId = payload.heartbeat_id ? ` (${payload.heartbeat_id.slice(0, 8)})` : '';
464
+ this.messages.push({ type: 'system', content: `Heartbeat${hbId}: ${msg}`, ts: event.ts });
465
+ this.messageVersion++;
466
+ this._notify();
467
+ return;
468
+ }
469
+ if (type === 'heartbeat.triggered' || type === 'heartbeat.failed') {
470
+ if (this.debug) {
471
+ const detail = type === 'heartbeat.failed' ? (payload.error || 'failed') : `action=${payload.action_type}`;
472
+ this.messages.push({ type: 'event', eventType: type, payload, ts: event.ts });
473
+ this.messageVersion++;
474
+ this._notify();
475
+ }
476
+ return;
477
+ }
478
+
479
+ // MCP events
480
+ if (type === 'mcp.connected') {
481
+ this.messages.push({ type: 'system', content: `MCP server connected: ${payload.server_id || 'unknown'}`, ts: event.ts });
482
+ this.messageVersion++;
483
+ this._notify();
484
+ return;
485
+ }
486
+ if (type === 'mcp.disconnected') {
487
+ this.messages.push({ type: 'system', content: `MCP server disconnected: ${payload.server_id || 'unknown'}`, ts: event.ts });
488
+ this.messageVersion++;
489
+ this._notify();
490
+ return;
491
+ }
492
+ if (type === 'mcp.connection_failed') {
493
+ const error = payload.error ? `: ${payload.error}` : '';
494
+ this.messages.push({ type: 'system', content: `MCP connection failed: ${payload.server_id || 'unknown'}${error}`, ts: event.ts });
495
+ this.messageVersion++;
496
+ this._notify();
497
+ return;
498
+ }
499
+ if (type === 'mcp.tool_invoked') {
500
+ this.messages.push({
501
+ type: 'tool', name: `mcp:${payload.server_id || '?'}/${payload.name || 'unknown'}`, status: 'invoked',
502
+ arguments: formatParams(payload.arguments),
503
+ rawArguments: payload.arguments,
504
+ ts: event.ts,
505
+ });
506
+ this.messageVersion++;
507
+ this._notify();
508
+ return;
509
+ }
510
+ if (type === 'mcp.tool_completed') {
511
+ const toolName = `mcp:${payload.server_id || '?'}/${payload.name || 'unknown'}`;
512
+ const last = this.messages[this.messages.length - 1];
513
+ if (last && last.type === 'tool' && last.name === toolName && last.status === 'invoked') {
514
+ last.status = 'completed';
515
+ last.result = formatParams(payload.result);
516
+ last.rawResult = payload.result;
517
+ }
518
+ this.messageVersion++;
519
+ this._notify();
520
+ return;
521
+ }
522
+ if (type === 'mcp.tool_failed') {
523
+ const toolName = `mcp:${payload.server_id || '?'}/${payload.name || 'unknown'}`;
524
+ const last = this.messages[this.messages.length - 1];
525
+ if (last && last.type === 'tool' && last.name === toolName && last.status === 'invoked') {
526
+ last.status = 'error';
527
+ last.error = payload.error || 'unknown error';
528
+ } else {
529
+ this.messages.push({
530
+ type: 'tool', name: toolName, status: 'error',
531
+ error: payload.error || 'unknown error',
532
+ ts: event.ts,
533
+ });
534
+ }
535
+ this.messageVersion++;
536
+ this._notify();
537
+ return;
538
+ }
539
+
540
+ // Route events from sub-agents
541
+ if (event.agent_id && event.agent_id !== this.agentId && this._subAgents.has(event.agent_id)) {
542
+ this._processSubAgentEvent(event);
543
+ return;
544
+ }
545
+
546
+ // Show channel messages (from Telegram, Discord, etc.) as user messages
547
+ if (type === 'channel.message_received') {
548
+ const sender = payload.sender_name || 'User';
549
+ const channel = payload.channel_type || 'channel';
550
+ const task = payload.task || '';
551
+ this._assistantBuffer = '';
552
+ this.messages.push({
553
+ type: 'channel', sender, channel, content: task, ts: event.ts,
554
+ });
555
+ this._startThinking();
556
+ this.messageVersion++;
557
+ this._notify();
558
+ return;
559
+ }
560
+
561
+ // Handle user.ask = agent is asking the user a question
562
+ if (type === 'user.ask_question') {
563
+ if (this.thinking) this._stopThinking();
564
+ const questionId = payload.question_id;
565
+ const question = payload.question || 'The agent is asking for input.';
566
+ this.pendingAsk = { questionId, question };
567
+ this.messages.push({ type: 'ask', question, questionId, ts: event.ts });
568
+ this.messageVersion++;
569
+ this._notify();
570
+ return;
571
+ }
572
+
573
+ // Handle user.ask_answered = question was answered, clear pending state
574
+ if (type === 'user.ask_answered') {
575
+ this.pendingAsk = null;
576
+ this._startThinking();
577
+ this._notify();
578
+ return;
579
+ }
580
+
581
+ if (type === 'message.delta') {
582
+ if (this.thinking) this._stopThinking();
583
+ this._assistantBuffer += (payload.content || payload.text || '');
584
+ // Debounce renders
585
+ if (!this._deltaTimer) {
586
+ this._deltaTimer = setTimeout(() => {
587
+ this._deltaTimer = null;
588
+ this._flushDelta();
589
+ }, DELTA_FLUSH_MS);
590
+ }
591
+ return;
592
+ }
593
+
594
+ if (type === 'message.final') {
595
+ if (this.thinking) this._stopThinking();
596
+ if (this._deltaTimer) {
597
+ clearTimeout(this._deltaTimer);
598
+ this._deltaTimer = null;
599
+ }
600
+ const finalContent = payload.content || payload.text || this._assistantBuffer;
601
+ this._assistantBuffer = '';
602
+ const last = this.messages[this.messages.length - 1];
603
+ if (last && last.type === 'assistant' && last.streaming) {
604
+ last.content = finalContent;
605
+ last.streaming = false;
606
+ } else {
607
+ this.messages.push({ type: 'assistant', content: finalContent, streaming: false, ts: event.ts });
608
+ }
609
+ this.messageVersion++;
610
+ this._notify();
611
+ return;
612
+ }
613
+
614
+ // Stop thinking on run completion or errors
615
+ if (type === 'run.completed' || type === 'run.failed') {
616
+ if (this.thinking) this._stopThinking();
617
+ }
618
+
619
+ // Silence errors for hidden tool prefixes (e.g. agent.*)
620
+ if (type === 'primitive.failed' && isHiddenTool(payload.name || '')) {
621
+ return;
622
+ }
623
+
624
+ // For user-facing error events, show as a friendly system message
625
+ // (raw event format only in debug mode)
626
+ if (!this.debug && FRIENDLY_ERROR_EVENTS.has(type)) {
627
+ const detail = payload.error || payload.message || 'Unknown error';
628
+ const label = type === 'run.failed' ? 'Run failed' : 'Tool error';
629
+ this.messages.push({ type: 'system', content: `${label}: ${detail}`, ts: event.ts });
630
+ this.messageVersion++;
631
+ this._notify();
632
+ return;
633
+ }
634
+
635
+ // Show tool activity events as compact messages
636
+ if (TOOL_ACTIVITY_EVENTS.has(type)) {
637
+ if (type === 'primitive.invoked') {
638
+ if (isHiddenTool(payload.name || '')) return;
639
+ this.messages.push({
640
+ type: 'tool', name: payload.name || 'unknown', status: 'invoked',
641
+ arguments: formatParams(payload.arguments),
642
+ rawArguments: payload.arguments,
643
+ ts: event.ts,
644
+ });
645
+ this.messageVersion++;
646
+ this._notify();
647
+ return;
648
+ }
649
+ if (type === 'primitive.completed') {
650
+ if (isHiddenTool(payload.name || '')) return;
651
+ // Update the last tool message for the same primitive if it was just invoked
652
+ const last = this.messages[this.messages.length - 1];
653
+ if (last && last.type === 'tool' && last.name === (payload.name || 'unknown') && last.status === 'invoked') {
654
+ last.status = 'completed';
655
+ last.result = formatParams(payload.result);
656
+ last.rawResult = payload.result;
657
+ }
658
+ this.messageVersion++;
659
+ this._notify();
660
+ return;
661
+ }
662
+ }
663
+
664
+ // Show skill activity events as compact bordered messages
665
+ if (type === 'skill.invoked') {
666
+ this.messages.push({
667
+ type: 'skill', name: payload.name || 'unknown', status: 'running',
668
+ description: payload.description || '', ts: event.ts,
669
+ });
670
+ this.messageVersion++;
671
+ this._notify();
672
+ return;
673
+ }
674
+ if (type === 'skill.completed') {
675
+ // Update the last skill message if it matches
676
+ for (let i = this.messages.length - 1; i >= 0; i--) {
677
+ const m = this.messages[i];
678
+ if (m.type === 'skill' && m.name === (payload.name || 'unknown') && m.status === 'running') {
679
+ m.status = 'completed';
680
+ break;
681
+ }
682
+ }
683
+ this.messageVersion++;
684
+ this._notify();
685
+ return;
686
+ }
687
+ if (type === 'skill.failed') {
688
+ // Update the last skill message if it matches, otherwise show as error
689
+ let found = false;
690
+ for (let i = this.messages.length - 1; i >= 0; i--) {
691
+ const m = this.messages[i];
692
+ if (m.type === 'skill' && m.name === (payload.name || 'unknown') && m.status === 'running') {
693
+ m.status = 'error';
694
+ m.error = payload.error || 'unknown error';
695
+ found = true;
696
+ break;
697
+ }
698
+ }
699
+ if (!found) {
700
+ this.messages.push({
701
+ type: 'skill', name: payload.name || 'unknown', status: 'error',
702
+ error: payload.error || 'unknown error', ts: event.ts,
703
+ });
704
+ }
705
+ this.messageVersion++;
706
+ this._notify();
707
+ return;
708
+ }
709
+
710
+ // Show tool error events
711
+ if (type === 'primitive.failed') {
712
+ // Update the last tool message for the same primitive if it was just invoked
713
+ const last = this.messages[this.messages.length - 1];
714
+ if (last && last.type === 'tool' && last.name === (payload.name || 'unknown') && last.status === 'invoked') {
715
+ last.status = 'error';
716
+ last.error = payload.error || 'unknown error';
717
+ this.messageVersion++;
718
+ this._notify();
719
+ return;
720
+ }
721
+ }
722
+
723
+ // Show error events always; show all events in debug mode
724
+ if (ERROR_EVENTS.has(type) || this.debug) {
725
+ this.messages.push({ type: 'event', eventType: type, payload, ts: event.ts });
726
+ this.messageVersion++;
727
+ this._notify();
728
+ }
729
+ }
730
+ }