groove-dev 0.27.8 → 0.27.11
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/node_modules/@groove-dev/daemon/src/api.js +460 -25
- package/node_modules/@groove-dev/daemon/src/index.js +7 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
- package/node_modules/@groove-dev/daemon/src/process.js +67 -7
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
- package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +14 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
- package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
- package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
- package/node_modules/@groove-dev/gui/vite.config.js +3 -0
- package/package.json +7 -2
- package/packages/daemon/src/api.js +460 -25
- package/packages/daemon/src/index.js +7 -0
- package/packages/daemon/src/introducer.js +72 -4
- package/packages/daemon/src/journalist.js +66 -11
- package/packages/daemon/src/process.js +67 -7
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/repo-import.js +541 -0
- package/packages/daemon/src/rotator.js +28 -1
- package/packages/daemon/src/supervisor.js +2 -1
- package/packages/daemon/src/tunnel-manager.js +504 -0
- package/packages/daemon/src/validate.js +13 -0
- package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
- package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
- package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
- package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
- package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
- package/packages/gui/src/app.css +14 -0
- package/packages/gui/src/app.jsx +13 -0
- package/packages/gui/src/components/agents/agent-config.jsx +130 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
- package/packages/gui/src/components/layout/app-shell.jsx +7 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/packages/gui/src/components/layout/command-palette.jsx +14 -4
- package/packages/gui/src/components/layout/status-bar.jsx +46 -11
- package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
- package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
- package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
- package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/lib/edition.js +4 -0
- package/packages/gui/src/lib/electron.js +17 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +139 -6
- package/packages/gui/src/views/dashboard.jsx +38 -39
- package/packages/gui/src/views/marketplace.jsx +82 -0
- package/packages/gui/src/views/settings.jsx +66 -0
- package/packages/gui/vite.config.js +3 -0
- package/integrations/FEDERATION_PLAN.md +0 -583
- package/integrations/VOICE_PLAN.md +0 -232
- package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
- package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
- package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
- package/test-slack.mjs +0 -28
- /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { useKeyboard } from '../../lib/hooks/use-keyboard';
|
|
5
|
+
import { isElectron } from '../../lib/electron';
|
|
6
|
+
import { cn } from '../../lib/cn';
|
|
5
7
|
import { TooltipProvider } from '../ui/tooltip';
|
|
6
8
|
import { ToastContainer } from '../ui/toast';
|
|
7
9
|
import { ActivityBar } from './activity-bar';
|
|
@@ -9,6 +11,8 @@ import { BreadcrumbBar } from './breadcrumb-bar';
|
|
|
9
11
|
import { StatusBar } from './status-bar';
|
|
10
12
|
import { DetailPanel } from './detail-panel';
|
|
11
13
|
import { CommandPalette } from './command-palette';
|
|
14
|
+
import { ApprovalModal } from '../ui/approval-modal';
|
|
15
|
+
import { QuickConnect } from '../settings/quick-connect';
|
|
12
16
|
import { TeamTabBar } from '../../views/agents';
|
|
13
17
|
|
|
14
18
|
export function AppShell({ children, detailContent, terminalContent }) {
|
|
@@ -55,7 +59,7 @@ export function AppShell({ children, detailContent, terminalContent }) {
|
|
|
55
59
|
|
|
56
60
|
return (
|
|
57
61
|
<TooltipProvider>
|
|
58
|
-
<div className=
|
|
62
|
+
<div className={cn('w-full h-full flex flex-col bg-surface-2 text-text-1 font-sans', isElectron() && 'electron-app')}>
|
|
59
63
|
<BreadcrumbBar
|
|
60
64
|
activeView={activeView}
|
|
61
65
|
connected={connected}
|
|
@@ -113,6 +117,8 @@ export function AppShell({ children, detailContent, terminalContent }) {
|
|
|
113
117
|
/>
|
|
114
118
|
|
|
115
119
|
<CommandPalette />
|
|
120
|
+
<QuickConnect />
|
|
121
|
+
<ApprovalModal />
|
|
116
122
|
<ToastContainer />
|
|
117
123
|
</div>
|
|
118
124
|
</TooltipProvider>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { Search, Plus, ChevronRight } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
|
+
import { isElectron, getPlatform } from '../../lib/electron';
|
|
4
5
|
|
|
5
6
|
const VIEW_LABELS = {
|
|
6
7
|
agents: 'Agents',
|
|
@@ -23,17 +24,34 @@ export function BreadcrumbBar({
|
|
|
23
24
|
crumbs.push(editorActiveFile.split('/').pop());
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
const electron = isElectron();
|
|
28
|
+
const darwinDrag = electron && getPlatform() === 'darwin';
|
|
29
|
+
|
|
26
30
|
return (
|
|
27
|
-
<header
|
|
31
|
+
<header
|
|
32
|
+
className={cn(
|
|
33
|
+
'h-11 flex-shrink-0 flex items-center gap-3 px-4 bg-surface-3 border-b border-border',
|
|
34
|
+
darwinDrag && 'pl-20 electron-drag electron-no-drag-children',
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
28
37
|
{/* Logo */}
|
|
29
38
|
<img src="/favicon.png" alt="Groove" className="h-7 w-7 rounded-full flex-shrink-0" />
|
|
30
39
|
|
|
31
|
-
{/* Host badge */}
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
{/* Host badge — show instance name from ?instance= or raw host */}
|
|
41
|
+
{(() => {
|
|
42
|
+
const instance = new URLSearchParams(window.location.search).get('instance');
|
|
43
|
+
if (instance) return (
|
|
44
|
+
<span className="text-2xs font-mono font-semibold text-accent bg-accent/10 px-1.5 py-0.5 rounded flex-shrink-0">
|
|
45
|
+
{instance}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
if (daemonHost) return (
|
|
49
|
+
<span className="text-2xs font-mono font-semibold text-text-3 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0">
|
|
50
|
+
{daemonHost}
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
return null;
|
|
54
|
+
})()}
|
|
37
55
|
|
|
38
56
|
<div className="flex-1 min-w-4" />
|
|
39
57
|
|
|
@@ -43,7 +61,7 @@ export function BreadcrumbBar({
|
|
|
43
61
|
className={cn(
|
|
44
62
|
'flex items-center gap-2.5 h-8 px-4 rounded-full w-full max-w-md',
|
|
45
63
|
'bg-surface-1 border border-border-subtle',
|
|
46
|
-
'text-
|
|
64
|
+
'text-xs text-text-4 font-sans',
|
|
47
65
|
'hover:border-border hover:text-text-3 transition-colors cursor-pointer',
|
|
48
66
|
)}
|
|
49
67
|
>
|
|
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
4
4
|
import {
|
|
5
5
|
Network, Code2, ChartSpline, Puzzle, Users, Plus,
|
|
6
6
|
RotateCw, Skull, MessageSquare, Terminal, Newspaper,
|
|
7
|
-
Search,
|
|
7
|
+
Search, Radio, ExternalLink,
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { cn } from '../../lib/cn';
|
|
10
10
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -26,6 +26,7 @@ export function CommandPalette() {
|
|
|
26
26
|
const open = useGrooveStore((s) => s.commandPaletteOpen);
|
|
27
27
|
const toggle = useGrooveStore((s) => s.toggleCommandPalette);
|
|
28
28
|
const agents = useGrooveStore((s) => s.agents);
|
|
29
|
+
const savedTunnels = useGrooveStore((s) => s.savedTunnels);
|
|
29
30
|
const store = useGrooveStore;
|
|
30
31
|
|
|
31
32
|
const [query, setQuery] = useState('');
|
|
@@ -41,8 +42,17 @@ export function CommandPalette() {
|
|
|
41
42
|
{ id: `kill:${a.id}`, label: `Kill ${a.name}`, icon: Skull, category: 'Agents', action: (s) => { s.killAgent(a.id); } },
|
|
42
43
|
] : []),
|
|
43
44
|
]);
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const tunnelCommands = [
|
|
46
|
+
{ id: 'action:quickconnect', label: 'Quick Connect', icon: Radio, category: 'Remote', action: (s) => { s.toggleQuickConnect(); } },
|
|
47
|
+
...savedTunnels.map((t) => t.active
|
|
48
|
+
? { id: `tunnel:open:${t.id}`, label: `Open ${t.name}`, icon: ExternalLink, category: 'Remote', action: () => {
|
|
49
|
+
window.open(`http://localhost:${t.localPort}?instance=${encodeURIComponent(t.name)}`, '_blank');
|
|
50
|
+
}}
|
|
51
|
+
: { id: `tunnel:connect:${t.id}`, label: `Connect to ${t.name}`, icon: Radio, category: 'Remote', action: (s) => { s.connectTunnel(t.id); } }
|
|
52
|
+
),
|
|
53
|
+
];
|
|
54
|
+
return [...STATIC_COMMANDS, ...agentCommands, ...tunnelCommands];
|
|
55
|
+
}, [agents, savedTunnels]);
|
|
46
56
|
|
|
47
57
|
// Filter
|
|
48
58
|
const filtered = useMemo(() => {
|
|
@@ -104,7 +114,7 @@ export function CommandPalette() {
|
|
|
104
114
|
value={query}
|
|
105
115
|
onChange={(e) => { setQuery(e.target.value); setSelectedIndex(0); }}
|
|
106
116
|
placeholder="Type a command..."
|
|
107
|
-
className="flex-1 bg-transparent text-
|
|
117
|
+
className="flex-1 bg-transparent text-xs text-text-0 font-sans placeholder:text-text-4 focus:outline-none"
|
|
108
118
|
/>
|
|
109
119
|
</div>
|
|
110
120
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Terminal, BookOpen } from 'lucide-react';
|
|
2
|
+
import { Terminal, BookOpen, Radio, Plug } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { StatusDot } from '../ui/status-dot';
|
|
5
5
|
import { fmtUptime } from '../../lib/format';
|
|
6
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
7
|
+
import { isElectron, openExternal } from '../../lib/electron';
|
|
6
8
|
|
|
7
9
|
export function StatusBar({
|
|
8
10
|
connected,
|
|
@@ -12,6 +14,10 @@ export function StatusBar({
|
|
|
12
14
|
terminalVisible,
|
|
13
15
|
onToggleTerminal,
|
|
14
16
|
}) {
|
|
17
|
+
const savedTunnels = useGrooveStore((s) => s.savedTunnels);
|
|
18
|
+
const activeTunnel = savedTunnels.find((t) => t.active);
|
|
19
|
+
const electron = isElectron();
|
|
20
|
+
|
|
15
21
|
return (
|
|
16
22
|
<footer className="h-6 flex-shrink-0 flex items-center px-3 bg-surface-3 border-t border-border text-2xs font-sans select-none">
|
|
17
23
|
{/* Left: connection + stats */}
|
|
@@ -19,7 +25,7 @@ export function StatusBar({
|
|
|
19
25
|
<div className="flex items-center gap-1.5">
|
|
20
26
|
<StatusDot status={connected ? 'running' : 'crashed'} size="sm" />
|
|
21
27
|
<span className={connected ? 'text-text-2' : 'text-danger'}>
|
|
22
|
-
{connected ? 'Connected' : 'Offline'}
|
|
28
|
+
{connected ? (electron ? 'Desktop' : 'Connected') : 'Offline'}
|
|
23
29
|
</span>
|
|
24
30
|
</div>
|
|
25
31
|
{connected && uptime > 0 && (
|
|
@@ -28,20 +34,49 @@ export function StatusBar({
|
|
|
28
34
|
{connected && agentCount > 0 && (
|
|
29
35
|
<span className="text-text-4">{runningCount}/{agentCount} agents</span>
|
|
30
36
|
)}
|
|
37
|
+
{activeTunnel ? (
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => {
|
|
40
|
+
const port = activeTunnel.localPort;
|
|
41
|
+
const name = encodeURIComponent(activeTunnel.name);
|
|
42
|
+
openExternal(`http://localhost:${port}?instance=${name}`);
|
|
43
|
+
}}
|
|
44
|
+
className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
|
|
45
|
+
title="Open remote GUI"
|
|
46
|
+
>
|
|
47
|
+
<Radio size={10} className="text-success" />
|
|
48
|
+
<span>{activeTunnel.name}</span>
|
|
49
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
50
|
+
{activeTunnel.latencyMs != null && (
|
|
51
|
+
<span className="text-text-4">{activeTunnel.latencyMs}ms</span>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
) : savedTunnels.length > 0 && (
|
|
55
|
+
<button
|
|
56
|
+
onClick={() => useGrooveStore.getState().toggleQuickConnect()}
|
|
57
|
+
className="flex items-center gap-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
|
|
58
|
+
title="Quick Connect to remote server"
|
|
59
|
+
>
|
|
60
|
+
<Plug size={10} />
|
|
61
|
+
<span>Connect</span>
|
|
62
|
+
</button>
|
|
63
|
+
)}
|
|
31
64
|
</div>
|
|
32
65
|
|
|
33
66
|
<div className="flex-1" />
|
|
34
67
|
|
|
35
68
|
{/* Right: docs + terminal toggle */}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
{!electron && (
|
|
70
|
+
<a
|
|
71
|
+
href="https://docs.groovedev.ai"
|
|
72
|
+
target="_blank"
|
|
73
|
+
rel="noopener noreferrer"
|
|
74
|
+
className="flex items-center gap-1.5 px-2 h-full text-text-3 hover:text-text-1 hover:bg-surface-5 transition-colors no-underline"
|
|
75
|
+
>
|
|
76
|
+
<BookOpen size={12} />
|
|
77
|
+
<span>Docs</span>
|
|
78
|
+
</a>
|
|
79
|
+
)}
|
|
45
80
|
<button
|
|
46
81
|
onClick={onToggleTerminal}
|
|
47
82
|
className={cn(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { FolderOpen, Trash2, Bomb } from 'lucide-react';
|
|
3
|
+
import { Badge } from '../ui/badge';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { timeAgo } from '../../lib/format';
|
|
6
|
+
|
|
7
|
+
export function RepoCard({ repo, onRemove, onNuke, onOpen }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="rounded-lg border border-border-subtle bg-surface-2 p-3 flex items-center gap-3">
|
|
10
|
+
<div className="flex-1 min-w-0">
|
|
11
|
+
<div className="flex items-center gap-2">
|
|
12
|
+
<span className="text-xs font-semibold text-text-0 font-sans truncate">{repo.repoName || repo.name}</span>
|
|
13
|
+
<span className="text-2xs text-text-3 font-sans">{repo.repoOwner || repo.owner}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
16
|
+
{repo.language && <Badge variant="outline" className="text-2xs">{repo.language}</Badge>}
|
|
17
|
+
<span className="text-2xs text-text-4 font-mono truncate max-w-[180px]">{repo.clonedTo || repo.path}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<span className="text-2xs text-text-4 font-sans mt-0.5 block">
|
|
20
|
+
{repo.clonedAt ? `Imported ${timeAgo(repo.clonedAt)}` : repo.status || ''}
|
|
21
|
+
</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
25
|
+
{onOpen && (
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => onOpen(repo)}
|
|
28
|
+
className={cn(
|
|
29
|
+
'flex items-center gap-1 px-2 py-1 rounded text-2xs font-sans cursor-pointer',
|
|
30
|
+
'text-accent bg-accent/10 hover:bg-accent/20 border-0 transition-colors',
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<FolderOpen size={11} />
|
|
34
|
+
Open
|
|
35
|
+
</button>
|
|
36
|
+
)}
|
|
37
|
+
{onRemove && (
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => onRemove(repo)}
|
|
40
|
+
className={cn(
|
|
41
|
+
'flex items-center gap-1 px-2 py-1 rounded text-2xs font-sans cursor-pointer',
|
|
42
|
+
'text-text-3 hover:text-text-1 hover:bg-surface-4 bg-transparent border-0 transition-colors',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<Trash2 size={11} />
|
|
46
|
+
Remove
|
|
47
|
+
</button>
|
|
48
|
+
)}
|
|
49
|
+
{onNuke && (
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => onNuke(repo)}
|
|
52
|
+
className={cn(
|
|
53
|
+
'flex items-center gap-1 px-2 py-1 rounded text-2xs font-sans cursor-pointer',
|
|
54
|
+
'text-danger bg-danger/10 hover:bg-danger/20 border-0 transition-colors',
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<Bomb size={11} />
|
|
58
|
+
Nuke
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Search, GitBranch, Star, ExternalLink, Loader2, Check,
|
|
5
|
+
FolderOpen, HardDrive, Package, PenLine, Users, ArrowLeft, Download,
|
|
6
|
+
} from 'lucide-react';
|
|
7
|
+
import { Button } from '../ui/button';
|
|
8
|
+
import { Badge } from '../ui/badge';
|
|
9
|
+
import { cn } from '../../lib/cn';
|
|
10
|
+
import { fmtNum } from '../../lib/format';
|
|
11
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
12
|
+
import { useToast } from '../../lib/hooks/use-toast';
|
|
13
|
+
|
|
14
|
+
const GITHUB_RE = /github\.com\/([^/]+)\/([^/\s#?]+)/;
|
|
15
|
+
|
|
16
|
+
export function RepoImport() {
|
|
17
|
+
const [step, setStep] = useState('input');
|
|
18
|
+
const [url, setUrl] = useState('');
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [preview, setPreview] = useState(null);
|
|
21
|
+
const [pathOption, setPathOption] = useState('standalone');
|
|
22
|
+
const [customPath, setCustomPath] = useState('');
|
|
23
|
+
const [createTeam, setCreateTeam] = useState(true);
|
|
24
|
+
const [teamName, setTeamName] = useState('');
|
|
25
|
+
|
|
26
|
+
const previewRepo = useGrooveStore((s) => s.previewRepo);
|
|
27
|
+
const importRepo = useGrooveStore((s) => s.importRepo);
|
|
28
|
+
const importInProgress = useGrooveStore((s) => s.importInProgress);
|
|
29
|
+
const toast = useToast();
|
|
30
|
+
|
|
31
|
+
const doPreview = useCallback(async (repoUrl) => {
|
|
32
|
+
const match = repoUrl.match(GITHUB_RE);
|
|
33
|
+
if (!match) return;
|
|
34
|
+
setLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
const data = await previewRepo(repoUrl);
|
|
37
|
+
setPreview(data);
|
|
38
|
+
setTeamName(data.name || match[2]);
|
|
39
|
+
setStep('preview');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
toast.error('Preview failed', err.message);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}, [previewRepo, toast]);
|
|
46
|
+
|
|
47
|
+
const handleUrlChange = useCallback((e) => {
|
|
48
|
+
const val = e.target.value;
|
|
49
|
+
setUrl(val);
|
|
50
|
+
if (GITHUB_RE.test(val) && step === 'input') {
|
|
51
|
+
doPreview(val);
|
|
52
|
+
}
|
|
53
|
+
}, [step, doPreview]);
|
|
54
|
+
|
|
55
|
+
const handleImport = useCallback(async () => {
|
|
56
|
+
if (!preview) return;
|
|
57
|
+
let targetPath;
|
|
58
|
+
if (pathOption === 'standalone') targetPath = `~/Projects/${preview.name}`;
|
|
59
|
+
else if (pathOption === 'subdirectory') targetPath = `./packages/${preview.name}`;
|
|
60
|
+
else targetPath = customPath;
|
|
61
|
+
try {
|
|
62
|
+
await importRepo(url, targetPath, createTeam, teamName);
|
|
63
|
+
toast.success(`Importing ${preview.name}`, 'Setup agent will handle the rest');
|
|
64
|
+
setStep('input');
|
|
65
|
+
setUrl('');
|
|
66
|
+
setPreview(null);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
toast.error('Import failed', err.message);
|
|
69
|
+
}
|
|
70
|
+
}, [preview, pathOption, customPath, url, createTeam, teamName, importRepo, toast]);
|
|
71
|
+
|
|
72
|
+
// ── Step 1: URL Input ──────────────────────────────────
|
|
73
|
+
if (step === 'input') {
|
|
74
|
+
return (
|
|
75
|
+
<div className="relative">
|
|
76
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
value={url}
|
|
80
|
+
onChange={handleUrlChange}
|
|
81
|
+
placeholder="Paste a GitHub URL..."
|
|
82
|
+
className={cn(
|
|
83
|
+
'w-full h-9 rounded-lg pl-9 pr-20 text-sm font-sans',
|
|
84
|
+
'bg-surface-1 border border-border text-text-0',
|
|
85
|
+
'placeholder:text-text-4',
|
|
86
|
+
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
|
87
|
+
'transition-colors duration-100',
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
<Button
|
|
91
|
+
variant="primary"
|
|
92
|
+
size="sm"
|
|
93
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2"
|
|
94
|
+
onClick={() => doPreview(url)}
|
|
95
|
+
disabled={!GITHUB_RE.test(url) || loading}
|
|
96
|
+
>
|
|
97
|
+
{loading ? <Loader2 size={12} className="animate-spin" /> : 'Preview'}
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Step 2: Preview ────────────────────────────────────
|
|
104
|
+
if (step === 'preview' && preview) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-4">
|
|
107
|
+
<div className="rounded-lg border border-border-subtle bg-surface-2 p-5">
|
|
108
|
+
<div className="flex items-start gap-4">
|
|
109
|
+
<div className="w-12 h-12 rounded-xl bg-accent/8 flex items-center justify-center flex-shrink-0">
|
|
110
|
+
<GitBranch size={22} className="text-accent" />
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<div className="flex items-center gap-2.5 mb-1">
|
|
114
|
+
<span className="text-lg font-bold text-text-0 font-sans">{preview.name}</span>
|
|
115
|
+
<span className="text-xs text-text-4 font-sans">{preview.owner}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-3 text-2xs text-text-3 font-sans">
|
|
118
|
+
{preview.language && <Badge variant="outline" className="text-2xs">{preview.language}</Badge>}
|
|
119
|
+
{preview.stars != null && (
|
|
120
|
+
<span className="flex items-center gap-1">
|
|
121
|
+
<Star size={10} className="text-warning" fill="currentColor" />
|
|
122
|
+
{fmtNum(preview.stars)}
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
{preview.license && <span>{preview.license}</span>}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
129
|
+
<Button variant="primary" size="sm" onClick={() => setStep('configure')} className="h-8 text-xs gap-1.5 px-4">
|
|
130
|
+
<FolderOpen size={13} />
|
|
131
|
+
Clone & Setup
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="sm"
|
|
136
|
+
onClick={() => window.open(url.startsWith('http') ? url : `https://${url}`, '_blank')}
|
|
137
|
+
className="h-8 text-xs gap-1.5"
|
|
138
|
+
>
|
|
139
|
+
<ExternalLink size={12} />
|
|
140
|
+
GitHub
|
|
141
|
+
</Button>
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => { setStep('input'); setPreview(null); }}
|
|
144
|
+
className="text-2xs text-text-4 font-sans hover:text-text-2 cursor-pointer bg-transparent border-0 ml-1"
|
|
145
|
+
>
|
|
146
|
+
Cancel
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{preview.description && (
|
|
153
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 px-5 py-4">
|
|
154
|
+
<h4 className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2">About</h4>
|
|
155
|
+
<p className="text-sm text-text-1 font-sans leading-relaxed">{preview.description}</p>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{preview.readmePreview && (
|
|
160
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 px-5 py-4">
|
|
161
|
+
<h4 className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-3">README</h4>
|
|
162
|
+
<div className="text-sm text-text-2 font-sans leading-relaxed whitespace-pre-wrap">{preview.readmePreview}</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Step 3: Configure ──────────────────────────────────
|
|
170
|
+
if (step === 'configure' && preview) {
|
|
171
|
+
const pathOptions = [
|
|
172
|
+
{
|
|
173
|
+
id: 'standalone',
|
|
174
|
+
icon: HardDrive,
|
|
175
|
+
title: 'Standalone project',
|
|
176
|
+
description: 'Clone to its own directory, separate from this workspace',
|
|
177
|
+
path: `~/Projects/${preview.name}`,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'subdirectory',
|
|
181
|
+
icon: Package,
|
|
182
|
+
title: 'Workspace package',
|
|
183
|
+
description: 'Add as a package inside this project\'s monorepo',
|
|
184
|
+
path: `./packages/${preview.name}`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'custom',
|
|
188
|
+
icon: PenLine,
|
|
189
|
+
title: 'Custom location',
|
|
190
|
+
description: 'Choose your own path',
|
|
191
|
+
path: null,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="rounded-xl border border-border-subtle bg-surface-2 overflow-hidden">
|
|
197
|
+
{/* Header */}
|
|
198
|
+
<div className="px-5 py-4 border-b border-border-subtle bg-surface-3/50">
|
|
199
|
+
<div className="flex items-center gap-3">
|
|
200
|
+
<div className="w-9 h-9 rounded-lg bg-accent/10 flex items-center justify-center flex-shrink-0">
|
|
201
|
+
<Download size={16} className="text-accent" />
|
|
202
|
+
</div>
|
|
203
|
+
<div>
|
|
204
|
+
<h3 className="text-sm font-semibold text-text-0 font-sans">
|
|
205
|
+
Clone {preview.name}
|
|
206
|
+
</h3>
|
|
207
|
+
<p className="text-2xs text-text-4 font-sans mt-0.5">
|
|
208
|
+
{preview.owner}/{preview.name} — configure where to install
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div className="px-5 py-4 space-y-5">
|
|
215
|
+
{/* Location picker */}
|
|
216
|
+
<div>
|
|
217
|
+
<label className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2.5 block">
|
|
218
|
+
Install location
|
|
219
|
+
</label>
|
|
220
|
+
<div className="space-y-2">
|
|
221
|
+
{pathOptions.map((opt) => {
|
|
222
|
+
const Icon = opt.icon;
|
|
223
|
+
const selected = pathOption === opt.id;
|
|
224
|
+
return (
|
|
225
|
+
<button
|
|
226
|
+
key={opt.id}
|
|
227
|
+
onClick={() => setPathOption(opt.id)}
|
|
228
|
+
className={cn(
|
|
229
|
+
'w-full text-left rounded-lg border p-3.5 transition-all duration-150 cursor-pointer',
|
|
230
|
+
selected
|
|
231
|
+
? 'border-accent bg-accent/5 ring-1 ring-accent/30'
|
|
232
|
+
: 'border-border-subtle bg-surface-1 hover:border-border hover:bg-surface-1/80',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
<div className="flex items-start gap-3">
|
|
236
|
+
<div className={cn(
|
|
237
|
+
'w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 mt-0.5',
|
|
238
|
+
selected ? 'bg-accent/15 text-accent' : 'bg-surface-3 text-text-4',
|
|
239
|
+
)}>
|
|
240
|
+
<Icon size={15} />
|
|
241
|
+
</div>
|
|
242
|
+
<div className="flex-1 min-w-0">
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<span className={cn(
|
|
245
|
+
'text-xs font-semibold font-sans',
|
|
246
|
+
selected ? 'text-text-0' : 'text-text-2',
|
|
247
|
+
)}>
|
|
248
|
+
{opt.title}
|
|
249
|
+
</span>
|
|
250
|
+
{selected && (
|
|
251
|
+
<div className="w-4 h-4 rounded-full bg-accent flex items-center justify-center">
|
|
252
|
+
<Check size={10} className="text-white" />
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
<p className="text-2xs text-text-4 font-sans mt-0.5 leading-relaxed">
|
|
257
|
+
{opt.description}
|
|
258
|
+
</p>
|
|
259
|
+
{opt.path && selected && (
|
|
260
|
+
<code className="text-2xs text-accent/80 font-mono mt-1.5 block truncate">
|
|
261
|
+
{opt.path}
|
|
262
|
+
</code>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</button>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{/* Custom path input */}
|
|
272
|
+
{pathOption === 'custom' && (
|
|
273
|
+
<div className="mt-2.5 ml-11">
|
|
274
|
+
<input
|
|
275
|
+
value={customPath}
|
|
276
|
+
onChange={(e) => setCustomPath(e.target.value)}
|
|
277
|
+
placeholder="/path/to/clone"
|
|
278
|
+
autoFocus
|
|
279
|
+
className={cn(
|
|
280
|
+
'w-full h-9 px-3 text-xs font-mono rounded-md',
|
|
281
|
+
'bg-surface-0 border border-border text-text-0',
|
|
282
|
+
'placeholder:text-text-4',
|
|
283
|
+
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
|
284
|
+
'transition-colors',
|
|
285
|
+
)}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Team toggle */}
|
|
292
|
+
<div className="border-t border-border-subtle pt-4">
|
|
293
|
+
<div className="flex items-center justify-between">
|
|
294
|
+
<div className="flex items-center gap-2.5">
|
|
295
|
+
<div className="w-8 h-8 rounded-md bg-surface-3 flex items-center justify-center">
|
|
296
|
+
<Users size={14} className="text-text-4" />
|
|
297
|
+
</div>
|
|
298
|
+
<div>
|
|
299
|
+
<span className="text-xs font-semibold text-text-2 font-sans block">Create a team</span>
|
|
300
|
+
<span className="text-2xs text-text-4 font-sans">Organize agents working on this repo into their own team</span>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<button
|
|
304
|
+
onClick={() => setCreateTeam(!createTeam)}
|
|
305
|
+
className={cn(
|
|
306
|
+
'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer flex-shrink-0',
|
|
307
|
+
createTeam ? 'bg-accent' : 'bg-surface-5',
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
<div className={cn(
|
|
311
|
+
'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
|
|
312
|
+
createTeam ? 'translate-x-4' : 'translate-x-0',
|
|
313
|
+
)} />
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
{createTeam && (
|
|
317
|
+
<div className="mt-2.5 ml-11">
|
|
318
|
+
<input
|
|
319
|
+
value={teamName}
|
|
320
|
+
onChange={(e) => setTeamName(e.target.value)}
|
|
321
|
+
placeholder="Team name"
|
|
322
|
+
className={cn(
|
|
323
|
+
'w-full h-9 px-3 text-xs font-sans rounded-md',
|
|
324
|
+
'bg-surface-0 border border-border text-text-0',
|
|
325
|
+
'placeholder:text-text-4',
|
|
326
|
+
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
|
327
|
+
'transition-colors',
|
|
328
|
+
)}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{/* Footer */}
|
|
336
|
+
<div className="px-5 py-3.5 border-t border-border-subtle bg-surface-3/30 flex items-center justify-between">
|
|
337
|
+
<button
|
|
338
|
+
onClick={() => setStep('preview')}
|
|
339
|
+
className="flex items-center gap-1.5 text-2xs text-text-4 font-sans hover:text-text-2 cursor-pointer bg-transparent border-0 transition-colors"
|
|
340
|
+
>
|
|
341
|
+
<ArrowLeft size={11} />
|
|
342
|
+
Back
|
|
343
|
+
</button>
|
|
344
|
+
<Button
|
|
345
|
+
variant="primary"
|
|
346
|
+
size="sm"
|
|
347
|
+
onClick={handleImport}
|
|
348
|
+
disabled={importInProgress || (pathOption === 'custom' && !customPath.trim())}
|
|
349
|
+
className="h-8 text-xs gap-1.5 px-5"
|
|
350
|
+
>
|
|
351
|
+
{importInProgress ? (
|
|
352
|
+
<><Loader2 size={12} className="animate-spin" /> Importing...</>
|
|
353
|
+
) : (
|
|
354
|
+
<><Download size={12} /> Clone & Setup</>
|
|
355
|
+
)}
|
|
356
|
+
</Button>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return null;
|
|
363
|
+
}
|