groove-dev 0.27.139 → 0.27.141

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 (75) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +225 -16
  5. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  6. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  9. package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
  20. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  22. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  23. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
  24. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  25. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
  30. package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
  31. package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
  32. package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
  33. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  34. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/integrations-registry.json +12 -44
  38. package/packages/daemon/package.json +1 -1
  39. package/packages/daemon/src/api.js +225 -16
  40. package/packages/daemon/src/index.js +2 -0
  41. package/packages/daemon/src/integrations.js +10 -0
  42. package/packages/daemon/src/introducer.js +17 -1
  43. package/packages/daemon/src/journalist.js +169 -0
  44. package/packages/daemon/src/keeper.js +277 -0
  45. package/packages/daemon/src/model-lab.js +11 -0
  46. package/packages/daemon/src/process.js +76 -0
  47. package/packages/daemon/src/validate.js +9 -0
  48. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  49. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  50. package/packages/gui/dist/index.html +2 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/app.jsx +4 -0
  53. package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
  55. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  56. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  57. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  58. package/packages/gui/src/components/editor/file-tree.jsx +2 -86
  59. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  60. package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
  61. package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  63. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  64. package/packages/gui/src/stores/groove.js +302 -20
  65. package/packages/gui/src/views/agents.jsx +118 -60
  66. package/packages/gui/src/views/editor.jsx +67 -219
  67. package/packages/gui/src/views/memory.jsx +460 -0
  68. package/packages/gui/src/views/model-lab.jsx +1 -6
  69. package/packages/gui/src/views/models.jsx +658 -565
  70. package/plan_files/keeper-manual.md +295 -0
  71. package/plan_files/keeper-memory-system.md +223 -0
  72. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
  73. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
  74. package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
  75. package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
@@ -115,6 +115,12 @@ export const useGrooveStore = create((set, get) => ({
115
115
  conversationReasoningEffort: loadJSON('groove:conversationReasoningEffort'),
116
116
  conversationVerbosity: loadJSON('groove:conversationVerbosity'),
117
117
 
118
+ // ── Keeper (tagged memory) ─────────────────────────────────
119
+ keeperItems: [],
120
+ keeperTree: [],
121
+ keeperEditing: null, // { tag, content, isNew, readOnly } — drives the editor modal
122
+ keeperInstructOpen: false,
123
+
118
124
  // ── Approvals ─────────────────────────────────────────────
119
125
  pendingApprovals: [],
120
126
  resolvedApprovals: [],
@@ -207,6 +213,7 @@ export const useGrooveStore = create((set, get) => ({
207
213
 
208
214
  // ── Editor (Cursor-style) ────────────────────────────────
209
215
  editorSelectedAgent: null,
216
+ editorPendingSnippet: null,
210
217
  editorViewMode: 'code',
211
218
  editorAiPanelOpen: false,
212
219
  editorAiPanelWidth: Number(localStorage.getItem('groove:editorAiPanelWidth')) || 360,
@@ -659,6 +666,7 @@ export const useGrooveStore = create((set, get) => ({
659
666
  case 'lab:runtime:added':
660
667
  case 'lab:runtime:updated':
661
668
  case 'lab:runtime:removed':
669
+ case 'llama:server:stopped':
662
670
  get().fetchLabRuntimes();
663
671
  break;
664
672
 
@@ -1212,6 +1220,12 @@ export const useGrooveStore = create((set, get) => ({
1212
1220
  set((s) => ({ networkBenchmarks: [...s.networkBenchmarks, sd].slice(-100) }));
1213
1221
  break;
1214
1222
  }
1223
+
1224
+ case 'keeper:saved':
1225
+ case 'keeper:updated':
1226
+ case 'keeper:deleted':
1227
+ get().fetchKeeperItems();
1228
+ break;
1215
1229
  }
1216
1230
  };
1217
1231
 
@@ -1230,6 +1244,189 @@ export const useGrooveStore = create((set, get) => ({
1230
1244
 
1231
1245
  setActiveView(view) { set({ activeView: view }); },
1232
1246
 
1247
+ // ── Keeper (tagged memory) ────────────────────────────────
1248
+
1249
+ async fetchKeeperItems() {
1250
+ try {
1251
+ const data = await api.get('/keeper');
1252
+ const treeData = await api.get('/keeper/tree');
1253
+ set({ keeperItems: data.items || [], keeperTree: treeData.tree || [] });
1254
+ } catch { /* ignore */ }
1255
+ },
1256
+
1257
+ async saveKeeperItem(tag, content) {
1258
+ try {
1259
+ const item = await api.post('/keeper', { tag, content });
1260
+ get().fetchKeeperItems();
1261
+ get().addToast('success', `Saved #${item.tag}`);
1262
+ return item;
1263
+ } catch (err) {
1264
+ get().addToast('error', 'Failed to save memory', err.message);
1265
+ throw err;
1266
+ }
1267
+ },
1268
+
1269
+ async appendKeeperItem(tag, content) {
1270
+ try {
1271
+ const item = await api.post('/keeper/append', { tag, content });
1272
+ get().fetchKeeperItems();
1273
+ get().addToast('success', `Appended to #${item.tag}`);
1274
+ return item;
1275
+ } catch (err) {
1276
+ get().addToast('error', 'Failed to append', err.message);
1277
+ throw err;
1278
+ }
1279
+ },
1280
+
1281
+ async updateKeeperItem(tag, content) {
1282
+ try {
1283
+ const item = await api.patch(`/keeper/${tag}`, { content });
1284
+ get().fetchKeeperItems();
1285
+ get().addToast('success', `Updated #${item.tag}`);
1286
+ return item;
1287
+ } catch (err) {
1288
+ get().addToast('error', 'Failed to update memory', err.message);
1289
+ throw err;
1290
+ }
1291
+ },
1292
+
1293
+ async deleteKeeperItem(tag) {
1294
+ try {
1295
+ await api.delete(`/keeper/${tag}`);
1296
+ get().fetchKeeperItems();
1297
+ get().addToast('success', `Deleted #${tag}`);
1298
+ } catch (err) {
1299
+ get().addToast('error', 'Failed to delete memory', err.message);
1300
+ }
1301
+ },
1302
+
1303
+ async getKeeperItem(tag) {
1304
+ try {
1305
+ return await api.get(`/keeper/${tag}`);
1306
+ } catch {
1307
+ return null;
1308
+ }
1309
+ },
1310
+
1311
+ async searchKeeper(query) {
1312
+ try {
1313
+ const data = await api.get(`/keeper/search?q=${encodeURIComponent(query)}`);
1314
+ return data.results || [];
1315
+ } catch {
1316
+ return [];
1317
+ }
1318
+ },
1319
+
1320
+ setKeeperEditing(editing) {
1321
+ set({ keeperEditing: editing });
1322
+ },
1323
+
1324
+ async _handleKeeperCommand(agentId, message, command) {
1325
+ const rest = message.replace(/\[\w+[-\w]*\]|\b(?:save|append|update|delete|view|doc|link|read|instruct)\b/i, '').trim();
1326
+ const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
1327
+
1328
+ const addSystemMsg = (text) => {
1329
+ get().addChatMessage(agentId, 'system', text);
1330
+ };
1331
+
1332
+ try {
1333
+ switch (command) {
1334
+ case 'instruct': {
1335
+ set({ keeperInstructOpen: true });
1336
+ return true;
1337
+ }
1338
+
1339
+ case 'save': {
1340
+ if (tags.length === 0) { addSystemMsg('Usage: save #tag your message here'); return true; }
1341
+ const content = rest.replace(/#[\w/.-]+/g, '').trim();
1342
+ if (!content) { addSystemMsg('Usage: save #tag your message here'); return true; }
1343
+ await get().saveKeeperItem(tags[0], content);
1344
+ addSystemMsg(`Saved to #${tags[0]}`);
1345
+ return { passthrough: content };
1346
+ }
1347
+
1348
+ case 'append': {
1349
+ if (tags.length === 0) { addSystemMsg('Usage: append #tag content to add'); return true; }
1350
+ const content = rest.replace(/#[\w/.-]+/g, '').trim();
1351
+ if (!content) { addSystemMsg('Usage: append #tag content to add'); return true; }
1352
+ await get().appendKeeperItem(tags[0], content);
1353
+ addSystemMsg(`Appended to #${tags[0]}`);
1354
+ return { passthrough: content };
1355
+ }
1356
+
1357
+ case 'update': {
1358
+ if (tags.length === 0) { addSystemMsg('Usage: [update] #tag'); return true; }
1359
+ get().addChatMessage(agentId, 'user', message, false);
1360
+ const existing = await get().getKeeperItem(tags[0]);
1361
+ set({ keeperEditing: { tag: tags[0], content: existing?.content || '', isNew: !existing } });
1362
+ return true;
1363
+ }
1364
+
1365
+ case 'delete': {
1366
+ if (tags.length === 0) { addSystemMsg('Usage: [delete] #tag'); return true; }
1367
+ get().addChatMessage(agentId, 'user', message, false);
1368
+ await get().deleteKeeperItem(tags[0]);
1369
+ addSystemMsg(`Deleted #${tags[0]}`);
1370
+ return true;
1371
+ }
1372
+
1373
+ case 'view': {
1374
+ if (tags.length === 0) { addSystemMsg('Usage: [view] #tag'); return true; }
1375
+ get().addChatMessage(agentId, 'user', message, false);
1376
+ const item = await get().getKeeperItem(tags[0]);
1377
+ if (item) {
1378
+ set({ keeperEditing: { tag: tags[0], content: item.content, isNew: false, readOnly: true } });
1379
+ } else {
1380
+ addSystemMsg(`#${tags[0]} not found`);
1381
+ }
1382
+ return true;
1383
+ }
1384
+
1385
+ case 'read': {
1386
+ if (tags.length === 0) { addSystemMsg('Usage: [read] #tag1 #tag2 ...'); return true; }
1387
+ get().addChatMessage(agentId, 'user', message, false);
1388
+ const readBrief = await api.post('/keeper/pull', { tags });
1389
+ if (readBrief?.brief) {
1390
+ await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, {
1391
+ message: `Here is context from my tagged memories:\n\n${readBrief.brief}`,
1392
+ });
1393
+ addSystemMsg(`Sent ${tags.map(t => '#' + t).join(', ')} to agent`);
1394
+ } else {
1395
+ addSystemMsg(`No memories found for ${tags.map(t => '#' + t).join(', ')}`);
1396
+ }
1397
+ return true;
1398
+ }
1399
+
1400
+ case 'doc': {
1401
+ if (tags.length === 0) { addSystemMsg('Usage: [doc] #tag'); return true; }
1402
+ get().addChatMessage(agentId, 'user', message, false);
1403
+ addSystemMsg(`Generating doc for #${tags[0]}...`);
1404
+ const history = get().chatHistory[agentId] || [];
1405
+ const result = await api.post('/keeper/doc', { tag: tags[0], chatHistory: history, agentId });
1406
+ if (result?.content) {
1407
+ addSystemMsg(`Doc #${tags[0]} generated (${result.size}B)`);
1408
+ set({ keeperEditing: { tag: tags[0], content: result.content, isNew: false } });
1409
+ }
1410
+ return true;
1411
+ }
1412
+
1413
+ case 'link': {
1414
+ const linkMatch = rest.match(/^((?:#[\w/.-]+\s*)+)\s+(.+)$/);
1415
+ if (!linkMatch || tags.length === 0) { addSystemMsg('Usage: [link] #tag path/to/doc'); return true; }
1416
+ const docPath = linkMatch[2].trim();
1417
+ get().addChatMessage(agentId, 'user', message, false);
1418
+ await api.post('/keeper/link', { tag: tags[0], docPath });
1419
+ addSystemMsg(`Linked #${tags[0]} → ${docPath}`);
1420
+ return true;
1421
+ }
1422
+ }
1423
+ } catch (err) {
1424
+ addSystemMsg(`Keeper error: ${err.message}`);
1425
+ return true;
1426
+ }
1427
+ return false;
1428
+ },
1429
+
1233
1430
  // ── Teams ─────────────────────────────────────────────────
1234
1431
 
1235
1432
  async fetchTeams() {
@@ -1834,7 +2031,6 @@ export const useGrooveStore = create((set, get) => ({
1834
2031
  ...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
1835
2032
  ...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
1836
2033
  ...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
1837
- ...(tlc?.mode && { mode: tlc.mode }),
1838
2034
  };
1839
2035
  const result = await api.post('/recommended-team/launch', body);
1840
2036
  const totalOk = (result.launched || 0) + (result.reused || 0);
@@ -2487,6 +2683,16 @@ export const useGrooveStore = create((set, get) => ({
2487
2683
  },
2488
2684
 
2489
2685
  async instructAgent(id, message) {
2686
+ // ── Keeper command interception ─────────────────────────
2687
+ const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
2688
+ if (keeperCmd) {
2689
+ const handled = await get()._handleKeeperCommand(id, message, (keeperCmd[1] || keeperCmd[2]).toLowerCase());
2690
+ if (handled === true) return { status: 'keeper_handled' };
2691
+ if (handled?.passthrough) {
2692
+ message = handled.passthrough;
2693
+ }
2694
+ }
2695
+
2490
2696
  get().addChatMessage(id, 'user', message, false);
2491
2697
  set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
2492
2698
 
@@ -3022,10 +3228,27 @@ export const useGrooveStore = create((set, get) => ({
3022
3228
  set({ editorQuickSearchOpen: open });
3023
3229
  },
3024
3230
 
3231
+ attachSnippet(snippet) {
3232
+ set({ editorPendingSnippet: snippet });
3233
+ if (!get().editorAiPanelOpen) {
3234
+ set({ editorAiPanelOpen: true });
3235
+ }
3236
+ },
3237
+
3238
+ clearSnippet() {
3239
+ set({ editorPendingSnippet: null });
3240
+ },
3241
+
3025
3242
  async sendCodeToAgent(agentId, instruction, filePath, lineStart, lineEnd, selectedCode) {
3026
3243
  if (!agentId) return;
3027
- const message = `Instruction: ${instruction}\nFile: ${filePath}\nLines ${lineStart}-${lineEnd}:\n\`\`\`\n${selectedCode}\n\`\`\``;
3028
- await get().instructAgent(agentId, message);
3244
+ get().attachSnippet({
3245
+ type: 'code',
3246
+ instruction,
3247
+ filePath,
3248
+ lineStart,
3249
+ lineEnd,
3250
+ code: selectedCode,
3251
+ });
3029
3252
  },
3030
3253
 
3031
3254
  async fetchGitStatus() {
@@ -3640,11 +3863,17 @@ export const useGrooveStore = create((set, get) => ({
3640
3863
  const payload = line.slice(6);
3641
3864
  if (payload === '[DONE]') continue;
3642
3865
  try {
3643
- const chunk = JSON.parse(payload);
3644
- if (chunk.type === 'reasoning' && chunk.content) {
3866
+ const parsed = JSON.parse(payload);
3867
+
3868
+ // Support both raw OpenAI format (piped) and legacy wrapper format
3869
+ const delta = parsed.choices?.[0]?.delta;
3870
+ const reasoningText = delta?.reasoning_content || (parsed.type === 'reasoning' ? parsed.content : null);
3871
+ const contentText = delta?.content || (parsed.type === 'token' ? parsed.content : null);
3872
+
3873
+ if (reasoningText) {
3645
3874
  if (!firstTokenTime) firstTokenTime = performance.now();
3646
3875
  tokenCount++;
3647
- fullReasoning += chunk.content;
3876
+ fullReasoning += reasoningText;
3648
3877
  set((s) => {
3649
3878
  const sessions = s.labSessions.map((sess) => {
3650
3879
  if (sess.id !== sessionId) return sess;
@@ -3655,10 +3884,10 @@ export const useGrooveStore = create((set, get) => ({
3655
3884
  return { labSessions: sessions };
3656
3885
  });
3657
3886
  }
3658
- if (chunk.type === 'token' && chunk.content) {
3887
+ if (contentText) {
3659
3888
  if (!firstTokenTime) firstTokenTime = performance.now();
3660
3889
  tokenCount++;
3661
- fullContent += chunk.content;
3890
+ fullContent += contentText;
3662
3891
  set((s) => {
3663
3892
  const sessions = s.labSessions.map((sess) => {
3664
3893
  if (sess.id !== sessionId) return sess;
@@ -3669,11 +3898,13 @@ export const useGrooveStore = create((set, get) => ({
3669
3898
  return { labSessions: sessions };
3670
3899
  });
3671
3900
  }
3672
- if (chunk.type === 'done' && chunk.metrics) {
3901
+
3902
+ // Handle done event (legacy wrapper) or finish_reason (raw OpenAI)
3903
+ if (parsed.type === 'done' && parsed.metrics) {
3673
3904
  const elapsed = performance.now() - startTime;
3674
3905
  const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3675
3906
  const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3676
- const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...chunk.metrics };
3907
+ const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...parsed.metrics };
3677
3908
 
3678
3909
  set((s) => {
3679
3910
  const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
@@ -3687,15 +3918,15 @@ export const useGrooveStore = create((set, get) => ({
3687
3918
  labSessions: sessions,
3688
3919
  labMetrics: {
3689
3920
  ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist,
3690
- memory: chunk.metrics.memoryUsage || s.labMetrics.memory,
3691
- totalTokens: s.labMetrics.totalTokens + (chunk.metrics.totalTokens || tokenCount),
3692
- generationTime: chunk.metrics.generationTime || elapsed,
3921
+ memory: parsed.metrics.memoryUsage || s.labMetrics.memory,
3922
+ totalTokens: s.labMetrics.totalTokens + (parsed.metrics.totalTokens || tokenCount),
3923
+ generationTime: parsed.metrics.generationTime || elapsed,
3693
3924
  },
3694
3925
  };
3695
3926
  });
3696
3927
  }
3697
- if (chunk.type === 'error') {
3698
- throw new Error(chunk.error || 'Inference error');
3928
+ if (parsed.type === 'error') {
3929
+ throw new Error(parsed.error || 'Inference error');
3699
3930
  }
3700
3931
  } catch (e) {
3701
3932
  if (e.message && e.message !== 'Inference error' && !e.message.startsWith('HTTP ')) continue;
@@ -3704,14 +3935,24 @@ export const useGrooveStore = create((set, get) => ({
3704
3935
  }
3705
3936
  }
3706
3937
 
3707
- // Final metrics if no done event came
3708
- if (!get().labSessions.find((s) => s.id === sessionId)?.messages.slice(-1)[0]?.metrics) {
3709
- const elapsed = performance.now() - startTime;
3710
- const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3711
- const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3938
+ // Compute final metrics from client-side timing
3939
+ const elapsed = performance.now() - startTime;
3940
+ const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3941
+ const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3942
+ if (tokenCount > 0) {
3712
3943
  set((s) => {
3713
3944
  const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
3945
+ const sessions = s.labSessions.map((sess) => {
3946
+ if (sess.id !== sessionId) return sess;
3947
+ const msgs = [...sess.messages];
3948
+ const last = msgs[msgs.length - 1];
3949
+ if (!last?.metrics) {
3950
+ msgs[msgs.length - 1] = { ...last, metrics: { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed } };
3951
+ }
3952
+ return { ...sess, messages: msgs };
3953
+ });
3714
3954
  return {
3955
+ labSessions: sessions,
3715
3956
  labMetrics: { ...s.labMetrics, ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist, totalTokens: s.labMetrics.totalTokens + tokenCount, generationTime: elapsed },
3716
3957
  };
3717
3958
  });
@@ -3836,4 +4077,45 @@ export const useGrooveStore = create((set, get) => ({
3836
4077
  return false;
3837
4078
  }
3838
4079
  },
4080
+
4081
+ // ── Integration Agent Install ────────────────────────────
4082
+
4083
+ async installViaExistingAgent(integration, agentId) {
4084
+ const message = buildIntegrationPrompt(integration);
4085
+ await get().instructAgent(agentId, message);
4086
+ get().setActiveView('agents');
4087
+ get().selectAgent(agentId);
4088
+ },
4089
+
4090
+ async spawnIntegrationTeam(integration) {
4091
+ const team = await get().createTeam(integration.name);
4092
+ const prompt = buildIntegrationPrompt(integration);
4093
+ const agent = await get().spawnAgent({ role: 'planner', prompt, teamId: team.id });
4094
+ get().setActiveView('agents');
4095
+ get().selectAgent(agent.id);
4096
+ return agent;
4097
+ },
3839
4098
  }));
4099
+
4100
+ function buildIntegrationPrompt(integration) {
4101
+ const lines = [
4102
+ `Set up the "${integration.name}" integration for this project.`,
4103
+ '',
4104
+ ];
4105
+ if (integration.description) lines.push(`**Description:** ${integration.description}`);
4106
+ if (integration.npmPackage) lines.push(`**npm package:** ${integration.npmPackage}`);
4107
+ if (integration.authType) lines.push(`**Auth type:** ${integration.authType}`);
4108
+ if (integration.envKeys?.length) {
4109
+ lines.push('', '**Environment keys required:**');
4110
+ for (const k of integration.envKeys) {
4111
+ lines.push(`- \`${k.key}\` — ${k.label}${k.required ? ' (required)' : ''}`);
4112
+ }
4113
+ }
4114
+ if (integration.setupSteps?.length) {
4115
+ lines.push('', '**Setup steps:**');
4116
+ integration.setupSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
4117
+ }
4118
+ if (integration.setupUrl) lines.push(``, `**Setup URL:** ${integration.setupUrl}`);
4119
+ if (integration.agentInstructions) lines.push('', `**Agent instructions:** ${integration.agentInstructions}`);
4120
+ return lines.join('\n');
4121
+ }