groove-dev 0.27.8 → 0.27.12

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 (125) hide show
  1. package/groove-icon.png +0 -0
  2. package/node_modules/@groove-dev/daemon/src/api.js +460 -25
  3. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  5. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  6. package/node_modules/@groove-dev/daemon/src/process.js +67 -7
  7. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  8. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  10. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  13. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  14. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  15. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  18. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  19. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  23. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  24. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  25. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  28. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  29. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  31. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  32. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  33. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  34. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  36. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  37. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  38. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  39. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  41. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  42. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  43. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  44. package/node_modules/@groove-dev/gui/src/lib/electron.js +25 -0
  45. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  46. package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
  47. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
  48. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  49. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  50. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  51. package/package.json +7 -2
  52. package/packages/daemon/src/api.js +460 -25
  53. package/packages/daemon/src/index.js +7 -0
  54. package/packages/daemon/src/introducer.js +72 -4
  55. package/packages/daemon/src/journalist.js +66 -11
  56. package/packages/daemon/src/process.js +67 -7
  57. package/packages/daemon/src/registry.js +1 -1
  58. package/packages/daemon/src/repo-import.js +541 -0
  59. package/packages/daemon/src/rotator.js +28 -1
  60. package/packages/daemon/src/supervisor.js +2 -1
  61. package/packages/daemon/src/tunnel-manager.js +504 -0
  62. package/packages/daemon/src/validate.js +13 -0
  63. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  64. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  65. package/packages/gui/dist/index.html +2 -2
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  70. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  71. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  72. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  75. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  76. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  77. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  78. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  79. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  80. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  81. package/packages/gui/src/app.css +14 -0
  82. package/packages/gui/src/app.jsx +13 -0
  83. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  84. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  85. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  86. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  87. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  88. package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
  89. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  90. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  91. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  92. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  93. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  94. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  95. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  96. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  97. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  98. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  99. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  100. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  101. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  102. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  103. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  104. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  105. package/packages/gui/src/components/ui/toast.jsx +1 -1
  106. package/packages/gui/src/lib/edition.js +4 -0
  107. package/packages/gui/src/lib/electron.js +25 -0
  108. package/packages/gui/src/lib/status.js +1 -0
  109. package/packages/gui/src/stores/groove.js +139 -6
  110. package/packages/gui/src/views/dashboard.jsx +38 -39
  111. package/packages/gui/src/views/marketplace.jsx +82 -0
  112. package/packages/gui/src/views/settings.jsx +66 -0
  113. package/packages/gui/vite.config.js +3 -0
  114. package/integrations/FEDERATION_PLAN.md +0 -583
  115. package/integrations/VOICE_PLAN.md +0 -232
  116. package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
  117. package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
  118. package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
  119. package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
  120. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  121. package/test-slack.mjs +0 -28
  122. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  123. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -0,0 +1,192 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { Dialog, DialogContent } from '../ui/dialog';
4
+ import { Button } from '../ui/button';
5
+ import { FolderBrowser } from '../agents/folder-browser';
6
+ import { cn } from '../../lib/cn';
7
+ import { FolderSearch } from 'lucide-react';
8
+
9
+ export function ServerDialog({ open, onOpenChange, server, onSave }) {
10
+ const [name, setName] = useState('');
11
+ const [host, setHost] = useState('');
12
+ const [user, setUser] = useState('');
13
+ const [sshPort, setSshPort] = useState(22);
14
+ const [sshKeyPath, setSshKeyPath] = useState('');
15
+ const [autoStart, setAutoStart] = useState(false);
16
+ const [autoConnect, setAutoConnect] = useState(false);
17
+ const [keyBrowserOpen, setKeyBrowserOpen] = useState(false);
18
+ const [saving, setSaving] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (open) {
22
+ if (server) {
23
+ setName(server.name || '');
24
+ setHost(server.host || '');
25
+ setUser(server.user || '');
26
+ setSshPort(server.port || 22);
27
+ setSshKeyPath(server.sshKeyPath || '');
28
+ setAutoStart(server.autoStart || false);
29
+ setAutoConnect(server.autoConnect || false);
30
+ } else {
31
+ setName('');
32
+ setHost('');
33
+ setUser('');
34
+ setSshPort(22);
35
+ setSshKeyPath('');
36
+ setAutoStart(false);
37
+ setAutoConnect(false);
38
+ }
39
+ }
40
+ }, [open, server]);
41
+
42
+ async function handleSave() {
43
+ if (!name.trim() || !host.trim() || !user.trim()) return;
44
+ setSaving(true);
45
+ try {
46
+ const data = {
47
+ name: name.trim(),
48
+ host: host.trim(),
49
+ user: user.trim(),
50
+ port: sshPort,
51
+ sshKeyPath: sshKeyPath.trim(),
52
+ autoStart,
53
+ autoConnect,
54
+ };
55
+ if (server?.id) data.id = server.id;
56
+ await onSave(data);
57
+ onOpenChange(false);
58
+ } catch {}
59
+ setSaving(false);
60
+ }
61
+
62
+ return (
63
+ <Dialog open={open} onOpenChange={onOpenChange}>
64
+ <DialogContent
65
+ title={server ? `Edit ${server.name}` : 'Add Remote Server'}
66
+ description="Configure SSH connection to a remote server"
67
+ className="max-w-[460px]"
68
+ >
69
+ <div className="px-5 py-4 space-y-4">
70
+ {/* Name */}
71
+ <div>
72
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Name</label>
73
+ <input
74
+ value={name}
75
+ onChange={(e) => setName(e.target.value)}
76
+ placeholder="api-vps"
77
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
78
+ autoFocus
79
+ />
80
+ </div>
81
+
82
+ {/* Host */}
83
+ <div>
84
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Host</label>
85
+ <input
86
+ value={host}
87
+ onChange={(e) => setHost(e.target.value)}
88
+ placeholder="165.22.180.45 or hostname"
89
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
90
+ />
91
+ </div>
92
+
93
+ {/* User + SSH Port */}
94
+ <div className="flex gap-3">
95
+ <div className="flex-1">
96
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">User</label>
97
+ <input
98
+ value={user}
99
+ onChange={(e) => setUser(e.target.value)}
100
+ placeholder="root"
101
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
102
+ />
103
+ </div>
104
+ <div className="w-24">
105
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">SSH Port</label>
106
+ <input
107
+ value={sshPort}
108
+ onChange={(e) => setSshPort(Number(e.target.value) || 22)}
109
+ type="number"
110
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
111
+ />
112
+ </div>
113
+ </div>
114
+
115
+ {/* SSH Key */}
116
+ <div>
117
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">SSH Key</label>
118
+ <div className="flex items-center gap-1.5">
119
+ <input
120
+ value={sshKeyPath}
121
+ onChange={(e) => setSshKeyPath(e.target.value)}
122
+ placeholder="~/.ssh/id_ed25519"
123
+ className="flex-1 h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
124
+ />
125
+ <Button
126
+ variant="secondary"
127
+ size="sm"
128
+ onClick={() => setKeyBrowserOpen(true)}
129
+ className="h-9 px-2.5 flex-shrink-0"
130
+ >
131
+ <FolderSearch size={13} />
132
+ </Button>
133
+ </div>
134
+ </div>
135
+
136
+ {/* Toggles */}
137
+ <div className="space-y-3 pt-1">
138
+ <label className="flex items-center justify-between cursor-pointer">
139
+ <span className="text-xs text-text-2 font-sans">Auto-start daemon on connect</span>
140
+ <ToggleSwitch value={autoStart} onChange={setAutoStart} />
141
+ </label>
142
+ <label className="flex items-center justify-between cursor-pointer">
143
+ <span className="text-xs text-text-2 font-sans">Auto-connect on Groove launch</span>
144
+ <ToggleSwitch value={autoConnect} onChange={setAutoConnect} />
145
+ </label>
146
+ </div>
147
+
148
+ {/* Actions */}
149
+ <div className="flex justify-end gap-2 pt-2">
150
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} className="h-8 text-xs px-4 text-text-3">
151
+ Cancel
152
+ </Button>
153
+ <Button
154
+ variant="primary"
155
+ size="sm"
156
+ onClick={handleSave}
157
+ disabled={!name.trim() || !host.trim() || !user.trim() || saving}
158
+ className="h-8 text-xs px-4"
159
+ >
160
+ {saving ? 'Saving...' : 'Save'}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+
165
+ {/* File browser for SSH key */}
166
+ <FolderBrowser
167
+ open={keyBrowserOpen}
168
+ onOpenChange={setKeyBrowserOpen}
169
+ currentPath={sshKeyPath || '~/.ssh'}
170
+ onSelect={(path) => setSshKeyPath(path)}
171
+ />
172
+ </DialogContent>
173
+ </Dialog>
174
+ );
175
+ }
176
+
177
+ function ToggleSwitch({ value, onChange }) {
178
+ return (
179
+ <button
180
+ onClick={() => onChange(!value)}
181
+ className={cn(
182
+ 'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer',
183
+ value ? 'bg-accent' : 'bg-surface-5',
184
+ )}
185
+ >
186
+ <div className={cn(
187
+ 'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
188
+ value ? 'translate-x-4' : 'translate-x-0',
189
+ )} />
190
+ </button>
191
+ );
192
+ }
@@ -0,0 +1,63 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { ShieldCheck, ShieldX, AlertTriangle } from 'lucide-react';
4
+ import { Button } from '../ui/button';
5
+ import { useGrooveStore } from '../../stores/groove';
6
+
7
+ export function ApprovalModal() {
8
+ const pendingApprovals = useGrooveStore((s) => s.pendingApprovals);
9
+ const approveRequest = useGrooveStore((s) => s.approveRequest);
10
+ const rejectRequest = useGrooveStore((s) => s.rejectRequest);
11
+
12
+ if (!pendingApprovals?.length) return null;
13
+
14
+ return (
15
+ <div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-md flex flex-col gap-2 px-4">
16
+ <AnimatePresence>
17
+ {pendingApprovals.map((approval) => (
18
+ <motion.div
19
+ key={approval.id}
20
+ initial={{ y: 20, opacity: 0 }}
21
+ animate={{ y: 0, opacity: 1 }}
22
+ exit={{ y: 20, opacity: 0 }}
23
+ transition={{ duration: 0.2 }}
24
+ className="rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden"
25
+ >
26
+ <div className="px-4 py-3 flex items-start gap-3">
27
+ <AlertTriangle size={16} className="text-warning shrink-0 mt-0.5" />
28
+ <div className="flex-1 min-w-0">
29
+ <p className="text-sm font-semibold text-text-0 font-sans truncate">
30
+ {approval.agentName || 'Agent'} needs approval
31
+ </p>
32
+ {approval.action?.description && (
33
+ <p className="text-2xs text-text-3 font-sans mt-0.5 line-clamp-2">
34
+ {approval.action.description}
35
+ </p>
36
+ )}
37
+ </div>
38
+ </div>
39
+ <div className="px-4 py-2.5 border-t border-border-subtle flex items-center justify-end gap-2">
40
+ <Button
41
+ size="sm"
42
+ variant="ghost"
43
+ className="text-danger hover:bg-danger/10"
44
+ onClick={() => rejectRequest(approval.id)}
45
+ >
46
+ <ShieldX size={14} className="mr-1" />
47
+ Reject
48
+ </Button>
49
+ <Button
50
+ size="sm"
51
+ variant="accent"
52
+ onClick={() => approveRequest(approval.id)}
53
+ >
54
+ <ShieldCheck size={14} className="mr-1" />
55
+ Approve
56
+ </Button>
57
+ </div>
58
+ </motion.div>
59
+ ))}
60
+ </AnimatePresence>
61
+ </div>
62
+ );
63
+ }
@@ -78,7 +78,7 @@ export function ToastContainer() {
78
78
  const toasts = useGrooveStore((s) => s.toasts);
79
79
 
80
80
  return (
81
- <div className="fixed bottom-4 right-4 z-[100] flex flex-col-reverse gap-2">
81
+ <div className="fixed bottom-10 left-[60px] z-[100] flex flex-col-reverse gap-2">
82
82
  <AnimatePresence mode="popLayout">
83
83
  {toasts.slice(-3).map((toast) => (
84
84
  <ToastItem key={toast.id} toast={toast} />
@@ -0,0 +1,4 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export const isPro = __GROOVE_EDITION__ === 'pro';
4
+ export const edition = __GROOVE_EDITION__;
@@ -0,0 +1,25 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export function isElectron() {
4
+ return !!(window.groove || navigator.userAgent.includes('Electron'));
5
+ }
6
+
7
+ export function getPlatform() {
8
+ return window.groove?.platform || 'browser';
9
+ }
10
+
11
+ export function openExternal(url) {
12
+ if (window.groove) {
13
+ window.groove.openExternal(url);
14
+ } else {
15
+ window.open(url, '_blank');
16
+ }
17
+ }
18
+
19
+ export const electronAuth = {
20
+ login: () => window.groove?.auth?.login(),
21
+ logout: () => window.groove?.auth?.logout(),
22
+ status: () => window.groove?.auth?.status(),
23
+ onChanged: (cb) => window.groove?.auth?.onChanged(cb),
24
+ onSubscriptionStatus: (cb) => window.groove?.auth?.onSubscriptionStatus(cb),
25
+ };
@@ -42,6 +42,7 @@ export const ROLE_COLORS = {
42
42
  analyst: { bg: 'rgba(198, 120, 221, 0.12)', text: '#c678dd', border: '#c678dd' },
43
43
  creative: { bg: 'rgba(229, 192, 123, 0.12)', text: '#e5c07b', border: '#e5c07b' },
44
44
  slides: { bg: 'rgba(209, 154, 102, 0.12)', text: '#d19a66', border: '#d19a66' },
45
+ chat: { bg: 'rgba(198, 120, 221, 0.12)', text: '#c678dd', border: '#c678dd' },
45
46
  };
46
47
 
47
48
  export function roleColor(role) {
@@ -7,6 +7,7 @@ import { api } from '../lib/api';
7
7
  const WS_URL = `ws://${window.location.hostname}:${window.location.port || 31415}`;
8
8
 
9
9
  let toastCounter = 0;
10
+ let plannerPollInterval = null;
10
11
 
11
12
  function loadJSON(key, fallback = {}) {
12
13
  try { return JSON.parse(localStorage.getItem(key) || JSON.stringify(fallback)); }
@@ -46,6 +47,7 @@ export const useGrooveStore = create((set, get) => ({
46
47
  detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
47
48
  teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
48
49
  commandPaletteOpen: false,
50
+ quickConnectOpen: false,
49
51
 
50
52
  // ── Node expansion (click-to-open persistent panels) ───────
51
53
  expandedNodes: loadJSON('groove:expandedNodes'),
@@ -79,6 +81,14 @@ export const useGrooveStore = create((set, get) => ({
79
81
  // ── Toasts ────────────────────────────────────────────────
80
82
  toasts: [],
81
83
 
84
+ // ── Tunnels ────────────────────────────────────────────────
85
+ savedTunnels: [],
86
+ activeTunnelId: null,
87
+
88
+ // ── GitHub Repo Import ────────────────────────────────────
89
+ importedRepos: [],
90
+ importInProgress: false,
91
+
82
92
  // ── Editor state ──────────────────────────────────────────
83
93
  editorFiles: {},
84
94
  editorActiveFile: null,
@@ -132,6 +142,22 @@ export const useGrooveStore = create((set, get) => ({
132
142
  || p.contextUsage !== a.contextUsage || p.name !== a.name || p.model !== a.model;
133
143
  });
134
144
  set({ agents: changed ? msg.data : prev, tokenTimeline: timeline, hydrated: true });
145
+
146
+ // Poll for recommended-team.json while a planner is running
147
+ const hasRunningPlanner = msg.data.some((a) => a.role === 'planner' && a.status === 'running');
148
+ if (hasRunningPlanner && !plannerPollInterval && !get().recommendedTeam) {
149
+ plannerPollInterval = setInterval(() => {
150
+ if (get().recommendedTeam) {
151
+ clearInterval(plannerPollInterval);
152
+ plannerPollInterval = null;
153
+ return;
154
+ }
155
+ get().checkRecommendedTeam();
156
+ }, 3000);
157
+ } else if ((!hasRunningPlanner || get().recommendedTeam) && plannerPollInterval) {
158
+ clearInterval(plannerPollInterval);
159
+ plannerPollInterval = null;
160
+ }
135
161
  break;
136
162
  }
137
163
 
@@ -320,7 +346,6 @@ export const useGrooveStore = create((set, get) => ({
320
346
 
321
347
  case 'approval:request':
322
348
  set((s) => ({ pendingApprovals: [...s.pendingApprovals, msg.data] }));
323
- get().addToast('warning', `Approval needed: ${msg.data?.agentName || 'agent'}`, msg.data?.action?.description);
324
349
  break;
325
350
 
326
351
  case 'approval:resolved': {
@@ -351,10 +376,32 @@ export const useGrooveStore = create((set, get) => ({
351
376
  case 'gateway:status':
352
377
  set({ gateways: msg.data || [] });
353
378
  break;
379
+
380
+ case 'tunnel.connected':
381
+ set({ activeTunnelId: msg.data?.id || null });
382
+ get().fetchTunnels();
383
+ break;
384
+
385
+ case 'tunnel.disconnected':
386
+ set({ activeTunnelId: null });
387
+ get().fetchTunnels();
388
+ break;
389
+
390
+ case 'tunnel.health': {
391
+ const tunnels = get().savedTunnels.map((t) =>
392
+ t.id === msg.data?.id ? { ...t, latencyMs: msg.data.latencyMs, healthy: msg.data.healthy } : t,
393
+ );
394
+ set({ savedTunnels: tunnels });
395
+ break;
396
+ }
354
397
  }
355
398
  };
356
399
 
357
400
  ws.onclose = () => {
401
+ if (plannerPollInterval) {
402
+ clearInterval(plannerPollInterval);
403
+ plannerPollInterval = null;
404
+ }
358
405
  set({ connected: false, hydrated: false, ws: null, daemonHost: null, tunneled: false });
359
406
  setTimeout(() => get().connect(), 2000);
360
407
  };
@@ -462,6 +509,7 @@ export const useGrooveStore = create((set, get) => ({
462
509
  set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
463
510
  },
464
511
  toggleCommandPalette() { set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })); },
512
+ toggleQuickConnect() { set((s) => ({ quickConnectOpen: !s.quickConnectOpen })); },
465
513
 
466
514
  setDetailPanelWidth(w) {
467
515
  set({ detailPanelWidth: w });
@@ -596,9 +644,7 @@ export const useGrooveStore = create((set, get) => ({
596
644
 
597
645
  // Check if all recommended roles already exist in the planner's team.
598
646
  // If so, auto-delegate instead of showing the "Launch Team" modal.
599
- const planners = get().agents.filter((a) => a.role === 'planner');
600
- const planner = planners.sort((a, b) => (b.lastActivity || '').localeCompare(a.lastActivity || ''))[0];
601
- const teamId = planner?.teamId;
647
+ const teamId = data.teamId || null;
602
648
 
603
649
  if (teamId) {
604
650
  const teamAgents = get().agents.filter((a) => a.teamId === teamId && a.role !== 'planner');
@@ -624,7 +670,7 @@ export const useGrooveStore = create((set, get) => ({
624
670
  }
625
671
 
626
672
  // New agents needed — show the modal for approval
627
- set({ recommendedTeam: data });
673
+ set({ recommendedTeam: { ...data, teamId: data.teamId || null } });
628
674
  } catch {
629
675
  set({ recommendedTeam: null });
630
676
  }
@@ -632,9 +678,10 @@ export const useGrooveStore = create((set, get) => ({
632
678
 
633
679
  async launchRecommendedTeam(modifiedAgents) {
634
680
  try {
681
+ const teamId = get().recommendedTeam?.teamId || null;
635
682
  set({ recommendedTeam: null }); // Dismiss modal immediately
636
683
  get().addToast('info', 'Launching team...');
637
- const body = modifiedAgents ? { agents: modifiedAgents } : undefined;
684
+ const body = { ...(modifiedAgents && { agents: modifiedAgents }), ...(teamId && { teamId }) };
638
685
  const result = await api.post('/recommended-team/launch', body);
639
686
  const sub = [
640
687
  result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
@@ -657,6 +704,92 @@ export const useGrooveStore = create((set, get) => ({
657
704
  }
658
705
  },
659
706
 
707
+ // ── GitHub Repo Import ────────────────────────────────────
708
+
709
+ async fetchImportedRepos() {
710
+ try {
711
+ const repos = await api.get('/repos/imported');
712
+ set({ importedRepos: repos });
713
+ } catch { /* ignore */ }
714
+ },
715
+
716
+ async previewRepo(repoUrl) {
717
+ return api.post('/repos/preview', { repoUrl });
718
+ },
719
+
720
+ async importRepo(repoUrl, targetPath, createTeam, teamName) {
721
+ set({ importInProgress: true });
722
+ try {
723
+ const result = await api.post('/repos/import', { repoUrl, targetPath, createTeam, teamName });
724
+ get().fetchImportedRepos();
725
+ return result;
726
+ } finally {
727
+ set({ importInProgress: false });
728
+ }
729
+ },
730
+
731
+ async softRemoveRepo(importId) {
732
+ await api.delete('/repos/' + importId + '/remove');
733
+ get().fetchImportedRepos();
734
+ },
735
+
736
+ async hardNukeRepo(importId, deleteFiles = true) {
737
+ await api.delete('/repos/' + importId + '/nuke?deleteFiles=' + deleteFiles);
738
+ get().fetchImportedRepos();
739
+ },
740
+
741
+ // ── Tunnels ──────────────────────────────────────────────
742
+
743
+ async fetchTunnels() {
744
+ try {
745
+ const tunnels = await api.get('/tunnels');
746
+ set({ savedTunnels: Array.isArray(tunnels) ? tunnels : [] });
747
+ } catch {}
748
+ },
749
+
750
+ async saveTunnel(config) {
751
+ const result = await api.post('/tunnels', config);
752
+ get().fetchTunnels();
753
+ return result;
754
+ },
755
+
756
+ async updateTunnel(id, config) {
757
+ const result = await api.patch('/tunnels/' + id, config);
758
+ get().fetchTunnels();
759
+ return result;
760
+ },
761
+
762
+ async deleteTunnel(id) {
763
+ await api.delete('/tunnels/' + id);
764
+ get().fetchTunnels();
765
+ },
766
+
767
+ async testTunnel(id) {
768
+ return api.post('/tunnels/' + id + '/test');
769
+ },
770
+
771
+ async connectTunnel(id) {
772
+ const result = await api.post('/tunnels/' + id + '/connect');
773
+ set({ activeTunnelId: id });
774
+ get().fetchTunnels();
775
+ if (result.url) window.open(result.url, '_blank');
776
+ return result;
777
+ },
778
+
779
+ async disconnectTunnel(id) {
780
+ await api.post('/tunnels/' + id + '/disconnect');
781
+ set({ activeTunnelId: null });
782
+ get().fetchTunnels();
783
+ },
784
+
785
+ async installTunnel(id) {
786
+ return api.post('/tunnels/' + id + '/install');
787
+ },
788
+
789
+ async startTunnel(id) {
790
+ return api.post('/tunnels/' + id + '/start');
791
+ },
792
+
660
793
  // ── Journalist ────────────────────────────────────────────
661
794
 
662
795
  async fetchJournalist() {
@@ -112,54 +112,53 @@ export default function DashboardView() {
112
112
 
113
113
  <KpiStrip kpis={kpis} />
114
114
 
115
- <div className="flex-1 min-h-0 grid" style={{
116
- gridTemplateRows: 'minmax(0, 1fr) minmax(0, 1fr)',
117
- gridTemplateColumns: '2fr 2.5fr 1.5fr',
118
- background: '#282c34',
119
- gap: '1px',
120
- }}>
121
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 relative">
122
- <TokenChart data={snapshots} />
123
- </div>
115
+ <div className="flex-1 min-h-0 flex flex-col" style={{ background: '#282c34', gap: '1px' }}>
116
+ <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '3fr 1.5fr 1.5fr', gap: '0 1px' }}>
117
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 relative">
118
+ <TokenChart data={snapshots} />
119
+ </div>
124
120
 
125
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
126
- <div className="px-3 pt-2.5 pb-1">
127
- <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Cache Performance</span>
121
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
122
+ <div className="px-3 pt-2.5 pb-1">
123
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Cache Performance</span>
124
+ </div>
125
+ <CacheRing
126
+ cacheRead={tokens.cacheReadTokens}
127
+ cacheCreation={tokens.cacheCreationTokens}
128
+ totalInput={tokens.totalInputTokens}
129
+ />
128
130
  </div>
129
- <CacheRing
130
- cacheRead={tokens.cacheReadTokens}
131
- cacheCreation={tokens.cacheCreationTokens}
132
- totalInput={tokens.totalInputTokens}
133
- />
134
- </div>
135
131
 
136
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
137
- <div className="px-3 pt-2.5 pb-1">
138
- <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Model Routing</span>
132
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
133
+ <div className="px-3 pt-2.5 pb-1">
134
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Model Routing</span>
135
+ </div>
136
+ <RoutingChart routing={routing} agentBreakdown={agentBreakdown} />
139
137
  </div>
140
- <RoutingChart routing={routing} agentBreakdown={agentBreakdown} />
141
138
  </div>
142
139
 
143
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-border">
144
- <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
145
- <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
140
+ <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '2fr 2.5fr 1.5fr', gap: '0 1px' }}>
141
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-border">
142
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
143
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
144
+ </div>
145
+ <FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
146
146
  </div>
147
- <FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
148
- </div>
149
147
 
150
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
151
- <IntelPanel
152
- tokens={tokens}
153
- rotation={rotation}
154
- adaptive={adaptive}
155
- journalist={journalist}
156
- agentBreakdown={agentBreakdown}
157
- memory={memory}
158
- />
159
- </div>
148
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
149
+ <IntelPanel
150
+ tokens={tokens}
151
+ rotation={rotation}
152
+ adaptive={adaptive}
153
+ journalist={journalist}
154
+ agentBreakdown={agentBreakdown}
155
+ memory={memory}
156
+ />
157
+ </div>
160
158
 
161
- <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
162
- <TeamBurnPanel teams={teamBurn} />
159
+ <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
160
+ <TeamBurnPanel teams={teamBurn} />
161
+ </div>
163
162
  </div>
164
163
  </div>
165
164