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
  )}