groove-dev 0.27.169 → 0.27.172
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/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
- package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/routes/files.js +18 -5
- package/packages/daemon/src/tunnel-manager.js +16 -6
- package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/packages/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
- package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
- package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/packages/gui/src/views/agents.jsx +2 -11
- package/packages/gui/src/views/editor.jsx +63 -2
- package/packages/gui/src/views/settings.jsx +2 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BvXojcnr.css +0 -1
- package/packages/gui/dist/assets/index-BvXojcnr.css +0 -1
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BsCp-oqa.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BrMU-6gi.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -6,7 +6,7 @@ import { api } from '../../lib/api';
|
|
|
6
6
|
import { cn } from '../../lib/cn';
|
|
7
7
|
import {
|
|
8
8
|
FolderOpen, FolderClosed, ChevronRight, Home, HardDrive,
|
|
9
|
-
ArrowUp, Check, Loader2,
|
|
9
|
+
ArrowUp, Check, Loader2, FileKey,
|
|
10
10
|
} from 'lucide-react';
|
|
11
11
|
|
|
12
12
|
function BreadcrumbPath({ path, onNavigate }) {
|
|
@@ -43,29 +43,39 @@ function BreadcrumbPath({ path, onNavigate }) {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homePath, mandatory = false, title }) {
|
|
46
|
+
export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homePath, mandatory = false, title, mode = 'directory' }) {
|
|
47
47
|
const home = homePath || '/home';
|
|
48
48
|
const [path, setPath] = useState(currentPath || home);
|
|
49
49
|
const [entries, setEntries] = useState([]);
|
|
50
|
+
const [files, setFiles] = useState([]);
|
|
51
|
+
const [selectedFile, setSelectedFile] = useState(null);
|
|
50
52
|
const [loading, setLoading] = useState(false);
|
|
51
53
|
const [error, setError] = useState(null);
|
|
52
54
|
|
|
55
|
+
const isFileMode = mode === 'file';
|
|
56
|
+
|
|
53
57
|
useEffect(() => {
|
|
54
58
|
if (open) {
|
|
55
|
-
|
|
59
|
+
setSelectedFile(null);
|
|
60
|
+
const startPath = currentPath || home;
|
|
61
|
+
navigateTo(isFileMode && startPath.includes('/') ? startPath.split('/').slice(0, -1).join('/') || '/' : startPath);
|
|
56
62
|
}
|
|
57
63
|
}, [open]);
|
|
58
64
|
|
|
59
65
|
async function navigateTo(target) {
|
|
60
66
|
setLoading(true);
|
|
61
67
|
setError(null);
|
|
68
|
+
setSelectedFile(null);
|
|
62
69
|
try {
|
|
63
|
-
const
|
|
70
|
+
const params = `path=${encodeURIComponent(target)}${isFileMode ? '&files=true&hidden=true' : ''}`;
|
|
71
|
+
const data = await api.get(`/browse-system?${params}`);
|
|
64
72
|
setPath(data.current || target);
|
|
65
73
|
setEntries(data.dirs || []);
|
|
74
|
+
setFiles(isFileMode ? (data.files || []) : []);
|
|
66
75
|
} catch (err) {
|
|
67
76
|
setError(err.message);
|
|
68
77
|
setEntries([]);
|
|
78
|
+
setFiles([]);
|
|
69
79
|
}
|
|
70
80
|
setLoading(false);
|
|
71
81
|
}
|
|
@@ -80,7 +90,7 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
function handleSelect() {
|
|
83
|
-
onSelect(path);
|
|
93
|
+
onSelect(isFileMode && selectedFile ? selectedFile : path);
|
|
84
94
|
if (!mandatory) onOpenChange(false);
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -137,9 +147,9 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
|
|
|
137
147
|
<p className="text-xs text-danger font-sans">{error}</p>
|
|
138
148
|
</div>
|
|
139
149
|
)}
|
|
140
|
-
{!loading && !error && entries.length === 0 && (
|
|
150
|
+
{!loading && !error && entries.length === 0 && files.length === 0 && (
|
|
141
151
|
<div className="px-4 py-6 text-center">
|
|
142
|
-
<p className="text-xs text-text-3 font-sans">No subdirectories</p>
|
|
152
|
+
<p className="text-xs text-text-3 font-sans">{isFileMode ? 'No files found' : 'No subdirectories'}</p>
|
|
143
153
|
</div>
|
|
144
154
|
)}
|
|
145
155
|
{!loading && !error && entries.map((entry) => (
|
|
@@ -161,13 +171,31 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
|
|
|
161
171
|
)}
|
|
162
172
|
</button>
|
|
163
173
|
))}
|
|
174
|
+
{!loading && !error && files.map((file) => (
|
|
175
|
+
<button
|
|
176
|
+
key={file.path}
|
|
177
|
+
onClick={() => setSelectedFile(file.path)}
|
|
178
|
+
className={cn(
|
|
179
|
+
'w-full flex items-center gap-2.5 px-3.5 py-2 text-left cursor-pointer',
|
|
180
|
+
'transition-colors border-b border-border-subtle last:border-0',
|
|
181
|
+
selectedFile === file.path ? 'bg-accent/10' : 'hover:bg-surface-4',
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<FileKey size={15} className={cn('flex-shrink-0', selectedFile === file.path ? 'text-accent' : 'text-text-3')} />
|
|
185
|
+
<span className={cn('text-sm font-sans truncate flex-1', selectedFile === file.path ? 'text-accent font-medium' : 'text-text-0')}>{file.name}</span>
|
|
186
|
+
</button>
|
|
187
|
+
))}
|
|
164
188
|
</div>
|
|
165
189
|
</div>
|
|
166
190
|
|
|
167
191
|
{/* Current selection */}
|
|
168
192
|
<div className="flex items-center gap-3 bg-surface-4/50 rounded-lg px-3.5 py-2.5 border border-border-subtle">
|
|
169
|
-
|
|
170
|
-
|
|
193
|
+
{isFileMode && selectedFile ? (
|
|
194
|
+
<FileKey size={16} className="text-accent flex-shrink-0" />
|
|
195
|
+
) : (
|
|
196
|
+
<FolderOpen size={16} className="text-accent flex-shrink-0" />
|
|
197
|
+
)}
|
|
198
|
+
<span className="text-xs font-mono text-text-1 truncate flex-1">{isFileMode && selectedFile ? selectedFile : path}</span>
|
|
171
199
|
</div>
|
|
172
200
|
|
|
173
201
|
{/* Actions */}
|
|
@@ -175,8 +203,8 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
|
|
|
175
203
|
{!mandatory && (
|
|
176
204
|
<Button variant="ghost" size="md" onClick={() => onOpenChange(false)}>Cancel</Button>
|
|
177
205
|
)}
|
|
178
|
-
<Button variant="primary" size="md" onClick={handleSelect} className="gap-1.5">
|
|
179
|
-
<Check size={14} /> Select Folder
|
|
206
|
+
<Button variant="primary" size="md" onClick={handleSelect} disabled={isFileMode && !selectedFile} className="gap-1.5">
|
|
207
|
+
<Check size={14} /> {isFileMode ? 'Select File' : 'Select Folder'}
|
|
180
208
|
</Button>
|
|
181
209
|
</div>
|
|
182
210
|
</div>
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
|
|
7
|
+
import { TuningSlider } from '../ui/slider';
|
|
8
|
+
import {
|
|
9
|
+
Rocket, X, ChevronDown, Settings2, Zap, Shield, Server, Monitor, Code2, TestTube, Cpu, Activity, Gauge,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
const ROLE_ICONS = { backend: Server, frontend: Monitor, fullstack: Code2, testing: TestTube, security: Shield };
|
|
13
|
+
const PROVIDER_TEMP_SUPPORT = new Set(['codex', 'grok', 'local']);
|
|
14
|
+
const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
15
|
+
|
|
16
|
+
function sanitizeName(raw) {
|
|
17
|
+
return raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function RecommendedTeamCard() {
|
|
21
|
+
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
22
|
+
const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
|
|
23
|
+
const teamLaunchConfig = useGrooveStore((s) => s.teamLaunchConfig);
|
|
24
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
25
|
+
const [launching, setLaunching] = useState(false);
|
|
26
|
+
const [editedAgents, setEditedAgents] = useState(null);
|
|
27
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
28
|
+
const [providers, setProviders] = useState([]);
|
|
29
|
+
|
|
30
|
+
const [tsProvider, setTsProvider] = useState(teamLaunchConfig?.provider || '');
|
|
31
|
+
const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
|
|
32
|
+
const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
|
|
33
|
+
const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
|
|
34
|
+
const [expandedAgent, setExpandedAgent] = useState(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchProviders().then((list) => {
|
|
38
|
+
if (Array.isArray(list)) setProviders(list.filter((p) => p.installed));
|
|
39
|
+
}).catch(() => {});
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
if (!recommendedTeam?.agents?.length) return null;
|
|
43
|
+
|
|
44
|
+
const agents = recommendedTeam.agents;
|
|
45
|
+
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
46
|
+
const phase2 = agents.filter((a) => a.phase === 2);
|
|
47
|
+
|
|
48
|
+
const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
|
|
49
|
+
|
|
50
|
+
const selectedProvider = providers.find((p) => p.id === tsProvider);
|
|
51
|
+
const tsModels = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
52
|
+
const showTemp = PROVIDER_TEMP_SUPPORT.has(tsProvider);
|
|
53
|
+
|
|
54
|
+
function handleNameChange(i, raw) {
|
|
55
|
+
const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
|
|
56
|
+
setEditedAgents(next);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleAgentField(i, updates) {
|
|
60
|
+
if (typeof updates === 'string') {
|
|
61
|
+
const [field, value] = [updates, arguments[2]];
|
|
62
|
+
setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, [field]: value } : a));
|
|
63
|
+
} else {
|
|
64
|
+
setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, ...updates } : a));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleTsProviderChange(id) {
|
|
69
|
+
setTsProvider(id);
|
|
70
|
+
const p = providers.find((x) => x.id === id);
|
|
71
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
72
|
+
setTsModel(pModels[0]?.id || '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function handleLaunch() {
|
|
76
|
+
setLaunching(true);
|
|
77
|
+
useGrooveStore.setState({
|
|
78
|
+
teamLaunchConfig: {
|
|
79
|
+
...(tsProvider && { provider: tsProvider, model: tsModel }),
|
|
80
|
+
reasoningEffort: tsReasoning,
|
|
81
|
+
...(showTemp && { temperature: tsTemp }),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const modified = [...agentEdits, ...phase2];
|
|
86
|
+
await launchRecommendedTeam(modified);
|
|
87
|
+
} catch { /* toast handles */ }
|
|
88
|
+
setLaunching(false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleDismiss() {
|
|
92
|
+
useGrooveStore.setState({ recommendedTeam: null });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-lg">
|
|
97
|
+
<div className="mx-4 rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden">
|
|
98
|
+
<div className="px-4 py-3 border-b border-border-subtle flex items-center gap-2">
|
|
99
|
+
<Rocket size={16} className="text-accent" />
|
|
100
|
+
<span className="text-sm font-semibold text-text-0 font-sans flex-1">Planner Recommends a Team</span>
|
|
101
|
+
<button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Collapsible Team Settings */}
|
|
105
|
+
<div className="border-b border-border-subtle">
|
|
106
|
+
<button
|
|
107
|
+
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
108
|
+
className="w-full flex items-center gap-2 px-4 py-2 text-left cursor-pointer hover:bg-surface-3/50 transition-colors"
|
|
109
|
+
>
|
|
110
|
+
<ChevronDown size={12} className={cn('text-text-4 transition-transform duration-200', !settingsOpen && '-rotate-90')} />
|
|
111
|
+
<Settings2 size={12} className="text-text-3" />
|
|
112
|
+
<span className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wider">Team Settings</span>
|
|
113
|
+
{tsProvider && (
|
|
114
|
+
<span className="ml-auto text-2xs text-accent font-mono">{tsProvider}{tsModel ? ` / ${tsModel}` : ''}</span>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
{settingsOpen && (
|
|
118
|
+
<div className="px-4 pb-3 space-y-3">
|
|
119
|
+
<div className="flex gap-3">
|
|
120
|
+
<div className="flex-1 space-y-1">
|
|
121
|
+
<label className="text-2xs text-text-3 font-sans">Provider</label>
|
|
122
|
+
<Select value={tsProvider} onValueChange={handleTsProviderChange}>
|
|
123
|
+
<SelectTrigger placeholder="Default" className="bg-surface-4 h-7 text-xs" />
|
|
124
|
+
<SelectContent>
|
|
125
|
+
{providers.map((p) => (
|
|
126
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
127
|
+
))}
|
|
128
|
+
</SelectContent>
|
|
129
|
+
</Select>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex-1 space-y-1">
|
|
132
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
133
|
+
<Select value={tsModel} onValueChange={setTsModel}>
|
|
134
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-4 h-7 text-xs" />
|
|
135
|
+
<SelectContent>
|
|
136
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
137
|
+
{tsModels.map((m) => (
|
|
138
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
139
|
+
))}
|
|
140
|
+
</SelectContent>
|
|
141
|
+
</Select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<TuningSlider
|
|
145
|
+
label="Reasoning"
|
|
146
|
+
value={tsReasoning}
|
|
147
|
+
onChange={setTsReasoning}
|
|
148
|
+
min={0} max={100} step={1}
|
|
149
|
+
/>
|
|
150
|
+
{showTemp && (
|
|
151
|
+
<TuningSlider
|
|
152
|
+
label="Temperature"
|
|
153
|
+
value={tsTemp}
|
|
154
|
+
onChange={setTsTemp}
|
|
155
|
+
min={0} max={1} step={0.01}
|
|
156
|
+
formatValue={(v) => v.toFixed(2)}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="px-4 py-3 space-y-1.5">
|
|
164
|
+
{agentEdits.map((a, i) => {
|
|
165
|
+
const Icon = ROLE_ICONS[a.role] || Code2;
|
|
166
|
+
const nameValid = !a.name || NAME_RE.test(a.name);
|
|
167
|
+
const isExpanded = expandedAgent === i;
|
|
168
|
+
const agentProvider = providers.find((p) => p.id === (a.provider || tsProvider));
|
|
169
|
+
const agentModels = (agentProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
170
|
+
return (
|
|
171
|
+
<div key={i} className="rounded-md bg-surface-4 border border-border-subtle overflow-hidden">
|
|
172
|
+
<div
|
|
173
|
+
className="flex items-center gap-2 px-2.5 py-1.5 cursor-pointer hover:bg-surface-5/50 transition-colors"
|
|
174
|
+
onClick={() => setExpandedAgent(isExpanded ? null : i)}
|
|
175
|
+
>
|
|
176
|
+
<Icon size={12} className="text-text-2 shrink-0" />
|
|
177
|
+
<input
|
|
178
|
+
type="text"
|
|
179
|
+
value={a.name}
|
|
180
|
+
onChange={(e) => handleNameChange(i, e.target.value)}
|
|
181
|
+
onClick={(e) => e.stopPropagation()}
|
|
182
|
+
placeholder={a.role}
|
|
183
|
+
className={cn(
|
|
184
|
+
'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
|
|
185
|
+
!nameValid && 'text-red-400',
|
|
186
|
+
)}
|
|
187
|
+
maxLength={64}
|
|
188
|
+
spellCheck={false}
|
|
189
|
+
/>
|
|
190
|
+
{a.provider && a.provider !== tsProvider && (
|
|
191
|
+
<span className="text-2xs text-accent font-mono shrink-0">{a.provider}</span>
|
|
192
|
+
)}
|
|
193
|
+
{a.scope?.length > 0 && (
|
|
194
|
+
<span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
|
|
195
|
+
{a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
<ChevronDown size={10} className={cn('text-text-4 shrink-0 transition-transform duration-200', !isExpanded && '-rotate-90')} />
|
|
199
|
+
</div>
|
|
200
|
+
{isExpanded && (
|
|
201
|
+
<div className="px-2.5 pb-2.5 pt-1 space-y-2.5 border-t border-border-subtle">
|
|
202
|
+
<div className="flex gap-2">
|
|
203
|
+
<div className="flex-1 space-y-1">
|
|
204
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Cpu size={10} />Provider</label>
|
|
205
|
+
<Select value={a.provider || ''} onValueChange={(id) => {
|
|
206
|
+
const p = providers.find((x) => x.id === id);
|
|
207
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
208
|
+
handleAgentField(i, { provider: id, model: pModels[0]?.id || '' });
|
|
209
|
+
}}>
|
|
210
|
+
<SelectTrigger placeholder="Team default" className="bg-surface-3 h-7 text-xs" />
|
|
211
|
+
<SelectContent>
|
|
212
|
+
<SelectItem value="">Team default</SelectItem>
|
|
213
|
+
{providers.map((p) => (
|
|
214
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
215
|
+
))}
|
|
216
|
+
</SelectContent>
|
|
217
|
+
</Select>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="flex-1 space-y-1">
|
|
220
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
221
|
+
<Select value={a.model || ''} onValueChange={(v) => handleAgentField(i, 'model', v)}>
|
|
222
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
|
|
223
|
+
<SelectContent>
|
|
224
|
+
<SelectItem value="">Auto</SelectItem>
|
|
225
|
+
{agentModels.map((m) => (
|
|
226
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
227
|
+
))}
|
|
228
|
+
</SelectContent>
|
|
229
|
+
</Select>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="space-y-1">
|
|
233
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Activity size={10} />Model Routing</label>
|
|
234
|
+
<div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
|
|
235
|
+
{[{ value: 'fixed', label: 'Fixed' }, { value: 'auto', label: 'Auto' }, { value: 'auto-floor', label: 'Auto + Floor' }].map((opt) => (
|
|
236
|
+
<button
|
|
237
|
+
key={opt.value}
|
|
238
|
+
onClick={() => handleAgentField(i, 'routingMode', opt.value)}
|
|
239
|
+
className={cn(
|
|
240
|
+
'flex-1 px-2 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
241
|
+
(a.routingMode || 'auto') === opt.value
|
|
242
|
+
? 'bg-accent/15 text-accent shadow-sm'
|
|
243
|
+
: 'text-text-3 hover:text-text-1',
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
{opt.label}
|
|
247
|
+
</button>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="space-y-1">
|
|
252
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Gauge size={10} />Effort Level</label>
|
|
253
|
+
<div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
|
|
254
|
+
{[{ value: 'min', label: 'Min' }, { value: 'low', label: 'Low' }, { value: 'default', label: 'Default' }, { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }].map((opt) => (
|
|
255
|
+
<button
|
|
256
|
+
key={opt.value}
|
|
257
|
+
onClick={() => handleAgentField(i, 'effort', opt.value)}
|
|
258
|
+
className={cn(
|
|
259
|
+
'flex-1 px-1.5 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
260
|
+
(a.effort || 'default') === opt.value
|
|
261
|
+
? 'bg-accent/15 text-accent shadow-sm'
|
|
262
|
+
: 'text-text-3 hover:text-text-1',
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
{opt.label}
|
|
266
|
+
</button>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
})}
|
|
275
|
+
|
|
276
|
+
{recommendedTeam.projectDir && (
|
|
277
|
+
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
278
|
+
<span className="text-text-4">Project:</span>
|
|
279
|
+
<span className="text-accent">{recommendedTeam.projectDir}/</span>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{phase2.length > 0 && (
|
|
284
|
+
<div className="flex items-center gap-1.5 text-2xs text-text-3 font-sans">
|
|
285
|
+
<Shield size={10} />
|
|
286
|
+
<span>{phase2.length} QC agent{phase2.length > 1 ? 's' : ''} will auto-spawn after builders complete</span>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div className="px-4 py-3 border-t border-border-subtle">
|
|
292
|
+
<Button variant="primary" size="md" onClick={handleLaunch} disabled={launching} className="w-full gap-2">
|
|
293
|
+
<Zap size={14} />
|
|
294
|
+
{launching ? 'Launching...' : `Launch ${phase1.length} Agent${phase1.length > 1 ? 's' : ''}`}
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
3
3
|
import { LayoutList } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { FleetPane } from './fleet-pane';
|
|
6
|
+
import { RecommendedTeamCard } from '../agents/recommended-team-card';
|
|
6
7
|
|
|
7
8
|
export function FleetContent() {
|
|
9
|
+
const allAgents = useGrooveStore((s) => s.agents);
|
|
10
|
+
const checkRecommendedTeam = useGrooveStore((s) => s.checkRecommendedTeam);
|
|
11
|
+
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const hasPlanner = allAgents.some((a) => a.role === 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
15
|
+
if (!hasPlanner) return;
|
|
16
|
+
const interval = setInterval(() => checkRecommendedTeam(), 5000);
|
|
17
|
+
return () => clearInterval(interval);
|
|
18
|
+
}, [allAgents, checkRecommendedTeam]);
|
|
8
19
|
const rawSelected = useGrooveStore((s) => s.fleetSelectedAgents);
|
|
9
20
|
const lastSelectedRef = useRef(rawSelected);
|
|
10
21
|
if (rawSelected[0] || rawSelected[1]) lastSelectedRef.current = rawSelected;
|
|
@@ -67,7 +78,7 @@ export function FleetContent() {
|
|
|
67
78
|
if (!selected[0] && !selected[1]) {
|
|
68
79
|
return (
|
|
69
80
|
<div
|
|
70
|
-
className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3"
|
|
81
|
+
className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 relative"
|
|
71
82
|
onDragOver={(e) => handleDragOver(e, 'left')}
|
|
72
83
|
onDragLeave={handleDragLeave}
|
|
73
84
|
onDrop={(e) => handleDrop(e, 0)}
|
|
@@ -75,13 +86,15 @@ export function FleetContent() {
|
|
|
75
86
|
<LayoutList size={32} strokeWidth={1} className="text-text-4" />
|
|
76
87
|
<p className="text-sm font-sans">Select an agent or drag one here</p>
|
|
77
88
|
<p className="text-xs font-sans text-text-4">Cmd+Click or drag to open side-by-side</p>
|
|
89
|
+
<RecommendedTeamCard />
|
|
78
90
|
</div>
|
|
79
91
|
);
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
if (!splitMode || !selected[1]) {
|
|
83
95
|
return (
|
|
84
|
-
<div className="flex-1 flex min-w-0 min-h-0">
|
|
96
|
+
<div className="flex-1 flex min-w-0 min-h-0 relative">
|
|
97
|
+
<RecommendedTeamCard />
|
|
85
98
|
<div className="flex-1 min-w-0 min-h-0">
|
|
86
99
|
<FleetPane agentId={selected[0]} paneIndex={0} />
|
|
87
100
|
</div>
|
|
@@ -107,7 +120,8 @@ export function FleetContent() {
|
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
return (
|
|
110
|
-
<div className="flex-1 flex min-w-0 min-h-0">
|
|
123
|
+
<div className="flex-1 flex min-w-0 min-h-0 relative">
|
|
124
|
+
<RecommendedTeamCard />
|
|
111
125
|
<div
|
|
112
126
|
ref={leftRef}
|
|
113
127
|
className={`min-w-0 min-h-0 ${dropTarget === 'left' ? 'ring-2 ring-inset ring-accent/30' : ''}`}
|