groove-dev 0.27.14 → 0.27.17
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/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- 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 +1 -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-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -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 +13 -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/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 +373 -58
- 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 +32 -132
- 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 +587 -68
- 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 +1 -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-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -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 +13 -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/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 +373 -58
- 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 +32 -132
- 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 }) {
|
|
@@ -85,7 +56,7 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
85
56
|
async function handleSetKey() {
|
|
86
57
|
if (!keyInput.trim()) return;
|
|
87
58
|
try {
|
|
88
|
-
await api.post(`/credentials/${provider.id}`, { key: keyInput.trim() });
|
|
59
|
+
await api.post(`/credentials/${encodeURIComponent(provider.id)}`, { key: keyInput.trim() });
|
|
89
60
|
addToast('success', `API key set for ${provider.name}`);
|
|
90
61
|
setKeyInput('');
|
|
91
62
|
setSettingKey(false);
|
|
@@ -97,7 +68,7 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
97
68
|
|
|
98
69
|
async function handleDeleteKey() {
|
|
99
70
|
try {
|
|
100
|
-
await api.delete(`/credentials/${provider.id}`);
|
|
71
|
+
await api.delete(`/credentials/${encodeURIComponent(provider.id)}`);
|
|
101
72
|
addToast('info', `Removed ${provider.name} key`);
|
|
102
73
|
if (onKeyChange) onKeyChange();
|
|
103
74
|
} catch (err) {
|
|
@@ -577,7 +548,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
577
548
|
// Fetch channels when connected Slack gateway has no chatId
|
|
578
549
|
useEffect(() => {
|
|
579
550
|
if (gateway.connected && !gateway.chatId && gateway.type === 'slack') {
|
|
580
|
-
api.get(`/gateways/${gateway.id}/channels`).then((ch) => setChannels(Array.isArray(ch) ? ch : [])).catch(() => {});
|
|
551
|
+
api.get(`/gateways/${encodeURIComponent(gateway.id)}/channels`).then((ch) => setChannels(Array.isArray(ch) ? ch : [])).catch(() => {});
|
|
581
552
|
}
|
|
582
553
|
}, [gateway.connected, gateway.chatId, gateway.id, gateway.type]);
|
|
583
554
|
|
|
@@ -587,9 +558,9 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
587
558
|
async function handleSaveToken() {
|
|
588
559
|
if (!tokenInput.trim()) return;
|
|
589
560
|
try {
|
|
590
|
-
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'bot_token', value: tokenInput.trim() });
|
|
561
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/credentials`, { key: 'bot_token', value: tokenInput.trim() });
|
|
591
562
|
if (isSlack && appTokenInput.trim()) {
|
|
592
|
-
await api.post(`/gateways/${gateway.id}/credentials`, { key: 'app_token', value: appTokenInput.trim() });
|
|
563
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/credentials`, { key: 'app_token', value: appTokenInput.trim() });
|
|
593
564
|
}
|
|
594
565
|
addToast('success', `Token saved — connecting...`);
|
|
595
566
|
setTokenInput('');
|
|
@@ -597,7 +568,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
597
568
|
setSettingToken(false);
|
|
598
569
|
// Auto-connect after saving tokens
|
|
599
570
|
try {
|
|
600
|
-
await api.post(`/gateways/${gateway.id}/connect`);
|
|
571
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/connect`);
|
|
601
572
|
addToast('success', `${GATEWAY_LABELS[gateway.type]} connected!`);
|
|
602
573
|
} catch (connErr) {
|
|
603
574
|
addToast('error', 'Token saved but connect failed', connErr.message);
|
|
@@ -611,7 +582,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
611
582
|
async function handleTest() {
|
|
612
583
|
setTesting(true);
|
|
613
584
|
try {
|
|
614
|
-
await api.post(`/gateways/${gateway.id}/test`);
|
|
585
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/test`);
|
|
615
586
|
addToast('success', 'Test message sent!');
|
|
616
587
|
} catch (err) {
|
|
617
588
|
addToast('error', 'Test failed', err.message);
|
|
@@ -623,10 +594,10 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
623
594
|
setConnecting(true);
|
|
624
595
|
try {
|
|
625
596
|
if (gateway.connected) {
|
|
626
|
-
await api.post(`/gateways/${gateway.id}/disconnect`);
|
|
597
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/disconnect`);
|
|
627
598
|
addToast('info', `${GATEWAY_LABELS[gateway.type]} disconnected`);
|
|
628
599
|
} else {
|
|
629
|
-
await api.post(`/gateways/${gateway.id}/connect`);
|
|
600
|
+
await api.post(`/gateways/${encodeURIComponent(gateway.id)}/connect`);
|
|
630
601
|
addToast('success', `${GATEWAY_LABELS[gateway.type]} connected!`);
|
|
631
602
|
}
|
|
632
603
|
onRefresh();
|
|
@@ -638,7 +609,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
638
609
|
|
|
639
610
|
async function handleToggleEnabled(enabled) {
|
|
640
611
|
try {
|
|
641
|
-
await api.patch(`/gateways/${gateway.id}`, { enabled });
|
|
612
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { enabled });
|
|
642
613
|
onRefresh();
|
|
643
614
|
} catch (err) {
|
|
644
615
|
addToast('error', 'Update failed', err.message);
|
|
@@ -647,7 +618,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
647
618
|
|
|
648
619
|
async function handlePresetChange(preset) {
|
|
649
620
|
try {
|
|
650
|
-
await api.patch(`/gateways/${gateway.id}`, { notifications: { preset } });
|
|
621
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { notifications: { preset } });
|
|
651
622
|
onRefresh();
|
|
652
623
|
} catch (err) {
|
|
653
624
|
addToast('error', 'Update failed', err.message);
|
|
@@ -656,7 +627,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
656
627
|
|
|
657
628
|
async function handlePermissionChange(perm) {
|
|
658
629
|
try {
|
|
659
|
-
await api.patch(`/gateways/${gateway.id}`, { commandPermission: perm });
|
|
630
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { commandPermission: perm });
|
|
660
631
|
onRefresh();
|
|
661
632
|
} catch (err) {
|
|
662
633
|
addToast('error', 'Update failed', err.message);
|
|
@@ -665,7 +636,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
665
636
|
|
|
666
637
|
async function handleDelete() {
|
|
667
638
|
try {
|
|
668
|
-
await api.delete(`/gateways/${gateway.id}`);
|
|
639
|
+
await api.delete(`/gateways/${encodeURIComponent(gateway.id)}`);
|
|
669
640
|
addToast('info', `${GATEWAY_LABELS[gateway.type]} gateway removed`);
|
|
670
641
|
onRefresh();
|
|
671
642
|
} catch (err) {
|
|
@@ -718,7 +689,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
718
689
|
</code>
|
|
719
690
|
<button
|
|
720
691
|
onClick={async () => {
|
|
721
|
-
try { await api.patch(`/gateways/${gateway.id}`, { chatId: null }); onRefresh(); }
|
|
692
|
+
try { await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: null }); onRefresh(); }
|
|
722
693
|
catch (err) { addToast('error', 'Failed', err.message); }
|
|
723
694
|
}}
|
|
724
695
|
className="text-2xs text-text-4 hover:text-text-1 cursor-pointer font-sans"
|
|
@@ -729,7 +700,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
729
700
|
onChange={async (e) => {
|
|
730
701
|
if (!e.target.value) return;
|
|
731
702
|
try {
|
|
732
|
-
await api.patch(`/gateways/${gateway.id}`, { chatId: e.target.value });
|
|
703
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: e.target.value });
|
|
733
704
|
onRefresh();
|
|
734
705
|
} catch (err) { addToast('error', 'Failed to set channel', err.message); }
|
|
735
706
|
}}
|
|
@@ -752,7 +723,7 @@ function GatewayCard({ gateway, onRefresh }) {
|
|
|
752
723
|
onKeyDown={async (e) => {
|
|
753
724
|
if (e.key === 'Enter' && e.target.value.trim()) {
|
|
754
725
|
try {
|
|
755
|
-
await api.patch(`/gateways/${gateway.id}`, { chatId: e.target.value.trim() });
|
|
726
|
+
await api.patch(`/gateways/${encodeURIComponent(gateway.id)}`, { chatId: e.target.value.trim() });
|
|
756
727
|
onRefresh();
|
|
757
728
|
} catch (err) { addToast('error', 'Failed to set channel', err.message); }
|
|
758
729
|
}
|
|
@@ -977,14 +948,7 @@ export default function SettingsView() {
|
|
|
977
948
|
const [gwList, setGwList] = useState([]);
|
|
978
949
|
const [loading, setLoading] = useState(true);
|
|
979
950
|
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
951
|
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
952
|
|
|
989
953
|
function loadProviders() {
|
|
990
954
|
api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
|
|
@@ -998,7 +962,6 @@ export default function SettingsView() {
|
|
|
998
962
|
Promise.all([api.get('/providers'), api.get('/config'), api.get('/status'), api.get('/gateways')])
|
|
999
963
|
.then(([p, c, s, g]) => { setProviders(Array.isArray(p) ? p : []); setConfig(c); setDaemonInfo(s); setGwList(Array.isArray(g) ? g : []); setLoading(false); })
|
|
1000
964
|
.catch(() => setLoading(false));
|
|
1001
|
-
useGrooveStore.getState().fetchTunnels();
|
|
1002
965
|
}, []);
|
|
1003
966
|
|
|
1004
967
|
async function addGateway(type) {
|
|
@@ -1056,22 +1019,6 @@ export default function SettingsView() {
|
|
|
1056
1019
|
{daemonInfo?.uptime > 0 && <span>Up {fmtUptime(daemonInfo.uptime)}</span>}
|
|
1057
1020
|
</div>
|
|
1058
1021
|
|
|
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
1022
|
<StatusDot status="running" size="sm" />
|
|
1076
1023
|
</div>
|
|
1077
1024
|
|
|
@@ -1137,7 +1084,17 @@ export default function SettingsView() {
|
|
|
1137
1084
|
<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
1085
|
{config.defaultWorkingDir || 'Project root'}
|
|
1139
1086
|
</code>
|
|
1140
|
-
<Button variant="secondary" size="sm" onClick={() =>
|
|
1087
|
+
<Button variant="secondary" size="sm" onClick={async () => {
|
|
1088
|
+
if (window.groove?.folders?.select) {
|
|
1089
|
+
const dir = await window.groove.folders.select({
|
|
1090
|
+
title: 'Select Working Directory',
|
|
1091
|
+
defaultPath: config?.defaultWorkingDir || undefined,
|
|
1092
|
+
});
|
|
1093
|
+
if (dir) updateConfig('defaultWorkingDir', dir);
|
|
1094
|
+
} else {
|
|
1095
|
+
setFolderBrowserOpen(true);
|
|
1096
|
+
}
|
|
1097
|
+
}} className="h-8 px-2 flex-shrink-0">
|
|
1141
1098
|
<FolderSearch size={12} />
|
|
1142
1099
|
</Button>
|
|
1143
1100
|
</div>
|
|
@@ -1229,67 +1186,10 @@ export default function SettingsView() {
|
|
|
1229
1186
|
</div>
|
|
1230
1187
|
)}
|
|
1231
1188
|
|
|
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
1189
|
|
|
1275
1190
|
</div>
|
|
1276
1191
|
</ScrollArea>
|
|
1277
1192
|
|
|
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
1193
|
{/* Folder Browser Modal */}
|
|
1294
1194
|
<FolderBrowser
|
|
1295
1195
|
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>
|