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
|
@@ -12,15 +12,12 @@ import { Sheet, SheetContent } from '../components/ui/sheet';
|
|
|
12
12
|
import { api } from '../lib/api';
|
|
13
13
|
import { cn } from '../lib/cn';
|
|
14
14
|
import { fmtUptime } from '../lib/format';
|
|
15
|
-
import { RemoteServerCard } from '../components/settings/remote-server-card';
|
|
16
|
-
import { ServerDialog } from '../components/settings/server-dialog';
|
|
17
|
-
import { ProGate } from '../components/pro/pro-gate';
|
|
18
15
|
import {
|
|
19
|
-
Key, Eye, EyeOff, Check, Cpu,
|
|
20
|
-
FolderOpen, FolderSearch,
|
|
21
|
-
|
|
22
|
-
Newspaper,
|
|
23
|
-
Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
|
|
16
|
+
Key, Eye, EyeOff, Check, Cpu,
|
|
17
|
+
FolderOpen, FolderSearch, Users, Gauge,
|
|
18
|
+
ShieldCheck, Settings,
|
|
19
|
+
Newspaper, Radio, Send, MessageSquare, MessageCircle,
|
|
20
|
+
Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
|
|
24
21
|
} from 'lucide-react';
|
|
25
22
|
|
|
26
23
|
/* ── Toggle ────────────────────────────────────────────────── */
|
|
@@ -42,32 +39,6 @@ function Toggle({ value, onChange }) {
|
|
|
42
39
|
);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
/* ── Profile Pic ──────────────────────────────────────────── */
|
|
46
|
-
|
|
47
|
-
function ProfilePic({ user }) {
|
|
48
|
-
const [broken, setBroken] = useState(false);
|
|
49
|
-
const src = user?.avatar || user?.picture || user?.photoURL || user?.photo;
|
|
50
|
-
|
|
51
|
-
if (src && !broken) {
|
|
52
|
-
return (
|
|
53
|
-
<img
|
|
54
|
-
src={src}
|
|
55
|
-
alt=""
|
|
56
|
-
className="w-6 h-6 rounded-full"
|
|
57
|
-
referrerPolicy="no-referrer"
|
|
58
|
-
crossOrigin="anonymous"
|
|
59
|
-
onError={() => setBroken(true)}
|
|
60
|
-
/>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className="w-6 h-6 rounded-full bg-accent/10 flex items-center justify-center">
|
|
66
|
-
<User size={12} className="text-accent" />
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
42
|
/* ── Provider Card ─────────────────────────────────────────── */
|
|
72
43
|
|
|
73
44
|
function ProviderCard({ provider, onKeyChange }) {
|
|
@@ -79,13 +50,14 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
79
50
|
|
|
80
51
|
const isLocal = provider.authType === 'local';
|
|
81
52
|
const isSubscription = provider.authType === 'subscription';
|
|
82
|
-
|
|
83
|
-
|
|
53
|
+
const isReady = isLocal ? provider.installed
|
|
54
|
+
: isSubscription ? (provider.installed || provider.authStatus?.authenticated)
|
|
55
|
+
: provider.hasKey;
|
|
84
56
|
|
|
85
57
|
async function handleSetKey() {
|
|
86
58
|
if (!keyInput.trim()) return;
|
|
87
59
|
try {
|
|
88
|
-
await api.post(`/credentials/${provider.id}`, { key: keyInput.trim() });
|
|
60
|
+
await api.post(`/credentials/${encodeURIComponent(provider.id)}`, { key: keyInput.trim() });
|
|
89
61
|
addToast('success', `API key set for ${provider.name}`);
|
|
90
62
|
setKeyInput('');
|
|
91
63
|
setSettingKey(false);
|
|
@@ -97,7 +69,7 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
97
69
|
|
|
98
70
|
async function handleDeleteKey() {
|
|
99
71
|
try {
|
|
100
|
-
await api.delete(`/credentials/${provider.id}`);
|
|
72
|
+
await api.delete(`/credentials/${encodeURIComponent(provider.id)}`);
|
|
101
73
|
addToast('info', `Removed ${provider.name} key`);
|
|
102
74
|
if (onKeyChange) onKeyChange();
|
|
103
75
|
} catch (err) {
|
|
@@ -577,7 +549,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
577
549
|
// Fetch channels when connected Slack gateway has no chatId
|
|
578
550
|
useEffect(() => {
|
|
579
551
|
if (gateway.connected && !gateway.chatId && gateway.type === 'slack') {
|
|
580
|
-
api.get(`/gateways/${gateway.id}/channels`).then((ch) => setChannels(Array.isArray(ch) ? ch : [])).catch(() => {});
|
|
552
|
+
api.get(`/gateways/${encodeURIComponent(gateway.id)}/channels`).then((ch) => setChannels(Array.isArray(ch) ? ch : [])).catch(() => {});
|
|
581
553
|
}
|
|
582
554
|
}, [gateway.connected, gateway.chatId, gateway.id, gateway.type]);
|
|
583
555
|
|
|
@@ -587,9 +559,9 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
587
559
|
async function handleSaveToken() {
|
|
588
560
|
if (!tokenInput.trim()) return;
|
|
589
561
|
try {
|
|
590
|
-
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'bot_token', value: tokenInput.trim() });
|
|
562
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/credentials`, { key: 'bot_token', value: tokenInput.trim() });
|
|
591
563
|
if (isSlack && appTokenInput.trim()) {
|
|
592
|
-
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'app_token', value: appTokenInput.trim() });
|
|
564
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/credentials`, { key: 'app_token', value: appTokenInput.trim() });
|
|
593
565
|
}
|
|
594
566
|
addToast('success', `Token saved — connecting...`);
|
|
595
567
|
setTokenInput('');
|
|
@@ -597,7 +569,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
597
569
|
setSettingToken(false);
|
|
598
570
|
// Auto-connect after saving tokens
|
|
599
571
|
try {
|
|
600
|
-
await api.post(`/gateways/${gateway.id}/connect`);
|
|
572
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/connect`);
|
|
601
573
|
addToast('success', `${GATEWAY_LABELS[gateway.type]} connected!`);
|
|
602
574
|
} catch (connErr) {
|
|
603
575
|
addToast('error', 'Token saved but connect failed', connErr.message);
|
|
@@ -611,7 +583,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
611
583
|
async function handleTest() {
|
|
612
584
|
setTesting(true);
|
|
613
585
|
try {
|
|
614
|
-
await api.post(`/gateways/${gateway.id}/test`);
|
|
586
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/test`);
|
|
615
587
|
addToast('success', 'Test message sent!');
|
|
616
588
|
} catch (err) {
|
|
617
589
|
addToast('error', 'Test failed', err.message);
|
|
@@ -623,10 +595,10 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
623
595
|
setConnecting(true);
|
|
624
596
|
try {
|
|
625
597
|
if (gateway.connected) {
|
|
626
|
-
await api.post(`/gateways/${gateway.id}/disconnect`);
|
|
598
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/disconnect`);
|
|
627
599
|
addToast('info', `${GATEWAY_LABELS[gateway.type]} disconnected`);
|
|
628
600
|
} else {
|
|
629
|
-
await api.post(`/gateways/${gateway.id}/connect`);
|
|
601
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/connect`);
|
|
630
602
|
addToast('success', `${GATEWAY_LABELS[gateway.type]} connected!`);
|
|
631
603
|
}
|
|
632
604
|
onRefresh();
|
|
@@ -638,7 +610,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
638
610
|
|
|
639
611
|
async function handleToggleEnabled(enabled) {
|
|
640
612
|
try {
|
|
641
|
-
await api.patch(`/gateways/${gateway.id}`, { enabled });
|
|
613
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { enabled });
|
|
642
614
|
onRefresh();
|
|
643
615
|
} catch (err) {
|
|
644
616
|
addToast('error', 'Update failed', err.message);
|
|
@@ -647,7 +619,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
647
619
|
|
|
648
620
|
async function handlePresetChange(preset) {
|
|
649
621
|
try {
|
|
650
|
-
await api.patch(`/gateways/${gateway.id}`, { notifications: { preset } });
|
|
622
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { notifications: { preset } });
|
|
651
623
|
onRefresh();
|
|
652
624
|
} catch (err) {
|
|
653
625
|
addToast('error', 'Update failed', err.message);
|
|
@@ -656,7 +628,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
656
628
|
|
|
657
629
|
async function handlePermissionChange(perm) {
|
|
658
630
|
try {
|
|
659
|
-
await api.patch(`/gateways/${gateway.id}`, { commandPermission: perm });
|
|
631
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { commandPermission: perm });
|
|
660
632
|
onRefresh();
|
|
661
633
|
} catch (err) {
|
|
662
634
|
addToast('error', 'Update failed', err.message);
|
|
@@ -665,7 +637,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
665
637
|
|
|
666
638
|
async function handleDelete() {
|
|
667
639
|
try {
|
|
668
|
-
await api.delete(`/gateways/${gateway.id}`);
|
|
640
|
+
await api.delete(`/gateways/${encodeURIComponent(gateway.id)}`);
|
|
669
641
|
addToast('info', `${GATEWAY_LABELS[gateway.type]} gateway removed`);
|
|
670
642
|
onRefresh();
|
|
671
643
|
} catch (err) {
|
|
@@ -718,7 +690,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
718
690
|
</code>
|
|
719
691
|
<button
|
|
720
692
|
onClick={async () => {
|
|
721
|
-
try { await api.patch(`/gateways/${gateway.id}`, { chatId: null }); onRefresh(); }
|
|
693
|
+
try { await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: null }); onRefresh(); }
|
|
722
694
|
catch (err) { addToast('error', 'Failed', err.message); }
|
|
723
695
|
}}
|
|
724
696
|
className="text-2xs text-text-4 hover:text-text-1 cursor-pointer font-sans"
|
|
@@ -729,7 +701,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
729
701
|
onChange={async (e) => {
|
|
730
702
|
if (!e.target.value) return;
|
|
731
703
|
try {
|
|
732
|
-
await api.patch(`/gateways/${gateway.id}`, { chatId: e.target.value });
|
|
704
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: e.target.value });
|
|
733
705
|
onRefresh();
|
|
734
706
|
} catch (err) { addToast('error', 'Failed to set channel', err.message); }
|
|
735
707
|
}}
|
|
@@ -752,7 +724,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
752
724
|
onKeyDown={async (e) => {
|
|
753
725
|
if (e.key === 'Enter' && e.target.value.trim()) {
|
|
754
726
|
try {
|
|
755
|
-
await api.patch(`/gateways/${gateway.id}`, { chatId: e.target.value.trim() });
|
|
727
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: e.target.value.trim() });
|
|
756
728
|
onRefresh();
|
|
757
729
|
} catch (err) { addToast('error', 'Failed to set channel', err.message); }
|
|
758
730
|
}
|
|
@@ -977,14 +949,7 @@ export default function SettingsView() {
|
|
|
977
949
|
const [gwList, setGwList] = useState([]);
|
|
978
950
|
const [loading, setLoading] = useState(true);
|
|
979
951
|
const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
|
|
980
|
-
const [serverDialogOpen, setServerDialogOpen] = useState(false);
|
|
981
|
-
const [editingServer, setEditingServer] = useState(null);
|
|
982
|
-
const savedTunnels = useGrooveStore((s) => s.savedTunnels);
|
|
983
952
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
984
|
-
const marketplaceUser = useGrooveStore((s) => s.marketplaceUser);
|
|
985
|
-
const marketplaceAuthenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
|
|
986
|
-
const marketplaceLogin = useGrooveStore((s) => s.marketplaceLogin);
|
|
987
|
-
const marketplaceLogout = useGrooveStore((s) => s.marketplaceLogout);
|
|
988
953
|
|
|
989
954
|
function loadProviders() {
|
|
990
955
|
api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
|
|
@@ -998,7 +963,6 @@ export default function SettingsView() {
|
|
|
998
963
|
Promise.all([api.get('/providers'), api.get('/config'), api.get('/status'), api.get('/gateways')])
|
|
999
964
|
.then(([p, c, s, g]) => { setProviders(Array.isArray(p) ? p : []); setConfig(c); setDaemonInfo(s); setGwList(Array.isArray(g) ? g : []); setLoading(false); })
|
|
1000
965
|
.catch(() => setLoading(false));
|
|
1001
|
-
useGrooveStore.getState().fetchTunnels();
|
|
1002
966
|
}, []);
|
|
1003
967
|
|
|
1004
968
|
async function addGateway(type) {
|
|
@@ -1056,22 +1020,6 @@ export default function SettingsView() {
|
|
|
1056
1020
|
{daemonInfo?.uptime > 0 && <span>Up {fmtUptime(daemonInfo.uptime)}</span>}
|
|
1057
1021
|
</div>
|
|
1058
1022
|
|
|
1059
|
-
<div className="w-px h-4 bg-border-subtle" />
|
|
1060
|
-
|
|
1061
|
-
{marketplaceAuthenticated ? (
|
|
1062
|
-
<div className="flex items-center gap-2.5">
|
|
1063
|
-
<ProfilePic user={marketplaceUser} />
|
|
1064
|
-
<span className="text-xs font-medium text-text-0 font-sans">{marketplaceUser?.displayName || 'User'}</span>
|
|
1065
|
-
<button onClick={marketplaceLogout} className="text-2xs text-text-4 hover:text-text-1 cursor-pointer font-sans flex items-center gap-1">
|
|
1066
|
-
<LogOut size={10} /> Sign out
|
|
1067
|
-
</button>
|
|
1068
|
-
</div>
|
|
1069
|
-
) : (
|
|
1070
|
-
<Button variant="ghost" size="sm" onClick={marketplaceLogin} className="h-7 text-2xs gap-1.5 text-text-3">
|
|
1071
|
-
<LogIn size={11} /> Sign in
|
|
1072
|
-
</Button>
|
|
1073
|
-
)}
|
|
1074
|
-
|
|
1075
1023
|
<StatusDot status="running" size="sm" />
|
|
1076
1024
|
</div>
|
|
1077
1025
|
|
|
@@ -1137,7 +1085,17 @@ export default function SettingsView() {
|
|
|
1137
1085
|
<code className="flex-1 h-8 px-2 flex items-center bg-surface-0 border border-border-subtle rounded-md text-2xs font-mono text-text-2 truncate min-w-0">
|
|
1138
1086
|
{config.defaultWorkingDir || 'Project root'}
|
|
1139
1087
|
</code>
|
|
1140
|
-
<Button variant="secondary" size="sm" onClick={() =>
|
|
1088
|
+
<Button variant="secondary" size="sm" onClick={async () => {
|
|
1089
|
+
if (window.groove?.folders?.select) {
|
|
1090
|
+
const dir = await window.groove.folders.select({
|
|
1091
|
+
title: 'Select Working Directory',
|
|
1092
|
+
defaultPath: config?.defaultWorkingDir || undefined,
|
|
1093
|
+
});
|
|
1094
|
+
if (dir) updateConfig('defaultWorkingDir', dir);
|
|
1095
|
+
} else {
|
|
1096
|
+
setFolderBrowserOpen(true);
|
|
1097
|
+
}
|
|
1098
|
+
}} className="h-8 px-2 flex-shrink-0">
|
|
1141
1099
|
<FolderSearch size={12} />
|
|
1142
1100
|
</Button>
|
|
1143
1101
|
</div>
|
|
@@ -1229,67 +1187,10 @@ export default function SettingsView() {
|
|
|
1229
1187
|
</div>
|
|
1230
1188
|
)}
|
|
1231
1189
|
|
|
1232
|
-
{/* ═══════ REMOTE SERVERS ═══════ */}
|
|
1233
|
-
<div>
|
|
1234
|
-
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
1235
|
-
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Remote Servers</span>
|
|
1236
|
-
<div className="flex-1 h-px bg-border-subtle" />
|
|
1237
|
-
</div>
|
|
1238
|
-
<ProGate feature="Remote Access" description="Connect to remote servers via SSH tunnel and manage agents across machines">
|
|
1239
|
-
<div>
|
|
1240
|
-
<div className="flex justify-end mb-2.5">
|
|
1241
|
-
<Button
|
|
1242
|
-
variant="ghost"
|
|
1243
|
-
size="sm"
|
|
1244
|
-
onClick={() => { setEditingServer(null); setServerDialogOpen(true); }}
|
|
1245
|
-
className="h-6 text-2xs gap-1 text-text-3 hover:text-accent"
|
|
1246
|
-
>
|
|
1247
|
-
<Plus size={11} /> Add Server
|
|
1248
|
-
</Button>
|
|
1249
|
-
</div>
|
|
1250
|
-
{savedTunnels.length === 0 ? (
|
|
1251
|
-
<div className="rounded-lg border border-dashed border-border-subtle bg-surface-1/50 px-4 py-6 text-center">
|
|
1252
|
-
<Radio size={20} className="text-text-4 mx-auto mb-2" />
|
|
1253
|
-
<p className="text-xs text-text-3 font-sans">No remote servers configured.</p>
|
|
1254
|
-
<p className="text-2xs text-text-4 font-sans mt-1">Add one to connect to a VPS or remote machine.</p>
|
|
1255
|
-
</div>
|
|
1256
|
-
) : (
|
|
1257
|
-
<div className="grid grid-cols-2 gap-3">
|
|
1258
|
-
{savedTunnels.map((server) => (
|
|
1259
|
-
<RemoteServerCard
|
|
1260
|
-
key={server.id}
|
|
1261
|
-
server={server}
|
|
1262
|
-
onConnect={() => useGrooveStore.getState().connectTunnel(server.id)}
|
|
1263
|
-
onDisconnect={() => useGrooveStore.getState().disconnectTunnel(server.id)}
|
|
1264
|
-
onTest={() => useGrooveStore.getState().testTunnel(server.id)}
|
|
1265
|
-
onEdit={(s) => { setEditingServer(s); setServerDialogOpen(true); }}
|
|
1266
|
-
onDelete={(id) => useGrooveStore.getState().deleteTunnel(id)}
|
|
1267
|
-
/>
|
|
1268
|
-
))}
|
|
1269
|
-
</div>
|
|
1270
|
-
)}
|
|
1271
|
-
</div>
|
|
1272
|
-
</ProGate>
|
|
1273
|
-
</div>
|
|
1274
1190
|
|
|
1275
1191
|
</div>
|
|
1276
1192
|
</ScrollArea>
|
|
1277
1193
|
|
|
1278
|
-
{/* Server Dialog */}
|
|
1279
|
-
<ServerDialog
|
|
1280
|
-
open={serverDialogOpen}
|
|
1281
|
-
onOpenChange={setServerDialogOpen}
|
|
1282
|
-
server={editingServer}
|
|
1283
|
-
onSave={async (data) => {
|
|
1284
|
-
if (data.id) {
|
|
1285
|
-
await useGrooveStore.getState().updateTunnel(data.id, data);
|
|
1286
|
-
} else {
|
|
1287
|
-
await useGrooveStore.getState().saveTunnel(data);
|
|
1288
|
-
}
|
|
1289
|
-
addToast('success', data.id ? 'Server updated' : 'Server added');
|
|
1290
|
-
}}
|
|
1291
|
-
/>
|
|
1292
|
-
|
|
1293
1194
|
{/* Folder Browser Modal */}
|
|
1294
1195
|
<FolderBrowser
|
|
1295
1196
|
open={folderBrowserOpen}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
4
|
+
import { Badge } from '../components/ui/badge';
|
|
5
|
+
import { Button } from '../components/ui/button';
|
|
6
|
+
import { cn } from '../lib/cn';
|
|
7
|
+
import {
|
|
8
|
+
Crown, Sparkles, Users, Check, CreditCard, AlertTriangle,
|
|
9
|
+
Minus, Plus, Shield, Radio, Cloud, Server, Headphones,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
const FEATURE_LABELS = {
|
|
13
|
+
'remote-access': { label: 'Remote Access', icon: Radio },
|
|
14
|
+
'federation': { label: 'Federation', icon: Server },
|
|
15
|
+
'cloud-teams': { label: 'Cloud Teams', icon: Cloud },
|
|
16
|
+
'cloud-backup': { label: 'Cloud Backup', icon: Shield },
|
|
17
|
+
'shared-workspace': { label: 'Shared Workspace', icon: Users },
|
|
18
|
+
'admin-controls': { label: 'Admin Controls', icon: Shield },
|
|
19
|
+
'priority-support': { label: 'Priority Support', icon: Headphones },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function formatDate(iso) {
|
|
23
|
+
if (!iso) return '';
|
|
24
|
+
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function PlanBadge({ plan, status, cancelAtPeriodEnd }) {
|
|
28
|
+
if (status === 'past_due') {
|
|
29
|
+
return <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning text-2xs">Payment issue</Badge>;
|
|
30
|
+
}
|
|
31
|
+
if (cancelAtPeriodEnd) {
|
|
32
|
+
return <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning text-2xs">Cancels at period end</Badge>;
|
|
33
|
+
}
|
|
34
|
+
if (status === 'active' || status === 'trialing') {
|
|
35
|
+
return <Badge variant="outline" className="border-success/30 bg-success/10 text-success text-2xs">Active</Badge>;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function FeatureList({ features }) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-wrap gap-1.5 mt-3">
|
|
43
|
+
{features.map((key) => {
|
|
44
|
+
const f = FEATURE_LABELS[key] || { label: key, icon: Check };
|
|
45
|
+
const Icon = f.icon;
|
|
46
|
+
return (
|
|
47
|
+
<span key={key} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-surface-3 text-2xs text-text-2 font-sans">
|
|
48
|
+
<Icon size={10} className="text-accent" />
|
|
49
|
+
{f.label}
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function SeatControl({ seats, onChange }) {
|
|
58
|
+
const [value, setValue] = useState(seats);
|
|
59
|
+
|
|
60
|
+
useEffect(() => { setValue(seats); }, [seats]);
|
|
61
|
+
|
|
62
|
+
const dec = () => { if (value > 1) { setValue(value - 1); onChange(value - 1); } };
|
|
63
|
+
const inc = () => { if (value < 999) { setValue(value + 1); onChange(value + 1); } };
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="mt-4 flex items-center gap-3">
|
|
67
|
+
<span className="text-xs text-text-2 font-sans">Seats</span>
|
|
68
|
+
<div className="flex items-center gap-1 bg-surface-0 rounded-md border border-border-subtle p-0.5">
|
|
69
|
+
<button onClick={dec} disabled={value <= 1} className="w-6 h-6 flex items-center justify-center rounded text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed">
|
|
70
|
+
<Minus size={12} />
|
|
71
|
+
</button>
|
|
72
|
+
<span className="w-8 text-center text-xs font-semibold text-text-0 font-mono">{value}</span>
|
|
73
|
+
<button onClick={inc} disabled={value >= 999} className="w-6 h-6 flex items-center justify-center rounded text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed">
|
|
74
|
+
<Plus size={12} />
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ActivePlanCard({ subscription }) {
|
|
82
|
+
const openPortal = useGrooveStore((s) => s.openPortal);
|
|
83
|
+
const updateSeats = useGrooveStore((s) => s.updateSeats);
|
|
84
|
+
const planLabel = subscription.plan === 'team' ? 'Team Plan' : 'Pro Plan';
|
|
85
|
+
const PlanIcon = subscription.plan === 'team' ? Users : Crown;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="rounded-md border border-border-subtle bg-surface-1 p-4">
|
|
89
|
+
<div className="flex items-start justify-between">
|
|
90
|
+
<div className="flex items-center gap-2.5">
|
|
91
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-purple/15">
|
|
92
|
+
<PlanIcon size={16} className="text-purple" />
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<h4 className="text-sm font-semibold text-text-0 font-sans">{planLabel}</h4>
|
|
96
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
97
|
+
<PlanBadge plan={subscription.plan} status={subscription.status} cancelAtPeriodEnd={subscription.cancelAtPeriodEnd} />
|
|
98
|
+
{subscription.periodEnd && (
|
|
99
|
+
<span className="text-2xs text-text-3 font-sans">
|
|
100
|
+
{subscription.cancelAtPeriodEnd ? 'Ends' : 'Renews'} {formatDate(subscription.periodEnd)}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{subscription.features?.length > 0 && (
|
|
109
|
+
<FeatureList features={subscription.features} />
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{subscription.plan === 'team' && (
|
|
113
|
+
<SeatControl seats={subscription.seats || 1} onChange={updateSeats} />
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{subscription.status === 'past_due' && (
|
|
117
|
+
<div className="mt-3 flex items-center gap-2 rounded-md bg-warning/10 border border-warning/20 px-3 py-2">
|
|
118
|
+
<AlertTriangle size={14} className="text-warning shrink-0" />
|
|
119
|
+
<span className="text-2xs text-warning font-sans">There's an issue with your payment method.</span>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="flex gap-2 mt-4">
|
|
124
|
+
<Button size="sm" variant="ghost" onClick={openPortal} className="h-7 text-2xs gap-1.5 text-text-2 hover:text-accent">
|
|
125
|
+
<CreditCard size={12} />
|
|
126
|
+
Manage Subscription
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function PricingCard({ name, plan, price, interval, features, onUpgrade, highlighted }) {
|
|
134
|
+
const PlanIcon = plan === 'team' ? Users : Sparkles;
|
|
135
|
+
const perSeat = plan === 'team';
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className={cn(
|
|
139
|
+
'rounded-md border bg-surface-1 p-4 flex flex-col',
|
|
140
|
+
highlighted ? 'border-accent/40' : 'border-border-subtle',
|
|
141
|
+
)}>
|
|
142
|
+
<div className="flex items-center gap-2 mb-1">
|
|
143
|
+
<div className={cn(
|
|
144
|
+
'flex h-7 w-7 items-center justify-center rounded-md',
|
|
145
|
+
highlighted ? 'bg-accent/15' : 'bg-purple/15',
|
|
146
|
+
)}>
|
|
147
|
+
<PlanIcon size={14} className={highlighted ? 'text-accent' : 'text-purple'} />
|
|
148
|
+
</div>
|
|
149
|
+
<h4 className="text-sm font-semibold text-text-0 font-sans">{name}</h4>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="mt-2 mb-3">
|
|
153
|
+
<span className="text-lg font-bold text-text-0 font-sans">${price}</span>
|
|
154
|
+
<span className="text-2xs text-text-3 font-sans">/{interval === 'year' ? 'yr' : 'mo'}{perSeat ? '/seat' : ''}</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="flex-1 space-y-1.5 mb-4">
|
|
158
|
+
{features.map((key) => {
|
|
159
|
+
const f = FEATURE_LABELS[key] || { label: key };
|
|
160
|
+
return (
|
|
161
|
+
<div key={key} className="flex items-center gap-1.5 text-2xs text-text-2 font-sans">
|
|
162
|
+
<Check size={11} className="text-success shrink-0" />
|
|
163
|
+
{f.label}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<Button
|
|
170
|
+
size="sm"
|
|
171
|
+
onClick={onUpgrade}
|
|
172
|
+
className={cn(
|
|
173
|
+
'h-8 text-xs font-semibold w-full',
|
|
174
|
+
highlighted
|
|
175
|
+
? 'bg-accent/15 text-accent hover:bg-accent/25'
|
|
176
|
+
: 'bg-purple/15 text-purple hover:bg-purple/25',
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
Upgrade
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function SubscriptionPanel() {
|
|
186
|
+
const subscription = useGrooveStore((s) => s.subscription);
|
|
187
|
+
const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
|
|
188
|
+
const fetchSubscriptionPlans = useGrooveStore((s) => s.fetchSubscriptionPlans);
|
|
189
|
+
const checkMarketplaceAuth = useGrooveStore((s) => s.checkMarketplaceAuth);
|
|
190
|
+
const startCheckout = useGrooveStore((s) => s.startCheckout);
|
|
191
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
192
|
+
|
|
193
|
+
const [plans, setPlans] = useState(null);
|
|
194
|
+
const [billing, setBilling] = useState('monthly');
|
|
195
|
+
const [loading, setLoading] = useState(false);
|
|
196
|
+
const [planError, setPlanError] = useState(false);
|
|
197
|
+
const [verifying, setVerifying] = useState(false);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (authenticated && !subscription?.active) {
|
|
201
|
+
setVerifying(true);
|
|
202
|
+
checkMarketplaceAuth().finally(() => setVerifying(false));
|
|
203
|
+
}
|
|
204
|
+
}, [authenticated]);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!subscription?.active && !verifying && authenticated) {
|
|
208
|
+
setLoading(true);
|
|
209
|
+
setPlanError(false);
|
|
210
|
+
fetchSubscriptionPlans()
|
|
211
|
+
.then((data) => setPlans(data))
|
|
212
|
+
.catch(() => setPlanError(true))
|
|
213
|
+
.finally(() => setLoading(false));
|
|
214
|
+
}
|
|
215
|
+
}, [subscription?.active, verifying, authenticated, fetchSubscriptionPlans]);
|
|
216
|
+
|
|
217
|
+
const handleUpgrade = async (priceId) => {
|
|
218
|
+
try {
|
|
219
|
+
await startCheckout(priceId);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err.status === 409) {
|
|
222
|
+
addToast('info', 'Already subscribed', 'Use Manage Subscription to switch plans');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (subscription?.active) {
|
|
228
|
+
return (
|
|
229
|
+
<div className="py-2">
|
|
230
|
+
<ActivePlanCard subscription={subscription} />
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (verifying) {
|
|
236
|
+
return (
|
|
237
|
+
<div className="rounded-md border border-border-subtle bg-surface-1 p-4 flex items-center gap-3 my-2">
|
|
238
|
+
<div className="h-4 w-4 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
|
239
|
+
<span className="text-xs text-text-2 font-sans">Verifying subscription…</span>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!authenticated) {
|
|
245
|
+
return (
|
|
246
|
+
<div className="rounded-md border border-dashed border-border-subtle bg-surface-1/50 px-4 py-6 text-center my-2">
|
|
247
|
+
<Crown size={20} className="text-text-4 mx-auto mb-2" />
|
|
248
|
+
<p className="text-xs text-text-3 font-sans">Sign in to manage your subscription.</p>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (loading) {
|
|
254
|
+
return (
|
|
255
|
+
<div className="grid grid-cols-2 gap-3 py-2">
|
|
256
|
+
{[0, 1].map((i) => (
|
|
257
|
+
<div key={i} className="rounded-md border border-border-subtle bg-surface-1 p-4 h-52 animate-pulse" />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (planError || !plans) {
|
|
264
|
+
return (
|
|
265
|
+
<div className="rounded-md border border-dashed border-border-subtle bg-surface-1/50 px-4 py-6 text-center my-2">
|
|
266
|
+
<Sparkles size={20} className="text-text-4 mx-auto mb-2" />
|
|
267
|
+
<p className="text-xs text-text-3 font-sans">Plans unavailable right now. Visit groovedev.ai/pro for details.</p>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const proPlan = plans.pro;
|
|
273
|
+
const teamPlan = plans.team;
|
|
274
|
+
const isAnnual = billing === 'annual';
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div className="py-2">
|
|
278
|
+
<div className="flex justify-center mb-4">
|
|
279
|
+
<div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => setBilling('monthly')}
|
|
282
|
+
className={cn(
|
|
283
|
+
'px-3 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
284
|
+
billing === 'monthly' ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
285
|
+
)}
|
|
286
|
+
>
|
|
287
|
+
Monthly
|
|
288
|
+
</button>
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => setBilling('annual')}
|
|
291
|
+
className={cn(
|
|
292
|
+
'px-3 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
293
|
+
billing === 'annual' ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
Annual
|
|
297
|
+
<span className="ml-1 text-success">-20%</span>
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div className="grid grid-cols-2 gap-3">
|
|
303
|
+
{proPlan && (
|
|
304
|
+
<PricingCard
|
|
305
|
+
name="Pro"
|
|
306
|
+
plan="pro"
|
|
307
|
+
price={isAnnual ? proPlan.annual.price : proPlan.monthly.price}
|
|
308
|
+
interval={isAnnual ? 'year' : 'month'}
|
|
309
|
+
features={proPlan.features}
|
|
310
|
+
highlighted
|
|
311
|
+
onUpgrade={() => handleUpgrade(isAnnual ? proPlan.annual.priceId : proPlan.monthly.priceId)}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
{teamPlan && (
|
|
315
|
+
<PricingCard
|
|
316
|
+
name="Team"
|
|
317
|
+
plan="team"
|
|
318
|
+
price={isAnnual ? teamPlan.annual.price : teamPlan.monthly.price}
|
|
319
|
+
interval={isAnnual ? 'year' : 'month'}
|
|
320
|
+
features={teamPlan.features}
|
|
321
|
+
onUpgrade={() => handleUpgrade(isAnnual ? teamPlan.annual.priceId : teamPlan.monthly.priceId)}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
@@ -344,13 +344,13 @@ export default function TeamsView() {
|
|
|
344
344
|
</TabsList>
|
|
345
345
|
</div>
|
|
346
346
|
|
|
347
|
-
<TabsContent value="dashboard" className="flex-
|
|
347
|
+
<TabsContent value="dashboard" className="flex flex-col min-h-0">
|
|
348
348
|
<TeamsDashboard />
|
|
349
349
|
</TabsContent>
|
|
350
|
-
<TabsContent value="approvals" className="flex-
|
|
350
|
+
<TabsContent value="approvals" className="flex flex-col min-h-0">
|
|
351
351
|
<ApprovalsTab />
|
|
352
352
|
</TabsContent>
|
|
353
|
-
<TabsContent value="schedules" className="flex-
|
|
353
|
+
<TabsContent value="schedules" className="flex flex-col min-h-0">
|
|
354
354
|
<SchedulesTab />
|
|
355
355
|
</TabsContent>
|
|
356
356
|
</Tabs>
|