groove-dev 0.27.140 → 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 (64) 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 +82 -16
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  7. package/node_modules/@groove-dev/daemon/src/keeper.js +3 -3
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  9. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  10. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -3
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +7 -2
  17. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  18. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  19. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  20. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  21. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  22. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  23. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  24. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +110 -32
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  27. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  28. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  29. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/integrations-registry.json +12 -44
  33. package/packages/daemon/package.json +1 -1
  34. package/packages/daemon/src/api.js +82 -16
  35. package/packages/daemon/src/integrations.js +10 -0
  36. package/packages/daemon/src/journalist.js +169 -0
  37. package/packages/daemon/src/keeper.js +3 -3
  38. package/packages/daemon/src/model-lab.js +11 -0
  39. package/packages/daemon/src/process.js +76 -0
  40. package/packages/daemon/src/validate.js +8 -0
  41. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  42. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  43. package/packages/gui/dist/index.html +2 -2
  44. package/packages/gui/package.json +1 -1
  45. package/packages/gui/src/components/agents/agent-chat.jsx +3 -3
  46. package/packages/gui/src/components/agents/agent-file-tree.jsx +7 -2
  47. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  48. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  49. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  50. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  51. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  52. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  53. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  54. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  55. package/packages/gui/src/stores/groove.js +110 -32
  56. package/packages/gui/src/views/agents.jsx +114 -56
  57. package/packages/gui/src/views/memory.jsx +9 -9
  58. package/packages/gui/src/views/model-lab.jsx +1 -6
  59. package/packages/gui/src/views/models.jsx +658 -565
  60. package/plan_files/keeper-manual.md +53 -42
  61. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  62. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  63. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  64. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
@@ -4,18 +4,18 @@ import { useState, useEffect, useRef } from 'react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { Dialog, DialogContent } from '../ui/dialog';
6
6
  import { Button } from '../ui/button';
7
- import { Save, HelpCircle, Sparkles, Link2, FileText } from 'lucide-react';
7
+ import { Save } from 'lucide-react';
8
8
 
9
9
  const COMMANDS = [
10
- { cmd: '[save]', args: '#tag', desc: 'Save the current message as a tagged memory' },
11
- { cmd: '[append]', args: '#tag', desc: 'Add to an existing memory without overwriting' },
12
- { cmd: '[update]', args: '#tag', desc: 'Open the editor to modify a memory in place' },
13
- { cmd: '[delete]', args: '#tag', desc: 'Remove a memory permanently' },
14
- { cmd: '[view]', args: '#tag', desc: 'Read a memory in the viewer' },
15
- { cmd: '[read]', args: '#tag1 #tag2 ...', desc: 'Send memory content to the agent — agent reads it, chat stays clean' },
16
- { cmd: '[doc]', args: '#tag', desc: 'AI synthesizes the full conversation into a document' },
17
- { cmd: '[link]', args: '#tag path/to/doc', desc: 'Link a memory to a NORTHSTAR or external document' },
18
- { cmd: '[instruct]', args: '', desc: 'Show this command reference' },
10
+ { cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
11
+ { cmd: 'append', args: '#tag', desc: 'Add to an existing memory and send to agent' },
12
+ { cmd: 'update', args: '#tag', desc: 'Open the editor to modify a memory in place' },
13
+ { cmd: 'delete', args: '#tag', desc: 'Remove a memory permanently' },
14
+ { cmd: 'view', args: '#tag', desc: 'Read a memory in the viewer' },
15
+ { cmd: 'read', args: '#tag1 #tag2 ...', desc: 'Send memory content to the agent — chat stays clean' },
16
+ { cmd: 'doc', args: '#tag', desc: 'AI synthesizes the full conversation into a document' },
17
+ { cmd: 'link', args: '#tag path/to/doc', desc: 'Link a memory to a NORTHSTAR or external document' },
18
+ { cmd: '[instruct]', args: '', desc: 'Show this command reference' },
19
19
  ];
20
20
 
21
21
  function KeeperEditorModal() {
@@ -1,7 +1,87 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useRef, useCallback, useState } from 'react';
3
- import { Maximize2, Minimize2, Plus, X, Terminal } from 'lucide-react';
2
+ import { useRef, useCallback, useState, useEffect } from 'react';
3
+ import { Maximize2, Minimize2, Plus, X, Terminal, Send, ChevronDown, Sparkles } from 'lucide-react';
4
4
  import { cn } from '../../lib/cn';
5
+ import { useGrooveStore } from '../../stores/groove';
6
+ import { Tooltip } from '../ui/tooltip';
7
+
8
+ function AgentPicker({ onSelect, onClose }) {
9
+ const ref = useRef(null);
10
+ const agents = useGrooveStore((s) => s.agents);
11
+ const teams = useGrooveStore((s) => s.teams);
12
+
13
+ useEffect(() => {
14
+ function handleClick(e) {
15
+ if (ref.current && !ref.current.contains(e.target)) onClose();
16
+ }
17
+ function handleKey(e) {
18
+ if (e.key === 'Escape') onClose();
19
+ }
20
+ document.addEventListener('mousedown', handleClick);
21
+ document.addEventListener('keydown', handleKey);
22
+ return () => {
23
+ document.removeEventListener('mousedown', handleClick);
24
+ document.removeEventListener('keydown', handleKey);
25
+ };
26
+ }, [onClose]);
27
+
28
+ const running = agents.filter((a) => a.status === 'running' || a.status === 'starting');
29
+ const stopped = agents.filter((a) => a.status !== 'running' && a.status !== 'starting');
30
+
31
+ function teamName(teamId) {
32
+ const team = teams.find((t) => t.id === teamId);
33
+ return team?.name || 'Default';
34
+ }
35
+
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ className="absolute bottom-full right-0 mb-1 z-50 min-w-[220px] max-h-[300px] overflow-y-auto py-1 bg-surface-2 border border-border rounded-lg shadow-xl"
40
+ >
41
+ {running.length > 0 && (
42
+ <>
43
+ <div className="px-3 py-1 text-2xs text-text-4 font-sans font-medium uppercase tracking-wider">Active</div>
44
+ {running.map((agent) => (
45
+ <button
46
+ key={agent.id}
47
+ onClick={() => onSelect(agent.id)}
48
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-5 cursor-pointer transition-colors text-left"
49
+ >
50
+ <span className="w-1.5 h-1.5 rounded-full bg-success flex-shrink-0" />
51
+ <span className="truncate flex-1">{agent.name}</span>
52
+ <span className="text-2xs text-text-4">{teamName(agent.teamId)}</span>
53
+ </button>
54
+ ))}
55
+ </>
56
+ )}
57
+ {stopped.length > 0 && (
58
+ <>
59
+ <div className="px-3 py-1 text-2xs text-text-4 font-sans font-medium uppercase tracking-wider">
60
+ {running.length > 0 ? 'Other' : 'Agents'}
61
+ </div>
62
+ {stopped.slice(0, 10).map((agent) => (
63
+ <button
64
+ key={agent.id}
65
+ onClick={() => onSelect(agent.id)}
66
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs font-sans text-text-2 hover:bg-surface-5 cursor-pointer transition-colors text-left"
67
+ >
68
+ <span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0',
69
+ agent.status === 'completed' ? 'bg-accent' : agent.status === 'crashed' ? 'bg-danger' : 'bg-text-4',
70
+ )} />
71
+ <span className="truncate flex-1">{agent.name}</span>
72
+ <span className="text-2xs text-text-4">{teamName(agent.teamId)}</span>
73
+ </button>
74
+ ))}
75
+ </>
76
+ )}
77
+ {agents.length === 0 && (
78
+ <div className="px-3 py-3 text-xs text-text-4 font-sans text-center">
79
+ No agents available
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }
5
85
 
6
86
  export function TerminalPanel({
7
87
  children,
@@ -18,13 +98,22 @@ export function TerminalPanel({
18
98
  onMinimize,
19
99
  onClose,
20
100
  onRenameTab,
101
+ selectedText,
21
102
  }) {
22
103
  const dragging = useRef(false);
23
104
  const [renamingId, setRenamingId] = useState(null);
24
105
  const [renameValue, setRenameValue] = useState('');
106
+ const [showPicker, setShowPicker] = useState(false);
25
107
  const startY = useRef(0);
26
108
  const startH = useRef(0);
27
109
 
110
+ const activeAgent = useGrooveStore((s) => s.editorSelectedAgent);
111
+ const agents = useGrooveStore((s) => s.agents);
112
+ const attachSnippet = useGrooveStore((s) => s.attachSnippet);
113
+
114
+ const agent = agents.find((a) => a.id === activeAgent);
115
+ const hasSelection = selectedText && selectedText.trim().length > 0;
116
+
28
117
  const onMouseDown = useCallback((e) => {
29
118
  if (fullHeight) return;
30
119
  e.preventDefault();
@@ -49,6 +138,21 @@ export function TerminalPanel({
49
138
  document.addEventListener('mouseup', onMouseUp);
50
139
  }, [height, onHeightChange, fullHeight]);
51
140
 
141
+ function sendToAgent(agentId) {
142
+ if (!agentId || !selectedText?.trim()) return;
143
+ setShowPicker(false);
144
+ useGrooveStore.setState({ editorSelectedAgent: agentId });
145
+ attachSnippet({ type: 'terminal', code: selectedText.trim() });
146
+ }
147
+
148
+ function handleSendClick() {
149
+ if (activeAgent) {
150
+ sendToAgent(activeAgent);
151
+ } else {
152
+ setShowPicker(true);
153
+ }
154
+ }
155
+
52
156
  const tabList = tabs || [{ id: 'default', label: 'Terminal' }];
53
157
 
54
158
  return (
@@ -116,8 +220,53 @@ export function TerminalPanel({
116
220
  </button>
117
221
  </div>
118
222
 
119
- {/* Window controls */}
223
+ {/* Send to Agent + Window controls */}
120
224
  <div className="flex items-center gap-0.5 flex-shrink-0 ml-2">
225
+ {/* Send to Agent */}
226
+ {hasSelection && (
227
+ <div className="relative flex items-center">
228
+ <Tooltip content={activeAgent ? `Send to ${agent?.name || 'agent'}` : 'Send to agent'} side="top">
229
+ <button
230
+ onClick={handleSendClick}
231
+ disabled={sending}
232
+ className={cn(
233
+ 'flex items-center gap-1.5 px-2 py-1 rounded text-xs font-sans cursor-pointer transition-colors mr-1',
234
+ 'bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-50',
235
+ )}
236
+ >
237
+ {activeAgent ? (
238
+ <>
239
+ <Send size={11} />
240
+ <span className="text-2xs max-w-[80px] truncate">{agent?.name || 'Agent'}</span>
241
+ </>
242
+ ) : (
243
+ <>
244
+ <Sparkles size={11} />
245
+ <span className="text-2xs">Agent</span>
246
+ <ChevronDown size={9} />
247
+ </>
248
+ )}
249
+ </button>
250
+ </Tooltip>
251
+ {!activeAgent && hasSelection && (
252
+ <Tooltip content="Pick agent" side="top">
253
+ <button
254
+ onClick={() => setShowPicker(!showPicker)}
255
+ className="p-1 rounded text-text-3 hover:text-accent hover:bg-accent/10 cursor-pointer transition-colors mr-1"
256
+ >
257
+ <ChevronDown size={10} />
258
+ </button>
259
+ </Tooltip>
260
+ )}
261
+ {showPicker && (
262
+ <AgentPicker
263
+ onSelect={(id) => sendToAgent(id)}
264
+ onClose={() => setShowPicker(false)}
265
+ />
266
+ )}
267
+ </div>
268
+ )}
269
+
121
270
  {fullHeight ? (
122
271
  <button
123
272
  onClick={onMinimize}
@@ -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}