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.
- package/CLAUDE.md +0 -10
- package/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +586 -67
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +586 -67
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +14 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
- package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/status-bar.jsx +11 -1
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +388 -63
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +35 -134
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- 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
|
|
8
|
+
const subscription = useGrooveStore((s) => s.subscription);
|
|
9
|
+
const edition = useGrooveStore((s) => s.edition);
|
|
8
10
|
|
|
9
|
-
if (
|
|
10
|
-
|
|
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 (!
|
|
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: '
|
|
21
|
-
cta: '
|
|
26
|
+
heading: 'Pro Feature',
|
|
27
|
+
cta: 'Upgrade to Pro',
|
|
22
28
|
icon: Sparkles,
|
|
23
|
-
action: () =>
|
|
29
|
+
action: () => useGrooveStore.getState().setUpgradeModalOpen(true),
|
|
24
30
|
},
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
export function UpgradeCard({ feature, description, variant = 'community' }) {
|
|
28
|
-
const
|
|
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-
|
|
34
|
-
<Lock size={18} className="text-
|
|
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-
|
|
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
|
+
}
|