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.
- package/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
- package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
- package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +5 -5
- package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
- package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
- package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
- package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
- package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
- package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
- package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
- package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
- package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
- package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
- package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
- package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
- package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +53 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
- package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
- package/package.json +2 -2
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +5 -5
- package/packages/daemon/src/axom-connector.js +340 -0
- package/packages/daemon/src/axom-install.js +140 -0
- package/packages/daemon/src/axom-server.js +229 -0
- package/packages/daemon/src/chatstore.js +104 -0
- package/packages/daemon/src/federation.js +6 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/innerchat-docs.js +8 -1
- package/packages/daemon/src/innerchat-relay.js +89 -0
- package/packages/daemon/src/innerchat.js +261 -1
- package/packages/daemon/src/introducer.js +1 -1
- package/packages/daemon/src/network-guard.js +56 -0
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/axom.js +66 -0
- package/packages/daemon/src/providers/index.js +2 -0
- package/packages/daemon/src/routes/agents.js +1 -175
- package/packages/daemon/src/routes/axom.js +196 -0
- package/packages/daemon/src/routes/chat-history.js +34 -0
- package/packages/daemon/src/routes/innerchat.js +152 -20
- package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
- package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +2 -2
- package/packages/gui/src/app.css +53 -0
- package/packages/gui/src/components/agents/agent-config.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
- package/packages/gui/src/components/layout/command-palette.jsx +1 -1
- package/packages/gui/src/stores/groove.js +24 -163
- package/packages/gui/src/stores/slices/agents-slice.js +52 -2
- package/packages/gui/src/stores/slices/axom-slice.js +197 -0
- package/packages/gui/src/views/axom.jsx +1060 -0
- package/packages/gui/src/views/marketplace.jsx +1 -1
- package/packages/gui/src/views/settings.jsx +178 -64
- package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
- package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
- package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
- package/packages/daemon/src/conversations.js +0 -576
- package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
- package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
- package/packages/gui/src/components/chat/chat-header.jsx +0 -198
- package/packages/gui/src/components/chat/chat-input.jsx +0 -367
- package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
- package/packages/gui/src/components/chat/chat-view.jsx +0 -212
- package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
- package/packages/gui/src/components/chat/model-picker.jsx +0 -201
- package/packages/gui/src/stores/slices/chat-slice.js +0 -227
- 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
|
-
}
|