groove-dev 0.26.33 → 0.26.36

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.
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-CPF9iasK.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-VrNtEof4.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-BnNZzcsd.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-CEFKgLGB.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -134,7 +134,7 @@ function TeamSection({ team, members, rotatingSet }) {
134
134
  : <ChevronRight size={10} className="text-text-4 flex-shrink-0" />
135
135
  }
136
136
  <span className="text-2xs font-mono font-semibold text-text-2 uppercase tracking-widest flex-1 truncate">
137
- {team === 'ungrouped' ? 'Ungrouped' : team}
137
+ {team}
138
138
  </span>
139
139
  <span className="text-2xs font-mono text-text-3 tabular-nums">{fmtNum(totalTokens)}</span>
140
140
  {totalCost > 0 && (
@@ -154,7 +154,7 @@ function TeamSection({ team, members, rotatingSet }) {
154
154
  );
155
155
  }
156
156
 
157
- const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
157
+ const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [], teams: teamList = [] }) {
158
158
  if (!agentBreakdown?.length) {
159
159
  return (
160
160
  <div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
@@ -163,11 +163,14 @@ const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
163
163
  );
164
164
  }
165
165
 
166
- const teams = {};
166
+ const teamNameMap = {};
167
+ for (const t of teamList) teamNameMap[t.id] = t.name;
168
+
169
+ const groups = {};
167
170
  for (const a of agentBreakdown) {
168
171
  const team = a.teamId || 'ungrouped';
169
- if (!teams[team]) teams[team] = [];
170
- teams[team].push(a);
172
+ if (!groups[team]) groups[team] = [];
173
+ groups[team].push(a);
171
174
  }
172
175
 
173
176
  const rotatingSet = new Set(rotating);
@@ -175,8 +178,8 @@ const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
175
178
  return (
176
179
  <ScrollArea className="flex-1">
177
180
  <div className="py-1">
178
- {Object.entries(teams).map(([team, members]) => (
179
- <TeamSection key={team} team={team} members={members} rotatingSet={rotatingSet} />
181
+ {Object.entries(groups).map(([teamId, members]) => (
182
+ <TeamSection key={teamId} team={teamNameMap[teamId] || (teamId === 'ungrouped' ? 'Ungrouped' : teamId)} members={members} rotatingSet={rotatingSet} />
180
183
  ))}
181
184
  </div>
182
185
  </ScrollArea>
@@ -0,0 +1,514 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import { Dialog, DialogContent } from '../ui/dialog';
4
+ import { Button } from '../ui/button';
5
+ import { Input } from '../ui/input';
6
+ import { Badge } from '../ui/badge';
7
+ import { api } from '../../lib/api';
8
+ import { useToast } from '../../lib/hooks/use-toast';
9
+ import {
10
+ Check, CheckCircle, ExternalLink, Loader2, Eye, EyeOff,
11
+ Key, Shield, Trash2, ChevronRight, X,
12
+ } from 'lucide-react';
13
+
14
+ // Reuse integration logos from marketplace-card
15
+ const INTEGRATION_LOGOS = {
16
+ slack: 'https://cdn.simpleicons.org/slack/E01E5A',
17
+ github: 'https://cdn.simpleicons.org/github/white',
18
+ stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
19
+ gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
20
+ 'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
21
+ 'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
22
+ 'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
23
+ postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
24
+ notion: 'https://cdn.simpleicons.org/notion/white',
25
+ discord: 'https://cdn.simpleicons.org/discord/5865F2',
26
+ linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
27
+ 'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
28
+ 'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
29
+ };
30
+
31
+ function IntegrationIcon({ item, size = 48 }) {
32
+ const logoUrl = INTEGRATION_LOGOS[item.id];
33
+ if (logoUrl) {
34
+ return (
35
+ <div className="rounded-lg bg-surface-4 flex items-center justify-center flex-shrink-0 overflow-hidden" style={{ width: size, height: size }}>
36
+ <img src={logoUrl} alt={item.name} className="w-6 h-6" onError={(e) => { e.target.style.display = 'none'; }} />
37
+ </div>
38
+ );
39
+ }
40
+ const initial = (item.name || '?')[0].toUpperCase();
41
+ const hue = item.name ? item.name.charCodeAt(0) * 37 % 360 : 200;
42
+ return (
43
+ <div
44
+ className="rounded-lg flex items-center justify-center flex-shrink-0 text-xl font-bold font-sans"
45
+ style={{ width: size, height: size, background: `hsl(${hue}, 40%, 18%)`, color: `hsl(${hue}, 60%, 65%)` }}
46
+ >
47
+ {initial}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ // ── Password input with show/hide toggle ────────────────
53
+ function SecretInput({ value, onChange, placeholder, disabled }) {
54
+ const [visible, setVisible] = useState(false);
55
+ return (
56
+ <div className="relative">
57
+ <Input
58
+ type={visible ? 'text' : 'password'}
59
+ value={value}
60
+ onChange={(e) => onChange(e.target.value)}
61
+ placeholder={placeholder}
62
+ disabled={disabled}
63
+ mono
64
+ className="pr-9"
65
+ />
66
+ <button
67
+ type="button"
68
+ onClick={() => setVisible((v) => !v)}
69
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded text-text-4 hover:text-text-1 transition-colors cursor-pointer"
70
+ tabIndex={-1}
71
+ >
72
+ {visible ? <EyeOff size={14} /> : <Eye size={14} />}
73
+ </button>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ // ── Credential row for api-key auth type ────────────────
79
+ function CredentialRow({ integrationId, envKey, onSaved }) {
80
+ const toast = useToast();
81
+ const [value, setValue] = useState('');
82
+ const [saving, setSaving] = useState(false);
83
+ const [saved, setSaved] = useState(envKey.set);
84
+ const [deleting, setDeleting] = useState(false);
85
+
86
+ async function handleSave() {
87
+ if (!value.trim()) return;
88
+ setSaving(true);
89
+ try {
90
+ await api.post(`/integrations/${integrationId}/credentials`, { key: envKey.key, value: value.trim() });
91
+ setSaved(true);
92
+ setValue('');
93
+ toast.success(`${envKey.label} saved`);
94
+ onSaved?.();
95
+ } catch (err) {
96
+ toast.error('Failed to save', err.message);
97
+ }
98
+ setSaving(false);
99
+ }
100
+
101
+ async function handleDelete() {
102
+ setDeleting(true);
103
+ try {
104
+ await api.delete(`/integrations/${integrationId}/credentials/${envKey.key}`);
105
+ setSaved(false);
106
+ toast.success(`${envKey.label} removed`);
107
+ onSaved?.();
108
+ } catch (err) {
109
+ toast.error('Failed to remove', err.message);
110
+ }
111
+ setDeleting(false);
112
+ }
113
+
114
+ return (
115
+ <div className="space-y-1.5">
116
+ <div className="flex items-center gap-2">
117
+ <label className="text-xs font-medium text-text-2 font-sans flex items-center gap-1.5">
118
+ <Key size={11} className="text-text-4" />
119
+ {envKey.label}
120
+ {envKey.required && <span className="text-danger">*</span>}
121
+ </label>
122
+ {saved && (
123
+ <span className="flex items-center gap-1 text-2xs text-success font-sans">
124
+ <Check size={10} /> Set
125
+ </span>
126
+ )}
127
+ </div>
128
+
129
+ {saved ? (
130
+ <div className="flex items-center gap-2">
131
+ <div className="flex-1 h-8 rounded-md px-3 bg-surface-2 border border-border-subtle flex items-center">
132
+ <span className="text-xs text-text-4 font-mono tracking-widest">{'*'.repeat(16)}</span>
133
+ </div>
134
+ <Button variant="ghost" size="sm" onClick={handleDelete} disabled={deleting} className="text-text-3 hover:text-danger">
135
+ {deleting ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
136
+ </Button>
137
+ </div>
138
+ ) : (
139
+ <div className="flex items-center gap-2">
140
+ <div className="flex-1">
141
+ <SecretInput
142
+ value={value}
143
+ onChange={setValue}
144
+ placeholder={envKey.placeholder || `Enter ${envKey.label.toLowerCase()}...`}
145
+ disabled={saving}
146
+ />
147
+ </div>
148
+ <Button variant="primary" size="sm" onClick={handleSave} disabled={saving || !value.trim()}>
149
+ {saving ? <Loader2 size={12} className="animate-spin" /> : 'Save'}
150
+ </Button>
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ // ── Step: Overview ──────────────────────────────────────
158
+ function OverviewStep({ item, status, installing, onInstall, onUninstall, onNext }) {
159
+ const isInstalled = status?.installed;
160
+
161
+ return (
162
+ <div className="px-5 py-5 space-y-5">
163
+ {/* Header */}
164
+ <div className="flex items-start gap-4">
165
+ <IntegrationIcon item={item} size={52} />
166
+ <div className="flex-1 min-w-0">
167
+ <div className="flex items-center gap-2">
168
+ <h2 className="text-base font-bold text-text-0 font-sans">{item.name}</h2>
169
+ {(item.verified === 'mcp-official' || item.verified === 'verified') && (
170
+ <Badge variant="accent" className="text-2xs gap-1">
171
+ <Shield size={9} /> Verified
172
+ </Badge>
173
+ )}
174
+ </div>
175
+ <p className="text-xs text-text-3 font-sans mt-0.5">{item.author || 'Community'}</p>
176
+ {item.category && (
177
+ <Badge variant="default" className="text-2xs mt-2">{item.category}</Badge>
178
+ )}
179
+ </div>
180
+ </div>
181
+
182
+ {/* Description */}
183
+ <p className="text-sm text-text-2 font-sans leading-relaxed">{item.description}</p>
184
+
185
+ {/* Tags */}
186
+ {item.tags?.length > 0 && (
187
+ <div className="flex flex-wrap gap-1.5">
188
+ {item.tags.map((tag) => (
189
+ <span key={tag} className="text-2xs text-text-3 font-sans px-2 py-0.5 rounded bg-surface-4">{tag}</span>
190
+ ))}
191
+ </div>
192
+ )}
193
+
194
+ <div className="h-px bg-border-subtle" />
195
+
196
+ {/* Action */}
197
+ {isInstalled ? (
198
+ <div className="flex items-center gap-3">
199
+ <div className="flex-1 flex items-center gap-2">
200
+ <CheckCircle size={16} className="text-success" />
201
+ <span className="text-sm font-medium text-success font-sans">Installed</span>
202
+ </div>
203
+ <Button variant="ghost" size="sm" onClick={onUninstall} className="text-text-3 hover:text-danger gap-1.5">
204
+ <Trash2 size={12} /> Uninstall
205
+ </Button>
206
+ <Button variant="primary" size="sm" onClick={onNext} className="gap-1">
207
+ Configure <ChevronRight size={12} />
208
+ </Button>
209
+ </div>
210
+ ) : (
211
+ <Button
212
+ variant="primary"
213
+ size="lg"
214
+ onClick={onInstall}
215
+ disabled={installing}
216
+ className="w-full gap-2"
217
+ >
218
+ {installing ? (
219
+ <>
220
+ <Loader2 size={14} className="animate-spin" />
221
+ Installing...
222
+ </>
223
+ ) : (
224
+ 'Install'
225
+ )}
226
+ </Button>
227
+ )}
228
+
229
+ {installing && (
230
+ <p className="text-2xs text-text-4 font-sans text-center">This may take up to 30 seconds...</p>
231
+ )}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ // ── Step: Configure ─────────────────────────────────────
237
+ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
238
+ const toast = useToast();
239
+ const [authenticating, setAuthenticating] = useState(false);
240
+ const authType = item.authType;
241
+
242
+ async function handleGoogleAutoAuth() {
243
+ setAuthenticating(true);
244
+ try {
245
+ await api.post(`/integrations/${item.id}/authenticate`);
246
+ toast.success('Browser opened — complete sign-in there');
247
+ } catch (err) {
248
+ toast.error('Auth failed', err.message);
249
+ }
250
+ setAuthenticating(false);
251
+ }
252
+
253
+ async function handleOAuthStart() {
254
+ setAuthenticating(true);
255
+ try {
256
+ const data = await api.post(`/integrations/${item.id}/oauth/start`);
257
+ if (data.url) {
258
+ window.open(data.url, '_blank', 'noopener');
259
+ toast.success('Browser opened — complete sign-in there');
260
+ }
261
+ } catch (err) {
262
+ toast.error('OAuth failed', err.message);
263
+ }
264
+ setAuthenticating(false);
265
+ }
266
+
267
+ // Check if all required keys are set
268
+ const envKeys = status?.envKeys || [];
269
+ const allRequired = envKeys.filter((ek) => ek.required && !ek.hidden);
270
+ const allSet = allRequired.length === 0 || allRequired.every((ek) => ek.set);
271
+
272
+ return (
273
+ <div className="px-5 py-5 space-y-5">
274
+ {/* Header */}
275
+ <div className="flex items-center gap-3">
276
+ <IntegrationIcon item={item} size={36} />
277
+ <div>
278
+ <h2 className="text-sm font-bold text-text-0 font-sans">Configure {item.name}</h2>
279
+ <p className="text-2xs text-text-3 font-sans">Set up credentials to connect</p>
280
+ </div>
281
+ </div>
282
+
283
+ {/* Setup steps */}
284
+ {item.setupSteps?.length > 0 && (
285
+ <div className="bg-surface-2 rounded-md px-4 py-3 space-y-2">
286
+ <span className="text-xs font-semibold text-text-1 font-sans">Setup guide</span>
287
+ <ol className="space-y-1.5">
288
+ {item.setupSteps.map((step, i) => (
289
+ <li key={i} className="flex gap-2 text-xs text-text-2 font-sans leading-relaxed">
290
+ <span className="text-text-4 font-mono flex-shrink-0 w-4 text-right">{i + 1}.</span>
291
+ <span>{step}</span>
292
+ </li>
293
+ ))}
294
+ </ol>
295
+ {item.setupUrl && (
296
+ <a
297
+ href={item.setupUrl}
298
+ target="_blank"
299
+ rel="noopener noreferrer"
300
+ className="inline-flex items-center gap-1 text-xs text-accent font-sans hover:underline mt-1"
301
+ >
302
+ <ExternalLink size={11} />
303
+ {new URL(item.setupUrl).hostname}
304
+ </a>
305
+ )}
306
+ </div>
307
+ )}
308
+
309
+ <div className="h-px bg-border-subtle" />
310
+
311
+ {/* Auth type specific UI */}
312
+ {authType === 'api-key' && (
313
+ <div className="space-y-4">
314
+ {envKeys.filter((ek) => !ek.hidden).map((ek) => (
315
+ <CredentialRow
316
+ key={ek.key}
317
+ integrationId={item.id}
318
+ envKey={ek}
319
+ onSaved={onRefreshStatus}
320
+ />
321
+ ))}
322
+ </div>
323
+ )}
324
+
325
+ {authType === 'google-autoauth' && (
326
+ <div className="space-y-3">
327
+ <Button
328
+ variant="primary"
329
+ size="lg"
330
+ onClick={handleGoogleAutoAuth}
331
+ disabled={authenticating}
332
+ className="w-full gap-2"
333
+ >
334
+ {authenticating ? (
335
+ <>
336
+ <Loader2 size={14} className="animate-spin" />
337
+ Opening browser...
338
+ </>
339
+ ) : (
340
+ <>
341
+ <img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
342
+ Sign in with Google
343
+ </>
344
+ )}
345
+ </Button>
346
+ <p className="text-2xs text-text-4 font-sans text-center">
347
+ A browser window will open for Google authorization
348
+ </p>
349
+ </div>
350
+ )}
351
+
352
+ {authType === 'oauth-google' && (
353
+ <div className="space-y-3">
354
+ <Button
355
+ variant="primary"
356
+ size="lg"
357
+ onClick={handleOAuthStart}
358
+ disabled={authenticating}
359
+ className="w-full gap-2"
360
+ >
361
+ {authenticating ? (
362
+ <>
363
+ <Loader2 size={14} className="animate-spin" />
364
+ Connecting...
365
+ </>
366
+ ) : (
367
+ <>
368
+ <img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
369
+ Connect with Google
370
+ </>
371
+ )}
372
+ </Button>
373
+ <p className="text-2xs text-text-4 font-sans text-center">
374
+ Authorize Groove to access your {item.name}
375
+ </p>
376
+ </div>
377
+ )}
378
+
379
+ {/* Done button */}
380
+ <Button
381
+ variant={allSet ? 'primary' : 'secondary'}
382
+ size="lg"
383
+ onClick={onDone}
384
+ className="w-full gap-1.5"
385
+ >
386
+ {allSet ? (
387
+ <>
388
+ <Check size={14} />
389
+ Done
390
+ </>
391
+ ) : (
392
+ 'Skip for now'
393
+ )}
394
+ </Button>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ // ── Step: Done ──────────────────────────────────────────
400
+ function DoneStep({ item, onClose }) {
401
+ return (
402
+ <div className="px-5 py-10 flex flex-col items-center text-center space-y-4">
403
+ <div className="w-14 h-14 rounded-full bg-success/15 flex items-center justify-center">
404
+ <CheckCircle size={28} className="text-success" />
405
+ </div>
406
+ <div>
407
+ <h2 className="text-base font-bold text-text-0 font-sans">Integration ready</h2>
408
+ <p className="text-sm text-text-3 font-sans mt-1">
409
+ {item.name} is installed and configured. Agents can now use it.
410
+ </p>
411
+ </div>
412
+ <Button variant="primary" size="lg" onClick={onClose} className="mt-2">
413
+ Close
414
+ </Button>
415
+ </div>
416
+ );
417
+ }
418
+
419
+ // ── Main Wizard ─────────────────────────────────────────
420
+ export function IntegrationWizard({ integration, open, onClose }) {
421
+ const toast = useToast();
422
+ const [step, setStep] = useState('overview'); // overview | configure | done
423
+ const [status, setStatus] = useState(null);
424
+ const [installing, setInstalling] = useState(false);
425
+ const [loadingStatus, setLoadingStatus] = useState(true);
426
+
427
+ const integrationId = integration?.id;
428
+
429
+ const fetchStatus = useCallback(async () => {
430
+ if (!integrationId) return;
431
+ try {
432
+ const data = await api.get(`/integrations/${integrationId}/status`);
433
+ setStatus(data);
434
+ } catch {
435
+ setStatus(null);
436
+ }
437
+ setLoadingStatus(false);
438
+ }, [integrationId]);
439
+
440
+ useEffect(() => {
441
+ if (open && integration) {
442
+ setStep('overview');
443
+ setLoadingStatus(true);
444
+ fetchStatus();
445
+ }
446
+ }, [open, integration, fetchStatus]);
447
+
448
+ async function handleInstall() {
449
+ setInstalling(true);
450
+ try {
451
+ await api.post(`/integrations/${integration.id}/install`);
452
+ toast.success(`${integration.name} installed`);
453
+ await fetchStatus();
454
+ setStep('configure');
455
+ } catch (err) {
456
+ toast.error('Install failed', err.message);
457
+ }
458
+ setInstalling(false);
459
+ }
460
+
461
+ async function handleUninstall() {
462
+ try {
463
+ await api.delete(`/integrations/${integration.id}`);
464
+ toast.success(`${integration.name} uninstalled`);
465
+ await fetchStatus();
466
+ } catch (err) {
467
+ toast.error('Uninstall failed', err.message);
468
+ }
469
+ }
470
+
471
+ function handleConfigureNext() {
472
+ setStep('configure');
473
+ }
474
+
475
+ function handleDone() {
476
+ setStep('done');
477
+ }
478
+
479
+ if (!integration) return null;
480
+
481
+ return (
482
+ <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
483
+ <DialogContent
484
+ title={step === 'overview' ? integration.name : step === 'configure' ? 'Configure' : 'Complete'}
485
+ description={`Setup wizard for ${integration.name}`}
486
+ className="max-w-md"
487
+ >
488
+ {loadingStatus ? (
489
+ <div className="px-5 py-10 flex items-center justify-center">
490
+ <Loader2 size={20} className="animate-spin text-text-4" />
491
+ </div>
492
+ ) : step === 'overview' ? (
493
+ <OverviewStep
494
+ item={integration}
495
+ status={status}
496
+ installing={installing}
497
+ onInstall={handleInstall}
498
+ onUninstall={handleUninstall}
499
+ onNext={handleConfigureNext}
500
+ />
501
+ ) : step === 'configure' ? (
502
+ <ConfigureStep
503
+ item={integration}
504
+ status={status}
505
+ onDone={handleDone}
506
+ onRefreshStatus={fetchStatus}
507
+ />
508
+ ) : (
509
+ <DoneStep item={integration} onClose={onClose} />
510
+ )}
511
+ </DialogContent>
512
+ </Dialog>
513
+ );
514
+ }
@@ -1,5 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useDashboard } from '../lib/hooks/use-dashboard';
3
+ import { useGrooveStore } from '../stores/groove';
3
4
  import { DashboardHeader } from '../components/dashboard/header-bar';
4
5
  import { KpiStrip } from '../components/dashboard/kpi-card';
5
6
  import { FleetPanel } from '../components/dashboard/fleet-panel';
@@ -36,6 +37,7 @@ export default function DashboardView() {
36
37
  agentBreakdown, routing, rotation, adaptive, journalist, rotating,
37
38
  } = useDashboard();
38
39
 
40
+ const teams = useGrooveStore((s) => s.teams);
39
41
  const runningCount = agents.filter((a) => a.status === 'running').length;
40
42
 
41
43
  if (!connected) {
@@ -143,7 +145,7 @@ export default function DashboardView() {
143
145
  <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
144
146
  <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
145
147
  </div>
146
- <FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} />
148
+ <FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
147
149
  </div>
148
150
 
149
151
  {/* R4C2-3: Intel Panel (spans 2 cols) */}
@@ -17,6 +17,7 @@ import { api } from '../lib/api';
17
17
  import { useToast } from '../lib/hooks/use-toast';
18
18
  import { fmtNum, timeAgo } from '../lib/format';
19
19
  import { useGrooveStore } from '../stores/groove';
20
+ import { IntegrationWizard } from '../components/marketplace/integration-wizard';
20
21
  import {
21
22
  ChevronLeft, ChevronDown, Sparkles, Plug, LogIn, LogOut,
22
23
  User, Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
@@ -278,15 +279,30 @@ function IntegrationsBrowse() {
278
279
  const [items, setItems] = useState([]);
279
280
  const [loading, setLoading] = useState(true);
280
281
  const [search, setSearch] = useState('');
281
- const toast = useToast();
282
+ const [selectedIntegration, setSelectedIntegration] = useState(null);
283
+ const [showWizard, setShowWizard] = useState(false);
282
284
 
283
- useEffect(() => {
285
+ const fetchItems = () => {
284
286
  setLoading(true);
285
287
  api.get(`/integrations/registry?search=${encodeURIComponent(search)}`)
286
288
  .then((d) => setItems(d.integrations || d.items || (Array.isArray(d) ? d : [])))
287
289
  .catch(() => setItems([]))
288
290
  .finally(() => setLoading(false));
289
- }, [search]);
291
+ };
292
+
293
+ useEffect(() => { fetchItems(); }, [search]);
294
+
295
+ function handleCardClick(item) {
296
+ setSelectedIntegration(item);
297
+ setShowWizard(true);
298
+ }
299
+
300
+ function handleWizardClose() {
301
+ setShowWizard(false);
302
+ setSelectedIntegration(null);
303
+ // Refresh list to pick up install/uninstall changes
304
+ fetchItems();
305
+ }
290
306
 
291
307
  return (
292
308
  <ScrollArea className="h-full">
@@ -302,7 +318,7 @@ function IntegrationsBrowse() {
302
318
  <div className="mt-4 grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))' }}>
303
319
  {loading
304
320
  ? Array.from({ length: 6 }).map((_, i) => <SkillCardSkeleton key={i} />)
305
- : items.map((item) => <MarketplaceCard key={item.id} item={item} onClick={() => toast.info(`${item.name} — install via CLI: groove integrations install ${item.id}`)} />)
321
+ : items.map((item) => <MarketplaceCard key={item.id} item={item} onClick={() => handleCardClick(item)} />)
306
322
  }
307
323
  </div>
308
324
 
@@ -310,6 +326,12 @@ function IntegrationsBrowse() {
310
326
  <div className="text-center py-16 text-text-4 font-sans text-sm">No integrations found.</div>
311
327
  )}
312
328
  </div>
329
+
330
+ <IntegrationWizard
331
+ integration={selectedIntegration}
332
+ open={showWizard}
333
+ onClose={handleWizardClose}
334
+ />
313
335
  </ScrollArea>
314
336
  );
315
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.26.33",
3
+ "version": "0.26.36",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -75,9 +75,11 @@ export class ClaudeCodeProvider extends Provider {
75
75
  }
76
76
 
77
77
  buildHeadlessCommand(prompt, model) {
78
- const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
78
+ // Pass prompt via stdin to avoid OS argument length limits.
79
+ // Long prompts (journalist synthesis with agent logs) can exceed ARG_MAX.
80
+ const args = ['-p', '--output-format', 'stream-json', '--verbose'];
79
81
  if (model) args.push('--model', model);
80
- return { command: 'claude', args, env: {} };
82
+ return { command: 'claude', args, env: {}, stdin: prompt };
81
83
  }
82
84
 
83
85
  buildFullPrompt(agent) {