groove-dev 0.27.55 → 0.27.57
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/ai-chat/CHAT_MASTER_PLAN.md +184 -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 +169 -0
- package/node_modules/@groove-dev/daemon/src/conversations.js +423 -0
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- 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 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +138 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +112 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +347 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +165 -0
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +154 -0
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +143 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +220 -0
- package/node_modules/@groove-dev/gui/src/views/chat.jsx +6 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +169 -0
- package/packages/daemon/src/conversations.js +423 -0
- package/packages/daemon/src/index.js +2 -0
- package/packages/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/packages/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +138 -0
- package/packages/gui/src/components/chat/chat-input.jsx +112 -0
- package/packages/gui/src/components/chat/chat-messages.jsx +347 -0
- package/packages/gui/src/components/chat/chat-view.jsx +165 -0
- package/packages/gui/src/components/chat/conversation-list.jsx +154 -0
- package/packages/gui/src/components/chat/model-picker.jsx +143 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/groove.js +220 -0
- package/packages/gui/src/views/chat.jsx +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +0 -1
- package/packages/gui/dist/assets/index-CyVj0fHl.css +0 -1
|
@@ -0,0 +1,423 @@
|
|
|
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 } 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._load();
|
|
16
|
+
this._listenForAgentExits();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_load() {
|
|
20
|
+
if (!existsSync(this.filePath)) return;
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
23
|
+
if (Array.isArray(data)) {
|
|
24
|
+
for (const conv of data) this.conversations.set(conv.id, conv);
|
|
25
|
+
}
|
|
26
|
+
} catch { /* ignore corrupt file */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_save() {
|
|
30
|
+
writeFileSync(this.filePath, JSON.stringify([...this.conversations.values()], null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_listenForAgentExits() {
|
|
34
|
+
this.daemon.registry.on('change', (delta) => {
|
|
35
|
+
if (!delta?.changed) return;
|
|
36
|
+
for (const agentId of delta.changed) {
|
|
37
|
+
const agent = this.daemon.registry.get(agentId);
|
|
38
|
+
if (!agent) continue;
|
|
39
|
+
const conv = this._findByAgentId(agentId);
|
|
40
|
+
if (!conv) continue;
|
|
41
|
+
if (agent.status === 'completed' || agent.status === 'killed' || agent.status === 'crashed') {
|
|
42
|
+
conv.agentStatus = agent.status;
|
|
43
|
+
conv.updatedAt = new Date().toISOString();
|
|
44
|
+
this._save();
|
|
45
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_findByAgentId(agentId) {
|
|
52
|
+
for (const conv of this.conversations.values()) {
|
|
53
|
+
if (conv.agentId === agentId) return conv;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async create(provider, model, title, mode = 'api') {
|
|
59
|
+
const id = randomUUID().slice(0, 12);
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
let agentId = null;
|
|
63
|
+
|
|
64
|
+
if (mode === 'agent') {
|
|
65
|
+
const defaultTeam = this.daemon.teams.getDefault();
|
|
66
|
+
const workingDir = defaultTeam?.workingDir || this.daemon.projectDir;
|
|
67
|
+
|
|
68
|
+
const agent = await this.daemon.processes.spawn({
|
|
69
|
+
role: 'chat',
|
|
70
|
+
provider,
|
|
71
|
+
model: model || null,
|
|
72
|
+
workingDir,
|
|
73
|
+
teamId: defaultTeam?.id || null,
|
|
74
|
+
permission: 'full',
|
|
75
|
+
});
|
|
76
|
+
agentId = agent.id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const conversation = {
|
|
80
|
+
id,
|
|
81
|
+
title: title || 'New Chat',
|
|
82
|
+
agentId,
|
|
83
|
+
provider,
|
|
84
|
+
model: model || null,
|
|
85
|
+
mode: mode === 'agent' ? 'agent' : 'api',
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
pinned: false,
|
|
89
|
+
archived: false,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.conversations.set(id, conversation);
|
|
93
|
+
this._save();
|
|
94
|
+
this.daemon.broadcast({ type: 'conversation:created', data: conversation });
|
|
95
|
+
return conversation;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(id) {
|
|
99
|
+
const conv = this.conversations.get(id);
|
|
100
|
+
if (!conv) return null;
|
|
101
|
+
if (conv.mode === 'api' || !conv.agentId) {
|
|
102
|
+
return { ...conv, agentStatus: conv.agentStatus || null };
|
|
103
|
+
}
|
|
104
|
+
const agent = this.daemon.registry.get(conv.agentId);
|
|
105
|
+
return {
|
|
106
|
+
...conv,
|
|
107
|
+
agentStatus: agent?.status || conv.agentStatus || 'unknown',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
list() {
|
|
112
|
+
const all = [...this.conversations.values()].map((conv) => {
|
|
113
|
+
if (conv.mode === 'api' || !conv.agentId) {
|
|
114
|
+
return { ...conv, agentStatus: conv.agentStatus || null };
|
|
115
|
+
}
|
|
116
|
+
const agent = this.daemon.registry.get(conv.agentId);
|
|
117
|
+
return {
|
|
118
|
+
...conv,
|
|
119
|
+
agentStatus: agent?.status || conv.agentStatus || 'unknown',
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
all.sort((a, b) => {
|
|
123
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
124
|
+
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
125
|
+
});
|
|
126
|
+
return all;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
rename(id, title) {
|
|
130
|
+
const conv = this.conversations.get(id);
|
|
131
|
+
if (!conv) throw new Error('Conversation not found');
|
|
132
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
133
|
+
throw new Error('Title is required');
|
|
134
|
+
}
|
|
135
|
+
conv.title = title.trim().slice(0, 200);
|
|
136
|
+
conv.updatedAt = new Date().toISOString();
|
|
137
|
+
this._save();
|
|
138
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
139
|
+
return conv;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pin(id, pinned) {
|
|
143
|
+
const conv = this.conversations.get(id);
|
|
144
|
+
if (!conv) throw new Error('Conversation not found');
|
|
145
|
+
conv.pinned = !!pinned;
|
|
146
|
+
conv.updatedAt = new Date().toISOString();
|
|
147
|
+
this._save();
|
|
148
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
149
|
+
return conv;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
archive(id, archived) {
|
|
153
|
+
const conv = this.conversations.get(id);
|
|
154
|
+
if (!conv) throw new Error('Conversation not found');
|
|
155
|
+
conv.archived = !!archived;
|
|
156
|
+
conv.updatedAt = new Date().toISOString();
|
|
157
|
+
this._save();
|
|
158
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
159
|
+
return conv;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async delete(id) {
|
|
163
|
+
const conv = this.conversations.get(id);
|
|
164
|
+
if (!conv) throw new Error('Conversation not found');
|
|
165
|
+
|
|
166
|
+
if (conv.agentId) {
|
|
167
|
+
const agent = this.daemon.registry.get(conv.agentId);
|
|
168
|
+
if (agent && (agent.status === 'running' || agent.status === 'starting')) {
|
|
169
|
+
try { await this.daemon.processes.kill(conv.agentId); } catch { /* ignore */ }
|
|
170
|
+
}
|
|
171
|
+
if (agent) {
|
|
172
|
+
this.daemon.registry.remove(conv.agentId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Kill any active API mode streaming process
|
|
177
|
+
this._killStreamingProcess(id);
|
|
178
|
+
|
|
179
|
+
this.conversations.delete(id);
|
|
180
|
+
this._save();
|
|
181
|
+
this.daemon.broadcast({ type: 'conversation:deleted', data: { id } });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
touchUpdatedAt(id) {
|
|
186
|
+
const conv = this.conversations.get(id);
|
|
187
|
+
if (!conv) return;
|
|
188
|
+
conv.updatedAt = new Date().toISOString();
|
|
189
|
+
this._save();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
autoTitle(id, message) {
|
|
193
|
+
const conv = this.conversations.get(id);
|
|
194
|
+
if (!conv) return;
|
|
195
|
+
if (conv.title !== 'New Chat') return;
|
|
196
|
+
const cleaned = message.trim().replace(/\s+/g, ' ').slice(0, 50);
|
|
197
|
+
conv.title = cleaned || 'New Chat';
|
|
198
|
+
conv.updatedAt = new Date().toISOString();
|
|
199
|
+
this._save();
|
|
200
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async setMode(id, mode) {
|
|
204
|
+
const conv = this.conversations.get(id);
|
|
205
|
+
if (!conv) throw new Error('Conversation not found');
|
|
206
|
+
if (mode !== 'api' && mode !== 'agent') throw new Error('Mode must be "api" or "agent"');
|
|
207
|
+
if (conv.mode === mode) return conv;
|
|
208
|
+
|
|
209
|
+
if (mode === 'agent') {
|
|
210
|
+
const defaultTeam = this.daemon.teams.getDefault();
|
|
211
|
+
const workingDir = defaultTeam?.workingDir || this.daemon.projectDir;
|
|
212
|
+
const agent = await this.daemon.processes.spawn({
|
|
213
|
+
role: 'chat',
|
|
214
|
+
provider: conv.provider,
|
|
215
|
+
model: conv.model || null,
|
|
216
|
+
workingDir,
|
|
217
|
+
teamId: defaultTeam?.id || null,
|
|
218
|
+
permission: 'full',
|
|
219
|
+
});
|
|
220
|
+
conv.agentId = agent.id;
|
|
221
|
+
} else {
|
|
222
|
+
// Switching to API mode — kill the agent if running
|
|
223
|
+
this._killStreamingProcess(id);
|
|
224
|
+
if (conv.agentId) {
|
|
225
|
+
const agent = this.daemon.registry.get(conv.agentId);
|
|
226
|
+
if (agent && (agent.status === 'running' || agent.status === 'starting')) {
|
|
227
|
+
try { await this.daemon.processes.kill(conv.agentId); } catch { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
if (agent) this.daemon.registry.remove(conv.agentId);
|
|
230
|
+
conv.agentId = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
conv.mode = mode;
|
|
235
|
+
conv.updatedAt = new Date().toISOString();
|
|
236
|
+
this._save();
|
|
237
|
+
this.daemon.broadcast({ type: 'conversation:updated', data: conv });
|
|
238
|
+
return conv;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_buildHistoryPrompt(history, newMessage) {
|
|
242
|
+
const parts = [];
|
|
243
|
+
if (history && history.length > 0) {
|
|
244
|
+
parts.push('Previous conversation:');
|
|
245
|
+
for (const msg of history) {
|
|
246
|
+
const role = msg.from === 'user' ? 'User' : 'Assistant';
|
|
247
|
+
parts.push(`${role}: ${msg.text}`);
|
|
248
|
+
}
|
|
249
|
+
parts.push('');
|
|
250
|
+
}
|
|
251
|
+
parts.push(`User: ${newMessage}`);
|
|
252
|
+
return parts.join('\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_getStreamingProcesses() {
|
|
256
|
+
if (!this._streamingProcesses) this._streamingProcesses = new Map();
|
|
257
|
+
return this._streamingProcesses;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_killStreamingProcess(conversationId) {
|
|
261
|
+
const procs = this._getStreamingProcesses();
|
|
262
|
+
const proc = procs.get(conversationId);
|
|
263
|
+
if (proc && !proc.killed) {
|
|
264
|
+
proc.kill();
|
|
265
|
+
}
|
|
266
|
+
procs.delete(conversationId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async sendMessage(id, message, history) {
|
|
270
|
+
const conv = this.conversations.get(id);
|
|
271
|
+
if (!conv) throw new Error('Conversation not found');
|
|
272
|
+
if (conv.mode !== 'api') throw new Error('sendMessage only works in API mode');
|
|
273
|
+
|
|
274
|
+
// Kill any previous streaming process for this conversation
|
|
275
|
+
this._killStreamingProcess(id);
|
|
276
|
+
|
|
277
|
+
const prompt = this._buildHistoryPrompt(history, message);
|
|
278
|
+
|
|
279
|
+
// Resolve the provider for this conversation
|
|
280
|
+
let provider = getProvider(conv.provider);
|
|
281
|
+
let modelId = conv.model;
|
|
282
|
+
|
|
283
|
+
if (!provider || !provider.constructor.isInstalled()) {
|
|
284
|
+
const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
|
|
285
|
+
const installed = getInstalledProviders();
|
|
286
|
+
const fallbackId = priority.find((p) => installed.some((i) => i.id === p));
|
|
287
|
+
if (!fallbackId) throw new Error('No provider available for chat');
|
|
288
|
+
provider = getProvider(fallbackId);
|
|
289
|
+
modelId = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
|
|
293
|
+
const { command, args, env, stdin: stdinData, cwd } = headlessCmd;
|
|
294
|
+
|
|
295
|
+
const spawnOpts = {
|
|
296
|
+
env: { ...process.env, ...env },
|
|
297
|
+
cwd: cwd || this.daemon.projectDir,
|
|
298
|
+
stdio: stdinData ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const proc = cpSpawn(command, args, spawnOpts);
|
|
302
|
+
this._getStreamingProcesses().set(id, proc);
|
|
303
|
+
|
|
304
|
+
if (stdinData) {
|
|
305
|
+
proc.stdin.write(stdinData);
|
|
306
|
+
proc.stdin.end();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let fullOutput = '';
|
|
310
|
+
|
|
311
|
+
proc.stdout.on('data', (data) => {
|
|
312
|
+
const text = data.toString();
|
|
313
|
+
fullOutput += text;
|
|
314
|
+
|
|
315
|
+
// Parse provider output for streaming chunks
|
|
316
|
+
const lines = text.split('\n');
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (!trimmed) continue;
|
|
320
|
+
|
|
321
|
+
// Try to parse as JSON (stream-json format)
|
|
322
|
+
try {
|
|
323
|
+
const json = JSON.parse(trimmed);
|
|
324
|
+
|
|
325
|
+
// Claude Code stream-json: assistant message content
|
|
326
|
+
if (json.type === 'assistant' && json.message?.content) {
|
|
327
|
+
for (const block of json.message.content) {
|
|
328
|
+
if (block.type === 'text' && block.text) {
|
|
329
|
+
this.daemon.broadcast({
|
|
330
|
+
type: 'conversation:chunk',
|
|
331
|
+
data: { conversationId: id, text: block.text },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Claude Code stream-json: content_block_delta
|
|
339
|
+
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
340
|
+
this.daemon.broadcast({
|
|
341
|
+
type: 'conversation:chunk',
|
|
342
|
+
data: { conversationId: id, text: json.delta.text },
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Claude Code stream-json: result block
|
|
348
|
+
if (json.type === 'result' && json.result) {
|
|
349
|
+
this.daemon.broadcast({
|
|
350
|
+
type: 'conversation:chunk',
|
|
351
|
+
data: { conversationId: id, text: json.result },
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Groove Network: token events
|
|
357
|
+
if (json.type === 'token' && json.text != null) {
|
|
358
|
+
this.daemon.broadcast({
|
|
359
|
+
type: 'conversation:chunk',
|
|
360
|
+
data: { conversationId: id, text: json.text },
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Groove Network: done/complete/result
|
|
366
|
+
if ((json.type === 'done' || json.type === 'complete' || json.type === 'result') && json.text) {
|
|
367
|
+
this.daemon.broadcast({
|
|
368
|
+
type: 'conversation:chunk',
|
|
369
|
+
data: { conversationId: id, text: json.text },
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Gemini / Codex: content text
|
|
375
|
+
if (json.content?.[0]?.text) {
|
|
376
|
+
this.daemon.broadcast({
|
|
377
|
+
type: 'conversation:chunk',
|
|
378
|
+
data: { conversationId: id, text: json.content[0].text },
|
|
379
|
+
});
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
} catch { /* not JSON — treat as raw text */ }
|
|
383
|
+
|
|
384
|
+
// Non-JSON output: broadcast raw text (some providers output plain text)
|
|
385
|
+
if (!trimmed.startsWith('{')) {
|
|
386
|
+
this.daemon.broadcast({
|
|
387
|
+
type: 'conversation:chunk',
|
|
388
|
+
data: { conversationId: id, text: trimmed },
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
proc.on('error', (err) => {
|
|
395
|
+
this._getStreamingProcesses().delete(id);
|
|
396
|
+
this.daemon.broadcast({
|
|
397
|
+
type: 'conversation:error',
|
|
398
|
+
data: { conversationId: id, error: err.message },
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
proc.on('exit', (code) => {
|
|
403
|
+
this._getStreamingProcesses().delete(id);
|
|
404
|
+
this.daemon.broadcast({
|
|
405
|
+
type: 'conversation:complete',
|
|
406
|
+
data: { conversationId: id, exitCode: code },
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const timeout = setTimeout(() => {
|
|
411
|
+
if (!proc.killed) proc.kill();
|
|
412
|
+
}, 120_000);
|
|
413
|
+
proc.on('exit', () => clearTimeout(timeout));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
stopStreaming(id) {
|
|
417
|
+
this._killStreamingProcess(id);
|
|
418
|
+
this.daemon.broadcast({
|
|
419
|
+
type: 'conversation:complete',
|
|
420
|
+
data: { conversationId: id, stopped: true },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -41,6 +41,7 @@ import { TunnelManager } from './tunnel-manager.js';
|
|
|
41
41
|
import { ModelManager } from './model-manager.js';
|
|
42
42
|
import { LlamaServerManager } from './llama-server.js';
|
|
43
43
|
import { RepoImporter } from './repo-import.js';
|
|
44
|
+
import { ConversationManager } from './conversations.js';
|
|
44
45
|
import { Toys } from './toys.js';
|
|
45
46
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
46
47
|
import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
|
|
@@ -126,6 +127,7 @@ export class Daemon {
|
|
|
126
127
|
this.rotator = new Rotator(this);
|
|
127
128
|
this.adaptive = new AdaptiveThresholds(this.grooveDir);
|
|
128
129
|
this.teams = new Teams(this);
|
|
130
|
+
this.conversations = new ConversationManager(this);
|
|
129
131
|
this.credentials = new CredentialStore(this.grooveDir);
|
|
130
132
|
this.classifier = new TaskClassifier();
|
|
131
133
|
this.router = new ModelRouter(this);
|