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.
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
@@ -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().fetchConversations();
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. Visit the Marketplace to get started.',
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.196",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.196",
3
+ "version": "0.27.197",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.196",
3
+ "version": "0.27.197",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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', 'defaultChatProvider', 'defaultChatModel',
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 Marketplace that matches your task.
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.