groove-dev 0.27.15 → 0.27.18

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 (172) hide show
  1. package/CLAUDE.md +0 -10
  2. package/README.md +37 -1
  3. package/developerID_application.cer +0 -0
  4. package/node_modules/@groove-dev/daemon/src/api.js +586 -67
  5. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  6. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  7. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  10. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  12. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  13. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  14. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  15. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  16. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  17. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
  19. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  20. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  21. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  22. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  23. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  24. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  25. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  26. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  27. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  28. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  29. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  30. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  31. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  32. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  33. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  34. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  35. package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  37. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  38. package/node_modules/@groove-dev/gui/index.html +1 -0
  39. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  40. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  45. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  46. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  48. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  49. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  50. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  51. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
  52. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  53. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  54. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  55. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
  56. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  57. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  58. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  59. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  60. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  61. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  62. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  66. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  67. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  68. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  71. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  74. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  75. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  76. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  77. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  78. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  79. package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
  80. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  81. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  82. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  84. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
  85. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  86. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  88. package/package.json +1 -1
  89. package/packages/daemon/src/api.js +586 -67
  90. package/packages/daemon/src/classifier.js +24 -0
  91. package/packages/daemon/src/credentials.js +12 -2
  92. package/packages/daemon/src/federation/ambassador.js +204 -0
  93. package/packages/daemon/src/federation/connection.js +359 -0
  94. package/packages/daemon/src/federation/contracts.js +112 -0
  95. package/packages/daemon/src/federation/whitelist.js +190 -0
  96. package/packages/daemon/src/federation.js +166 -7
  97. package/packages/daemon/src/index.js +172 -19
  98. package/packages/daemon/src/introducer.js +52 -7
  99. package/packages/daemon/src/journalist.js +46 -1
  100. package/packages/daemon/src/memory.js +36 -16
  101. package/packages/daemon/src/process.js +140 -23
  102. package/packages/daemon/src/providers/base.js +1 -0
  103. package/packages/daemon/src/providers/claude-code.js +14 -0
  104. package/packages/daemon/src/providers/codex.js +124 -28
  105. package/packages/daemon/src/providers/gemini.js +104 -17
  106. package/packages/daemon/src/providers/index.js +17 -0
  107. package/packages/daemon/src/registry.js +10 -1
  108. package/packages/daemon/src/rotator.js +93 -30
  109. package/packages/daemon/src/skills.js +33 -3
  110. package/packages/daemon/src/terminal-pty.js +9 -1
  111. package/packages/daemon/src/tool-executor.js +11 -5
  112. package/packages/daemon/src/toys.js +69 -0
  113. package/packages/daemon/src/tunnel-manager.js +24 -5
  114. package/packages/daemon/templates/toys-catalog.json +242 -0
  115. package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  116. package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  117. package/packages/gui/dist/index.html +3 -2
  118. package/packages/gui/index.html +1 -0
  119. package/packages/gui/src/app.css +7 -0
  120. package/packages/gui/src/app.jsx +37 -10
  121. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  122. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  123. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  124. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  125. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  126. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  127. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  128. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  129. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  130. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  131. package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
  132. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  133. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  134. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  135. package/packages/gui/src/components/layout/status-bar.jsx +11 -1
  136. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  137. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  138. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  139. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  140. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  141. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  142. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  143. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  144. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  145. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  146. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  147. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  148. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  149. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  150. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  151. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  152. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  153. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  154. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  155. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  156. package/packages/gui/src/components/ui/toast.jsx +2 -2
  157. package/packages/gui/src/lib/electron.js +15 -0
  158. package/packages/gui/src/lib/format.js +1 -0
  159. package/packages/gui/src/stores/groove.js +388 -63
  160. package/packages/gui/src/views/agents.jsx +148 -42
  161. package/packages/gui/src/views/editor.jsx +92 -2
  162. package/packages/gui/src/views/federation.jsx +37 -0
  163. package/packages/gui/src/views/marketplace.jsx +2 -42
  164. package/packages/gui/src/views/settings.jsx +35 -134
  165. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  166. package/packages/gui/src/views/teams.jsx +3 -3
  167. package/packages/gui/src/views/toys.jsx +162 -0
  168. package/plans/chat-persistence-refactor.md +154 -0
  169. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  170. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  171. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  172. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -1,20 +1,27 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useGrooveStore } from '../../stores/groove';
3
3
  import { UpgradeCard } from './upgrade-card';
4
+ import { isElectron } from '../../lib/electron';
4
5
 
5
- export function ProGate({ feature, description, children }) {
6
+ export function ProGate({ feature, featureKey, description, children }) {
6
7
  const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
7
- const user = useGrooveStore((s) => s.marketplaceUser);
8
+ const subscription = useGrooveStore((s) => s.subscription);
9
+ const edition = useGrooveStore((s) => s.edition);
8
10
 
9
- if (__GROOVE_EDITION__ !== 'pro') {
10
- return <UpgradeCard feature={feature} description={description} variant="community" />;
11
+ if (edition !== 'pro') {
12
+ const variant = isElectron() ? 'community-electron' : 'community';
13
+ return <UpgradeCard feature={feature} description={description} variant={variant} />;
11
14
  }
12
15
 
13
16
  if (!authenticated) {
14
17
  return <UpgradeCard feature={feature} description={description} variant="sign-in" />;
15
18
  }
16
19
 
17
- if (!user?.subscription?.active) {
20
+ if (!subscription?.active) {
21
+ return <UpgradeCard feature={feature} description={description} variant="subscribe" />;
22
+ }
23
+
24
+ if (featureKey && subscription?.plan !== 'pro' && !(subscription.features || []).includes(featureKey)) {
18
25
  return <UpgradeCard feature={feature} description={description} variant="subscribe" />;
19
26
  }
20
27
 
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { Lock, Download, LogIn, Sparkles } from 'lucide-react';
3
- import { openExternal } from '../../lib/electron';
3
+ import { isElectron, openExternal } from '../../lib/electron';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
 
6
6
  const VARIANTS = {
@@ -10,6 +10,12 @@ const VARIANTS = {
10
10
  icon: Download,
11
11
  action: () => openExternal('https://groovedev.ai/download'),
12
12
  },
13
+ 'community-electron': {
14
+ heading: 'Pro Feature',
15
+ cta: 'Upgrade to Pro',
16
+ icon: Sparkles,
17
+ action: () => useGrooveStore.getState().setUpgradeModalOpen(true),
18
+ },
13
19
  'sign-in': {
14
20
  heading: 'Sign in to unlock',
15
21
  cta: 'Sign in',
@@ -17,28 +23,29 @@ const VARIANTS = {
17
23
  action: () => useGrooveStore.getState().marketplaceLogin(),
18
24
  },
19
25
  subscribe: {
20
- heading: 'Upgrade to Pro',
21
- cta: 'Subscribe',
26
+ heading: 'Pro Feature',
27
+ cta: 'Upgrade to Pro',
22
28
  icon: Sparkles,
23
- action: () => openExternal('https://groovedev.ai/pro'),
29
+ action: () => useGrooveStore.getState().setUpgradeModalOpen(true),
24
30
  },
25
31
  };
26
32
 
27
33
  export function UpgradeCard({ feature, description, variant = 'community' }) {
28
- const v = VARIANTS[variant] || VARIANTS.community;
34
+ const resolvedVariant = variant === 'community' && isElectron() ? 'community-electron' : variant;
35
+ const v = VARIANTS[resolvedVariant] || VARIANTS.community;
29
36
  const CtaIcon = v.icon;
30
37
 
31
38
  return (
32
39
  <div className="rounded-lg border border-border-subtle bg-surface-1/50 px-5 py-6 text-center">
33
- <div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-purple/10">
34
- <Lock size={18} className="text-purple" />
40
+ <div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-accent/10">
41
+ <Lock size={18} className="text-accent" />
35
42
  </div>
36
43
  <h3 className="text-sm font-semibold text-text-1 font-sans">{v.heading}</h3>
37
44
  <p className="mt-1.5 text-2xs text-text-3 font-sans">{feature}</p>
38
45
  <p className="mt-1 text-2xs text-text-4 font-sans max-w-xs mx-auto">{description}</p>
39
46
  <button
40
47
  onClick={v.action}
41
- className="mt-4 inline-flex items-center gap-1.5 h-7 px-4 rounded-full bg-purple/15 text-purple text-xs font-semibold font-sans hover:bg-purple/25 transition-colors cursor-pointer"
48
+ className="mt-4 inline-flex items-center gap-1.5 h-7 px-4 rounded-full bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer"
42
49
  >
43
50
  <CtaIcon size={13} />
44
51
  {v.cta}
@@ -0,0 +1,151 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { Dialog, DialogContent } from '../ui/dialog';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+ import { openExternal } from '../../lib/electron';
6
+ import { cn } from '../../lib/cn';
7
+ import { Sparkles, Check, Radio, Server, Cloud, LogIn } from 'lucide-react';
8
+
9
+ const PRO_FEATURES = [
10
+ { icon: Radio, label: 'SSH Tunnel', desc: 'Connect to remote servers' },
11
+ { icon: Server, label: 'Federation', desc: 'Multi-machine daemon pairing' },
12
+ { icon: Cloud, label: 'Cloud Teams', desc: 'Coming soon' },
13
+ ];
14
+
15
+ export function UpgradeModal() {
16
+ const open = useGrooveStore(s => s.upgradeModalOpen);
17
+ const setOpen = useGrooveStore(s => s.setUpgradeModalOpen);
18
+ const authenticated = useGrooveStore(s => s.marketplaceAuthenticated);
19
+ const marketplaceLogin = useGrooveStore(s => s.marketplaceLogin);
20
+ const startCheckout = useGrooveStore(s => s.startCheckout);
21
+ const addToast = useGrooveStore(s => s.addToast);
22
+
23
+ const [plans, setPlans] = useState(null);
24
+ const [billing, setBilling] = useState('monthly');
25
+ const [loading, setLoading] = useState(false);
26
+
27
+ useEffect(() => {
28
+ if (open && !plans) {
29
+ useGrooveStore.getState().fetchSubscriptionPlans()
30
+ .then(p => setPlans(p))
31
+ .catch(() => {});
32
+ }
33
+ }, [open, plans]);
34
+
35
+ const price = plans?.pro?.[billing];
36
+ const displayPrice = billing === 'annual'
37
+ ? `$${Math.round((price?.price || 96) / 12)}/mo`
38
+ : `$${price?.price || 10}/mo`;
39
+
40
+ async function handleSubscribe() {
41
+ if (!authenticated) {
42
+ marketplaceLogin();
43
+ return;
44
+ }
45
+ if (!price?.priceId) {
46
+ openExternal('https://groovedev.ai/pro');
47
+ setOpen(false);
48
+ return;
49
+ }
50
+ setLoading(true);
51
+ try {
52
+ await startCheckout(price.priceId);
53
+ setOpen(false);
54
+ } catch (err) {
55
+ if (err.status === 401 || err.message?.includes('Not authenticated')) {
56
+ addToast('info', 'Please sign in first');
57
+ marketplaceLogin();
58
+ } else if (err.status === 409) {
59
+ addToast('info', 'You already have a subscription');
60
+ setOpen(false);
61
+ }
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ }
66
+
67
+ return (
68
+ <Dialog open={open} onOpenChange={setOpen}>
69
+ <DialogContent title="Upgrade to Pro" className="max-w-[440px]">
70
+ <div className="px-6 py-5">
71
+ <div className="text-center mb-6">
72
+ <div className="mx-auto mb-3 w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center">
73
+ <Sparkles size={22} className="text-accent" />
74
+ </div>
75
+ <h2 className="text-lg font-bold text-text-0">Upgrade to Groove Pro</h2>
76
+ <p className="text-sm text-text-2 mt-1">Unlock powerful features for your AI workflow</p>
77
+ </div>
78
+
79
+ <div className="space-y-3 mb-6">
80
+ {PRO_FEATURES.map(f => (
81
+ <div key={f.label} className="flex items-center gap-3">
82
+ <div className="w-8 h-8 rounded-md bg-surface-4 flex items-center justify-center flex-shrink-0">
83
+ <f.icon size={15} className="text-accent" />
84
+ </div>
85
+ <div>
86
+ <p className="text-sm font-medium text-text-0">{f.label}</p>
87
+ <p className="text-2xs text-text-3">{f.desc}</p>
88
+ </div>
89
+ <Check size={14} className="text-success ml-auto flex-shrink-0" />
90
+ </div>
91
+ ))}
92
+ </div>
93
+
94
+ <div className="flex gap-1 mb-5 bg-surface-3 p-0.5 rounded-md">
95
+ <button
96
+ type="button"
97
+ onClick={() => setBilling('monthly')}
98
+ className={cn(
99
+ 'flex-1 h-8 rounded text-xs font-medium transition-colors cursor-pointer',
100
+ billing === 'monthly' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
101
+ )}
102
+ >
103
+ Monthly
104
+ </button>
105
+ <button
106
+ type="button"
107
+ onClick={() => setBilling('annual')}
108
+ className={cn(
109
+ 'flex-1 h-8 rounded text-xs font-medium transition-colors cursor-pointer',
110
+ billing === 'annual' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
111
+ )}
112
+ >
113
+ Annual
114
+ <span className="ml-1 text-success text-2xs">Save 20%</span>
115
+ </button>
116
+ </div>
117
+
118
+ <div className="text-center mb-5">
119
+ <span className="text-3xl font-bold text-text-0">{displayPrice}</span>
120
+ {billing === 'annual' && (
121
+ <p className="text-2xs text-text-3 mt-1">Billed ${price?.price || 96}/year</p>
122
+ )}
123
+ </div>
124
+
125
+ <button
126
+ type="button"
127
+ onClick={handleSubscribe}
128
+ disabled={loading}
129
+ className="w-full h-10 rounded-lg bg-accent text-white font-semibold text-sm hover:bg-accent/90 transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2"
130
+ >
131
+ {loading ? (
132
+ 'Processing...'
133
+ ) : !authenticated ? (
134
+ <><LogIn size={15} /> Sign in to subscribe</>
135
+ ) : (
136
+ <><Sparkles size={15} /> Subscribe — {displayPrice}</>
137
+ )}
138
+ </button>
139
+
140
+ <button
141
+ type="button"
142
+ onClick={() => setOpen(false)}
143
+ className="w-full mt-2 text-xs text-text-4 hover:text-text-2 transition-colors cursor-pointer py-1"
144
+ >
145
+ Maybe later
146
+ </button>
147
+ </div>
148
+ </DialogContent>
149
+ </Dialog>
150
+ );
151
+ }
@@ -0,0 +1,98 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { Badge } from '../ui/badge';
5
+ import { ScrollArea } from '../ui/scroll-area';
6
+ import { cn } from '../../lib/cn';
7
+ import { timeAgo } from '../../lib/format';
8
+ import {
9
+ ArrowUpRight, ArrowDownLeft, MessageSquare,
10
+ } from 'lucide-react';
11
+
12
+ const FILTERS = ['All', 'Sent', 'Received'];
13
+
14
+ export function FederationActivity() {
15
+ const pouchLog = useGrooveStore((s) => s.federation.pouchLog);
16
+ const [filter, setFilter] = useState('All');
17
+
18
+ const filtered = filter === 'All'
19
+ ? pouchLog
20
+ : pouchLog.filter((e) => e.direction === filter.toLowerCase());
21
+
22
+ const entries = [...filtered].reverse().slice(0, 200);
23
+
24
+ return (
25
+ <div className="space-y-3">
26
+ <div className="flex items-center justify-between">
27
+ <div className="flex items-center gap-2">
28
+ <MessageSquare size={12} className="text-accent" />
29
+ <span className="text-xs font-semibold text-text-1 font-sans">Activity</span>
30
+ {pouchLog.length > 0 && (
31
+ <Badge variant="default" className="text-2xs">{pouchLog.length}</Badge>
32
+ )}
33
+ </div>
34
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
35
+ {FILTERS.map((f) => (
36
+ <button
37
+ key={f}
38
+ onClick={() => setFilter(f)}
39
+ className={cn(
40
+ 'px-2.5 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
41
+ filter === f
42
+ ? 'bg-accent/15 text-accent shadow-sm'
43
+ : 'text-text-3 hover:text-text-1',
44
+ )}
45
+ >
46
+ {f}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ </div>
51
+
52
+ {entries.length === 0 ? (
53
+ <div className="rounded-lg border border-dashed border-border-subtle bg-surface-1/50 px-4 py-6 text-center">
54
+ <MessageSquare size={18} className="text-text-4 mx-auto mb-1.5" />
55
+ <p className="text-2xs text-text-4 font-sans">
56
+ {filter === 'All'
57
+ ? 'No diplomatic pouches exchanged yet.'
58
+ : `No ${filter.toLowerCase()} pouches.`}
59
+ </p>
60
+ </div>
61
+ ) : (
62
+ <ScrollArea className="max-h-80">
63
+ <div className="relative pl-5">
64
+ <div className="absolute left-[7px] top-2 bottom-2 w-px bg-border-subtle" />
65
+
66
+ {entries.map((entry, i) => {
67
+ const isSent = entry.direction === 'sent';
68
+ return (
69
+ <div key={entry.id || i} className="relative flex items-start gap-3 pb-3 last:pb-0">
70
+ <div className={cn(
71
+ 'absolute left-[-13px] top-1.5 w-2.5 h-2.5 rounded-full border-2 border-surface-2 z-10',
72
+ isSent ? 'bg-accent' : 'bg-success',
73
+ )} />
74
+ <div className="flex items-center gap-2 flex-1 rounded-md bg-surface-1 px-3 py-2 min-w-0">
75
+ {isSent ? (
76
+ <ArrowUpRight size={11} className="text-accent flex-shrink-0" />
77
+ ) : (
78
+ <ArrowDownLeft size={11} className="text-success flex-shrink-0" />
79
+ )}
80
+ <span className="text-2xs text-text-1 font-sans truncate flex-1">
81
+ {entry.contractType || entry.type || 'message'}
82
+ </span>
83
+ <span className="text-2xs text-text-4 font-mono truncate max-w-24">
84
+ {entry.peerId || ''}
85
+ </span>
86
+ <span className="text-2xs text-text-3 font-sans flex-shrink-0">
87
+ {timeAgo(entry.timestamp || entry.ts)}
88
+ </span>
89
+ </div>
90
+ </div>
91
+ );
92
+ })}
93
+ </div>
94
+ </ScrollArea>
95
+ )}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,290 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
5
+ import { Button } from '../ui/button';
6
+ import { Badge } from '../ui/badge';
7
+ import { Input } from '../ui/input';
8
+ import { StatusDot } from '../ui/status-dot';
9
+ import { cn } from '../../lib/cn';
10
+ import { FederationPeers } from './federation-peers';
11
+ import { FederationActivity } from './federation-activity';
12
+ import { FederationWizard } from './federation-wizard';
13
+ import {
14
+ Shield, Plus, Trash2, Loader2, Globe, Users, Search, ChevronUp,
15
+ } from 'lucide-react';
16
+
17
+ function statusBadge(status) {
18
+ switch (status) {
19
+ case 'mutual': return <Badge variant="success" className="text-2xs gap-1"><StatusDot status="running" size="sm" /> Mutual</Badge>;
20
+ case 'connected': return <Badge variant="info" className="text-2xs">Connected</Badge>;
21
+ default: return <Badge variant="default" className="text-2xs">Waiting</Badge>;
22
+ }
23
+ }
24
+
25
+ function WhitelistTab() {
26
+ const whitelist = useGrooveStore((s) => s.federation.whitelist);
27
+ const addToWhitelist = useGrooveStore((s) => s.addToWhitelist);
28
+ const removeFromWhitelist = useGrooveStore((s) => s.removeFromWhitelist);
29
+
30
+ const [showForm, setShowForm] = useState(false);
31
+ const [ip, setIp] = useState('');
32
+ const [port, setPort] = useState('31415');
33
+ const [serverName, setServerName] = useState('');
34
+ const [adding, setAdding] = useState(false);
35
+ const [search, setSearch] = useState('');
36
+
37
+ async function handleAdd(e) {
38
+ e.preventDefault();
39
+ if (!ip.trim()) return;
40
+ setAdding(true);
41
+ try {
42
+ await addToWhitelist(ip.trim(), parseInt(port, 10) || 31415, serverName.trim() || undefined);
43
+ setIp('');
44
+ setPort('31415');
45
+ setServerName('');
46
+ setShowForm(false);
47
+ } catch {}
48
+ setAdding(false);
49
+ }
50
+
51
+ const filtered = useMemo(() => {
52
+ if (!search.trim()) return whitelist;
53
+ const q = search.toLowerCase();
54
+ return whitelist.filter((entry) => {
55
+ const key = typeof entry === 'string' ? entry : entry.ip;
56
+ const name = typeof entry === 'object' ? entry.name : '';
57
+ return key?.toLowerCase().includes(q) || name?.toLowerCase().includes(q);
58
+ });
59
+ }, [whitelist, search]);
60
+
61
+ return (
62
+ <div className="space-y-3">
63
+ <div className="flex items-center justify-between">
64
+ <div className="flex items-center gap-2">
65
+ <Shield size={12} className="text-accent" />
66
+ <span className="text-xs font-semibold text-text-1 font-sans">Whitelist</span>
67
+ {whitelist.length > 0 && (
68
+ <Badge variant="default" className="text-2xs">{whitelist.length}</Badge>
69
+ )}
70
+ </div>
71
+ <Button
72
+ size="sm"
73
+ variant={showForm ? 'ghost' : 'primary'}
74
+ onClick={() => setShowForm(!showForm)}
75
+ className="h-7 text-2xs gap-1.5"
76
+ >
77
+ {showForm ? (
78
+ <><ChevronUp size={11} /> Hide</>
79
+ ) : (
80
+ <><Plus size={11} /> Add Server</>
81
+ )}
82
+ </Button>
83
+ </div>
84
+
85
+ <div className="grid grid-cols-2 gap-3">
86
+ {/* Add form card */}
87
+ {showForm && (
88
+ <form onSubmit={handleAdd} className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5">
89
+ <div className="flex items-center gap-2 mb-3">
90
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
91
+ <Plus size={12} className="text-accent" />
92
+ </div>
93
+ <span className="text-[13px] font-medium text-text-0 font-sans">New Server</span>
94
+ </div>
95
+ <div className="space-y-2.5">
96
+ <div>
97
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">Name</label>
98
+ <Input
99
+ placeholder="Server name"
100
+ value={serverName}
101
+ onChange={(e) => setServerName(e.target.value)}
102
+ className="h-7 text-xs"
103
+ />
104
+ </div>
105
+ <div>
106
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">IP Address</label>
107
+ <Input
108
+ placeholder="100.64.0.2"
109
+ value={ip}
110
+ onChange={(e) => setIp(e.target.value)}
111
+ mono
112
+ className="h-7 text-xs"
113
+ />
114
+ </div>
115
+ <div>
116
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">Port</label>
117
+ <Input
118
+ placeholder="31415"
119
+ value={port}
120
+ onChange={(e) => setPort(e.target.value)}
121
+ mono
122
+ className="h-7 text-xs w-28"
123
+ />
124
+ </div>
125
+ <div className="pt-1">
126
+ <Button type="submit" variant="primary" size="sm" disabled={adding || !ip.trim()} className="h-7 text-2xs gap-1">
127
+ {adding ? <Loader2 size={11} className="animate-spin" /> : <Plus size={11} />}
128
+ Add to Whitelist
129
+ </Button>
130
+ </div>
131
+ </div>
132
+ </form>
133
+ )}
134
+
135
+ {/* Server list card */}
136
+ <div className={cn(
137
+ 'rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5',
138
+ !showForm && 'col-span-2',
139
+ )}>
140
+ <div className="flex items-center gap-2 mb-3">
141
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
142
+ <Globe size={12} className="text-accent" />
143
+ </div>
144
+ <span className="text-[13px] font-medium text-text-0 font-sans">Servers</span>
145
+ {whitelist.length >= 5 && (
146
+ <div className="relative ml-auto">
147
+ <Search size={10} className="absolute left-2 top-1/2 -translate-y-1/2 text-text-4" />
148
+ <input
149
+ type="text"
150
+ placeholder="Filter…"
151
+ value={search}
152
+ onChange={(e) => setSearch(e.target.value)}
153
+ className="h-6 pl-6 pr-2 w-32 text-2xs font-sans bg-surface-0 border border-border-subtle rounded-md text-text-0 placeholder:text-text-4 focus:outline-none focus:border-accent"
154
+ />
155
+ </div>
156
+ )}
157
+ </div>
158
+
159
+ {filtered.length === 0 && whitelist.length === 0 ? (
160
+ <div className="px-2 py-4 text-center">
161
+ <Globe size={16} className="text-text-4 mx-auto mb-1.5" />
162
+ <p className="text-2xs text-text-4 font-sans">No peers whitelisted yet.</p>
163
+ </div>
164
+ ) : filtered.length === 0 ? (
165
+ <div className="text-2xs text-text-4 font-sans py-3 text-center">No servers match your filter.</div>
166
+ ) : (
167
+ <div className="space-y-1.5">
168
+ {filtered.map((entry) => {
169
+ const key = typeof entry === 'string' ? entry : entry.ip;
170
+ const entryName = typeof entry === 'object' ? entry.name : null;
171
+ const status = typeof entry === 'object' ? entry.status : 'waiting';
172
+ const entryPort = typeof entry === 'object' ? entry.port : null;
173
+
174
+ return (
175
+ <div key={key} className="flex items-center gap-2.5 px-2.5 py-2 rounded-md bg-surface-0 border border-border-subtle">
176
+ <div className="flex-1 min-w-0">
177
+ {entryName && <span className="text-xs font-sans font-medium text-text-0 block truncate">{entryName}</span>}
178
+ <span className={cn('font-mono truncate block', entryName ? 'text-2xs text-text-3' : 'text-xs text-text-1')}>
179
+ {key}{entryPort ? `:${entryPort}` : ''}
180
+ </span>
181
+ </div>
182
+ {statusBadge(status)}
183
+ <button
184
+ onClick={() => removeFromWhitelist(key)}
185
+ className="p-1 rounded text-text-4 hover:text-danger hover:bg-danger/10 cursor-pointer transition-colors flex-shrink-0"
186
+ title="Remove"
187
+ >
188
+ <Trash2 size={11} />
189
+ </button>
190
+ </div>
191
+ );
192
+ })}
193
+ </div>
194
+ )}
195
+ </div>
196
+ </div>
197
+ </div>
198
+ );
199
+ }
200
+
201
+ function AmbassadorsTab() {
202
+ const ambassadors = useGrooveStore((s) => s.federation.ambassadors);
203
+
204
+ return (
205
+ <div className="space-y-3">
206
+ <div className="flex items-center gap-2">
207
+ <Users size={12} className="text-accent" />
208
+ <span className="text-xs font-semibold text-text-1 font-sans">Ambassadors</span>
209
+ {ambassadors.length > 0 && (
210
+ <Badge variant="info" className="text-2xs">{ambassadors.length}</Badge>
211
+ )}
212
+ </div>
213
+
214
+ {ambassadors.length === 0 ? (
215
+ <div className="rounded-lg border border-dashed border-border-subtle bg-surface-1/50 px-4 py-6 text-center">
216
+ <Users size={18} className="text-text-4 mx-auto mb-1.5" />
217
+ <p className="text-2xs text-text-4 font-sans">No active ambassadors. Ambassadors appear when agents are shared across federated peers.</p>
218
+ </div>
219
+ ) : (
220
+ <div className="grid grid-cols-2 gap-2">
221
+ {ambassadors.map((amb) => (
222
+ <div key={amb.id || amb.agentId} className="rounded-lg border border-border-subtle bg-surface-1 px-3.5 py-3">
223
+ <div className="flex items-center gap-2 mb-1.5">
224
+ <StatusDot status={amb.status === 'active' ? 'running' : 'stopped'} size="sm" />
225
+ <span className="text-xs font-sans font-medium text-text-0 truncate flex-1">
226
+ {amb.name || amb.agentId || amb.id}
227
+ </span>
228
+ {amb.role && (
229
+ <Badge variant="purple" className="text-2xs">{amb.role}</Badge>
230
+ )}
231
+ </div>
232
+ <div className="flex items-center justify-between text-2xs">
233
+ {amb.peerId ? (
234
+ <span className="text-text-3 font-mono truncate max-w-28">{amb.peerId}</span>
235
+ ) : (
236
+ <span className="text-text-4 font-sans">Local</span>
237
+ )}
238
+ <Badge variant={amb.status === 'active' ? 'success' : 'default'} className="text-2xs">
239
+ {amb.status || 'idle'}
240
+ </Badge>
241
+ </div>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ )}
246
+ </div>
247
+ );
248
+ }
249
+
250
+ export function FederationPanel() {
251
+ const fetchFederationStatus = useGrooveStore((s) => s.fetchFederationStatus);
252
+ const fetchPouchLog = useGrooveStore((s) => s.fetchPouchLog);
253
+ const [wizardOpen, setWizardOpen] = useState(false);
254
+
255
+ useEffect(() => {
256
+ fetchFederationStatus();
257
+ fetchPouchLog();
258
+ }, []);
259
+
260
+ return (
261
+ <div>
262
+ <Tabs defaultValue="whitelist">
263
+ <TabsList className="mb-4">
264
+ <TabsTrigger value="whitelist" className="text-xs px-3 py-1.5">Whitelist</TabsTrigger>
265
+ <TabsTrigger value="peers" className="text-xs px-3 py-1.5">Peers</TabsTrigger>
266
+ <TabsTrigger value="ambassadors" className="text-xs px-3 py-1.5">Ambassadors</TabsTrigger>
267
+ <TabsTrigger value="activity" className="text-xs px-3 py-1.5">Activity</TabsTrigger>
268
+ </TabsList>
269
+
270
+ <TabsContent value="whitelist">
271
+ <WhitelistTab />
272
+ </TabsContent>
273
+
274
+ <TabsContent value="peers">
275
+ <FederationPeers onOpenWizard={() => setWizardOpen(true)} />
276
+ </TabsContent>
277
+
278
+ <TabsContent value="ambassadors">
279
+ <AmbassadorsTab />
280
+ </TabsContent>
281
+
282
+ <TabsContent value="activity">
283
+ <FederationActivity />
284
+ </TabsContent>
285
+ </Tabs>
286
+
287
+ <FederationWizard open={wizardOpen} onOpenChange={setWizardOpen} />
288
+ </div>
289
+ );
290
+ }