groove-dev 0.27.196 → 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.
- 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 +3 -5
- package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
- package/node_modules/@groove-dev/daemon/src/index.js +3 -2
- package/node_modules/@groove-dev/daemon/src/process.js +1 -1
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
- package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
- package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Da2nbd6M.js +1043 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zmrIbwNm.css +1 -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 +0 -2
- 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 +2 -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 +5 -163
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +0 -61
- 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 +3 -5
- package/packages/daemon/src/chatstore.js +104 -0
- package/packages/daemon/src/index.js +3 -2
- package/packages/daemon/src/process.js +1 -1
- package/packages/daemon/src/routes/agents.js +1 -175
- package/packages/daemon/src/routes/chat-history.js +34 -0
- package/packages/gui/dist/assets/index-Da2nbd6M.js +1043 -0
- package/packages/gui/dist/assets/index-zmrIbwNm.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +0 -2
- 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 +2 -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 +5 -163
- package/packages/gui/src/stores/slices/agents-slice.js +52 -2
- package/packages/gui/src/views/marketplace.jsx +1 -1
- package/packages/gui/src/views/settings.jsx +0 -61
- 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
|
@@ -7,7 +7,6 @@ import { persistJSON, persistChatHistory } from './helpers.js';
|
|
|
7
7
|
import { createUiSlice } from './slices/ui-slice.js';
|
|
8
8
|
import { createAgentsSlice } from './slices/agents-slice.js';
|
|
9
9
|
import { createTeamsSlice } from './slices/teams-slice.js';
|
|
10
|
-
import { createChatSlice } from './slices/chat-slice.js';
|
|
11
10
|
import { createEditorSlice } from './slices/editor-slice.js';
|
|
12
11
|
import { createProvidersSlice } from './slices/providers-slice.js';
|
|
13
12
|
import { createNetworkSlice } from './slices/network-slice.js';
|
|
@@ -33,7 +32,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
33
32
|
...createUiSlice(set, get),
|
|
34
33
|
...createAgentsSlice(set, get),
|
|
35
34
|
...createTeamsSlice(set, get),
|
|
36
|
-
...createChatSlice(set, get),
|
|
37
35
|
...createEditorSlice(set, get),
|
|
38
36
|
...createProvidersSlice(set, get),
|
|
39
37
|
...createNetworkSlice(set, get),
|
|
@@ -69,7 +67,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
69
67
|
if (isTunneled) get().fetchProjectDir();
|
|
70
68
|
}).catch(() => {});
|
|
71
69
|
get().fetchTeams();
|
|
72
|
-
get().
|
|
70
|
+
get().fetchChatHistory();
|
|
73
71
|
get().fetchApprovals();
|
|
74
72
|
get().checkMarketplaceAuth();
|
|
75
73
|
get().fetchTunnels();
|
|
@@ -266,27 +264,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
266
264
|
set({ chatHistory: history });
|
|
267
265
|
persistChatHistory(history);
|
|
268
266
|
}
|
|
269
|
-
|
|
270
|
-
const conv = get().conversations.find((c) => c.agentId === agentId);
|
|
271
|
-
if (conv) {
|
|
272
|
-
const convMsgs = { ...get().conversationMessages };
|
|
273
|
-
if (!convMsgs[conv.id]) convMsgs[conv.id] = [];
|
|
274
|
-
const convArr = [...convMsgs[conv.id]];
|
|
275
|
-
const lastConv = convArr[convArr.length - 1];
|
|
276
|
-
const isRecentConv = lastConv && lastConv.from === 'assistant' && (Date.now() - lastConv.timestamp) < 8000;
|
|
277
|
-
const isConvDupe = isRecentConv && (lastConv.text === trimmed || lastConv.text.endsWith(trimmed));
|
|
278
|
-
if (!isConvDupe) {
|
|
279
|
-
if (isRecentConv) {
|
|
280
|
-
const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
|
|
281
|
-
convArr[convArr.length - 1] = { ...lastConv, text: lastConv.text + sep + trimmed, timestamp: Date.now() };
|
|
282
|
-
} else {
|
|
283
|
-
convArr.push({ from: 'assistant', text: trimmed, timestamp: Date.now() });
|
|
284
|
-
}
|
|
285
|
-
convMsgs[conv.id] = convArr.slice(-200);
|
|
286
|
-
set({ conversationMessages: convMsgs, streamingConversationId: conv.id });
|
|
287
|
-
persistJSON('groove:conversationMessages', convMsgs);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
267
|
}
|
|
291
268
|
|
|
292
269
|
if (activityText && activityText.trim()) {
|
|
@@ -347,11 +324,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
347
324
|
});
|
|
348
325
|
}
|
|
349
326
|
|
|
350
|
-
const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
|
|
351
|
-
if (exitConv && get().streamingConversationId === exitConv.id) {
|
|
352
|
-
set({ sendingMessage: false, streamingConversationId: null });
|
|
353
|
-
}
|
|
354
|
-
|
|
355
327
|
if (msg.error && msg.agentId) {
|
|
356
328
|
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
357
329
|
}
|
|
@@ -385,8 +357,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
385
357
|
isQuery: false,
|
|
386
358
|
innerchat: { turnId: turn.id, threadId: thread.id, kind: turn.kind, peer, direction },
|
|
387
359
|
});
|
|
360
|
+
const pushed = [];
|
|
388
361
|
const push = (agentId, item) => {
|
|
389
362
|
history[agentId] = [...(history[agentId] || []), item].slice(-100);
|
|
363
|
+
pushed.push([agentId, item]);
|
|
390
364
|
};
|
|
391
365
|
|
|
392
366
|
if (turn.kind === 'ask') {
|
|
@@ -403,6 +377,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
403
377
|
}
|
|
404
378
|
|
|
405
379
|
persistChatHistory(history);
|
|
380
|
+
// Mirror to the daemon so these survive an origin/port change too.
|
|
381
|
+
for (const [agentId, item] of pushed) get().persistMessageRemote(agentId, item);
|
|
406
382
|
|
|
407
383
|
const answers = { ...s.innerchatAnswers };
|
|
408
384
|
if (turn.kind === 'answer') {
|
|
@@ -967,140 +943,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
967
943
|
get().fetchNetworkInstallStatus();
|
|
968
944
|
break;
|
|
969
945
|
|
|
970
|
-
case 'conversation:created': {
|
|
971
|
-
const conv = msg.data;
|
|
972
|
-
if (conv) set((s) => ({ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)] }));
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case 'conversation:updated': {
|
|
977
|
-
const conv = msg.data;
|
|
978
|
-
if (conv) set((s) => ({ conversations: s.conversations.map((c) => c.id === conv.id ? { ...c, ...conv } : c) }));
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
case 'conversation:deleted': {
|
|
983
|
-
const id = msg.data?.id || msg.id;
|
|
984
|
-
if (id) {
|
|
985
|
-
set((s) => {
|
|
986
|
-
const conversations = s.conversations.filter((c) => c.id !== id);
|
|
987
|
-
const conversationMessages = { ...s.conversationMessages };
|
|
988
|
-
delete conversationMessages[id];
|
|
989
|
-
const activeConversationId = s.activeConversationId === id ? null : s.activeConversationId;
|
|
990
|
-
if (activeConversationId !== s.activeConversationId) localStorage.setItem('groove:activeConversationId', '');
|
|
991
|
-
return { conversations, conversationMessages, activeConversationId };
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
case 'conversation:tool': {
|
|
998
|
-
const { conversationId, name, summary } = msg.data || msg;
|
|
999
|
-
if (!conversationId) break;
|
|
1000
|
-
set((s) => {
|
|
1001
|
-
const tools = { ...s.conversationActiveTools };
|
|
1002
|
-
tools[conversationId] = { name: name || 'Tool', summary: summary || null, timestamp: Date.now() };
|
|
1003
|
-
return { conversationActiveTools: tools };
|
|
1004
|
-
});
|
|
1005
|
-
break;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
case 'conversation:chunk': {
|
|
1009
|
-
const { conversationId, text } = msg.data || msg;
|
|
1010
|
-
if (!conversationId || !text) break;
|
|
1011
|
-
set((s) => {
|
|
1012
|
-
const msgs = { ...s.conversationMessages };
|
|
1013
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1014
|
-
const arr = [...msgs[conversationId]];
|
|
1015
|
-
const last = arr[arr.length - 1];
|
|
1016
|
-
if (last && last.from === 'assistant' && (Date.now() - last.timestamp) < 30000) {
|
|
1017
|
-
arr[arr.length - 1] = { ...last, text: last.text + text, timestamp: Date.now() };
|
|
1018
|
-
} else {
|
|
1019
|
-
arr.push({ from: 'assistant', text, timestamp: Date.now() });
|
|
1020
|
-
}
|
|
1021
|
-
msgs[conversationId] = arr.slice(-200);
|
|
1022
|
-
const tools = { ...s.conversationActiveTools };
|
|
1023
|
-
delete tools[conversationId];
|
|
1024
|
-
return { conversationMessages: msgs, streamingConversationId: conversationId, conversationActiveTools: tools };
|
|
1025
|
-
});
|
|
1026
|
-
break;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
case 'conversation:complete': {
|
|
1030
|
-
const { conversationId } = msg.data || msg;
|
|
1031
|
-
if (conversationId && get().streamingConversationId === conversationId) {
|
|
1032
|
-
const tools = { ...get().conversationActiveTools };
|
|
1033
|
-
delete tools[conversationId];
|
|
1034
|
-
set({ sendingMessage: false, streamingConversationId: null, conversationActiveTools: tools });
|
|
1035
|
-
}
|
|
1036
|
-
if (conversationId) persistJSON('groove:conversationMessages', get().conversationMessages);
|
|
1037
|
-
break;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
case 'conversation:image': {
|
|
1041
|
-
const { conversationId, prompt, url, b64_json, mimeType, model: imgModel, provider: imgProvider } = msg.data || msg;
|
|
1042
|
-
if (!conversationId) break;
|
|
1043
|
-
const imageUrl = url || (b64_json ? `data:${mimeType || 'image/png'};base64,${b64_json}` : null);
|
|
1044
|
-
set((s) => {
|
|
1045
|
-
const msgs = { ...s.conversationMessages };
|
|
1046
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1047
|
-
const arr = [...msgs[conversationId]];
|
|
1048
|
-
const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading' && m.prompt === prompt);
|
|
1049
|
-
if (loadingIdx >= 0) {
|
|
1050
|
-
arr[loadingIdx] = { from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() };
|
|
1051
|
-
} else {
|
|
1052
|
-
arr.push({ from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() });
|
|
1053
|
-
}
|
|
1054
|
-
msgs[conversationId] = arr.slice(-200);
|
|
1055
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
1056
|
-
const isActive = s.streamingConversationId === conversationId;
|
|
1057
|
-
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
1058
|
-
});
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
case 'conversation:image-progress': {
|
|
1063
|
-
const { conversationId, status, prompt: imgPrompt, error: imgError } = msg.data || msg;
|
|
1064
|
-
if (!conversationId) break;
|
|
1065
|
-
if (status === 'generating') {
|
|
1066
|
-
set((s) => {
|
|
1067
|
-
const msgs = { ...s.conversationMessages };
|
|
1068
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1069
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'assistant', type: 'image-loading', prompt: imgPrompt, timestamp: Date.now() }];
|
|
1070
|
-
return { conversationMessages: msgs, streamingConversationId: conversationId };
|
|
1071
|
-
});
|
|
1072
|
-
} else if (status === 'error') {
|
|
1073
|
-
set((s) => {
|
|
1074
|
-
const msgs = { ...s.conversationMessages };
|
|
1075
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1076
|
-
const arr = [...msgs[conversationId]];
|
|
1077
|
-
const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading');
|
|
1078
|
-
if (loadingIdx >= 0) arr.splice(loadingIdx, 1);
|
|
1079
|
-
arr.push({ from: 'system', text: `Image generation failed: ${imgError || 'Unknown error'}`, timestamp: Date.now() });
|
|
1080
|
-
msgs[conversationId] = arr;
|
|
1081
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
1082
|
-
const isActive = s.streamingConversationId === conversationId;
|
|
1083
|
-
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
case 'conversation:error': {
|
|
1090
|
-
const { conversationId, error } = msg.data || msg;
|
|
1091
|
-
if (conversationId) {
|
|
1092
|
-
set((s) => {
|
|
1093
|
-
const msgs = { ...s.conversationMessages };
|
|
1094
|
-
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1095
|
-
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Error: ${error || 'Unknown error'}`, timestamp: Date.now() }];
|
|
1096
|
-
persistJSON('groove:conversationMessages', msgs);
|
|
1097
|
-
const isActive = s.streamingConversationId === conversationId;
|
|
1098
|
-
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
946
|
case 'network:token:timing': {
|
|
1105
947
|
const { __proto__: _a, constructor: _b, prototype: _c, ...td } = msg.data || {};
|
|
1106
948
|
const updates = {
|
|
@@ -89,6 +89,7 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
89
89
|
persistJSON('groove:activityLog', activityLog);
|
|
90
90
|
return { chatHistory, activityLog, tokenTimeline };
|
|
91
91
|
});
|
|
92
|
+
api.delete(`/chat-history/${encodeURIComponent(id)}`).catch(() => {});
|
|
92
93
|
}
|
|
93
94
|
} catch (err) {
|
|
94
95
|
get().addToast('error', 'Kill failed', err.message);
|
|
@@ -166,15 +167,64 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
166
167
|
// ── Chat ──────────────────────────────────────────────────
|
|
167
168
|
|
|
168
169
|
addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
|
|
170
|
+
const msg = { from, text, timestamp: Date.now(), isQuery };
|
|
171
|
+
if (attachments?.length) msg.attachments = attachments;
|
|
169
172
|
set((s) => {
|
|
170
173
|
const history = { ...s.chatHistory };
|
|
171
174
|
if (!history[agentId]) history[agentId] = [];
|
|
172
|
-
const msg = { from, text, timestamp: Date.now(), isQuery };
|
|
173
|
-
if (attachments?.length) msg.attachments = attachments;
|
|
174
175
|
history[agentId] = [...history[agentId].slice(-100), msg];
|
|
175
176
|
persistChatHistory(history);
|
|
176
177
|
return { chatHistory: history };
|
|
177
178
|
});
|
|
179
|
+
get().persistMessageRemote(agentId, msg);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Persist one message to the daemon so history survives on the server, not
|
|
183
|
+
// just in this browser's per-origin localStorage. Attachment payloads are
|
|
184
|
+
// stripped; the daemon keeps metadata only.
|
|
185
|
+
persistMessageRemote(agentId, msg) {
|
|
186
|
+
if (!agentId || !msg) return;
|
|
187
|
+
const clean = msg.attachments?.length
|
|
188
|
+
? { ...msg, attachments: msg.attachments.map(({ dataUrl, ...rest }) => rest) }
|
|
189
|
+
: msg;
|
|
190
|
+
api.post(`/chat-history/${encodeURIComponent(agentId)}`, { message: clean }).catch(() => {});
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Load server-side history on (re)connect and merge it in — the daemon is the
|
|
194
|
+
// source of truth, so this restores chats regardless of which tunnel port
|
|
195
|
+
// (and thus browser origin) we came up on.
|
|
196
|
+
//
|
|
197
|
+
// Also runs a ONE-TIME, per-origin seed: whatever chat history is stranded in
|
|
198
|
+
// THIS origin's localStorage gets pushed to the daemon for any agent the
|
|
199
|
+
// daemon doesn't yet know. Visiting each old origin once migrates its history
|
|
200
|
+
// server-side without clobbering anything already there.
|
|
201
|
+
async fetchChatHistory() {
|
|
202
|
+
try {
|
|
203
|
+
const { history } = await api.get('/chat-history');
|
|
204
|
+
const remote = (history && typeof history === 'object') ? history : {};
|
|
205
|
+
|
|
206
|
+
if (localStorage.getItem('groove:chatImported') !== '1') {
|
|
207
|
+
const local = get().chatHistory;
|
|
208
|
+
for (const [agentId, msgs] of Object.entries(local)) {
|
|
209
|
+
// Only seed agents the daemon has nothing for — never overwrite
|
|
210
|
+
// server history with an older local copy.
|
|
211
|
+
if (Array.isArray(msgs) && msgs.length && !(remote[agentId]?.length)) {
|
|
212
|
+
api.put(`/chat-history/${encodeURIComponent(agentId)}`, { messages: msgs }).catch(() => {});
|
|
213
|
+
remote[agentId] = msgs; // reflect so the merge below keeps it
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
localStorage.setItem('groove:chatImported', '1');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
set((s) => {
|
|
220
|
+
const merged = { ...s.chatHistory };
|
|
221
|
+
for (const [agentId, msgs] of Object.entries(remote)) {
|
|
222
|
+
if (Array.isArray(msgs) && msgs.length) merged[agentId] = msgs;
|
|
223
|
+
}
|
|
224
|
+
persistChatHistory(merged);
|
|
225
|
+
return { chatHistory: merged };
|
|
226
|
+
});
|
|
227
|
+
} catch { /* offline or old daemon without the endpoint — keep local */ }
|
|
178
228
|
},
|
|
179
229
|
|
|
180
230
|
async stopAgent(id) {
|
|
@@ -581,7 +581,7 @@ function MyLibrary() {
|
|
|
581
581
|
];
|
|
582
582
|
|
|
583
583
|
const emptyMessages = {
|
|
584
|
-
all: 'No skills or integrations installed yet.
|
|
584
|
+
all: 'No skills or integrations installed yet. Browse the Integrations tab to get started.',
|
|
585
585
|
skills: 'No skills installed.',
|
|
586
586
|
integrations: 'No integrations installed.',
|
|
587
587
|
};
|
|
@@ -1787,67 +1787,6 @@ export default function SettingsView() {
|
|
|
1787
1787
|
</div>
|
|
1788
1788
|
</ConfigCard>
|
|
1789
1789
|
|
|
1790
|
-
<ConfigCard icon={MessageSquare} label="Default Chat Model" description="Provider and model for new chat conversations.">
|
|
1791
|
-
<div className="space-y-2">
|
|
1792
|
-
<select
|
|
1793
|
-
value={config.defaultChatProvider || config.defaultProvider || 'claude-code'}
|
|
1794
|
-
onChange={(e) => {
|
|
1795
|
-
updateConfig('defaultChatProvider', e.target.value);
|
|
1796
|
-
const prov = providers.find((p) => p.id === e.target.value);
|
|
1797
|
-
const chatModels = (prov?.models || []).filter((m) => {
|
|
1798
|
-
const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
|
|
1799
|
-
return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
|
|
1800
|
-
});
|
|
1801
|
-
if (chatModels.length > 0) {
|
|
1802
|
-
const first = typeof chatModels[0] === 'string' ? chatModels[0] : chatModels[0].id;
|
|
1803
|
-
updateConfig('defaultChatModel', first);
|
|
1804
|
-
}
|
|
1805
|
-
}}
|
|
1806
|
-
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer"
|
|
1807
|
-
>
|
|
1808
|
-
{visibleProviders.filter((p) => p.installed && (p.authType === 'local' || (p.authType === 'subscription' && p.authStatus?.authenticated) || p.hasKey)).map((p) => (
|
|
1809
|
-
<option key={p.id} value={p.id}>{p.name}</option>
|
|
1810
|
-
))}
|
|
1811
|
-
</select>
|
|
1812
|
-
{(() => {
|
|
1813
|
-
const chatProv = providers.find((p) => p.id === (config.defaultChatProvider || config.defaultProvider || 'claude-code'));
|
|
1814
|
-
const chatModels = (chatProv?.models || []).filter((m) => {
|
|
1815
|
-
const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
|
|
1816
|
-
return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
|
|
1817
|
-
});
|
|
1818
|
-
const isChatLocal = (config.defaultChatProvider || config.defaultProvider || 'claude-code') === 'local';
|
|
1819
|
-
return (
|
|
1820
|
-
<select
|
|
1821
|
-
value={config.defaultChatModel || ''}
|
|
1822
|
-
onChange={(e) => updateConfig('defaultChatModel', e.target.value || null)}
|
|
1823
|
-
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer"
|
|
1824
|
-
>
|
|
1825
|
-
<option value="">Auto (Sonnet)</option>
|
|
1826
|
-
{isChatLocal ? (
|
|
1827
|
-
<>
|
|
1828
|
-
{chatModels.filter(m => m.source === 'ollama').length > 0 && (
|
|
1829
|
-
<optgroup label="Ollama">{chatModels.filter(m => m.source === 'ollama').map(m => <option key={m.id} value={m.id}>{m.name}</option>)}</optgroup>
|
|
1830
|
-
)}
|
|
1831
|
-
{chatModels.filter(m => m.source === 'gguf').length > 0 && (
|
|
1832
|
-
<optgroup label="Downloaded GGUFs">{chatModels.filter(m => m.source === 'gguf').map(m => <option key={m.id} value={m.id}>{m.name}{m.hasRuntime ? '' : ' (no runtime)'}</option>)}</optgroup>
|
|
1833
|
-
)}
|
|
1834
|
-
{chatModels.filter(m => m.source === 'runtime').length > 0 && (
|
|
1835
|
-
<optgroup label="Runtime Models">{chatModels.filter(m => m.source === 'runtime').map(m => <option key={m.id} value={m.id}>{m.name}</option>)}</optgroup>
|
|
1836
|
-
)}
|
|
1837
|
-
</>
|
|
1838
|
-
) : (
|
|
1839
|
-
chatModels.map((m) => {
|
|
1840
|
-
const id = typeof m === 'string' ? m : m.id;
|
|
1841
|
-
const name = typeof m === 'string' ? m : m.name || m.id;
|
|
1842
|
-
return <option key={id} value={id}>{name}</option>;
|
|
1843
|
-
})
|
|
1844
|
-
)}
|
|
1845
|
-
</select>
|
|
1846
|
-
);
|
|
1847
|
-
})()}
|
|
1848
|
-
</div>
|
|
1849
|
-
</ConfigCard>
|
|
1850
|
-
|
|
1851
1790
|
</div>
|
|
1852
1791
|
</div>
|
|
1853
1792
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.197",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -28,6 +28,7 @@ import { registerFileRoutes, resetEditorRoot } from './routes/files.js';
|
|
|
28
28
|
import { registerNetworkRoutes } from './routes/network.js';
|
|
29
29
|
import { registerScheduleRoutes } from './routes/schedules.js';
|
|
30
30
|
import { registerInnerChatRoutes } from './routes/innerchat.js';
|
|
31
|
+
import { registerChatHistoryRoutes } from './routes/chat-history.js';
|
|
31
32
|
import { registerWatchRoutes } from './routes/watch.js';
|
|
32
33
|
import { registerAutoAgentRoutes } from './routes/auto-agents.js';
|
|
33
34
|
|
|
@@ -179,6 +180,7 @@ export function createApi(app, daemon) {
|
|
|
179
180
|
registerNetworkRoutes(app, daemon);
|
|
180
181
|
registerScheduleRoutes(app, daemon);
|
|
181
182
|
registerInnerChatRoutes(app, daemon);
|
|
183
|
+
registerChatHistoryRoutes(app, daemon);
|
|
182
184
|
registerWatchRoutes(app, daemon);
|
|
183
185
|
registerAutoAgentRoutes(app, daemon);
|
|
184
186
|
|
|
@@ -250,8 +252,6 @@ export function createApi(app, daemon) {
|
|
|
250
252
|
}
|
|
251
253
|
});
|
|
252
254
|
|
|
253
|
-
// --- Conversations ---
|
|
254
|
-
|
|
255
255
|
// --- Approvals ---
|
|
256
256
|
|
|
257
257
|
app.get('/api/approvals', (req, res) => {
|
|
@@ -1348,10 +1348,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
1350
|
daemon.config.defaultProvider = provider;
|
|
1351
|
-
daemon.config.defaultChatProvider = provider;
|
|
1352
1351
|
if (model && typeof model === 'string' && model.length <= 100) {
|
|
1353
1352
|
daemon.config.defaultModel = model.trim();
|
|
1354
|
-
daemon.config.defaultChatModel = model.trim();
|
|
1355
1353
|
}
|
|
1356
1354
|
const { saveConfig } = await import('./firstrun.js');
|
|
1357
1355
|
saveConfig(daemon.grooveDir, daemon.config);
|
|
@@ -1439,7 +1437,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1439
1437
|
const ALLOWED_KEYS = [
|
|
1440
1438
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
1441
1439
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
1442
|
-
'onboardingDismissed', 'defaultModel',
|
|
1440
|
+
'onboardingDismissed', 'defaultModel',
|
|
1443
1441
|
'dataSharingDismissed', 'replayCeiling', 'velocityCeiling', 'journalistModelTier',
|
|
1444
1442
|
'resumeBudgetChars',
|
|
1445
1443
|
];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
// Per-agent history cap and attachment handling mirror the GUI's own limits so
|
|
7
|
+
// the two stay in step.
|
|
8
|
+
const MAX_PER_AGENT = 200;
|
|
9
|
+
const SAVE_DEBOUNCE_MS = 1500;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Server-side chat history.
|
|
13
|
+
*
|
|
14
|
+
* The GUI used to keep chat history only in the browser's localStorage, which
|
|
15
|
+
* is scoped per origin (scheme://host:PORT). For a remote GUI reached over an
|
|
16
|
+
* SSH tunnel the local port changes across reconnects, so the origin — and thus
|
|
17
|
+
* the entire chat store — changed out from under the user, stranding history on
|
|
18
|
+
* old ports. Keeping it on the daemon makes it independent of port, origin, and
|
|
19
|
+
* even which machine connects: reconnect from anywhere and the chats are there.
|
|
20
|
+
*/
|
|
21
|
+
export class ChatStore {
|
|
22
|
+
constructor(daemon) {
|
|
23
|
+
this.daemon = daemon;
|
|
24
|
+
this.path = resolve(daemon.grooveDir, 'chat-history.json');
|
|
25
|
+
this.history = this._load();
|
|
26
|
+
this._saveTimer = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_load() {
|
|
30
|
+
try {
|
|
31
|
+
if (existsSync(this.path)) {
|
|
32
|
+
const data = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
33
|
+
if (data && typeof data === 'object') return data;
|
|
34
|
+
}
|
|
35
|
+
} catch { /* corrupt or missing — start empty rather than throw */ }
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_scheduleSave() {
|
|
40
|
+
if (this._saveTimer) return;
|
|
41
|
+
this._saveTimer = setTimeout(() => {
|
|
42
|
+
this._saveTimer = null;
|
|
43
|
+
this._saveNow();
|
|
44
|
+
}, SAVE_DEBOUNCE_MS);
|
|
45
|
+
if (this._saveTimer.unref) this._saveTimer.unref();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_saveNow() {
|
|
49
|
+
try {
|
|
50
|
+
const tmp = `${this.path}.tmp`;
|
|
51
|
+
writeFileSync(tmp, JSON.stringify(this.history), { mode: 0o600 });
|
|
52
|
+
renameSync(tmp, this.path); // atomic — a reader never sees a half-written file
|
|
53
|
+
} catch { /* best effort */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Strip base64 attachment payloads — metadata only. Keeping data URLs would
|
|
57
|
+
// bloat the file the same way it bloated localStorage.
|
|
58
|
+
_clean(message) {
|
|
59
|
+
if (!message || typeof message !== 'object') return null;
|
|
60
|
+
const out = { ...message };
|
|
61
|
+
if (Array.isArray(out.attachments) && out.attachments.length) {
|
|
62
|
+
out.attachments = out.attachments.map(({ dataUrl, ...rest }) => rest);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
append(agentId, message) {
|
|
68
|
+
if (!agentId) return;
|
|
69
|
+
const clean = this._clean(message);
|
|
70
|
+
if (!clean) return;
|
|
71
|
+
const arr = this.history[agentId] || [];
|
|
72
|
+
arr.push(clean);
|
|
73
|
+
this.history[agentId] = arr.slice(-MAX_PER_AGENT);
|
|
74
|
+
this._scheduleSave();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Replace an agent's whole history — used when the GUI syncs a batch (e.g.
|
|
78
|
+
// messages it recorded while briefly disconnected).
|
|
79
|
+
replace(agentId, messages) {
|
|
80
|
+
if (!agentId || !Array.isArray(messages)) return;
|
|
81
|
+
this.history[agentId] = messages.map((m) => this._clean(m)).filter(Boolean).slice(-MAX_PER_AGENT);
|
|
82
|
+
this._scheduleSave();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getAll() {
|
|
86
|
+
return this.history;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get(agentId) {
|
|
90
|
+
return this.history[agentId] || [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
remove(agentId) {
|
|
94
|
+
if (this.history[agentId]) {
|
|
95
|
+
delete this.history[agentId];
|
|
96
|
+
this._scheduleSave();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stop() {
|
|
101
|
+
if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
|
|
102
|
+
this._saveNow();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -44,9 +44,9 @@ import { ModelLab } from './model-lab.js';
|
|
|
44
44
|
import { LlamaServerManager } from './llama-server.js';
|
|
45
45
|
import { MLXServerManager } from './mlx-server.js';
|
|
46
46
|
import { RepoImporter } from './repo-import.js';
|
|
47
|
-
import { ConversationManager } from './conversations.js';
|
|
48
47
|
import { Toys } from './toys.js';
|
|
49
48
|
import { InnerChat } from './innerchat.js';
|
|
49
|
+
import { ChatStore } from './chatstore.js';
|
|
50
50
|
import { Watcher } from './watcher.js';
|
|
51
51
|
import { AutoState } from './autostate.js';
|
|
52
52
|
import { Orchestrator } from './orchestrator.js';
|
|
@@ -139,7 +139,6 @@ export class Daemon {
|
|
|
139
139
|
this.rotator = new Rotator(this);
|
|
140
140
|
this.adaptive = new AdaptiveThresholds(this.grooveDir);
|
|
141
141
|
this.teams = new Teams(this);
|
|
142
|
-
this.conversations = new ConversationManager(this);
|
|
143
142
|
this.credentials = new CredentialStore(this.grooveDir);
|
|
144
143
|
this.classifier = new TaskClassifier();
|
|
145
144
|
this.router = new ModelRouter(this);
|
|
@@ -163,6 +162,7 @@ export class Daemon {
|
|
|
163
162
|
this.modelLab = new ModelLab(this);
|
|
164
163
|
this.toys = new Toys(this);
|
|
165
164
|
this.innerchat = new InnerChat(this);
|
|
165
|
+
this.chatStore = new ChatStore(this);
|
|
166
166
|
this.watcher = new Watcher(this);
|
|
167
167
|
this.autoState = new AutoState(this.grooveDir);
|
|
168
168
|
this.orchestrator = new Orchestrator(this);
|
|
@@ -874,6 +874,7 @@ export class Daemon {
|
|
|
874
874
|
this.scheduler.stop();
|
|
875
875
|
this.watcher.stop();
|
|
876
876
|
this.innerchat.stop();
|
|
877
|
+
this.chatStore.stop();
|
|
877
878
|
this.orchestrator.stop();
|
|
878
879
|
this.timeline.stop();
|
|
879
880
|
if (this._gcInterval) clearInterval(this._gcInterval);
|
|
@@ -71,7 +71,7 @@ Do NOT write code unless explicitly asked. Use your MCP tools (database queries,
|
|
|
71
71
|
- Adapting tone and style to the audience (formal, conversational, technical, marketing)
|
|
72
72
|
- Editing and polishing drafts for grammar, flow, and impact
|
|
73
73
|
- Researching topics to produce accurate, substantive writing
|
|
74
|
-
You CAN use code tools to create and edit text files, markdown documents, and structured content. For best results, apply a writing skill from the
|
|
74
|
+
You CAN use code tools to create and edit text files, markdown documents, and structured content. For best results, apply a writing skill from the Integrations tab that matches your task.
|
|
75
75
|
|
|
76
76
|
`,
|
|
77
77
|
chat: `You are a Chat agent — a conversational companion, friend, and assistant. You are warm, curious, and genuinely engaged. You can discuss anything: ideas, philosophy, science, culture, coding, life.
|