groove-dev 0.26.38 → 0.27.0

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 (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CLAUDE.md +24 -19
  3. package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
  6. package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
  7. package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
  8. package/node_modules/@groove-dev/daemon/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
  10. package/node_modules/@groove-dev/daemon/src/api.js +346 -22
  11. package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
  12. package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
  13. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
  14. package/node_modules/@groove-dev/daemon/src/index.js +28 -4
  15. package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
  17. package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
  18. package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
  19. package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
  20. package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
  21. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  22. package/node_modules/@groove-dev/daemon/src/process.js +141 -9
  23. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  24. package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
  25. package/node_modules/@groove-dev/daemon/src/router.js +43 -0
  26. package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
  27. package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
  28. package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
  30. package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
  31. package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
  32. package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
  33. package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
  34. package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
  35. package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
  38. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  39. package/node_modules/@groove-dev/gui/package.json +1 -4
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  49. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  50. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
  51. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
  52. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  53. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  54. package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
  55. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
  56. package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
  57. package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
  58. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
  59. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
  60. package/package.json +2 -8
  61. package/packages/cli/bin/groove.js +2 -0
  62. package/packages/cli/package.json +1 -1
  63. package/packages/cli/src/commands/nuke.js +16 -4
  64. package/packages/cli/src/commands/stop.js +17 -2
  65. package/packages/daemon/integrations-registry.json +681 -75
  66. package/packages/daemon/package.json +1 -1
  67. package/packages/daemon/src/adaptive.js +23 -25
  68. package/packages/daemon/src/api.js +346 -22
  69. package/packages/daemon/src/classifier.js +53 -6
  70. package/packages/daemon/src/firstrun.js +14 -1
  71. package/packages/daemon/src/gateways/manager.js +2 -2
  72. package/packages/daemon/src/index.js +28 -4
  73. package/packages/daemon/src/integrations.js +215 -14
  74. package/packages/daemon/src/introducer.js +84 -11
  75. package/packages/daemon/src/journalist.js +43 -1
  76. package/packages/daemon/src/lockmanager.js +60 -0
  77. package/packages/daemon/src/mcp-manager.js +270 -0
  78. package/packages/daemon/src/memory.js +370 -0
  79. package/packages/daemon/src/pm.js +1 -1
  80. package/packages/daemon/src/process.js +141 -9
  81. package/packages/daemon/src/registry.js +1 -1
  82. package/packages/daemon/src/rotator.js +334 -31
  83. package/packages/daemon/src/router.js +43 -0
  84. package/packages/daemon/src/tokentracker.js +70 -18
  85. package/packages/daemon/src/validate.js +5 -13
  86. package/packages/daemon/templates/groove-slides.cjs +306 -0
  87. package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
  88. package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
  89. package/packages/gui/dist/index.html +2 -2
  90. package/packages/gui/package.json +1 -4
  91. package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
  92. package/packages/gui/src/components/agents/agent-config.jsx +22 -1
  93. package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
  94. package/packages/gui/src/components/agents/agent-node.jsx +132 -90
  95. package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
  96. package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
  97. package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
  98. package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
  99. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  100. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  101. package/packages/gui/src/components/layout/app-shell.jsx +24 -19
  102. package/packages/gui/src/components/layout/command-palette.jsx +2 -2
  103. package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  104. package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  105. package/packages/gui/src/lib/format.js +0 -6
  106. package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
  107. package/packages/gui/src/stores/groove.js +59 -9
  108. package/packages/gui/src/views/agents.jsx +84 -10
  109. package/packages/gui/src/views/dashboard.jsx +24 -21
  110. package/packages/gui/src/views/marketplace.jsx +153 -85
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
  112. package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
  113. package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
  114. package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
  115. package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
  116. package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
  117. package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
  118. package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
  119. package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
  120. package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
  121. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
  122. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
  123. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
  124. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
  125. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
  126. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
  127. package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
  128. package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
  129. package/node_modules/@radix-ui/react-popover/README.md +0 -3
  130. package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
  131. package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
  132. package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
  133. package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
  134. package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
  135. package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
  136. package/node_modules/@radix-ui/react-popover/package.json +0 -82
  137. package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
  138. package/node_modules/@radix-ui/react-separator/README.md +0 -3
  139. package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
  140. package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
  141. package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
  142. package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
  143. package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
  144. package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
  145. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
  146. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
  147. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
  148. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
  149. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
  150. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
  151. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
  152. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
  153. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
  154. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
  155. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
  156. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
  157. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
  158. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
  159. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
  160. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
  161. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
  162. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
  163. package/node_modules/@radix-ui/react-separator/package.json +0 -69
  164. package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
  165. package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
  166. package/packages/gui/dist/groove-logo-short.png +0 -0
  167. package/packages/gui/dist/groove-logo.png +0 -0
  168. package/packages/gui/public/groove-logo-short.png +0 -0
  169. package/packages/gui/public/groove-logo.png +0 -0
  170. package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
  171. package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect, useCallback } from 'react';
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
3
  import { Dialog, DialogContent } from '../ui/dialog';
4
4
  import { Button } from '../ui/button';
5
5
  import { Input } from '../ui/input';
@@ -8,7 +8,7 @@ import { api } from '../../lib/api';
8
8
  import { useToast } from '../../lib/hooks/use-toast';
9
9
  import {
10
10
  Check, CheckCircle, ExternalLink, Loader2, Eye, EyeOff,
11
- Key, Shield, Trash2, ChevronRight, X,
11
+ Key, Shield, Trash2, ChevronRight, X, Copy, RefreshCw,
12
12
  } from 'lucide-react';
13
13
 
14
14
  // Reuse integration logos from marketplace-card
@@ -19,6 +19,9 @@ const INTEGRATION_LOGOS = {
19
19
  gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
20
20
  'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
21
21
  'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
22
+ 'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
23
+ 'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
24
+ 'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
22
25
  'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
23
26
  postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
24
27
  notion: 'https://cdn.simpleicons.org/notion/white',
@@ -233,13 +236,25 @@ function OverviewStep({ item, status, installing, onInstall, onUninstall, onNext
233
236
  );
234
237
  }
235
238
 
239
+ // ── Which APIs each Google integration needs ───────────
240
+ const GOOGLE_API_NAMES = {
241
+ gmail: 'Gmail API',
242
+ 'google-calendar': 'Google Calendar API',
243
+ 'google-drive': 'Google Drive API',
244
+ 'google-docs': 'Google Docs API',
245
+ 'google-sheets': 'Google Sheets API',
246
+ 'google-slides': 'Google Slides API',
247
+ };
248
+
236
249
  // ── Google OAuth Setup (shared by google-autoauth + oauth-google) ──
237
- function GoogleOAuthSetup({ onConfigured }) {
250
+ function GoogleOAuthSetup({ integrationId, onConfigured }) {
238
251
  const toast = useToast();
239
252
  const [clientId, setClientId] = useState('');
240
253
  const [clientSecret, setClientSecret] = useState('');
241
254
  const [saving, setSaving] = useState(false);
242
255
 
256
+ const apiName = GOOGLE_API_NAMES[integrationId] || 'the relevant Google API';
257
+
243
258
  async function handleSave() {
244
259
  if (!clientId.trim() || !clientSecret.trim()) return;
245
260
  setSaving(true);
@@ -256,23 +271,75 @@ function GoogleOAuthSetup({ onConfigured }) {
256
271
  setSaving(false);
257
272
  }
258
273
 
274
+ const redirectUri = 'http://localhost:31415/api/integrations/oauth/callback';
275
+
276
+ const steps = [
277
+ { text: 'Go to the Google Cloud Console and sign in with your Google account',
278
+ link: { url: 'https://console.cloud.google.com', label: 'Open Google Cloud Console' } },
279
+ { text: 'Create a new project (or select an existing one). Any name is fine — this is just a container for your credentials.' },
280
+ { text: <>Enable the <strong>{apiName}</strong> — search for it in the API Library and click <strong>Enable</strong></>,
281
+ link: { url: 'https://console.cloud.google.com/apis/library', label: 'Open API Library' } },
282
+ { text: <>Go to <strong>Credentials</strong> and click <strong>Create Credentials</strong> &rarr; <strong>OAuth client ID</strong></>,
283
+ link: { url: 'https://console.cloud.google.com/apis/credentials', label: 'Open Credentials page' } },
284
+ { text: <>If prompted to configure the consent screen, choose <strong>External</strong>, fill in an app name (e.g. &quot;Groove&quot;), your email, and save. You can skip optional fields.</> },
285
+ { text: <>For Application type, choose <strong>Web application</strong>. Give it any name.</> },
286
+ { text: <>Under <strong>Authorized redirect URIs</strong>, click <strong>Add URI</strong> and paste this exact URL:</>,
287
+ copyable: redirectUri },
288
+ { text: <>Click <strong>Create</strong>, then copy the <strong>Client ID</strong> and <strong>Client Secret</strong> and paste them below.</> },
289
+ ];
290
+
259
291
  return (
260
292
  <div className="space-y-4">
261
- <div className="bg-warning/8 border border-warning/15 rounded-md px-4 py-3">
262
- <p className="text-xs font-semibold text-warning font-sans">Google Cloud credentials required</p>
263
- <p className="text-2xs text-text-3 font-sans mt-1">
264
- Create an OAuth 2.0 Client ID in the Google Cloud Console, then paste the credentials below.
293
+ <div className="bg-surface-2 rounded-md px-4 py-3 space-y-3">
294
+ <span className="text-xs font-semibold text-text-1 font-sans">How to get your Google credentials</span>
295
+ <ol className="space-y-2.5">
296
+ {steps.map((step, i) => (
297
+ <li key={i} className="flex gap-2.5 text-xs text-text-2 font-sans leading-relaxed">
298
+ <span className="text-accent font-mono font-bold flex-shrink-0 w-4 text-right">{i + 1}.</span>
299
+ <div className="min-w-0">
300
+ <span>{step.text}</span>
301
+ {step.link && (
302
+ <a
303
+ href={step.link.url}
304
+ target="_blank"
305
+ rel="noopener noreferrer"
306
+ className="flex items-center gap-1 text-2xs text-accent font-sans hover:underline mt-0.5"
307
+ >
308
+ <ExternalLink size={9} />
309
+ {step.link.label}
310
+ </a>
311
+ )}
312
+ {step.copyable && (
313
+ <div className="mt-1.5 flex items-center gap-1.5">
314
+ <code className="flex-1 min-w-0 text-2xs font-mono text-accent bg-surface-4 px-2.5 py-1.5 rounded select-all break-all">
315
+ {step.copyable}
316
+ </code>
317
+ <button
318
+ type="button"
319
+ onClick={() => { navigator.clipboard.writeText(step.copyable); }}
320
+ className="flex-shrink-0 p-1.5 rounded text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
321
+ title="Copy to clipboard"
322
+ >
323
+ <Copy size={12} />
324
+ </button>
325
+ </div>
326
+ )}
327
+ </div>
328
+ </li>
329
+ ))}
330
+ </ol>
331
+ </div>
332
+
333
+ <div className="bg-accent/8 border border-accent/15 rounded-md px-4 py-2.5">
334
+ <p className="text-2xs text-text-2 font-sans leading-relaxed">
335
+ <strong className="text-text-1">One-time setup</strong> — these same credentials work for
336
+ Gmail, Calendar, Drive, Docs, Sheets, and Slides. You only need to do this once.
337
+ For each integration, just enable the matching API in your Google Cloud project.
265
338
  </p>
266
- <a
267
- href="https://console.cloud.google.com/apis/credentials"
268
- target="_blank"
269
- rel="noopener noreferrer"
270
- className="inline-flex items-center gap-1 text-2xs text-accent font-sans hover:underline mt-1.5"
271
- >
272
- <ExternalLink size={10} />
273
- console.cloud.google.com
274
- </a>
275
339
  </div>
340
+
341
+ <div className="h-px bg-border-subtle" />
342
+
276
343
  <div className="space-y-3">
277
344
  <div className="space-y-1.5">
278
345
  <label className="text-xs font-medium text-text-2 font-sans flex items-center gap-1.5">
@@ -318,17 +385,6 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
318
385
  }
319
386
  }, [needsGoogleOAuth]);
320
387
 
321
- async function handleGoogleAutoAuth() {
322
- setAuthenticating(true);
323
- try {
324
- await api.post(`/integrations/${item.id}/authenticate`);
325
- toast.success('Browser opened — complete sign-in there');
326
- } catch (err) {
327
- toast.error('Auth failed', err.message);
328
- }
329
- setAuthenticating(false);
330
- }
331
-
332
388
  async function handleOAuthStart() {
333
389
  setAuthenticating(true);
334
390
  try {
@@ -338,7 +394,7 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
338
394
  toast.success('Browser opened — complete sign-in there');
339
395
  }
340
396
  } catch (err) {
341
- toast.error('OAuth failed', err.message);
397
+ toast.error('Sign-in failed', err.message);
342
398
  }
343
399
  setAuthenticating(false);
344
400
  }
@@ -359,8 +415,8 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
359
415
  </div>
360
416
  </div>
361
417
 
362
- {/* Setup steps */}
363
- {item.setupSteps?.length > 0 && (
418
+ {/* Setup steps — hide when Google OAuth setup guide is showing (it has its own steps) */}
419
+ {item.setupSteps?.length > 0 && !(needsGoogleOAuth && googleOAuthReady === false) && (
364
420
  <div className="bg-surface-2 rounded-md px-4 py-3 space-y-2">
365
421
  <span className="text-xs font-semibold text-text-1 font-sans">Setup guide</span>
366
422
  <ol className="space-y-1.5">
@@ -385,7 +441,7 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
385
441
  </div>
386
442
  )}
387
443
 
388
- <div className="h-px bg-border-subtle" />
444
+ {!(needsGoogleOAuth && googleOAuthReady === false) && <div className="h-px bg-border-subtle" />}
389
445
 
390
446
  {/* Auth type specific UI */}
391
447
  {authType === 'api-key' && (
@@ -402,15 +458,15 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
402
458
  )}
403
459
 
404
460
  {needsGoogleOAuth && googleOAuthReady === false && (
405
- <GoogleOAuthSetup onConfigured={() => setGoogleOAuthReady(true)} />
461
+ <GoogleOAuthSetup integrationId={item.id} onConfigured={() => setGoogleOAuthReady(true)} />
406
462
  )}
407
463
 
408
- {authType === 'google-autoauth' && googleOAuthReady && (
464
+ {needsGoogleOAuth && googleOAuthReady && (
409
465
  <div className="space-y-3">
410
466
  <Button
411
467
  variant="primary"
412
468
  size="lg"
413
- onClick={handleGoogleAutoAuth}
469
+ onClick={handleOAuthStart}
414
470
  disabled={authenticating}
415
471
  className="w-full gap-2"
416
472
  >
@@ -427,35 +483,15 @@ function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
427
483
  )}
428
484
  </Button>
429
485
  <p className="text-2xs text-text-4 font-sans text-center">
430
- A browser window will open for Google authorization
486
+ A browser window will open sign in and allow access to your {item.name}
431
487
  </p>
432
- </div>
433
- )}
434
-
435
- {authType === 'oauth-google' && googleOAuthReady && (
436
- <div className="space-y-3">
437
- <Button
438
- variant="primary"
439
- size="lg"
440
- onClick={handleOAuthStart}
441
- disabled={authenticating}
442
- className="w-full gap-2"
488
+ <button
489
+ type="button"
490
+ onClick={() => setGoogleOAuthReady(false)}
491
+ className="w-full text-2xs text-text-4 font-sans hover:text-text-2 transition-colors cursor-pointer py-1"
443
492
  >
444
- {authenticating ? (
445
- <>
446
- <Loader2 size={14} className="animate-spin" />
447
- Connecting...
448
- </>
449
- ) : (
450
- <>
451
- <img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
452
- Connect with Google
453
- </>
454
- )}
455
- </Button>
456
- <p className="text-2xs text-text-4 font-sans text-center">
457
- Authorize Groove to access your {item.name}
458
- </p>
493
+ Reconfigure Google OAuth credentials
494
+ </button>
459
495
  </div>
460
496
  )}
461
497
 
@@ -601,3 +637,297 @@ export function IntegrationWizard({ integration, open, onClose }) {
601
637
  </Dialog>
602
638
  );
603
639
  }
640
+
641
+ // ── Google Workspace Wizard ────────────────────────────
642
+ const GOOGLE_IDS = ['gmail', 'google-calendar', 'google-drive', 'google-docs', 'google-sheets', 'google-slides'];
643
+
644
+ function ServiceRow({ item, status, onInstall, onUninstall, busy }) {
645
+ const installed = status?.installed;
646
+ const authenticated = status?.authenticated;
647
+
648
+ return (
649
+ <div className="flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-2 border border-border-subtle">
650
+ <IntegrationIcon item={item} size={32} />
651
+ <div className="flex-1 min-w-0">
652
+ <div className="text-xs font-semibold text-text-0 font-sans">{item.name}</div>
653
+ <div className="text-2xs text-text-3 font-sans truncate">{item.description}</div>
654
+ </div>
655
+ {installed && authenticated && !status?.needsReauth && (
656
+ <Badge variant="success" className="text-2xs flex-shrink-0 gap-1">
657
+ <Check size={8} /> Ready
658
+ </Badge>
659
+ )}
660
+ {installed && authenticated && status?.needsReauth && (
661
+ <Badge variant="warning" className="text-2xs flex-shrink-0 gap-1">
662
+ <RefreshCw size={8} /> Update
663
+ </Badge>
664
+ )}
665
+ {installed && !authenticated && (
666
+ <Badge variant="warning" className="text-2xs flex-shrink-0">Needs sign-in</Badge>
667
+ )}
668
+ <Button
669
+ variant={installed ? 'ghost' : 'primary'}
670
+ size="sm"
671
+ onClick={() => installed ? onUninstall(item.id) : onInstall(item.id)}
672
+ disabled={busy === item.id}
673
+ className={installed ? 'text-text-3 hover:text-danger' : ''}
674
+ >
675
+ {busy === item.id ? (
676
+ <Loader2 size={12} className="animate-spin" />
677
+ ) : installed ? (
678
+ <Trash2 size={12} />
679
+ ) : (
680
+ 'Install'
681
+ )}
682
+ </Button>
683
+ </div>
684
+ );
685
+ }
686
+
687
+ export function GoogleWorkspaceWizard({ integrations, open, onClose }) {
688
+ const toast = useToast();
689
+ const [googleOAuthReady, setGoogleOAuthReady] = useState(null);
690
+ const [statuses, setStatuses] = useState({});
691
+ const [busy, setBusy] = useState(null);
692
+ const [authenticating, setAuthenticating] = useState(false);
693
+ const [loading, setLoading] = useState(true);
694
+ const pollRef = useRef(null);
695
+
696
+ const googleItems = integrations.filter((i) => GOOGLE_IDS.includes(i.id));
697
+
698
+ const fetchStatuses = useCallback(async () => {
699
+ const results = {};
700
+ await Promise.all(googleItems.map(async (item) => {
701
+ try {
702
+ results[item.id] = await api.get(`/integrations/${item.id}/status`);
703
+ } catch {
704
+ results[item.id] = null;
705
+ }
706
+ }));
707
+ setStatuses(results);
708
+ setLoading(false);
709
+ }, [googleItems.map((i) => i.id).join(',')]);
710
+
711
+ useEffect(() => {
712
+ if (open) {
713
+ setLoading(true);
714
+ api.get('/integrations/google-oauth/status')
715
+ .then((d) => setGoogleOAuthReady(d.configured))
716
+ .catch(() => setGoogleOAuthReady(false));
717
+ fetchStatuses();
718
+ }
719
+ return () => { if (pollRef.current) clearInterval(pollRef.current); };
720
+ }, [open]);
721
+
722
+ const installedIds = Object.entries(statuses).filter(([, s]) => s?.installed).map(([id]) => id);
723
+ const allAuthenticated = installedIds.length > 0 && installedIds.every((id) => statuses[id]?.authenticated);
724
+ const needsAuth = installedIds.some((id) => !statuses[id]?.authenticated);
725
+ const needsReauth = allAuthenticated && installedIds.some((id) => statuses[id]?.needsReauth);
726
+
727
+ async function handleInstall(id) {
728
+ setBusy(id);
729
+ try {
730
+ await api.post(`/integrations/${id}/install`);
731
+ toast.success(`${googleItems.find((i) => i.id === id)?.name} installed`);
732
+ await fetchStatuses();
733
+ } catch (err) {
734
+ toast.error('Install failed', err.message);
735
+ }
736
+ setBusy(null);
737
+ }
738
+
739
+ async function handleUninstall(id) {
740
+ setBusy(id);
741
+ try {
742
+ await api.delete(`/integrations/${id}`);
743
+ toast.success(`${googleItems.find((i) => i.id === id)?.name} removed`);
744
+ await fetchStatuses();
745
+ } catch (err) {
746
+ toast.error('Uninstall failed', err.message);
747
+ }
748
+ setBusy(null);
749
+ }
750
+
751
+ async function handleConnect() {
752
+ if (!installedIds.length) {
753
+ toast.error('Install at least one service first');
754
+ return;
755
+ }
756
+ setAuthenticating(true);
757
+ try {
758
+ const data = await api.post('/integrations/google-workspace/oauth/start', { integrationIds: installedIds });
759
+ if (data.url) {
760
+ window.open(data.url, '_blank', 'noopener');
761
+ toast.success('Browser opened — complete sign-in there');
762
+ // Poll for auth completion
763
+ if (pollRef.current) clearInterval(pollRef.current);
764
+ pollRef.current = setInterval(async () => {
765
+ await fetchStatuses();
766
+ }, 3000);
767
+ // Stop polling after 3 minutes
768
+ setTimeout(() => { if (pollRef.current) clearInterval(pollRef.current); }, 180_000);
769
+ }
770
+ } catch (err) {
771
+ toast.error('Sign-in failed', err.message);
772
+ }
773
+ setAuthenticating(false);
774
+ }
775
+
776
+ // Stop polling once all installed services are authenticated
777
+ useEffect(() => {
778
+ if (allAuthenticated && pollRef.current) {
779
+ clearInterval(pollRef.current);
780
+ pollRef.current = null;
781
+ }
782
+ }, [allAuthenticated]);
783
+
784
+ if (!open) return null;
785
+
786
+ return (
787
+ <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
788
+ <DialogContent
789
+ title="Google Workspace"
790
+ description="Connect your Google services"
791
+ className="max-w-md"
792
+ >
793
+ <div className="px-5 py-5 space-y-5">
794
+ {/* Header */}
795
+ <div className="flex items-center gap-3">
796
+ <div className="w-11 h-11 rounded-lg bg-surface-4 flex items-center justify-center flex-shrink-0">
797
+ <img src="https://cdn.simpleicons.org/google/white" alt="Google" className="w-6 h-6" />
798
+ </div>
799
+ <div>
800
+ <h2 className="text-sm font-bold text-text-0 font-sans">Google Workspace</h2>
801
+ <p className="text-2xs text-text-3 font-sans">
802
+ One set of credentials for all Google services
803
+ </p>
804
+ </div>
805
+ </div>
806
+
807
+ {/* OAuth Setup */}
808
+ {googleOAuthReady === false && (
809
+ <GoogleOAuthSetup integrationId="gmail" onConfigured={() => setGoogleOAuthReady(true)} />
810
+ )}
811
+
812
+ {googleOAuthReady === null && (
813
+ <div className="flex justify-center py-3">
814
+ <Loader2 size={16} className="animate-spin text-text-4" />
815
+ </div>
816
+ )}
817
+
818
+ {/* Services list */}
819
+ {googleOAuthReady && (
820
+ <>
821
+ <div className="space-y-1.5">
822
+ <span className="text-xs font-semibold text-text-2 font-sans">Services</span>
823
+ <div className="space-y-1.5">
824
+ {loading ? (
825
+ Array.from({ length: 4 }).map((_, i) => (
826
+ <div key={i} className="h-14 rounded-md bg-surface-2 animate-pulse" />
827
+ ))
828
+ ) : (
829
+ googleItems.map((item) => (
830
+ <ServiceRow
831
+ key={item.id}
832
+ item={item}
833
+ status={statuses[item.id]}
834
+ onInstall={handleInstall}
835
+ onUninstall={handleUninstall}
836
+ busy={busy}
837
+ />
838
+ ))
839
+ )}
840
+ </div>
841
+ </div>
842
+
843
+ <div className="h-px bg-border-subtle" />
844
+
845
+ {/* Connect button */}
846
+ {installedIds.length > 0 && !allAuthenticated && (
847
+ <div className="space-y-3">
848
+ <Button
849
+ variant="primary"
850
+ size="lg"
851
+ onClick={handleConnect}
852
+ disabled={authenticating}
853
+ className="w-full gap-2"
854
+ >
855
+ {authenticating ? (
856
+ <><Loader2 size={14} className="animate-spin" /> Opening browser...</>
857
+ ) : (
858
+ <>
859
+ <img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
860
+ Sign in with Google
861
+ </>
862
+ )}
863
+ </Button>
864
+ <p className="text-2xs text-text-4 font-sans text-center">
865
+ Connects {installedIds.length} service{installedIds.length !== 1 ? 's' : ''} with one sign-in
866
+ </p>
867
+ </div>
868
+ )}
869
+
870
+ {allAuthenticated && installedIds.length > 0 && (
871
+ <div className="flex flex-col items-center text-center gap-2 py-2">
872
+ <div className="w-10 h-10 rounded-full bg-success/15 flex items-center justify-center">
873
+ <CheckCircle size={20} className="text-success" />
874
+ </div>
875
+ <p className="text-sm font-medium text-success font-sans">
876
+ All services connected
877
+ </p>
878
+ <p className="text-2xs text-text-3 font-sans">
879
+ Your agents can now use these Google integrations.
880
+ </p>
881
+ {needsReauth ? (
882
+ <div className="w-full space-y-2 pt-2">
883
+ <p className="text-2xs text-warning font-sans">
884
+ New permissions available — re-authenticate to enable all features.
885
+ </p>
886
+ <Button
887
+ variant="secondary"
888
+ size="sm"
889
+ onClick={handleConnect}
890
+ disabled={authenticating}
891
+ className="w-full gap-2"
892
+ >
893
+ {authenticating ? (
894
+ <><Loader2 size={12} className="animate-spin" /> Opening browser...</>
895
+ ) : (
896
+ <><RefreshCw size={12} /> Re-authenticate</>
897
+ )}
898
+ </Button>
899
+ </div>
900
+ ) : (
901
+ <button
902
+ onClick={handleConnect}
903
+ disabled={authenticating}
904
+ className="text-2xs text-text-4 hover:text-text-2 font-sans underline underline-offset-2 transition-colors mt-1"
905
+ >
906
+ {authenticating ? 'Opening browser...' : 'Re-authenticate'}
907
+ </button>
908
+ )}
909
+ </div>
910
+ )}
911
+
912
+ {installedIds.length === 0 && !loading && (
913
+ <p className="text-xs text-text-4 font-sans text-center py-2">
914
+ Install at least one service above, then connect with Google.
915
+ </p>
916
+ )}
917
+ </>
918
+ )}
919
+
920
+ {/* Close */}
921
+ <Button
922
+ variant="secondary"
923
+ size="lg"
924
+ onClick={onClose}
925
+ className="w-full"
926
+ >
927
+ {allAuthenticated && installedIds.length > 0 ? 'Done' : 'Close'}
928
+ </Button>
929
+ </div>
930
+ </DialogContent>
931
+ </Dialog>
932
+ );
933
+ }
@@ -5,20 +5,40 @@ import { Badge } from '../ui/badge';
5
5
  import { fmtNum } from '../../lib/format';
6
6
 
7
7
  // Well-known integration logos via CDN (simple-icons on cdn.simpleicons.org)
8
- const INTEGRATION_LOGOS = {
9
- slack: 'https://cdn.simpleicons.org/slack/E01E5A',
8
+ export const INTEGRATION_LOGOS = {
9
+ 'google-workspace': 'https://cdn.simpleicons.org/google/white',
10
10
  github: 'https://cdn.simpleicons.org/github/white',
11
11
  stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
12
12
  gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
13
13
  'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
14
14
  'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
15
+ 'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
16
+ 'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
17
+ 'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
15
18
  'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
16
19
  postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
17
20
  notion: 'https://cdn.simpleicons.org/notion/white',
18
- discord: 'https://cdn.simpleicons.org/discord/5865F2',
19
21
  linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
20
22
  'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
21
23
  'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
24
+ sentry: 'https://cdn.simpleicons.org/sentry/362D59',
25
+ elevenlabs: 'https://cdn.simpleicons.org/elevenlabs/white',
26
+ hubspot: 'https://cdn.simpleicons.org/hubspot/FF7A59',
27
+ jira: 'https://cdn.simpleicons.org/jira/0052CC',
28
+ sendgrid: 'https://cdn.simpleicons.org/sendgrid/1A82E2',
29
+ resend: 'https://cdn.simpleicons.org/resend/white',
30
+ replicate: 'https://cdn.simpleicons.org/replicate/white',
31
+ vercel: 'https://cdn.simpleicons.org/vercel/white',
32
+ supabase: 'https://cdn.simpleicons.org/supabase/3FCF8E',
33
+ mixpanel: 'https://cdn.simpleicons.org/mixpanel/7856FF',
34
+ datadog: 'https://cdn.simpleicons.org/datadog/632CA6',
35
+ airtable: 'https://cdn.simpleicons.org/airtable/18BFFF',
36
+ zendesk: 'https://cdn.simpleicons.org/zendesk/03363D',
37
+ intercom: 'https://cdn.simpleicons.org/intercom/6AFDEF',
38
+ twilio: 'https://cdn.simpleicons.org/twilio/F22F46',
39
+ telnyx: 'https://cdn.simpleicons.org/telnyx/00C08B',
40
+ aws: 'https://cdn.simpleicons.org/amazonaws/FF9900',
41
+ plaid: 'https://cdn.simpleicons.org/plaid/white',
22
42
  };
23
43
 
24
44
  function ItemIcon({ item, size = 40 }) {
@@ -62,7 +82,7 @@ function ItemIcon({ item, size = 40 }) {
62
82
  );
63
83
  }
64
84
 
65
- export function MarketplaceCard({ item, onClick, className }) {
85
+ export function MarketplaceCard({ item, onClick, className, statusBadge }) {
66
86
  const installed = item.installed;
67
87
 
68
88
  return (
@@ -107,9 +127,11 @@ export function MarketplaceCard({ item, onClick, className }) {
107
127
  </span>
108
128
  )}
109
129
  <span className="flex-1" />
110
- {installed && (
111
- <Badge variant="accent" className="text-2xs">Installed</Badge>
112
- )}
130
+ {statusBadge || (installed && (
131
+ <Badge variant="accent" className="text-2xs">
132
+ {item._installedCount ? `${item._installedCount} active` : 'Installed'}
133
+ </Badge>
134
+ ))}
113
135
  </div>
114
136
  </button>
115
137
  );
@@ -42,9 +42,3 @@ export function timeAgo(ts) {
42
42
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
43
43
  return `${Math.floor(diff / 86400)}d ago`;
44
44
  }
45
-
46
- export function formatDuration(ms) {
47
- if (!ms) return '0s';
48
- const s = Math.floor(ms / 1000);
49
- return fmtUptime(s);
50
- }