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.
Files changed (169) hide show
  1. package/README.md +37 -1
  2. package/developerID_application.cer +0 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +587 -68
  4. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  5. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  6. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  7. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  10. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  11. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  12. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  13. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  14. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  15. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  16. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  17. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  19. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  21. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  22. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  23. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  24. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  25. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  26. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  27. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  28. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  30. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  31. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  32. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  33. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  34. package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
  35. package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  37. package/node_modules/@groove-dev/gui/index.html +1 -0
  38. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  39. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  43. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  44. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  45. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  46. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  48. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  49. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  50. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
  51. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  52. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  53. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  54. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  55. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  56. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  57. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  58. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  59. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  60. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  61. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  62. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  66. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  67. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  68. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  71. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  72. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  74. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  75. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  76. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  77. package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
  78. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  79. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  80. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  81. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  82. package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
  83. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  84. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  85. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  86. package/package.json +1 -1
  87. package/packages/daemon/src/api.js +587 -68
  88. package/packages/daemon/src/classifier.js +24 -0
  89. package/packages/daemon/src/credentials.js +12 -2
  90. package/packages/daemon/src/federation/ambassador.js +204 -0
  91. package/packages/daemon/src/federation/connection.js +359 -0
  92. package/packages/daemon/src/federation/contracts.js +112 -0
  93. package/packages/daemon/src/federation/whitelist.js +190 -0
  94. package/packages/daemon/src/federation.js +166 -7
  95. package/packages/daemon/src/index.js +172 -19
  96. package/packages/daemon/src/introducer.js +52 -7
  97. package/packages/daemon/src/journalist.js +46 -1
  98. package/packages/daemon/src/memory.js +36 -16
  99. package/packages/daemon/src/process.js +140 -23
  100. package/packages/daemon/src/providers/base.js +1 -0
  101. package/packages/daemon/src/providers/claude-code.js +1 -0
  102. package/packages/daemon/src/providers/codex.js +124 -28
  103. package/packages/daemon/src/providers/gemini.js +104 -17
  104. package/packages/daemon/src/providers/index.js +17 -0
  105. package/packages/daemon/src/registry.js +10 -1
  106. package/packages/daemon/src/rotator.js +93 -30
  107. package/packages/daemon/src/skills.js +33 -3
  108. package/packages/daemon/src/terminal-pty.js +9 -1
  109. package/packages/daemon/src/tool-executor.js +11 -5
  110. package/packages/daemon/src/toys.js +69 -0
  111. package/packages/daemon/src/tunnel-manager.js +24 -5
  112. package/packages/daemon/templates/toys-catalog.json +242 -0
  113. package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
  114. package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
  115. package/packages/gui/dist/index.html +3 -2
  116. package/packages/gui/index.html +1 -0
  117. package/packages/gui/src/app.css +7 -0
  118. package/packages/gui/src/app.jsx +37 -10
  119. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  120. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  121. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  122. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  123. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  124. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  125. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  126. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  127. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  128. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  129. package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
  130. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  131. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  132. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  133. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  134. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  135. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  136. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  137. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  138. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  139. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  140. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  141. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  142. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  143. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  144. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  145. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  146. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  147. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  148. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  149. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  150. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  151. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  152. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  153. package/packages/gui/src/components/ui/toast.jsx +2 -2
  154. package/packages/gui/src/lib/electron.js +15 -0
  155. package/packages/gui/src/lib/format.js +1 -0
  156. package/packages/gui/src/stores/groove.js +373 -58
  157. package/packages/gui/src/views/agents.jsx +148 -42
  158. package/packages/gui/src/views/editor.jsx +92 -2
  159. package/packages/gui/src/views/federation.jsx +37 -0
  160. package/packages/gui/src/views/marketplace.jsx +2 -42
  161. package/packages/gui/src/views/settings.jsx +32 -132
  162. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  163. package/packages/gui/src/views/teams.jsx +3 -3
  164. package/packages/gui/src/views/toys.jsx +162 -0
  165. package/plans/chat-persistence-refactor.md +154 -0
  166. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  167. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  168. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  169. 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, ChevronDown,
20
- FolderOpen, FolderSearch, RotateCw, Users, Gauge, Zap,
21
- LogIn, LogOut, User, ShieldCheck, Settings,
22
- Newspaper, Layers, Radio, Send, MessageSquare, MessageCircle,
23
- Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink, ChevronRight,
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={() => setFolderBrowserOpen(true)} className="h-8 px-2 flex-shrink-0">
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-1 min-h-0">
347
+ <TabsContent value="dashboard" className="flex flex-col min-h-0">
348
348
  <TeamsDashboard />
349
349
  </TabsContent>
350
- <TabsContent value="approvals" className="flex-1 min-h-0">
350
+ <TabsContent value="approvals" className="flex flex-col min-h-0">
351
351
  <ApprovalsTab />
352
352
  </TabsContent>
353
- <TabsContent value="schedules" className="flex-1 min-h-0">
353
+ <TabsContent value="schedules" className="flex flex-col min-h-0">
354
354
  <SchedulesTab />
355
355
  </TabsContent>
356
356
  </Tabs>