groove-dev 0.27.14 → 0.27.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +587 -68
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +1 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +373 -58
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +32 -132
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
|
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
|
|
|
10
10
|
import { cn } from '../lib/cn';
|
|
11
11
|
import { Button } from '../components/ui/button';
|
|
12
12
|
import { Badge } from '../components/ui/badge';
|
|
13
|
-
import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil } from 'lucide-react';
|
|
13
|
+
import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, ChevronLeft, ChevronRight, FolderOpen } from 'lucide-react';
|
|
14
14
|
|
|
15
15
|
const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
|
|
16
16
|
const NODE_W = 220;
|
|
@@ -20,12 +20,28 @@ const NODE_Y_GAP = 130;
|
|
|
20
20
|
const MAX_PER_ROW = 4;
|
|
21
21
|
const ROOT_ID = '__groove_root__';
|
|
22
22
|
|
|
23
|
-
function loadPositions() {
|
|
24
|
-
|
|
23
|
+
function loadPositions(teamId) {
|
|
24
|
+
if (!teamId) return {};
|
|
25
|
+
try { return JSON.parse(localStorage.getItem(`groove:nodePositions:${teamId}`) || '{}'); } catch { return {}; }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Drop high-volume caches (chatHistory, activityLog) to free quota.
|
|
29
|
+
// Used as a fallback when setItem fails on savePositions.
|
|
30
|
+
function freeLocalStorage() {
|
|
31
|
+
let freed = false;
|
|
32
|
+
for (const key of ['groove:chatHistory', 'groove:activityLog']) {
|
|
33
|
+
if (localStorage.getItem(key) !== null) { localStorage.removeItem(key); freed = true; }
|
|
34
|
+
}
|
|
35
|
+
return freed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function savePositions(teamId, positions) {
|
|
39
|
+
if (!teamId) return;
|
|
40
|
+
const key = `groove:nodePositions:${teamId}`;
|
|
41
|
+
const s = JSON.stringify(positions);
|
|
42
|
+
try { localStorage.setItem(key, s); return; } catch { /* quota */ }
|
|
43
|
+
if (!freeLocalStorage()) return;
|
|
44
|
+
try { localStorage.setItem(key, s); } catch { /* still over — give up silently */ }
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
function loadTeamViewports() {
|
|
@@ -72,6 +88,29 @@ export function TeamTabBar() {
|
|
|
72
88
|
const submitting = useRef(false);
|
|
73
89
|
const [dragId, setDragId] = useState(null);
|
|
74
90
|
const [dragOverId, setDragOverId] = useState(null);
|
|
91
|
+
const scrollRef = useRef(null);
|
|
92
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
93
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
94
|
+
|
|
95
|
+
const checkScroll = useCallback(() => {
|
|
96
|
+
const el = scrollRef.current;
|
|
97
|
+
if (!el) return;
|
|
98
|
+
setCanScrollLeft(el.scrollLeft > 0);
|
|
99
|
+
setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const el = scrollRef.current;
|
|
104
|
+
if (!el) return;
|
|
105
|
+
checkScroll();
|
|
106
|
+
el.addEventListener('scroll', checkScroll);
|
|
107
|
+
const ro = new ResizeObserver(checkScroll);
|
|
108
|
+
ro.observe(el);
|
|
109
|
+
return () => {
|
|
110
|
+
el.removeEventListener('scroll', checkScroll);
|
|
111
|
+
ro.disconnect();
|
|
112
|
+
};
|
|
113
|
+
}, [checkScroll, teams.length]);
|
|
75
114
|
|
|
76
115
|
function handleCreate() {
|
|
77
116
|
const name = newName.trim();
|
|
@@ -79,7 +118,10 @@ export function TeamTabBar() {
|
|
|
79
118
|
submitting.current = true;
|
|
80
119
|
setNewName('');
|
|
81
120
|
setCreating(false);
|
|
82
|
-
createTeam(name).finally(() => {
|
|
121
|
+
createTeam(name).finally(() => {
|
|
122
|
+
submitting.current = false;
|
|
123
|
+
setTimeout(() => { if (scrollRef.current) scrollRef.current.scrollTo({ left: scrollRef.current.scrollWidth, behavior: 'smooth' }); }, 100);
|
|
124
|
+
});
|
|
83
125
|
}
|
|
84
126
|
|
|
85
127
|
function startRename(team) {
|
|
@@ -95,7 +137,20 @@ export function TeamTabBar() {
|
|
|
95
137
|
}
|
|
96
138
|
|
|
97
139
|
return (
|
|
98
|
-
<div className="flex items-end px-0 pt-0 pb-0 bg-surface-1 border-b border-border gap-0 flex-shrink-0">
|
|
140
|
+
<div className="flex items-end px-0 pt-0 pb-0 bg-surface-1 border-b border-border gap-0 flex-shrink-0 overflow-hidden">
|
|
141
|
+
{canScrollLeft && (
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => scrollRef.current?.scrollBy({ left: -300, behavior: 'smooth' })}
|
|
144
|
+
className="w-6 h-9 flex items-center justify-center bg-accent/15 text-accent hover:bg-accent/25 transition-colors flex-shrink-0 cursor-pointer"
|
|
145
|
+
>
|
|
146
|
+
<ChevronLeft size={14} />
|
|
147
|
+
</button>
|
|
148
|
+
)}
|
|
149
|
+
<div
|
|
150
|
+
ref={scrollRef}
|
|
151
|
+
className="flex items-end flex-1 min-w-0 overflow-x-auto gap-0"
|
|
152
|
+
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', WebkitOverflowScrolling: 'touch' }}
|
|
153
|
+
>
|
|
99
154
|
{teams.map((team) => {
|
|
100
155
|
const count = agents.filter((a) => a.teamId === team.id).length;
|
|
101
156
|
const isActive = team.id === activeTeamId;
|
|
@@ -122,7 +177,7 @@ export function TeamTabBar() {
|
|
|
122
177
|
onClick={() => !isRenaming && switchTeam(team.id)}
|
|
123
178
|
onDoubleClick={() => startRename(team)}
|
|
124
179
|
className={cn(
|
|
125
|
-
'group relative flex items-center gap-2 px-4 h-9 text-xs font-sans cursor-pointer select-none transition-colors',
|
|
180
|
+
'group relative flex items-center gap-2 px-4 h-9 text-xs font-sans cursor-pointer select-none transition-colors flex-shrink-0',
|
|
126
181
|
isActive
|
|
127
182
|
? 'text-text-0 font-semibold border-x border-x-border bg-[#242830]'
|
|
128
183
|
: 'text-text-3 hover:text-text-1 hover:bg-surface-3/50',
|
|
@@ -202,9 +257,8 @@ export function TeamTabBar() {
|
|
|
202
257
|
);
|
|
203
258
|
})}
|
|
204
259
|
|
|
205
|
-
{/* Create new team */}
|
|
206
260
|
{creating ? (
|
|
207
|
-
<div className="flex items-center gap-1.5 px-3 h-9
|
|
261
|
+
<div className="flex items-center gap-1.5 px-3 h-9 flex-shrink-0">
|
|
208
262
|
<input
|
|
209
263
|
value={newName}
|
|
210
264
|
onChange={(e) => setNewName(e.target.value)}
|
|
@@ -227,10 +281,20 @@ export function TeamTabBar() {
|
|
|
227
281
|
) : (
|
|
228
282
|
<button
|
|
229
283
|
onClick={() => setCreating(true)}
|
|
230
|
-
className="flex items-center justify-center w-
|
|
284
|
+
className="flex items-center justify-center w-6 h-6 my-auto mx-2 rounded-full bg-accent/15 text-accent hover:bg-accent/25 cursor-pointer transition-colors flex-shrink-0"
|
|
231
285
|
title="New team"
|
|
232
286
|
>
|
|
233
|
-
<Plus size={
|
|
287
|
+
<Plus size={12} />
|
|
288
|
+
</button>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
</div>
|
|
292
|
+
{canScrollRight && (
|
|
293
|
+
<button
|
|
294
|
+
onClick={() => scrollRef.current?.scrollBy({ left: 300, behavior: 'smooth' })}
|
|
295
|
+
className="w-6 h-9 flex items-center justify-center bg-accent/15 text-accent hover:bg-accent/25 transition-colors flex-shrink-0 cursor-pointer"
|
|
296
|
+
>
|
|
297
|
+
<ChevronRight size={14} />
|
|
234
298
|
</button>
|
|
235
299
|
)}
|
|
236
300
|
</div>
|
|
@@ -246,10 +310,23 @@ function AgentTreeInner() {
|
|
|
246
310
|
const selectAgent = useGrooveStore((s) => s.selectAgent);
|
|
247
311
|
const closeDetail = useGrooveStore((s) => s.closeDetail);
|
|
248
312
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
313
|
+
const prevAgentsRef = useRef([]);
|
|
314
|
+
const agents = useMemo(() => {
|
|
315
|
+
const next = allAgents
|
|
316
|
+
.filter((a) => a.teamId === activeTeamId)
|
|
317
|
+
.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id));
|
|
318
|
+
const prev = prevAgentsRef.current;
|
|
319
|
+
if (prev.length === next.length && prev.every((p, i) => p.id === next[i].id && p.status === next[i].status && p.name === next[i].name && p.model === next[i].model && p.tokensUsed === next[i].tokensUsed && p.contextUsage === next[i].contextUsage)) return prev;
|
|
320
|
+
prevAgentsRef.current = next;
|
|
321
|
+
return next;
|
|
322
|
+
}, [allAgents, activeTeamId]);
|
|
323
|
+
|
|
324
|
+
const positionsRef = useRef(loadPositions(activeTeamId));
|
|
325
|
+
const positionsTeamRef = useRef(activeTeamId);
|
|
326
|
+
if (positionsTeamRef.current !== activeTeamId) {
|
|
327
|
+
positionsTeamRef.current = activeTeamId;
|
|
328
|
+
positionsRef.current = loadPositions(activeTeamId);
|
|
329
|
+
}
|
|
253
330
|
|
|
254
331
|
const { fitView, setViewport } = useReactFlow();
|
|
255
332
|
const [prevCount, setPrevCount] = useState(0);
|
|
@@ -257,7 +334,7 @@ function AgentTreeInner() {
|
|
|
257
334
|
|
|
258
335
|
// Build nodes — positions are stable, data updates flow to node components
|
|
259
336
|
const targetNodes = useMemo(() => {
|
|
260
|
-
const saved =
|
|
337
|
+
const saved = positionsRef.current;
|
|
261
338
|
const runningCount = agents.filter((a) => a.status === 'running').length;
|
|
262
339
|
|
|
263
340
|
const nodes = [
|
|
@@ -271,17 +348,14 @@ function AgentTreeInner() {
|
|
|
271
348
|
},
|
|
272
349
|
];
|
|
273
350
|
|
|
274
|
-
// Track occupied positions so new nodes don't overlap existing ones
|
|
275
351
|
const occupied = new Set();
|
|
276
352
|
const posKey = (x, y) => `${Math.round(x / 100)},${Math.round(y / 100)}`;
|
|
277
353
|
|
|
278
|
-
// Mark root node position as occupied
|
|
279
354
|
const rootPos = saved[ROOT_ID] || { x: 0, y: 0 };
|
|
280
355
|
occupied.add(posKey(rootPos.x, rootPos.y));
|
|
281
356
|
|
|
282
|
-
// First pass: place agents with saved positions
|
|
283
357
|
const pending = [];
|
|
284
|
-
agents.forEach((agent
|
|
358
|
+
agents.forEach((agent) => {
|
|
285
359
|
const key = agent.name || agent.id;
|
|
286
360
|
if (saved[key]) {
|
|
287
361
|
const pos = saved[key];
|
|
@@ -292,37 +366,51 @@ function AgentTreeInner() {
|
|
|
292
366
|
draggable: true, selectable: true,
|
|
293
367
|
});
|
|
294
368
|
} else {
|
|
295
|
-
pending.push(
|
|
369
|
+
pending.push(agent);
|
|
296
370
|
}
|
|
297
371
|
});
|
|
298
372
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const totalInRow = Math.min(agents.length - row * MAX_PER_ROW, MAX_PER_ROW);
|
|
373
|
+
pending.forEach((agent, idx) => {
|
|
374
|
+
const row = Math.floor(idx / MAX_PER_ROW);
|
|
375
|
+
const col = idx % MAX_PER_ROW;
|
|
376
|
+
const totalInRow = Math.min(pending.length - row * MAX_PER_ROW, MAX_PER_ROW);
|
|
304
377
|
const offsetX = -((totalInRow - 1) * NODE_X_GAP) / 2;
|
|
305
378
|
let pos = { x: offsetX + col * NODE_X_GAP, y: NODE_Y_GAP + row * NODE_Y_GAP };
|
|
306
379
|
|
|
307
|
-
// If position is occupied, shift down until we find empty space
|
|
308
380
|
while (occupied.has(posKey(pos.x, pos.y))) {
|
|
309
381
|
pos = { x: pos.x, y: pos.y + NODE_Y_GAP };
|
|
310
382
|
}
|
|
311
383
|
occupied.add(posKey(pos.x, pos.y));
|
|
312
384
|
|
|
385
|
+
const key = agent.name || agent.id;
|
|
313
386
|
nodes.push({
|
|
314
387
|
id: agent.id, type: 'agentNode', position: pos,
|
|
315
388
|
data: { agent, timeline: tokenTimeline[agent.id] || [] },
|
|
316
389
|
draggable: true, selectable: true,
|
|
317
390
|
});
|
|
318
|
-
}
|
|
391
|
+
});
|
|
319
392
|
|
|
320
393
|
return nodes;
|
|
321
|
-
}, [agents, tokenTimeline]);
|
|
394
|
+
}, [agents, tokenTimeline, activeTeamId]);
|
|
395
|
+
|
|
396
|
+
// Auto-save positions for newly placed nodes to positionsRef + localStorage
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
const newPositions = {};
|
|
399
|
+
targetNodes.forEach((n) => {
|
|
400
|
+
const key = n.id === ROOT_ID ? ROOT_ID : (n.data?.agent?.name || n.id);
|
|
401
|
+
if (!positionsRef.current[key]) {
|
|
402
|
+
newPositions[key] = n.position;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (Object.keys(newPositions).length > 0) {
|
|
406
|
+
Object.assign(positionsRef.current, newPositions);
|
|
407
|
+
savePositions(activeTeamId, positionsRef.current);
|
|
408
|
+
}
|
|
409
|
+
}, [targetNodes, activeTeamId]);
|
|
322
410
|
|
|
323
411
|
// Build edges — compute closest handle based on saved node positions
|
|
324
412
|
const targetEdges = useMemo(() => {
|
|
325
|
-
const saved = loadPositions();
|
|
413
|
+
const saved = loadPositions(activeTeamId);
|
|
326
414
|
const rootPos = saved[ROOT_ID] || { x: 0, y: 0 };
|
|
327
415
|
|
|
328
416
|
return agents.map((agent, i) => {
|
|
@@ -354,7 +442,7 @@ function AgentTreeInner() {
|
|
|
354
442
|
animated: agent.status === 'running',
|
|
355
443
|
};
|
|
356
444
|
});
|
|
357
|
-
}, [agents]);
|
|
445
|
+
}, [agents, activeTeamId]);
|
|
358
446
|
|
|
359
447
|
const [nodes, setNodes, onNodesChange] = useNodesState(targetNodes);
|
|
360
448
|
const [edges, setEdges, onEdgesChange] = useEdgesState(targetEdges);
|
|
@@ -364,15 +452,12 @@ function AgentTreeInner() {
|
|
|
364
452
|
useEffect(() => {
|
|
365
453
|
setNodes((current) => {
|
|
366
454
|
const currentMap = new Map(current.map((n) => [n.id, n]));
|
|
367
|
-
const newIds = new Set(targetNodes.map((n) => n.id));
|
|
368
455
|
|
|
369
456
|
return targetNodes.map((tn) => {
|
|
370
457
|
const existing = currentMap.get(tn.id);
|
|
371
458
|
if (existing) {
|
|
372
|
-
// Preserve existing position, update data only
|
|
373
459
|
return { ...existing, data: tn.data };
|
|
374
460
|
}
|
|
375
|
-
// New node — use calculated position
|
|
376
461
|
return tn;
|
|
377
462
|
});
|
|
378
463
|
});
|
|
@@ -474,13 +559,12 @@ function AgentTreeInner() {
|
|
|
474
559
|
}, [nodes, setEdges]);
|
|
475
560
|
|
|
476
561
|
const onNodeDragStop = useCallback((_e, node) => {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
const saved = loadPositions();
|
|
562
|
+
const key = node.id === ROOT_ID ? ROOT_ID : (node.data?.agent?.name || node.id);
|
|
563
|
+
positionsRef.current[key] = node.position;
|
|
564
|
+
const saved = loadPositions(activeTeamId);
|
|
481
565
|
saved[key] = node.position;
|
|
482
|
-
savePositions(saved);
|
|
483
|
-
}, [
|
|
566
|
+
savePositions(activeTeamId, saved);
|
|
567
|
+
}, [activeTeamId]);
|
|
484
568
|
|
|
485
569
|
return (
|
|
486
570
|
<ReactFlow
|
|
@@ -556,6 +640,19 @@ function EmptyState({ onPlanner, onSpawn }) {
|
|
|
556
640
|
</button>
|
|
557
641
|
</div>
|
|
558
642
|
|
|
643
|
+
{window.groove?.openFolder && (
|
|
644
|
+
<div className="max-w-sm mx-auto">
|
|
645
|
+
<p className="text-xs text-text-3 mb-2">Or open a different project</p>
|
|
646
|
+
<button
|
|
647
|
+
onClick={() => window.groove.openFolder()}
|
|
648
|
+
className="w-full h-10 rounded-lg border border-border-subtle bg-surface-2 hover:bg-surface-3 text-sm text-text-1 font-medium flex items-center justify-center gap-2 cursor-pointer transition-colors"
|
|
649
|
+
>
|
|
650
|
+
<FolderOpen size={16} className="text-accent" />
|
|
651
|
+
Open Folder
|
|
652
|
+
</button>
|
|
653
|
+
</div>
|
|
654
|
+
)}
|
|
655
|
+
|
|
559
656
|
<p className="text-xs text-text-4 font-sans">
|
|
560
657
|
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+K</kbd>
|
|
561
658
|
<span className="mx-1.5">command palette</span>
|
|
@@ -745,12 +842,21 @@ export default function AgentsView() {
|
|
|
745
842
|
) : teamAgents.length === 0 ? (
|
|
746
843
|
<EmptyState onPlanner={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
|
|
747
844
|
) : (
|
|
748
|
-
<ReactFlowProvider>
|
|
845
|
+
<ReactFlowProvider key={activeTeamId}>
|
|
749
846
|
<AgentTreeInner />
|
|
750
847
|
</ReactFlowProvider>
|
|
751
848
|
)}
|
|
752
849
|
</div>
|
|
753
850
|
<RecommendedTeamCard />
|
|
851
|
+
{!isLoading && teamAgents.length > 0 && (
|
|
852
|
+
<button
|
|
853
|
+
onClick={() => openDetail({ type: 'spawn' })}
|
|
854
|
+
className="absolute bottom-4 left-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
|
|
855
|
+
>
|
|
856
|
+
<Plus size={14} />
|
|
857
|
+
Spawn
|
|
858
|
+
</button>
|
|
859
|
+
)}
|
|
754
860
|
</div>
|
|
755
861
|
);
|
|
756
862
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../stores/groove';
|
|
4
4
|
import { FileTree } from '../components/editor/file-tree';
|
|
5
5
|
import { EditorTabs } from '../components/editor/editor-tabs';
|
|
6
6
|
import { CodeEditor } from '../components/editor/code-editor';
|
|
7
7
|
import { MediaViewer, isMediaFile } from '../components/editor/media-viewer';
|
|
8
|
+
import { EditorStatusBar } from '../components/editor/editor-status-bar';
|
|
9
|
+
import { GotoLine } from '../components/editor/goto-line';
|
|
10
|
+
import { Breadcrumbs } from '../components/editor/breadcrumbs';
|
|
8
11
|
import { Code2, AlertTriangle, RefreshCw, X, Eye, FileCode } from 'lucide-react';
|
|
9
12
|
import { Button } from '../components/ui/button';
|
|
10
13
|
import { api } from '../lib/api';
|
|
@@ -15,6 +18,10 @@ function isHtmlFile(path) {
|
|
|
15
18
|
return ext === 'html' || ext === 'htm';
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
const SIDEBAR_DEFAULT = 240;
|
|
22
|
+
const SIDEBAR_MIN = 160;
|
|
23
|
+
const SIDEBAR_MAX = 400;
|
|
24
|
+
|
|
18
25
|
export default function EditorView() {
|
|
19
26
|
const activeFile = useGrooveStore((s) => s.editorActiveFile);
|
|
20
27
|
const files = useGrooveStore((s) => s.editorFiles);
|
|
@@ -23,10 +30,19 @@ export default function EditorView() {
|
|
|
23
30
|
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
24
31
|
const reloadFile = useGrooveStore((s) => s.reloadFile);
|
|
25
32
|
const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
|
|
33
|
+
const sidebarWidth = useGrooveStore((s) => s.editorSidebarWidth);
|
|
34
|
+
const setSidebarWidth = useGrooveStore((s) => s.setEditorSidebarWidth);
|
|
26
35
|
|
|
27
36
|
const [rootDir, setRootDir] = useState('');
|
|
28
37
|
const [previewMode, setPreviewMode] = useState(false);
|
|
29
38
|
const [previewKey, setPreviewKey] = useState(0);
|
|
39
|
+
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
|
|
40
|
+
const [showGotoLine, setShowGotoLine] = useState(false);
|
|
41
|
+
|
|
42
|
+
const editorViewRef = useRef(null);
|
|
43
|
+
const dragging = useRef(false);
|
|
44
|
+
const startX = useRef(0);
|
|
45
|
+
const startW = useRef(0);
|
|
30
46
|
|
|
31
47
|
// Fetch root dir
|
|
32
48
|
useEffect(() => {
|
|
@@ -36,6 +52,53 @@ export default function EditorView() {
|
|
|
36
52
|
// Reset preview mode when switching files
|
|
37
53
|
useEffect(() => { setPreviewMode(false); }, [activeFile]);
|
|
38
54
|
|
|
55
|
+
// Keyboard shortcuts
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
function handleKeyDown(e) {
|
|
58
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'g') {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setShowGotoLine(true);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
64
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Sidebar resize handlers
|
|
68
|
+
const onSidebarMouseDown = useCallback((e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
dragging.current = true;
|
|
71
|
+
startX.current = e.clientX;
|
|
72
|
+
startW.current = sidebarWidth;
|
|
73
|
+
|
|
74
|
+
function onMouseMove(e) {
|
|
75
|
+
if (!dragging.current) return;
|
|
76
|
+
const delta = e.clientX - startX.current;
|
|
77
|
+
const newW = Math.min(Math.max(startW.current + delta, SIDEBAR_MIN), SIDEBAR_MAX);
|
|
78
|
+
setSidebarWidth(newW);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onMouseUp() {
|
|
82
|
+
dragging.current = false;
|
|
83
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
84
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
88
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
89
|
+
}, [sidebarWidth, setSidebarWidth]);
|
|
90
|
+
|
|
91
|
+
function handleGoto(line) {
|
|
92
|
+
const view = editorViewRef.current;
|
|
93
|
+
if (!view) return;
|
|
94
|
+
const docLine = view.state.doc.line(Math.min(line, view.state.doc.lines));
|
|
95
|
+
view.dispatch({
|
|
96
|
+
selection: { anchor: docLine.from },
|
|
97
|
+
scrollIntoView: true,
|
|
98
|
+
});
|
|
99
|
+
view.focus();
|
|
100
|
+
}
|
|
101
|
+
|
|
39
102
|
const file = activeFile ? files[activeFile] : null;
|
|
40
103
|
const isMedia = activeFile && isMediaFile(activeFile);
|
|
41
104
|
const isHtml = activeFile && isHtmlFile(activeFile);
|
|
@@ -44,8 +107,14 @@ export default function EditorView() {
|
|
|
44
107
|
return (
|
|
45
108
|
<div className="flex h-full">
|
|
46
109
|
{/* File tree sidebar */}
|
|
47
|
-
<div className="
|
|
110
|
+
<div className="flex-shrink-0 border-r border-border relative" style={{ width: sidebarWidth }}>
|
|
48
111
|
<FileTree rootDir={rootDir} />
|
|
112
|
+
{/* Drag handle */}
|
|
113
|
+
<div
|
|
114
|
+
className="absolute top-0 right-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
115
|
+
onMouseDown={onSidebarMouseDown}
|
|
116
|
+
onDoubleClick={() => setSidebarWidth(SIDEBAR_DEFAULT)}
|
|
117
|
+
/>
|
|
49
118
|
</div>
|
|
50
119
|
|
|
51
120
|
{/* Editor area */}
|
|
@@ -53,8 +122,20 @@ export default function EditorView() {
|
|
|
53
122
|
{/* Tab bar */}
|
|
54
123
|
<EditorTabs />
|
|
55
124
|
|
|
125
|
+
{/* Breadcrumbs */}
|
|
126
|
+
{activeFile && !isMedia && <Breadcrumbs path={activeFile} />}
|
|
127
|
+
|
|
56
128
|
{/* Content */}
|
|
57
129
|
<div className="flex-1 relative min-h-0">
|
|
130
|
+
{/* Go to line dialog */}
|
|
131
|
+
{showGotoLine && (
|
|
132
|
+
<GotoLine
|
|
133
|
+
currentLine={cursorPos.line}
|
|
134
|
+
onGoto={handleGoto}
|
|
135
|
+
onClose={() => setShowGotoLine(false)}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
|
|
58
139
|
{/* External change banner */}
|
|
59
140
|
{hasExternalChange && (
|
|
60
141
|
<div className="absolute top-0 left-0 right-0 z-10 flex items-center gap-2 px-4 py-2 bg-warning/10 border-b border-warning/20">
|
|
@@ -120,6 +201,8 @@ export default function EditorView() {
|
|
|
120
201
|
language={file.language}
|
|
121
202
|
onChange={(content) => updateFileContent(activeFile, content)}
|
|
122
203
|
onSave={() => saveFile(activeFile)}
|
|
204
|
+
onCursorChange={setCursorPos}
|
|
205
|
+
viewRef={editorViewRef}
|
|
123
206
|
/>
|
|
124
207
|
)
|
|
125
208
|
)}
|
|
@@ -132,9 +215,16 @@ export default function EditorView() {
|
|
|
132
215
|
language={file.language}
|
|
133
216
|
onChange={(content) => updateFileContent(activeFile, content)}
|
|
134
217
|
onSave={() => saveFile(activeFile)}
|
|
218
|
+
onCursorChange={setCursorPos}
|
|
219
|
+
viewRef={editorViewRef}
|
|
135
220
|
/>
|
|
136
221
|
)}
|
|
137
222
|
</div>
|
|
223
|
+
|
|
224
|
+
{/* Status bar */}
|
|
225
|
+
{activeFile && !isMedia && (
|
|
226
|
+
<EditorStatusBar cursorPos={cursorPos} language={file?.language} />
|
|
227
|
+
)}
|
|
138
228
|
</div>
|
|
139
229
|
</div>
|
|
140
230
|
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
4
|
+
import { FederationPanel } from '../components/settings/federation-panel';
|
|
5
|
+
import { ProGate } from '../components/pro/pro-gate';
|
|
6
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
7
|
+
import { Globe } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
export default function FederationView() {
|
|
10
|
+
const fetchFederationStatus = useGrooveStore((s) => s.fetchFederationStatus);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetchFederationStatus();
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-col h-full bg-surface-0">
|
|
18
|
+
<div className="flex items-center gap-3 px-6 py-4 border-b border-border">
|
|
19
|
+
<div className="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center">
|
|
20
|
+
<Globe size={16} className="text-accent" />
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<h1 className="text-sm font-semibold text-text-0 font-sans">Federation</h1>
|
|
24
|
+
<p className="text-2xs text-text-3 font-sans">Connect to remote Groove daemons</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<ScrollArea className="flex-1">
|
|
29
|
+
<div className="max-w-2xl mx-auto px-6 py-5">
|
|
30
|
+
<ProGate feature="Federation" featureKey="federation" description="Daemon-to-daemon federation over Tailscale mesh for multi-machine agent coordination">
|
|
31
|
+
<FederationPanel />
|
|
32
|
+
</ProGate>
|
|
33
|
+
</div>
|
|
34
|
+
</ScrollArea>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -22,8 +22,8 @@ import { RepoImport } from '../components/marketplace/repo-import';
|
|
|
22
22
|
import { RepoCard } from '../components/marketplace/repo-card';
|
|
23
23
|
import { RepoNukeDialog } from '../components/marketplace/repo-nuke-dialog';
|
|
24
24
|
import {
|
|
25
|
-
ChevronLeft, ChevronDown, Sparkles, Plug, LogIn,
|
|
26
|
-
|
|
25
|
+
ChevronLeft, ChevronDown, Sparkles, Plug, LogIn,
|
|
26
|
+
Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
|
|
27
27
|
GitBranch,
|
|
28
28
|
} from 'lucide-react';
|
|
29
29
|
|
|
@@ -588,45 +588,6 @@ function MyLibrary() {
|
|
|
588
588
|
);
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
// ── Auth Area (header right) ─────────────────────────────
|
|
592
|
-
function AuthArea() {
|
|
593
|
-
const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
|
|
594
|
-
const user = useGrooveStore((s) => s.marketplaceUser);
|
|
595
|
-
const login = useGrooveStore((s) => s.marketplaceLogin);
|
|
596
|
-
const logout = useGrooveStore((s) => s.marketplaceLogout);
|
|
597
|
-
|
|
598
|
-
if (authenticated) {
|
|
599
|
-
return (
|
|
600
|
-
<div className="flex items-center gap-1">
|
|
601
|
-
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded bg-surface-3 border border-border-subtle">
|
|
602
|
-
<div className="w-4 h-4 rounded-full bg-accent/20 flex items-center justify-center">
|
|
603
|
-
<User size={9} className="text-accent" />
|
|
604
|
-
</div>
|
|
605
|
-
<span className="text-xs text-text-0 font-sans font-medium max-w-[120px] truncate">
|
|
606
|
-
{user?.displayName || user?.id || 'Account'}
|
|
607
|
-
</span>
|
|
608
|
-
</div>
|
|
609
|
-
<button
|
|
610
|
-
onClick={logout}
|
|
611
|
-
className="flex items-center gap-1 px-2 py-1.5 rounded text-xs text-text-3 hover:text-text-0 hover:bg-surface-3 font-sans cursor-pointer transition-colors"
|
|
612
|
-
>
|
|
613
|
-
<LogOut size={11} />
|
|
614
|
-
</button>
|
|
615
|
-
</div>
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return (
|
|
620
|
-
<button
|
|
621
|
-
onClick={login}
|
|
622
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold font-sans text-text-0 bg-accent/15 border border-accent/25 rounded hover:bg-accent/25 cursor-pointer transition-colors"
|
|
623
|
-
>
|
|
624
|
-
<LogIn size={12} />
|
|
625
|
-
Sign in
|
|
626
|
-
</button>
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
591
|
// ── GitHub Browse ───────────────────────────────────────
|
|
631
592
|
function GitHubBrowse() {
|
|
632
593
|
const importedRepos = useGrooveStore((s) => s.importedRepos);
|
|
@@ -735,7 +696,6 @@ export default function MarketplaceView() {
|
|
|
735
696
|
})}
|
|
736
697
|
</div>
|
|
737
698
|
<div className="flex-1" />
|
|
738
|
-
<AuthArea />
|
|
739
699
|
</div>
|
|
740
700
|
</div>
|
|
741
701
|
|