groove-dev 0.27.140 → 0.27.142

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 (98) 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 +100 -23
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +171 -1
  8. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  9. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +65 -0
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  13. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  15. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +12 -8
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  24. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  25. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +109 -12
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  28. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  29. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  30. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  31. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  32. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  33. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  34. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +151 -3
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  36. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -29
  37. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  38. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  39. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  40. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  41. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  42. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  43. package/package.json +1 -1
  44. package/packages/cli/package.json +1 -1
  45. package/packages/daemon/integrations-registry.json +12 -44
  46. package/packages/daemon/package.json +1 -1
  47. package/packages/daemon/src/api.js +100 -23
  48. package/packages/daemon/src/integrations.js +10 -0
  49. package/packages/daemon/src/introducer.js +1 -1
  50. package/packages/daemon/src/journalist.js +171 -1
  51. package/packages/daemon/src/keeper.js +2 -2
  52. package/packages/daemon/src/memory.js +8 -5
  53. package/packages/daemon/src/model-lab.js +11 -0
  54. package/packages/daemon/src/process.js +65 -0
  55. package/packages/daemon/src/rotator.js +25 -8
  56. package/packages/daemon/src/validate.js +8 -0
  57. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  58. package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
  59. package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
  60. package/packages/gui/dist/index.html +3 -3
  61. package/packages/gui/package.json +1 -1
  62. package/packages/gui/src/app.jsx +0 -2
  63. package/packages/gui/src/components/agents/agent-chat.jsx +3 -4
  64. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  65. package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -8
  66. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  67. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  68. package/packages/gui/src/components/agents/workspace-mode.jsx +109 -12
  69. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  70. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  71. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  72. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  73. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  74. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  75. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  76. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  77. package/packages/gui/src/components/layout/terminal-panel.jsx +151 -3
  78. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  79. package/packages/gui/src/stores/groove.js +107 -29
  80. package/packages/gui/src/views/agents.jsx +114 -56
  81. package/packages/gui/src/views/dashboard.jsx +2 -0
  82. package/packages/gui/src/views/marketplace.jsx +3 -71
  83. package/packages/gui/src/views/memory.jsx +9 -9
  84. package/packages/gui/src/views/model-lab.jsx +1 -6
  85. package/packages/gui/src/views/models.jsx +658 -565
  86. package/plan_files/keeper-manual.md +53 -42
  87. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  88. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  89. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  90. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  91. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  92. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  93. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  94. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  95. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  96. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  97. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  98. package/packages/gui/src/views/toys.jsx +0 -162
@@ -4,31 +4,43 @@ import { Dialog, DialogContent } from '../ui/dialog';
4
4
  import { Button } from '../ui/button';
5
5
  import { Input } from '../ui/input';
6
6
  import { Badge } from '../ui/badge';
7
+ import { ScrollArea } from '../ui/scroll-area';
7
8
  import { api } from '../../lib/api';
8
9
  import { useToast } from '../../lib/hooks/use-toast';
10
+ import { useGrooveStore } from '../../stores/groove';
9
11
  import { integrationOAuth } from '../../lib/electron';
10
12
  import {
11
13
  Check, CheckCircle, ExternalLink, Loader2, Eye, EyeOff,
12
- Key, Shield, Trash2, ChevronRight, X, Copy, RefreshCw,
14
+ Key, Shield, Trash2, ChevronRight, Copy, RefreshCw,
15
+ Users, Rocket, Bot,
13
16
  } from 'lucide-react';
14
17
 
15
18
  import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
16
19
 
20
+ const ICON_PALETTES = [
21
+ 'bg-accent/15 text-accent',
22
+ 'bg-purple/15 text-purple',
23
+ 'bg-success/15 text-success',
24
+ 'bg-warning/15 text-warning',
25
+ 'bg-danger/15 text-danger',
26
+ 'bg-info/15 text-info',
27
+ ];
28
+
17
29
  function IntegrationIcon({ item, size = 48 }) {
18
30
  const logoUrl = INTEGRATION_LOGOS[item.id];
19
31
  if (logoUrl) {
20
32
  return (
21
33
  <div className="rounded-lg bg-surface-4 flex items-center justify-center flex-shrink-0 overflow-hidden" style={{ width: size, height: size }}>
22
- <img src={logoUrl} alt={item.name} className="w-6 h-6" onError={(e) => { e.target.style.display = 'none'; }} />
34
+ <img src={logoUrl} alt={item.name} className="w-6 h-6" onError={(e) => { e.target.classList.add('hidden'); }} />
23
35
  </div>
24
36
  );
25
37
  }
26
38
  const initial = (item.name || '?')[0].toUpperCase();
27
- const hue = item.name ? item.name.charCodeAt(0) * 37 % 360 : 200;
39
+ const palette = ICON_PALETTES[(item.name || '').charCodeAt(0) % ICON_PALETTES.length];
28
40
  return (
29
41
  <div
30
- className="rounded-lg flex items-center justify-center flex-shrink-0 text-xl font-bold font-sans"
31
- style={{ width: size, height: size, background: `hsl(${hue}, 40%, 18%)`, color: `hsl(${hue}, 60%, 65%)` }}
42
+ className={`rounded-lg flex items-center justify-center flex-shrink-0 text-xl font-bold font-sans ${palette}`}
43
+ style={{ width: size, height: size }}
32
44
  >
33
45
  {initial}
34
46
  </div>
@@ -219,6 +231,199 @@ function OverviewStep({ item, status, installing, onInstall, onUninstall, onNext
219
231
  );
220
232
  }
221
233
 
234
+ // ── Step: Agent Setup ──────────────────────────────────
235
+ function AgentSetupStep({ item, onClose }) {
236
+ const agents = useGrooveStore((s) => s.agents);
237
+ const teams = useGrooveStore((s) => s.teams);
238
+ const installViaExistingAgent = useGrooveStore((s) => s.installViaExistingAgent);
239
+ const spawnIntegrationTeam = useGrooveStore((s) => s.spawnIntegrationTeam);
240
+ const [mode, setMode] = useState(null); // null | 'existing' | 'spawn'
241
+ const [spawning, setSpawning] = useState(false);
242
+ const [selectedAgentId, setSelectedAgentId] = useState(null);
243
+
244
+ const runningAgents = agents.filter((a) => a.status === 'running' || a.status === 'idle');
245
+
246
+ const agentsByTeam = {};
247
+ for (const agent of runningAgents) {
248
+ const teamId = agent.teamId || '_none';
249
+ if (!agentsByTeam[teamId]) agentsByTeam[teamId] = [];
250
+ agentsByTeam[teamId].push(agent);
251
+ }
252
+
253
+ const teamMap = {};
254
+ for (const t of teams) teamMap[t.id] = t.name;
255
+
256
+ async function handleExistingAgent() {
257
+ if (!selectedAgentId) return;
258
+ await installViaExistingAgent(item, selectedAgentId);
259
+ onClose();
260
+ }
261
+
262
+ async function handleSpawnNew() {
263
+ setSpawning(true);
264
+ try {
265
+ await spawnIntegrationTeam(item);
266
+ onClose();
267
+ } catch {
268
+ setSpawning(false);
269
+ }
270
+ }
271
+
272
+ // Option picker when no mode selected
273
+ if (!mode) {
274
+ return (
275
+ <div className="px-5 py-5 space-y-5">
276
+ <div className="flex items-start gap-4">
277
+ <IntegrationIcon item={item} size={52} />
278
+ <div className="flex-1 min-w-0">
279
+ <h2 className="text-base font-bold text-text-0 font-sans">Install {item.name}</h2>
280
+ <p className="text-xs text-text-3 font-sans mt-0.5">
281
+ Choose how to set up this integration
282
+ </p>
283
+ </div>
284
+ </div>
285
+
286
+ <div className="h-px bg-border-subtle" />
287
+
288
+ <div className="space-y-2.5">
289
+ <button
290
+ onClick={() => runningAgents.length > 0 ? setMode('existing') : null}
291
+ disabled={runningAgents.length === 0}
292
+ className="w-full text-left px-4 py-3.5 rounded-lg border border-border-subtle bg-surface-2 hover:bg-surface-3 hover:border-accent/30 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed group"
293
+ >
294
+ <div className="flex items-center gap-3">
295
+ <div className="w-9 h-9 rounded-lg bg-accent/10 flex items-center justify-center flex-shrink-0 group-hover:bg-accent/15 transition-colors">
296
+ <Users size={18} className="text-accent" />
297
+ </div>
298
+ <div className="flex-1 min-w-0">
299
+ <div className="text-sm font-semibold text-text-0 font-sans">Use Existing Agent</div>
300
+ <div className="text-2xs text-text-3 font-sans mt-0.5">
301
+ {runningAgents.length > 0
302
+ ? `Send setup instructions to one of ${runningAgents.length} running agent${runningAgents.length !== 1 ? 's' : ''}`
303
+ : 'No agents running — spawn one first'}
304
+ </div>
305
+ </div>
306
+ {runningAgents.length > 0 && <ChevronRight size={14} className="text-text-4 group-hover:text-accent transition-colors" />}
307
+ </div>
308
+ </button>
309
+
310
+ <button
311
+ onClick={() => setMode('spawn')}
312
+ className="w-full text-left px-4 py-3.5 rounded-lg border border-border-subtle bg-surface-2 hover:bg-surface-3 hover:border-accent/30 transition-all cursor-pointer group"
313
+ >
314
+ <div className="flex items-center gap-3">
315
+ <div className="w-9 h-9 rounded-lg bg-purple/10 flex items-center justify-center flex-shrink-0 group-hover:bg-purple/15 transition-colors">
316
+ <Rocket size={18} className="text-purple" />
317
+ </div>
318
+ <div className="flex-1 min-w-0">
319
+ <div className="text-sm font-semibold text-text-0 font-sans">Spawn New Agent</div>
320
+ <div className="text-2xs text-text-3 font-sans mt-0.5">
321
+ Create a dedicated team and planner for this integration
322
+ </div>
323
+ </div>
324
+ <ChevronRight size={14} className="text-text-4 group-hover:text-accent transition-colors" />
325
+ </div>
326
+ </button>
327
+ </div>
328
+ </div>
329
+ );
330
+ }
331
+
332
+ // Spawn new agent confirmation
333
+ if (mode === 'spawn') {
334
+ return (
335
+ <div className="px-5 py-5 space-y-5">
336
+ <div className="flex items-center gap-3">
337
+ <IntegrationIcon item={item} size={36} />
338
+ <div>
339
+ <h2 className="text-sm font-bold text-text-0 font-sans">Spawn Integration Agent</h2>
340
+ <p className="text-2xs text-text-3 font-sans">Creates a team and planner for {item.name}</p>
341
+ </div>
342
+ </div>
343
+
344
+ <div className="bg-surface-2 rounded-md px-4 py-3 space-y-2">
345
+ <span className="text-xs font-semibold text-text-1 font-sans">What happens next</span>
346
+ <ol className="space-y-1.5">
347
+ {[
348
+ `A new team "${item.name}" will be created`,
349
+ 'A planner agent will spawn with full integration context',
350
+ 'The agent will handle installation and configuration',
351
+ ].map((step, i) => (
352
+ <li key={i} className="flex gap-2 text-xs text-text-2 font-sans leading-relaxed">
353
+ <span className="text-accent font-mono flex-shrink-0 w-4 text-right">{i + 1}.</span>
354
+ <span>{step}</span>
355
+ </li>
356
+ ))}
357
+ </ol>
358
+ </div>
359
+
360
+ <div className="flex gap-2">
361
+ <Button variant="secondary" size="lg" onClick={() => setMode(null)} className="flex-1" disabled={spawning}>
362
+ Back
363
+ </Button>
364
+ <Button variant="primary" size="lg" onClick={handleSpawnNew} disabled={spawning} className="flex-1 gap-2">
365
+ {spawning ? <><Loader2 size={14} className="animate-spin" /> Spawning...</> : <><Rocket size={14} /> Spawn Agent</>}
366
+ </Button>
367
+ </div>
368
+ </div>
369
+ );
370
+ }
371
+
372
+ // Pick existing agent
373
+ return (
374
+ <div className="px-5 py-5 space-y-4">
375
+ <div className="flex items-center gap-3">
376
+ <IntegrationIcon item={item} size={36} />
377
+ <div>
378
+ <h2 className="text-sm font-bold text-text-0 font-sans">Choose an Agent</h2>
379
+ <p className="text-2xs text-text-3 font-sans">Send {item.name} setup instructions to a running agent</p>
380
+ </div>
381
+ </div>
382
+
383
+ <ScrollArea className="max-h-64">
384
+ <div className="space-y-3">
385
+ {Object.entries(agentsByTeam).map(([teamId, teamAgents]) => (
386
+ <div key={teamId}>
387
+ <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5 px-1">
388
+ {teamMap[teamId] || 'Unassigned'}
389
+ </div>
390
+ <div className="space-y-1">
391
+ {teamAgents.map((agent) => (
392
+ <button
393
+ key={agent.id}
394
+ onClick={() => setSelectedAgentId(agent.id)}
395
+ className={`w-full text-left px-3 py-2.5 rounded-md border transition-all cursor-pointer flex items-center gap-3 ${
396
+ selectedAgentId === agent.id
397
+ ? 'border-accent bg-accent/8'
398
+ : 'border-border-subtle bg-surface-2 hover:bg-surface-3 hover:border-border'
399
+ }`}
400
+ >
401
+ <Bot size={14} className={selectedAgentId === agent.id ? 'text-accent' : 'text-text-4'} />
402
+ <div className="flex-1 min-w-0">
403
+ <div className="text-xs font-semibold text-text-0 font-sans truncate">{agent.name || agent.id}</div>
404
+ <div className="text-2xs text-text-3 font-sans">{agent.role}</div>
405
+ </div>
406
+ {selectedAgentId === agent.id && <Check size={14} className="text-accent flex-shrink-0" />}
407
+ </button>
408
+ ))}
409
+ </div>
410
+ </div>
411
+ ))}
412
+ </div>
413
+ </ScrollArea>
414
+
415
+ <div className="flex gap-2">
416
+ <Button variant="secondary" size="lg" onClick={() => { setMode(null); setSelectedAgentId(null); }} className="flex-1">
417
+ Back
418
+ </Button>
419
+ <Button variant="primary" size="lg" onClick={handleExistingAgent} disabled={!selectedAgentId} className="flex-1 gap-2">
420
+ <Bot size={14} /> Send Instructions
421
+ </Button>
422
+ </div>
423
+ </div>
424
+ );
425
+ }
426
+
222
427
  // ── Which APIs each Google integration needs ───────────
223
428
  const GOOGLE_API_NAMES = {
224
429
  gmail: 'Gmail API',
@@ -569,7 +774,7 @@ function DoneStep({ item, onClose }) {
569
774
  // ── Main Wizard ─────────────────────────────────────────
570
775
  export function IntegrationWizard({ integration, open, onClose }) {
571
776
  const toast = useToast();
572
- const [step, setStep] = useState('overview'); // overview | configure | done
777
+ const [step, setStep] = useState('overview'); // overview | agent-setup | configure | done
573
778
  const [status, setStatus] = useState(null);
574
779
  const [installing, setInstalling] = useState(false);
575
780
  const [loadingStatus, setLoadingStatus] = useState(true);
@@ -595,17 +800,8 @@ export function IntegrationWizard({ integration, open, onClose }) {
595
800
  }
596
801
  }, [open, integration, fetchStatus]);
597
802
 
598
- async function handleInstall() {
599
- setInstalling(true);
600
- try {
601
- await api.post(`/integrations/${integration.id}/install`);
602
- toast.success(`${integration.name} installed`);
603
- await fetchStatus();
604
- setStep('configure');
605
- } catch (err) {
606
- toast.error('Install failed', err.message);
607
- }
608
- setInstalling(false);
803
+ function handleInstall() {
804
+ setStep('agent-setup');
609
805
  }
610
806
 
611
807
  async function handleUninstall() {
@@ -628,10 +824,17 @@ export function IntegrationWizard({ integration, open, onClose }) {
628
824
 
629
825
  if (!integration) return null;
630
826
 
827
+ const stepTitle = {
828
+ overview: integration.name,
829
+ 'agent-setup': 'Install',
830
+ configure: 'Configure',
831
+ done: 'Complete',
832
+ };
833
+
631
834
  return (
632
835
  <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
633
836
  <DialogContent
634
- title={step === 'overview' ? integration.name : step === 'configure' ? 'Configure' : 'Complete'}
837
+ title={stepTitle[step] || integration.name}
635
838
  description={`Setup wizard for ${integration.name}`}
636
839
  className="max-w-md"
637
840
  >
@@ -648,6 +851,8 @@ export function IntegrationWizard({ integration, open, onClose }) {
648
851
  onUninstall={handleUninstall}
649
852
  onNext={handleConfigureNext}
650
853
  />
854
+ ) : step === 'agent-setup' ? (
855
+ <AgentSetupStep item={integration} onClose={onClose} />
651
856
  ) : step === 'configure' ? (
652
857
  <ConfigureStep
653
858
  item={integration}
@@ -213,6 +213,7 @@ export const useGrooveStore = create((set, get) => ({
213
213
 
214
214
  // ── Editor (Cursor-style) ────────────────────────────────
215
215
  editorSelectedAgent: null,
216
+ editorPendingSnippet: null,
216
217
  editorViewMode: 'code',
217
218
  editorAiPanelOpen: false,
218
219
  editorAiPanelWidth: Number(localStorage.getItem('groove:editorAiPanelWidth')) || 360,
@@ -665,6 +666,7 @@ export const useGrooveStore = create((set, get) => ({
665
666
  case 'lab:runtime:added':
666
667
  case 'lab:runtime:updated':
667
668
  case 'lab:runtime:removed':
669
+ case 'llama:server:stopped':
668
670
  get().fetchLabRuntimes();
669
671
  break;
670
672
 
@@ -1335,23 +1337,21 @@ export const useGrooveStore = create((set, get) => ({
1335
1337
  }
1336
1338
 
1337
1339
  case 'save': {
1338
- if (tags.length === 0) { addSystemMsg('Usage: [save] #tag your message here'); return true; }
1340
+ if (tags.length === 0) { addSystemMsg('Usage: save #tag your message here'); return true; }
1339
1341
  const content = rest.replace(/#[\w/.-]+/g, '').trim();
1340
- if (!content) { addSystemMsg('Usage: [save] #tag your message here'); return true; }
1341
- get().addChatMessage(agentId, 'user', message, false);
1342
+ if (!content) { addSystemMsg('Usage: save #tag your message here'); return true; }
1342
1343
  await get().saveKeeperItem(tags[0], content);
1343
1344
  addSystemMsg(`Saved to #${tags[0]}`);
1344
- return true;
1345
+ return { passthrough: content };
1345
1346
  }
1346
1347
 
1347
1348
  case 'append': {
1348
- if (tags.length === 0) { addSystemMsg('Usage: [append] #tag content to add'); return true; }
1349
+ if (tags.length === 0) { addSystemMsg('Usage: append #tag content to add'); return true; }
1349
1350
  const content = rest.replace(/#[\w/.-]+/g, '').trim();
1350
- if (!content) { addSystemMsg('Usage: [append] #tag content to add'); return true; }
1351
- get().addChatMessage(agentId, 'user', message, false);
1351
+ if (!content) { addSystemMsg('Usage: append #tag content to add'); return true; }
1352
1352
  await get().appendKeeperItem(tags[0], content);
1353
1353
  addSystemMsg(`Appended to #${tags[0]}`);
1354
- return true;
1354
+ return { passthrough: content };
1355
1355
  }
1356
1356
 
1357
1357
  case 'update': {
@@ -2031,7 +2031,6 @@ export const useGrooveStore = create((set, get) => ({
2031
2031
  ...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
2032
2032
  ...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
2033
2033
  ...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
2034
- ...(tlc?.mode && { mode: tlc.mode }),
2035
2034
  };
2036
2035
  const result = await api.post('/recommended-team/launch', body);
2037
2036
  const totalOk = (result.launched || 0) + (result.reused || 0);
@@ -2688,7 +2687,10 @@ export const useGrooveStore = create((set, get) => ({
2688
2687
  const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
2689
2688
  if (keeperCmd) {
2690
2689
  const handled = await get()._handleKeeperCommand(id, message, keeperCmd[1].toLowerCase());
2691
- if (handled) return { status: 'keeper_handled' };
2690
+ if (handled === true) return { status: 'keeper_handled' };
2691
+ if (handled?.passthrough) {
2692
+ message = handled.passthrough;
2693
+ }
2692
2694
  }
2693
2695
 
2694
2696
  get().addChatMessage(id, 'user', message, false);
@@ -3226,10 +3228,27 @@ export const useGrooveStore = create((set, get) => ({
3226
3228
  set({ editorQuickSearchOpen: open });
3227
3229
  },
3228
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
+
3229
3242
  async sendCodeToAgent(agentId, instruction, filePath, lineStart, lineEnd, selectedCode) {
3230
3243
  if (!agentId) return;
3231
- const message = `Instruction: ${instruction}\nFile: ${filePath}\nLines ${lineStart}-${lineEnd}:\n\`\`\`\n${selectedCode}\n\`\`\``;
3232
- await get().instructAgent(agentId, message);
3244
+ get().attachSnippet({
3245
+ type: 'code',
3246
+ instruction,
3247
+ filePath,
3248
+ lineStart,
3249
+ lineEnd,
3250
+ code: selectedCode,
3251
+ });
3233
3252
  },
3234
3253
 
3235
3254
  async fetchGitStatus() {
@@ -3844,11 +3863,17 @@ export const useGrooveStore = create((set, get) => ({
3844
3863
  const payload = line.slice(6);
3845
3864
  if (payload === '[DONE]') continue;
3846
3865
  try {
3847
- const chunk = JSON.parse(payload);
3848
- 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) {
3849
3874
  if (!firstTokenTime) firstTokenTime = performance.now();
3850
3875
  tokenCount++;
3851
- fullReasoning += chunk.content;
3876
+ fullReasoning += reasoningText;
3852
3877
  set((s) => {
3853
3878
  const sessions = s.labSessions.map((sess) => {
3854
3879
  if (sess.id !== sessionId) return sess;
@@ -3859,10 +3884,10 @@ export const useGrooveStore = create((set, get) => ({
3859
3884
  return { labSessions: sessions };
3860
3885
  });
3861
3886
  }
3862
- if (chunk.type === 'token' && chunk.content) {
3887
+ if (contentText) {
3863
3888
  if (!firstTokenTime) firstTokenTime = performance.now();
3864
3889
  tokenCount++;
3865
- fullContent += chunk.content;
3890
+ fullContent += contentText;
3866
3891
  set((s) => {
3867
3892
  const sessions = s.labSessions.map((sess) => {
3868
3893
  if (sess.id !== sessionId) return sess;
@@ -3873,11 +3898,13 @@ export const useGrooveStore = create((set, get) => ({
3873
3898
  return { labSessions: sessions };
3874
3899
  });
3875
3900
  }
3876
- 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) {
3877
3904
  const elapsed = performance.now() - startTime;
3878
3905
  const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3879
3906
  const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3880
- const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...chunk.metrics };
3907
+ const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...parsed.metrics };
3881
3908
 
3882
3909
  set((s) => {
3883
3910
  const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
@@ -3891,15 +3918,15 @@ export const useGrooveStore = create((set, get) => ({
3891
3918
  labSessions: sessions,
3892
3919
  labMetrics: {
3893
3920
  ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist,
3894
- memory: chunk.metrics.memoryUsage || s.labMetrics.memory,
3895
- totalTokens: s.labMetrics.totalTokens + (chunk.metrics.totalTokens || tokenCount),
3896
- 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,
3897
3924
  },
3898
3925
  };
3899
3926
  });
3900
3927
  }
3901
- if (chunk.type === 'error') {
3902
- throw new Error(chunk.error || 'Inference error');
3928
+ if (parsed.type === 'error') {
3929
+ throw new Error(parsed.error || 'Inference error');
3903
3930
  }
3904
3931
  } catch (e) {
3905
3932
  if (e.message && e.message !== 'Inference error' && !e.message.startsWith('HTTP ')) continue;
@@ -3908,14 +3935,24 @@ export const useGrooveStore = create((set, get) => ({
3908
3935
  }
3909
3936
  }
3910
3937
 
3911
- // Final metrics if no done event came
3912
- if (!get().labSessions.find((s) => s.id === sessionId)?.messages.slice(-1)[0]?.metrics) {
3913
- const elapsed = performance.now() - startTime;
3914
- const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3915
- 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) {
3916
3943
  set((s) => {
3917
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
+ });
3918
3954
  return {
3955
+ labSessions: sessions,
3919
3956
  labMetrics: { ...s.labMetrics, ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist, totalTokens: s.labMetrics.totalTokens + tokenCount, generationTime: elapsed },
3920
3957
  };
3921
3958
  });
@@ -4040,4 +4077,45 @@ export const useGrooveStore = create((set, get) => ({
4040
4077
  return false;
4041
4078
  }
4042
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
+ },
4043
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
+ }