groove-dev 0.27.195 → 0.27.197

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 (68) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +3 -5
  4. package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -2
  6. package/node_modules/@groove-dev/daemon/src/process.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
  8. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
  9. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-Da2nbd6M.js +1043 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-zmrIbwNm.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/App.jsx +0 -2
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
  16. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
  17. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -3
  18. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +5 -163
  21. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
  22. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
  23. package/node_modules/@groove-dev/gui/src/views/settings.jsx +0 -61
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +3 -5
  28. package/packages/daemon/src/chatstore.js +104 -0
  29. package/packages/daemon/src/index.js +3 -2
  30. package/packages/daemon/src/process.js +1 -1
  31. package/packages/daemon/src/routes/agents.js +1 -175
  32. package/packages/daemon/src/routes/chat-history.js +34 -0
  33. package/packages/gui/dist/assets/index-Da2nbd6M.js +1043 -0
  34. package/packages/gui/dist/assets/index-zmrIbwNm.css +1 -0
  35. package/packages/gui/dist/index.html +2 -2
  36. package/packages/gui/package.json +1 -1
  37. package/packages/gui/src/App.jsx +0 -2
  38. package/packages/gui/src/components/agents/agent-config.jsx +2 -2
  39. package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
  40. package/packages/gui/src/components/layout/activity-bar.jsx +2 -3
  41. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  42. package/packages/gui/src/components/layout/command-palette.jsx +1 -1
  43. package/packages/gui/src/stores/groove.js +5 -163
  44. package/packages/gui/src/stores/slices/agents-slice.js +52 -2
  45. package/packages/gui/src/views/marketplace.jsx +1 -1
  46. package/packages/gui/src/views/settings.jsx +0 -61
  47. package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
  48. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
  49. package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  50. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
  51. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
  52. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
  53. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
  54. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
  55. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
  56. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
  57. package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
  58. package/packages/daemon/src/conversations.js +0 -576
  59. package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
  60. package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  61. package/packages/gui/src/components/chat/chat-header.jsx +0 -198
  62. package/packages/gui/src/components/chat/chat-input.jsx +0 -367
  63. package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
  64. package/packages/gui/src/components/chat/chat-view.jsx +0 -212
  65. package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
  66. package/packages/gui/src/components/chat/model-picker.jsx +0 -201
  67. package/packages/gui/src/stores/slices/chat-slice.js +0 -227
  68. 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
- }