groove-dev 0.27.196 → 0.27.198

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 (106) hide show
  1. package/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
  2. package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
  3. package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +5 -5
  7. package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
  8. package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
  9. package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
  10. package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  13. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
  14. package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
  15. package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  17. package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
  18. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  19. package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
  22. package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
  23. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
  24. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
  25. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
  26. package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
  27. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
  28. package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
  29. package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  31. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  32. package/node_modules/@groove-dev/gui/package.json +1 -1
  33. package/node_modules/@groove-dev/gui/src/App.jsx +2 -2
  34. package/node_modules/@groove-dev/gui/src/app.css +53 -0
  35. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
  36. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
  37. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  38. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  39. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
  40. package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
  41. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
  42. package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
  43. package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
  44. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
  46. package/package.json +2 -2
  47. package/packages/cli/package.json +1 -1
  48. package/packages/daemon/package.json +1 -1
  49. package/packages/daemon/src/api.js +5 -5
  50. package/packages/daemon/src/axom-connector.js +340 -0
  51. package/packages/daemon/src/axom-install.js +140 -0
  52. package/packages/daemon/src/axom-server.js +229 -0
  53. package/packages/daemon/src/chatstore.js +104 -0
  54. package/packages/daemon/src/federation.js +6 -0
  55. package/packages/daemon/src/index.js +14 -2
  56. package/packages/daemon/src/innerchat-docs.js +8 -1
  57. package/packages/daemon/src/innerchat-relay.js +89 -0
  58. package/packages/daemon/src/innerchat.js +261 -1
  59. package/packages/daemon/src/introducer.js +1 -1
  60. package/packages/daemon/src/network-guard.js +56 -0
  61. package/packages/daemon/src/process.js +2 -2
  62. package/packages/daemon/src/providers/axom.js +66 -0
  63. package/packages/daemon/src/providers/index.js +2 -0
  64. package/packages/daemon/src/routes/agents.js +1 -175
  65. package/packages/daemon/src/routes/axom.js +196 -0
  66. package/packages/daemon/src/routes/chat-history.js +34 -0
  67. package/packages/daemon/src/routes/innerchat.js +152 -20
  68. package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
  69. package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  70. package/packages/gui/dist/index.html +2 -2
  71. package/packages/gui/package.json +1 -1
  72. package/packages/gui/src/App.jsx +2 -2
  73. package/packages/gui/src/app.css +53 -0
  74. package/packages/gui/src/components/agents/agent-config.jsx +2 -2
  75. package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
  76. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  77. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  78. package/packages/gui/src/components/layout/command-palette.jsx +1 -1
  79. package/packages/gui/src/stores/groove.js +24 -163
  80. package/packages/gui/src/stores/slices/agents-slice.js +52 -2
  81. package/packages/gui/src/stores/slices/axom-slice.js +197 -0
  82. package/packages/gui/src/views/axom.jsx +1060 -0
  83. package/packages/gui/src/views/marketplace.jsx +1 -1
  84. package/packages/gui/src/views/settings.jsx +178 -64
  85. package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
  86. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
  87. package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  88. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
  89. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
  90. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
  91. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
  92. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
  93. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
  94. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
  95. package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
  96. package/packages/daemon/src/conversations.js +0 -576
  97. package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
  98. package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  99. package/packages/gui/src/components/chat/chat-header.jsx +0 -198
  100. package/packages/gui/src/components/chat/chat-input.jsx +0 -367
  101. package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
  102. package/packages/gui/src/components/chat/chat-view.jsx +0 -212
  103. package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
  104. package/packages/gui/src/components/chat/model-picker.jsx +0 -201
  105. package/packages/gui/src/stores/slices/chat-slice.js +0 -227
  106. package/packages/gui/src/views/chat.jsx +0 -6
@@ -1,576 +0,0 @@
1
- // GROOVE — Conversation Manager
2
- // FSL-1.1-Apache-2.0 — see LICENSE
3
-
4
- import { readFileSync, writeFileSync, existsSync } from 'fs';
5
- import { resolve } from 'path';
6
- import { randomUUID } from 'crypto';
7
- import { spawn as cpSpawn } from 'child_process';
8
- import { getProvider, getInstalledProviders, isProviderInstalled, resolveProviderCommand } from './providers/index.js';
9
-
10
- export class ConversationManager {
11
- constructor(daemon) {
12
- this.daemon = daemon;
13
- this.filePath = resolve(daemon.grooveDir, 'conversations.json');
14
- this.conversations = new Map();
15
- this._modeChanging = new Set();
16
- this._load();
17
- this._listenForAgentExits();
18
- }
19
-
20
- _load() {
21
- if (!existsSync(this.filePath)) return;
22
- try {
23
- const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
24
- if (Array.isArray(data)) {
25
- for (const conv of data) this.conversations.set(conv.id, conv);
26
- }
27
- } catch { /* ignore corrupt file */ }
28
- }
29
-
30
- _save() {
31
- writeFileSync(this.filePath, JSON.stringify([...this.conversations.values()], null, 2));
32
- }
33
-
34
- _listenForAgentExits() {
35
- this.daemon.registry.on('change', (delta) => {
36
- if (!delta?.changed) return;
37
- for (const agentId of delta.changed) {
38
- const agent = this.daemon.registry.get(agentId);
39
- if (!agent) continue;
40
- const conv = this._findByAgentId(agentId);
41
- if (!conv) continue;
42
- if (agent.status === 'completed' || agent.status === 'killed' || agent.status === 'crashed') {
43
- conv.agentStatus = agent.status;
44
- conv.updatedAt = new Date().toISOString();
45
- this._save();
46
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
47
- }
48
- }
49
- });
50
- }
51
-
52
- _findByAgentId(agentId) {
53
- for (const conv of this.conversations.values()) {
54
- if (conv.agentId === agentId) return conv;
55
- }
56
- return null;
57
- }
58
-
59
- _resolveAutoProviderModel(preferredProvider) {
60
- const priority = ['claude-code', 'codex', 'gemini', 'grok', 'ollama'];
61
- const candidates = preferredProvider ? [preferredProvider] : priority;
62
-
63
- for (const pid of candidates) {
64
- if (!isProviderInstalled(pid)) continue;
65
- const p = getProvider(pid);
66
- if (!p) continue;
67
- const models = p.constructor.models || [];
68
- const chatModel = models.find((m) => m.type !== 'image') || models[0];
69
- if (chatModel) return { provider: pid, model: chatModel.id };
70
- }
71
-
72
- return { provider: 'claude-code', model: 'claude-sonnet-4-6' };
73
- }
74
-
75
- async create(provider, model, title, mode = 'api', options = {}) {
76
- if (!provider && this.daemon.config?.defaultChatProvider) {
77
- provider = this.daemon.config.defaultChatProvider;
78
- }
79
- if (!model && this.daemon.config?.defaultChatModel) {
80
- model = this.daemon.config.defaultChatModel;
81
- }
82
-
83
- if (!provider || !model) {
84
- const resolved = this._resolveAutoProviderModel(provider);
85
- if (!provider) provider = resolved.provider;
86
- if (!model) model = resolved.model;
87
- }
88
-
89
- const id = randomUUID().slice(0, 12);
90
- const now = new Date().toISOString();
91
-
92
- const conversation = {
93
- id,
94
- title: title || 'New Chat',
95
- agentId: null,
96
- provider,
97
- model: model || null,
98
- mode: mode === 'agent' ? 'agent' : 'api',
99
- reasoningEffort: options.reasoningEffort || null,
100
- verbosity: options.verbosity || null,
101
- previousResponseId: null,
102
- createdAt: now,
103
- updatedAt: now,
104
- pinned: false,
105
- archived: false,
106
- };
107
-
108
- this.conversations.set(id, conversation);
109
- this._save();
110
- this.daemon.broadcast({ type: 'conversation:created', data: conversation });
111
- return conversation;
112
- }
113
-
114
- get(id) {
115
- const conv = this.conversations.get(id);
116
- if (!conv) return null;
117
- if (conv.mode === 'api' || !conv.agentId) {
118
- return { ...conv, agentStatus: conv.agentStatus || null };
119
- }
120
- const agent = this.daemon.registry.get(conv.agentId);
121
- return {
122
- ...conv,
123
- agentStatus: agent?.status || conv.agentStatus || 'unknown',
124
- };
125
- }
126
-
127
- list() {
128
- const all = [...this.conversations.values()].map((conv) => {
129
- if (conv.mode === 'api' || !conv.agentId) {
130
- return { ...conv, agentStatus: conv.agentStatus || null };
131
- }
132
- const agent = this.daemon.registry.get(conv.agentId);
133
- return {
134
- ...conv,
135
- agentStatus: agent?.status || conv.agentStatus || 'unknown',
136
- };
137
- });
138
- all.sort((a, b) => {
139
- if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
140
- return new Date(b.updatedAt) - new Date(a.updatedAt);
141
- });
142
- return all;
143
- }
144
-
145
- rename(id, title) {
146
- const conv = this.conversations.get(id);
147
- if (!conv) throw new Error('Conversation not found');
148
- if (!title || typeof title !== 'string' || !title.trim()) {
149
- throw new Error('Title is required');
150
- }
151
- conv.title = title.trim().slice(0, 200);
152
- conv.updatedAt = new Date().toISOString();
153
- this._save();
154
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
155
- return conv;
156
- }
157
-
158
- pin(id, pinned) {
159
- const conv = this.conversations.get(id);
160
- if (!conv) throw new Error('Conversation not found');
161
- conv.pinned = !!pinned;
162
- conv.updatedAt = new Date().toISOString();
163
- this._save();
164
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
165
- return conv;
166
- }
167
-
168
- archive(id, archived) {
169
- const conv = this.conversations.get(id);
170
- if (!conv) throw new Error('Conversation not found');
171
- conv.archived = !!archived;
172
- conv.updatedAt = new Date().toISOString();
173
- this._save();
174
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
175
- return conv;
176
- }
177
-
178
- async delete(id) {
179
- const conv = this.conversations.get(id);
180
- if (!conv) throw new Error('Conversation not found');
181
-
182
- if (conv.agentId) {
183
- const agent = this.daemon.registry.get(conv.agentId);
184
- if (agent && (agent.status === 'running' || agent.status === 'starting')) {
185
- try { await this.daemon.processes.kill(conv.agentId); } catch { /* ignore */ }
186
- }
187
- if (agent) {
188
- this.daemon.registry.remove(conv.agentId);
189
- }
190
- }
191
-
192
- // Kill any active API mode streaming process
193
- this._killStreamingProcess(id);
194
-
195
- this.conversations.delete(id);
196
- this._save();
197
- this.daemon.broadcast({ type: 'conversation:deleted', data: { id } });
198
- return true;
199
- }
200
-
201
- touchUpdatedAt(id) {
202
- const conv = this.conversations.get(id);
203
- if (!conv) return;
204
- conv.updatedAt = new Date().toISOString();
205
- this._save();
206
- }
207
-
208
- autoTitle(id, message) {
209
- const conv = this.conversations.get(id);
210
- if (!conv) return;
211
- if (conv.title !== 'New Chat') return;
212
- const cleaned = message.trim().replace(/\s+/g, ' ').slice(0, 50);
213
- conv.title = cleaned || 'New Chat';
214
- conv.updatedAt = new Date().toISOString();
215
- this._save();
216
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
217
- }
218
-
219
- updateModel(id, provider, model) {
220
- const conv = this.conversations.get(id);
221
- if (!conv) throw new Error('Conversation not found');
222
- conv.provider = provider;
223
- conv.model = model;
224
- conv.updatedAt = new Date().toISOString();
225
- this._save();
226
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
227
- return conv;
228
- }
229
-
230
- updateReasoningSettings(id, reasoningEffort, verbosity) {
231
- const conv = this.conversations.get(id);
232
- if (!conv) throw new Error('Conversation not found');
233
- if (reasoningEffort !== undefined) conv.reasoningEffort = reasoningEffort || null;
234
- if (verbosity !== undefined) conv.verbosity = verbosity || null;
235
- conv.updatedAt = new Date().toISOString();
236
- this._save();
237
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
238
- return conv;
239
- }
240
-
241
- async setMode(id, mode) {
242
- const conv = this.conversations.get(id);
243
- if (!conv) throw new Error('Conversation not found');
244
- if (mode !== 'api' && mode !== 'agent') throw new Error('Mode must be "api" or "agent"');
245
- if (conv.mode === mode) return conv;
246
-
247
- if (this._modeChanging.has(id)) return conv;
248
- this._modeChanging.add(id);
249
-
250
- try {
251
- this._killStreamingProcess(id);
252
-
253
- conv.mode = mode;
254
- conv.updatedAt = new Date().toISOString();
255
- this._save();
256
- this.daemon.broadcast({ type: 'conversation:updated', data: conv });
257
- return conv;
258
- } finally {
259
- this._modeChanging.delete(id);
260
- }
261
- }
262
-
263
- _buildHistoryPrompt(history, newMessage) {
264
- const parts = [];
265
- if (history && history.length > 0) {
266
- parts.push('Previous conversation:');
267
- for (const msg of history) {
268
- const role = msg.from === 'user' ? 'User' : 'Assistant';
269
- parts.push(`${role}: ${msg.text}`);
270
- }
271
- parts.push('');
272
- }
273
- parts.push(`User: ${newMessage}`);
274
- return parts.join('\n');
275
- }
276
-
277
- _getStreamingProcesses() {
278
- if (!this._streamingProcesses) this._streamingProcesses = new Map();
279
- return this._streamingProcesses;
280
- }
281
-
282
- _killStreamingProcess(conversationId) {
283
- const procs = this._getStreamingProcesses();
284
- const handle = procs.get(conversationId);
285
- if (!handle) return;
286
- if (handle.abort) {
287
- handle.abort();
288
- } else if (handle.kill && !handle.killed) {
289
- handle.kill();
290
- }
291
- procs.delete(conversationId);
292
- }
293
-
294
- _getApiKey(providerName) {
295
- const envMap = {
296
- 'claude-code': 'ANTHROPIC_API_KEY',
297
- 'codex': 'OPENAI_API_KEY',
298
- 'gemini': 'GEMINI_API_KEY',
299
- 'grok': 'XAI_API_KEY',
300
- 'nano-banana': 'GEMINI_API_KEY',
301
- };
302
- const envVar = envMap[providerName];
303
- if (envVar && process.env[envVar]) return process.env[envVar];
304
- try {
305
- return this.daemon.credentials?.getKey(providerName) || null;
306
- } catch { return null; }
307
- }
308
-
309
- async sendMessage(id, message, history, { reasoningEffort, verbosity } = {}) {
310
- const conv = this.conversations.get(id);
311
- if (!conv) throw new Error('Conversation not found');
312
-
313
- this._killStreamingProcess(id);
314
-
315
- let provider = getProvider(conv.provider);
316
- let modelId = conv.model;
317
- let providerName = conv.provider;
318
-
319
- if (!provider || !isProviderInstalled(conv.provider)) {
320
- const resolved = this._resolveAutoProviderModel(null);
321
- provider = getProvider(resolved.provider);
322
- if (!provider) throw new Error('No provider available for chat');
323
- providerName = resolved.provider;
324
- modelId = resolved.model;
325
- }
326
-
327
- if (!modelId) {
328
- const resolved = this._resolveAutoProviderModel(providerName);
329
- modelId = resolved.model;
330
- }
331
-
332
- // Build messages array for direct API call
333
- const messages = (history || []).map((m) => ({
334
- role: m.from === 'user' ? 'user' : 'assistant',
335
- content: m.text,
336
- }));
337
- messages.push({ role: 'user', content: message });
338
-
339
- const apiKey = this._getApiKey(providerName);
340
-
341
- const effectiveReasoningEffort = reasoningEffort || conv.reasoningEffort || null;
342
- const effectiveVerbosity = verbosity || conv.verbosity || null;
343
-
344
- // Trajectory capture for training data
345
- const tc = this.daemon.trajectoryCapture;
346
- let tcAgentId = null;
347
- let tcResponseText = '';
348
- if (tc) {
349
- try { tcAgentId = tc.onChatTurnStart(id, providerName, modelId, message); } catch { /* never block chat */ }
350
- }
351
-
352
- // Agent mode prefers headless CLI (has tools + web search), falls back to API
353
- if (conv.mode === 'agent') {
354
- const headlessCmd = provider.buildHeadlessCommand(
355
- this._buildHistoryPrompt(history, message), modelId,
356
- );
357
- if (headlessCmd) {
358
- return this._sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc);
359
- }
360
- // Provider has no CLI — fall through to API streaming
361
- }
362
-
363
- // Try direct API streaming first (sub-second latency)
364
- const controller = provider.streamChat(
365
- messages, modelId, apiKey,
366
- (text) => {
367
- if (tcAgentId) tcResponseText += text;
368
- this.daemon.broadcast({
369
- type: 'conversation:chunk',
370
- data: { conversationId: id, text },
371
- });
372
- },
373
- (result) => {
374
- if (result?.responseId) {
375
- conv.previousResponseId = result.responseId;
376
- this._save();
377
- }
378
- this._getStreamingProcesses().delete(id);
379
- if (tcAgentId && tc) {
380
- try {
381
- tc.onParsedOutput(tcAgentId, { type: 'activity', subtype: 'assistant', data: tcResponseText });
382
- tc.onParsedOutput(tcAgentId, { type: 'result', data: tcResponseText });
383
- tc.onAgentComplete(tcAgentId, { status: 'SUCCESS' });
384
- const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
385
- this.daemon.state.set('training_sessions_captured', count);
386
- } catch { /* never block chat */ }
387
- }
388
- this.daemon.broadcast({
389
- type: 'conversation:complete',
390
- data: { conversationId: id },
391
- });
392
- },
393
- (err) => {
394
- this._getStreamingProcesses().delete(id);
395
- if (tcAgentId && tc) {
396
- try { tc.onAgentCrash(tcAgentId, err); } catch { /* never block chat */ }
397
- }
398
- this.daemon.broadcast({
399
- type: 'conversation:error',
400
- data: { conversationId: id, error: err.message },
401
- });
402
- },
403
- {
404
- reasoningEffort: effectiveReasoningEffort,
405
- verbosity: effectiveVerbosity,
406
- previousResponseId: conv.previousResponseId,
407
- },
408
- );
409
-
410
- if (controller) {
411
- this._getStreamingProcesses().set(id, controller);
412
- return;
413
- }
414
-
415
- // Fallback: headless CLI spawn (for providers without streamChat or missing API key)
416
- return this._sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc);
417
- }
418
-
419
- _sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc) {
420
- let tcResponseText = '';
421
- const prompt = this._buildHistoryPrompt(history, message);
422
- const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
423
- if (!headlessCmd) {
424
- if (tcAgentId && tc) {
425
- try { tc.onAgentCrash(tcAgentId, new Error('No API key for chat')); } catch { /* never block chat */ }
426
- }
427
- this.daemon.broadcast({
428
- type: 'conversation:error',
429
- data: { conversationId: id, error: `${providerName} requires an API key for chat` },
430
- });
431
- return;
432
- }
433
- const { command: rawCommand, args, env, stdin: stdinData, cwd } = headlessCmd;
434
- const command = resolveProviderCommand(providerName) || rawCommand;
435
-
436
- const spawnOpts = {
437
- env: { ...process.env, ...env },
438
- cwd: cwd || this.daemon.projectDir,
439
- stdio: stdinData ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
440
- };
441
-
442
- const proc = cpSpawn(command, args, spawnOpts);
443
- this._getStreamingProcesses().set(id, proc);
444
-
445
- proc.on('error', (err) => {
446
- this._getStreamingProcesses().delete(id);
447
- if (tcAgentId && tc) {
448
- try { tc.onAgentCrash(tcAgentId, err); } catch { /* never block chat */ }
449
- }
450
- this.daemon.broadcast({
451
- type: 'conversation:error',
452
- data: { conversationId: id, error: err.message },
453
- });
454
- });
455
-
456
- if (stdinData) {
457
- proc.stdin.write(stdinData);
458
- proc.stdin.end();
459
- }
460
-
461
- const emitChunk = (text) => {
462
- if (tcAgentId) tcResponseText += text;
463
- this.daemon.broadcast({
464
- type: 'conversation:chunk',
465
- data: { conversationId: id, text },
466
- });
467
- };
468
-
469
- proc.stdout.on('data', (data) => {
470
- const text = data.toString();
471
- const lines = text.split('\n');
472
- for (const line of lines) {
473
- const trimmed = line.trim();
474
- if (!trimmed) continue;
475
-
476
- try {
477
- const json = JSON.parse(trimmed);
478
- let matched = false;
479
- if (json.type === 'assistant' && json.message?.content) {
480
- for (const block of json.message.content) {
481
- if (block.type === 'text' && block.text) {
482
- emitChunk(block.text);
483
- }
484
- }
485
- matched = true;
486
- }
487
- if (!matched && json.type === 'content_block_delta' && json.delta?.text) {
488
- emitChunk(json.delta.text);
489
- matched = true;
490
- }
491
- if (!matched && json.type === 'result' && json.result) {
492
- matched = true;
493
- }
494
- if (!matched && json.type === 'token' && json.text != null) {
495
- emitChunk(json.text);
496
- matched = true;
497
- }
498
- if (!matched && (json.type === 'done' || json.type === 'complete' || json.type === 'result') && json.text) {
499
- emitChunk(json.text);
500
- matched = true;
501
- }
502
- if (!matched && json.content?.[0]?.text) {
503
- emitChunk(json.content[0].text);
504
- matched = true;
505
- }
506
- // Fallback: use provider's parseOutput for provider-specific formats (Gemini tool_use, tool_result, Codex items, etc.)
507
- if (!matched && provider.parseOutput) {
508
- const parsed = provider.parseOutput(trimmed);
509
- if (parsed) {
510
- const blocks = Array.isArray(parsed.data) ? parsed.data : [];
511
- for (const block of blocks) {
512
- if (block.type === 'text' && block.text) {
513
- emitChunk(block.text);
514
- } else if (block.type === 'tool_use') {
515
- const cmd = block.input?.command;
516
- const path = block.input?.path;
517
- const summary = cmd || path || (block.input && Object.keys(block.input).length > 0
518
- ? Object.values(block.input)[0]
519
- : null);
520
- if (block.name || summary) {
521
- this.daemon.broadcast({
522
- type: 'conversation:tool',
523
- data: { conversationId: id, name: block.name || 'Tool', summary: summary ? String(summary).slice(0, 120) : null },
524
- });
525
- }
526
- }
527
- }
528
- if (!blocks.length && typeof parsed.data === 'string' && parsed.data) {
529
- emitChunk(parsed.data);
530
- }
531
- }
532
- }
533
- continue;
534
- } catch { /* not JSON */ }
535
-
536
- if (!trimmed.startsWith('{')) {
537
- emitChunk(trimmed);
538
- }
539
- }
540
- });
541
-
542
- proc.on('exit', (code) => {
543
- this._getStreamingProcesses().delete(id);
544
- if (tcAgentId && tc) {
545
- try {
546
- tc.onParsedOutput(tcAgentId, { type: 'activity', subtype: 'assistant', data: tcResponseText });
547
- tc.onParsedOutput(tcAgentId, { type: 'result', data: tcResponseText });
548
- if (code === 0 || code === null) {
549
- tc.onAgentComplete(tcAgentId, { status: 'SUCCESS' });
550
- } else {
551
- tc.onAgentCrash(tcAgentId, new Error(`Exit code ${code}`));
552
- }
553
- const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
554
- this.daemon.state.set('training_sessions_captured', count);
555
- } catch { /* never block chat */ }
556
- }
557
- this.daemon.broadcast({
558
- type: 'conversation:complete',
559
- data: { conversationId: id, exitCode: code },
560
- });
561
- });
562
-
563
- const timeout = setTimeout(() => {
564
- if (!proc.killed) proc.kill();
565
- }, 120_000);
566
- proc.on('exit', () => clearTimeout(timeout));
567
- }
568
-
569
- stopStreaming(id) {
570
- this._killStreamingProcess(id);
571
- this.daemon.broadcast({
572
- type: 'conversation:complete',
573
- data: { conversationId: id, stopped: true },
574
- });
575
- }
576
- }