@moxxy/cli 0.0.12 → 0.1.1

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 (149) 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 +349 -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 +954 -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/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. 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
+ }