groove-dev 0.27.15 → 0.27.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CLAUDE.md +0 -10
  2. package/README.md +37 -1
  3. package/developerID_application.cer +0 -0
  4. package/node_modules/@groove-dev/daemon/src/api.js +586 -67
  5. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  6. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  7. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  10. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  12. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  13. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  14. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  15. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  16. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  17. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
  19. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  20. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  21. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  22. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  23. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  24. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  25. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  26. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  27. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  28. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  29. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  30. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  31. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  32. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  33. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  34. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  35. package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  37. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  38. package/node_modules/@groove-dev/gui/index.html +1 -0
  39. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  40. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  45. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  46. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  48. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  49. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  50. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  51. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
  52. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  53. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  54. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  55. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
  56. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  57. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  58. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  59. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  60. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  61. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  62. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  66. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  67. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  68. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  71. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  74. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  75. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  76. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  77. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  78. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  79. package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
  80. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  81. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  82. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  84. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
  85. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  86. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  88. package/package.json +1 -1
  89. package/packages/daemon/src/api.js +586 -67
  90. package/packages/daemon/src/classifier.js +24 -0
  91. package/packages/daemon/src/credentials.js +12 -2
  92. package/packages/daemon/src/federation/ambassador.js +204 -0
  93. package/packages/daemon/src/federation/connection.js +359 -0
  94. package/packages/daemon/src/federation/contracts.js +112 -0
  95. package/packages/daemon/src/federation/whitelist.js +190 -0
  96. package/packages/daemon/src/federation.js +166 -7
  97. package/packages/daemon/src/index.js +172 -19
  98. package/packages/daemon/src/introducer.js +52 -7
  99. package/packages/daemon/src/journalist.js +46 -1
  100. package/packages/daemon/src/memory.js +36 -16
  101. package/packages/daemon/src/process.js +140 -23
  102. package/packages/daemon/src/providers/base.js +1 -0
  103. package/packages/daemon/src/providers/claude-code.js +14 -0
  104. package/packages/daemon/src/providers/codex.js +124 -28
  105. package/packages/daemon/src/providers/gemini.js +104 -17
  106. package/packages/daemon/src/providers/index.js +17 -0
  107. package/packages/daemon/src/registry.js +10 -1
  108. package/packages/daemon/src/rotator.js +93 -30
  109. package/packages/daemon/src/skills.js +33 -3
  110. package/packages/daemon/src/terminal-pty.js +9 -1
  111. package/packages/daemon/src/tool-executor.js +11 -5
  112. package/packages/daemon/src/toys.js +69 -0
  113. package/packages/daemon/src/tunnel-manager.js +24 -5
  114. package/packages/daemon/templates/toys-catalog.json +242 -0
  115. package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  116. package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  117. package/packages/gui/dist/index.html +3 -2
  118. package/packages/gui/index.html +1 -0
  119. package/packages/gui/src/app.css +7 -0
  120. package/packages/gui/src/app.jsx +37 -10
  121. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  122. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  123. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  124. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  125. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  126. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  127. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  128. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  129. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  130. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  131. package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
  132. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  133. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  134. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  135. package/packages/gui/src/components/layout/status-bar.jsx +11 -1
  136. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  137. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  138. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  139. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  140. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  141. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  142. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  143. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  144. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  145. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  146. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  147. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  148. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  149. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  150. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  151. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  152. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  153. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  154. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  155. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  156. package/packages/gui/src/components/ui/toast.jsx +2 -2
  157. package/packages/gui/src/lib/electron.js +15 -0
  158. package/packages/gui/src/lib/format.js +1 -0
  159. package/packages/gui/src/stores/groove.js +388 -63
  160. package/packages/gui/src/views/agents.jsx +148 -42
  161. package/packages/gui/src/views/editor.jsx +92 -2
  162. package/packages/gui/src/views/federation.jsx +37 -0
  163. package/packages/gui/src/views/marketplace.jsx +2 -42
  164. package/packages/gui/src/views/settings.jsx +35 -134
  165. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  166. package/packages/gui/src/views/teams.jsx +3 -3
  167. package/packages/gui/src/views/toys.jsx +162 -0
  168. package/plans/chat-persistence-refactor.md +154 -0
  169. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  170. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  171. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  172. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -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 }) {
@@ -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
- // "Ready" means: local + installed, subscription + installed, api-key + hasKey
83
- const isReady = isLocal ? provider.installed : isSubscription ? provider.installed : provider.hasKey;
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={() => setFolderBrowserOpen(true)} className="h-8 px-2 flex-shrink-0">
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-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>