groove-dev 0.8.0
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/CLAUDE.md +197 -0
- package/LICENSE +40 -0
- package/README.md +115 -0
- package/docs/GUI_DESIGN_SPEC.md +402 -0
- package/favicon.png +0 -0
- package/groove-logo-short.png +0 -0
- package/groove-logo.png +0 -0
- package/package.json +70 -0
- package/packages/cli/bin/groove.js +98 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/client.js +25 -0
- package/packages/cli/src/commands/agents.js +38 -0
- package/packages/cli/src/commands/approve.js +50 -0
- package/packages/cli/src/commands/config.js +35 -0
- package/packages/cli/src/commands/kill.js +15 -0
- package/packages/cli/src/commands/nuke.js +19 -0
- package/packages/cli/src/commands/providers.js +40 -0
- package/packages/cli/src/commands/rotate.js +16 -0
- package/packages/cli/src/commands/spawn.js +91 -0
- package/packages/cli/src/commands/start.js +31 -0
- package/packages/cli/src/commands/status.js +38 -0
- package/packages/cli/src/commands/stop.js +15 -0
- package/packages/cli/src/commands/team.js +77 -0
- package/packages/daemon/package.json +18 -0
- package/packages/daemon/src/adaptive.js +237 -0
- package/packages/daemon/src/api.js +533 -0
- package/packages/daemon/src/classifier.js +126 -0
- package/packages/daemon/src/credentials.js +121 -0
- package/packages/daemon/src/firstrun.js +93 -0
- package/packages/daemon/src/index.js +208 -0
- package/packages/daemon/src/introducer.js +238 -0
- package/packages/daemon/src/journalist.js +600 -0
- package/packages/daemon/src/lockmanager.js +58 -0
- package/packages/daemon/src/pm.js +108 -0
- package/packages/daemon/src/process.js +361 -0
- package/packages/daemon/src/providers/aider.js +72 -0
- package/packages/daemon/src/providers/base.js +38 -0
- package/packages/daemon/src/providers/claude-code.js +167 -0
- package/packages/daemon/src/providers/codex.js +68 -0
- package/packages/daemon/src/providers/gemini.js +62 -0
- package/packages/daemon/src/providers/index.js +38 -0
- package/packages/daemon/src/providers/ollama.js +94 -0
- package/packages/daemon/src/registry.js +89 -0
- package/packages/daemon/src/rotator.js +185 -0
- package/packages/daemon/src/router.js +132 -0
- package/packages/daemon/src/state.js +34 -0
- package/packages/daemon/src/supervisor.js +178 -0
- package/packages/daemon/src/teams.js +203 -0
- package/packages/daemon/src/terminal/base.js +27 -0
- package/packages/daemon/src/terminal/generic.js +27 -0
- package/packages/daemon/src/terminal/tmux.js +64 -0
- package/packages/daemon/src/tokentracker.js +124 -0
- package/packages/daemon/src/validate.js +122 -0
- package/packages/daemon/templates/api-builder.json +18 -0
- package/packages/daemon/templates/fullstack.json +18 -0
- package/packages/daemon/templates/monorepo.json +24 -0
- package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
- package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
- package/packages/gui/dist/favicon.png +0 -0
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/dist/index.html +13 -0
- package/packages/gui/index.html +12 -0
- package/packages/gui/package.json +22 -0
- package/packages/gui/public/favicon.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/App.jsx +215 -0
- package/packages/gui/src/components/AgentActions.jsx +347 -0
- package/packages/gui/src/components/AgentChat.jsx +479 -0
- package/packages/gui/src/components/AgentNode.jsx +117 -0
- package/packages/gui/src/components/AgentPanel.jsx +115 -0
- package/packages/gui/src/components/AgentStats.jsx +333 -0
- package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
- package/packages/gui/src/components/EmptyState.jsx +100 -0
- package/packages/gui/src/components/SpawnPanel.jsx +515 -0
- package/packages/gui/src/components/TeamSelector.jsx +162 -0
- package/packages/gui/src/main.jsx +9 -0
- package/packages/gui/src/stores/groove.js +247 -0
- package/packages/gui/src/theme.css +67 -0
- package/packages/gui/src/views/AgentTree.jsx +148 -0
- package/packages/gui/src/views/CommandCenter.jsx +620 -0
- package/packages/gui/src/views/JournalistFeed.jsx +149 -0
- package/packages/gui/vite.config.js +19 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// GROOVE GUI — Empty State
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useGrooveStore } from '../stores/groove';
|
|
6
|
+
|
|
7
|
+
export default function EmptyState() {
|
|
8
|
+
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
9
|
+
const connected = useGrooveStore((s) => s.connected);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div style={styles.container}>
|
|
13
|
+
<div style={styles.inner}>
|
|
14
|
+
{!connected ? (
|
|
15
|
+
<>
|
|
16
|
+
<div style={styles.pulseRing}>
|
|
17
|
+
<div style={styles.pulseCore} />
|
|
18
|
+
</div>
|
|
19
|
+
<div style={styles.title}>Connecting to daemon...</div>
|
|
20
|
+
<div style={styles.hint}>
|
|
21
|
+
Make sure the GROOVE daemon is running
|
|
22
|
+
</div>
|
|
23
|
+
<code style={styles.code}>groove start</code>
|
|
24
|
+
</>
|
|
25
|
+
) : (
|
|
26
|
+
<>
|
|
27
|
+
<div style={styles.readyIcon}>
|
|
28
|
+
<svg width="40" height="40" viewBox="0 0 40 40">
|
|
29
|
+
<circle cx="20" cy="20" r="18" fill="none" stroke="#2c313a" strokeWidth="1" />
|
|
30
|
+
<circle cx="20" cy="20" r="18" fill="none" stroke="#33afbc" strokeWidth="1" strokeDasharray="113" strokeDashoffset="28" strokeLinecap="round">
|
|
31
|
+
<animateTransform attributeName="transform" type="rotate" from="0 20 20" to="360 20 20" dur="3s" repeatCount="indefinite" />
|
|
32
|
+
</circle>
|
|
33
|
+
<text x="20" y="24" textAnchor="middle" fill="#33afbc" fontSize="14" fontWeight="700" fontFamily="JetBrains Mono, monospace">+</text>
|
|
34
|
+
</svg>
|
|
35
|
+
</div>
|
|
36
|
+
<div style={styles.title}>Ready to orchestrate</div>
|
|
37
|
+
<div style={styles.hint}>
|
|
38
|
+
Spawn your first agent to start building
|
|
39
|
+
</div>
|
|
40
|
+
<button onClick={() => openDetail({ type: 'spawn' })} style={styles.spawnBtn}>
|
|
41
|
+
Spawn Agent
|
|
42
|
+
</button>
|
|
43
|
+
</>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const styles = {
|
|
51
|
+
container: {
|
|
52
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
53
|
+
height: '100%', background: '#1a1e25',
|
|
54
|
+
},
|
|
55
|
+
inner: {
|
|
56
|
+
display: 'flex', flexDirection: 'column',
|
|
57
|
+
alignItems: 'center', gap: 12,
|
|
58
|
+
padding: 40,
|
|
59
|
+
},
|
|
60
|
+
pulseRing: {
|
|
61
|
+
width: 40, height: 40,
|
|
62
|
+
borderRadius: '50%',
|
|
63
|
+
border: '1px solid #33afbc',
|
|
64
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
65
|
+
animation: 'pulse 2s infinite',
|
|
66
|
+
},
|
|
67
|
+
pulseCore: {
|
|
68
|
+
width: 8, height: 8, borderRadius: '50%',
|
|
69
|
+
background: '#33afbc',
|
|
70
|
+
boxShadow: '0 0 12px rgba(51, 175, 188, 0.5)',
|
|
71
|
+
},
|
|
72
|
+
readyIcon: {
|
|
73
|
+
marginBottom: 4,
|
|
74
|
+
},
|
|
75
|
+
title: {
|
|
76
|
+
fontSize: 14, color: '#e6e6e6', fontWeight: 600,
|
|
77
|
+
letterSpacing: 0.5,
|
|
78
|
+
},
|
|
79
|
+
hint: {
|
|
80
|
+
fontSize: 12, color: '#6b7280', textAlign: 'center',
|
|
81
|
+
lineHeight: 1.6, maxWidth: 360,
|
|
82
|
+
},
|
|
83
|
+
spawnBtn: {
|
|
84
|
+
padding: '8px 24px',
|
|
85
|
+
background: 'rgba(51, 175, 188, 0.1)',
|
|
86
|
+
border: '1px solid #33afbc',
|
|
87
|
+
color: '#33afbc', fontSize: 12, fontWeight: 600,
|
|
88
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
89
|
+
cursor: 'pointer', marginTop: 4,
|
|
90
|
+
transition: 'background 0.2s',
|
|
91
|
+
},
|
|
92
|
+
divider: {
|
|
93
|
+
fontSize: 11, color: '#3e4451', margin: '4px 0',
|
|
94
|
+
},
|
|
95
|
+
code: {
|
|
96
|
+
background: '#252a33', padding: '8px 16px',
|
|
97
|
+
fontSize: 11, color: '#33afbc',
|
|
98
|
+
border: '1px solid #2c313a',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
// GROOVE GUI — Spawn Panel (detail sidebar)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect } from 'react';
|
|
5
|
+
import { useGrooveStore } from '../stores/groove';
|
|
6
|
+
|
|
7
|
+
const ROLE_PRESETS = [
|
|
8
|
+
{ id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'] },
|
|
9
|
+
{ id: 'frontend', label: 'Frontend', desc: 'UI components, views, styles', scope: ['src/components/**', 'src/views/**', 'src/pages/**', 'src/styles/**'] },
|
|
10
|
+
{ id: 'fullstack', label: 'Fullstack', desc: 'Full codebase access', scope: [] },
|
|
11
|
+
{ id: 'planner', label: 'Planner', desc: 'Architecture, research, planning', scope: [] },
|
|
12
|
+
{ id: 'testing', label: 'Testing', desc: 'Tests, specs, coverage', scope: ['tests/**', 'test/**', '**/*.test.*', '**/*.spec.*'] },
|
|
13
|
+
{ id: 'devops', label: 'DevOps', desc: 'Docker, CI/CD, infra', scope: ['Dockerfile*', 'docker-compose*', '.github/**', 'infra/**'] },
|
|
14
|
+
{ id: 'docs', label: 'Docs', desc: 'Documentation, READMEs', scope: ['docs/**', '*.md'] },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const PERMISSION_LEVELS = [
|
|
18
|
+
{ id: 'auto', label: 'Auto', desc: 'AI PM reviews risky operations before they happen', icon: '~' },
|
|
19
|
+
{ id: 'full', label: 'Full Send', desc: 'No reviews, maximum speed', icon: '>' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export default function SpawnPanel() {
|
|
23
|
+
const spawnAgent = useGrooveStore((s) => s.spawnAgent);
|
|
24
|
+
const closeDetail = useGrooveStore((s) => s.closeDetail);
|
|
25
|
+
|
|
26
|
+
const [role, setRole] = useState('');
|
|
27
|
+
const [customRole, setCustomRole] = useState('');
|
|
28
|
+
const [scope, setScope] = useState('');
|
|
29
|
+
const [prompt, setPrompt] = useState('');
|
|
30
|
+
const [permission, setPermission] = useState('auto');
|
|
31
|
+
const [provider, setProvider] = useState('claude-code');
|
|
32
|
+
const [model, setModel] = useState('auto');
|
|
33
|
+
const [providerList, setProviderList] = useState([]);
|
|
34
|
+
const [submitting, setSubmitting] = useState(false);
|
|
35
|
+
const [error, setError] = useState('');
|
|
36
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
37
|
+
const [connectingProvider, setConnectingProvider] = useState(null);
|
|
38
|
+
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
39
|
+
const [keySaving, setKeySaving] = useState(false);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchProviders();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
async function fetchProviders() {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch('/api/providers');
|
|
48
|
+
setProviderList(await res.json());
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
|
|
53
|
+
const effectiveScope = role === 'custom'
|
|
54
|
+
? scope
|
|
55
|
+
: selectedPreset?.scope.join(', ') || '';
|
|
56
|
+
|
|
57
|
+
const isPlanner = role === 'planner';
|
|
58
|
+
|
|
59
|
+
function handleProviderClick(p) {
|
|
60
|
+
if (p.installed && (p.authType === 'subscription' || p.authType === 'local' || p.hasKey)) {
|
|
61
|
+
// Ready to use
|
|
62
|
+
setProvider(p.id);
|
|
63
|
+
setModel('auto');
|
|
64
|
+
setConnectingProvider(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Needs setup — expand connection flow
|
|
68
|
+
setConnectingProvider(p.id);
|
|
69
|
+
setApiKeyInput('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleSaveKey() {
|
|
73
|
+
if (!apiKeyInput.trim() || !connectingProvider) return;
|
|
74
|
+
setKeySaving(true);
|
|
75
|
+
try {
|
|
76
|
+
await fetch(`/api/credentials/${connectingProvider}`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ key: apiKeyInput.trim() }),
|
|
80
|
+
});
|
|
81
|
+
setApiKeyInput('');
|
|
82
|
+
setConnectingProvider(null);
|
|
83
|
+
setProvider(connectingProvider);
|
|
84
|
+
setModel('auto');
|
|
85
|
+
await fetchProviders(); // Refresh to show updated hasKey status
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore
|
|
88
|
+
}
|
|
89
|
+
setKeySaving(false);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleSubmit(e) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
const finalRole = role === 'custom' ? customRole : role;
|
|
95
|
+
if (!finalRole) { setError('Select a role'); return; }
|
|
96
|
+
|
|
97
|
+
setSubmitting(true);
|
|
98
|
+
setError('');
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const scopeArr = effectiveScope
|
|
102
|
+
? effectiveScope.split(',').map((s) => s.trim()).filter(Boolean)
|
|
103
|
+
: [];
|
|
104
|
+
|
|
105
|
+
const finalPrompt = prompt || null;
|
|
106
|
+
// Role-specific prompt prefixes (e.g., planner constraints) are now
|
|
107
|
+
// applied daemon-side in process.js for consistency across all spawn paths
|
|
108
|
+
|
|
109
|
+
await spawnAgent({
|
|
110
|
+
role: finalRole,
|
|
111
|
+
scope: scopeArr,
|
|
112
|
+
prompt: finalPrompt,
|
|
113
|
+
model: model || 'auto',
|
|
114
|
+
provider,
|
|
115
|
+
permission,
|
|
116
|
+
});
|
|
117
|
+
closeDetail();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
setError(err.message);
|
|
120
|
+
} finally {
|
|
121
|
+
setSubmitting(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getProviderStatus(p) {
|
|
126
|
+
if (!p.installed) return 'not installed';
|
|
127
|
+
if (p.authType === 'api-key' && !p.hasKey) return 'needs key';
|
|
128
|
+
if (p.authType === 'subscription') return 'subscription';
|
|
129
|
+
if (p.authType === 'local') return 'local';
|
|
130
|
+
return 'ready';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isProviderReady(p) {
|
|
134
|
+
if (!p.installed) return false;
|
|
135
|
+
if (p.authType === 'api-key' && !p.hasKey) return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div style={{ paddingTop: 4 }}>
|
|
141
|
+
<div style={styles.title}>SPAWN AGENT</div>
|
|
142
|
+
|
|
143
|
+
<form onSubmit={handleSubmit}>
|
|
144
|
+
{/* Role picker */}
|
|
145
|
+
<div style={styles.label}>ROLE</div>
|
|
146
|
+
<div style={styles.roleGrid}>
|
|
147
|
+
{ROLE_PRESETS.map((preset) => (
|
|
148
|
+
<button
|
|
149
|
+
key={preset.id}
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => setRole(preset.id)}
|
|
152
|
+
style={{
|
|
153
|
+
...styles.roleBtn,
|
|
154
|
+
...(role === preset.id ? styles.roleBtnActive : {}),
|
|
155
|
+
}}
|
|
156
|
+
title={preset.desc}
|
|
157
|
+
>
|
|
158
|
+
{preset.label}
|
|
159
|
+
</button>
|
|
160
|
+
))}
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => setRole('custom')}
|
|
164
|
+
style={{
|
|
165
|
+
...styles.roleBtn,
|
|
166
|
+
...(role === 'custom' ? styles.roleBtnActive : {}),
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
Custom
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{selectedPreset && (
|
|
174
|
+
<div style={styles.roleDesc}>{selectedPreset.desc}</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{role === 'custom' && (
|
|
178
|
+
<input
|
|
179
|
+
style={{ ...styles.input, marginTop: 6 }}
|
|
180
|
+
placeholder="Custom role name..."
|
|
181
|
+
value={customRole}
|
|
182
|
+
onChange={(e) => setCustomRole(e.target.value)}
|
|
183
|
+
autoFocus
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Prompt */}
|
|
188
|
+
<div style={styles.label}>
|
|
189
|
+
{isPlanner ? 'WHAT TO PLAN' : 'TASK PROMPT'}
|
|
190
|
+
</div>
|
|
191
|
+
<textarea
|
|
192
|
+
style={styles.textarea}
|
|
193
|
+
placeholder={isPlanner
|
|
194
|
+
? 'What should this agent research or plan?'
|
|
195
|
+
: 'What should this agent work on?'}
|
|
196
|
+
value={prompt}
|
|
197
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
198
|
+
rows={3}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
{/* Permissions */}
|
|
202
|
+
<div style={styles.label}>PERMISSIONS</div>
|
|
203
|
+
<div style={styles.permGrid}>
|
|
204
|
+
{PERMISSION_LEVELS.map((perm) => (
|
|
205
|
+
<button
|
|
206
|
+
key={perm.id}
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => setPermission(perm.id)}
|
|
209
|
+
style={{
|
|
210
|
+
...styles.permBtn,
|
|
211
|
+
...(permission === perm.id ? styles.permBtnActive : {}),
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<span style={styles.permIcon}>{perm.icon}</span>
|
|
215
|
+
<div>
|
|
216
|
+
<div style={styles.permLabel}>{perm.label}</div>
|
|
217
|
+
<div style={styles.permDesc}>{perm.desc}</div>
|
|
218
|
+
</div>
|
|
219
|
+
</button>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Advanced toggle */}
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
227
|
+
style={styles.advancedToggle}
|
|
228
|
+
>
|
|
229
|
+
{showAdvanced ? '- hide advanced' : '+ advanced options'}
|
|
230
|
+
</button>
|
|
231
|
+
|
|
232
|
+
{showAdvanced && (
|
|
233
|
+
<>
|
|
234
|
+
{/* Provider selector with connection flow */}
|
|
235
|
+
<div style={styles.label}>PROVIDER</div>
|
|
236
|
+
{providerList.map((p) => {
|
|
237
|
+
const ready = isProviderReady(p);
|
|
238
|
+
const status = getProviderStatus(p);
|
|
239
|
+
const isSelected = provider === p.id;
|
|
240
|
+
const isConnecting = connectingProvider === p.id;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div key={p.id} style={{ marginBottom: 2 }}>
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={() => handleProviderClick(p)}
|
|
247
|
+
style={{
|
|
248
|
+
...styles.providerBtn,
|
|
249
|
+
borderColor: isSelected ? 'var(--accent)' : 'var(--border)',
|
|
250
|
+
opacity: ready || isConnecting ? 1 : 0.6,
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<div style={{ flex: 1 }}>
|
|
254
|
+
<span style={{
|
|
255
|
+
fontSize: 12, fontWeight: 600,
|
|
256
|
+
color: isSelected ? 'var(--text-bright)' : 'var(--text-primary)',
|
|
257
|
+
}}>
|
|
258
|
+
{p.name}
|
|
259
|
+
</span>
|
|
260
|
+
<span style={styles.providerModels}>
|
|
261
|
+
{p.models.map((m) => m.name).join(', ')}
|
|
262
|
+
</span>
|
|
263
|
+
</div>
|
|
264
|
+
<span style={{
|
|
265
|
+
fontSize: 10,
|
|
266
|
+
color: ready ? 'var(--green)' : status === 'not installed' ? 'var(--text-muted)' : 'var(--amber)',
|
|
267
|
+
}}>
|
|
268
|
+
{ready ? (isSelected ? 'active' : 'ready') : status}
|
|
269
|
+
</span>
|
|
270
|
+
</button>
|
|
271
|
+
|
|
272
|
+
{/* Connection flow — inline expand */}
|
|
273
|
+
{isConnecting && (
|
|
274
|
+
<div style={styles.connectBox}>
|
|
275
|
+
{!p.installed && (
|
|
276
|
+
<div>
|
|
277
|
+
<div style={styles.connectLabel}>Install first:</div>
|
|
278
|
+
<code style={styles.connectCode}>{p.installCommand}</code>
|
|
279
|
+
<div style={styles.connectHint}>Run this in your terminal, then click the provider again</div>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
{p.installed && p.authType === 'api-key' && !p.hasKey && (
|
|
283
|
+
<div>
|
|
284
|
+
<div style={styles.connectLabel}>
|
|
285
|
+
API Key {p.envKey ? `(${p.envKey})` : ''}
|
|
286
|
+
</div>
|
|
287
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
288
|
+
<input
|
|
289
|
+
type="password"
|
|
290
|
+
style={styles.input}
|
|
291
|
+
placeholder="sk-..."
|
|
292
|
+
value={apiKeyInput}
|
|
293
|
+
onChange={(e) => setApiKeyInput(e.target.value)}
|
|
294
|
+
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleSaveKey())}
|
|
295
|
+
/>
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
onClick={handleSaveKey}
|
|
299
|
+
disabled={keySaving || !apiKeyInput.trim()}
|
|
300
|
+
style={styles.connectSaveBtn}
|
|
301
|
+
>
|
|
302
|
+
{keySaving ? '...' : 'Save'}
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
<div style={styles.connectHint}>
|
|
306
|
+
Encrypted locally. Never sent to GROOVE servers.
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
{p.installed && p.authType === 'subscription' && (
|
|
311
|
+
<div>
|
|
312
|
+
<div style={styles.connectLabel}>Subscription auth</div>
|
|
313
|
+
<div style={styles.connectHint}>
|
|
314
|
+
{p.name} uses your existing subscription. Make sure you're logged in via the CLI.
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
{p.installed && p.authType === 'local' && (
|
|
319
|
+
<div>
|
|
320
|
+
<div style={styles.connectLabel}>Local model</div>
|
|
321
|
+
<div style={styles.connectHint}>
|
|
322
|
+
Make sure {p.name} is running locally. No API key needed.
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => setConnectingProvider(null)}
|
|
329
|
+
style={styles.connectCancel}
|
|
330
|
+
>
|
|
331
|
+
cancel
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
|
|
339
|
+
{/* Model selector */}
|
|
340
|
+
{(() => {
|
|
341
|
+
const currentProvider = providerList.find((p) => p.id === provider);
|
|
342
|
+
const models = currentProvider?.models || [];
|
|
343
|
+
if (models.length === 0) return null;
|
|
344
|
+
return (
|
|
345
|
+
<>
|
|
346
|
+
<div style={styles.label}>MODEL</div>
|
|
347
|
+
<select
|
|
348
|
+
style={styles.input}
|
|
349
|
+
value={model}
|
|
350
|
+
onChange={(e) => setModel(e.target.value)}
|
|
351
|
+
>
|
|
352
|
+
<option value="auto">Auto (recommended)</option>
|
|
353
|
+
{models.map((m) => (
|
|
354
|
+
<option key={m.id} value={m.id}>
|
|
355
|
+
{m.name} ({m.tier})
|
|
356
|
+
</option>
|
|
357
|
+
))}
|
|
358
|
+
</select>
|
|
359
|
+
</>
|
|
360
|
+
);
|
|
361
|
+
})()}
|
|
362
|
+
|
|
363
|
+
{/* Scope */}
|
|
364
|
+
<div style={styles.label}>FILE SCOPE</div>
|
|
365
|
+
<input
|
|
366
|
+
style={styles.input}
|
|
367
|
+
placeholder="e.g. src/api/**, src/lib/**"
|
|
368
|
+
value={role === 'custom' ? scope : effectiveScope}
|
|
369
|
+
onChange={(e) => { if (role === 'custom') setScope(e.target.value); }}
|
|
370
|
+
readOnly={role !== 'custom'}
|
|
371
|
+
/>
|
|
372
|
+
<div style={styles.hint}>
|
|
373
|
+
{role === 'custom'
|
|
374
|
+
? 'Comma-separated glob patterns'
|
|
375
|
+
: 'Auto-set by role preset'}
|
|
376
|
+
</div>
|
|
377
|
+
</>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
381
|
+
|
|
382
|
+
<button
|
|
383
|
+
type="submit"
|
|
384
|
+
disabled={submitting}
|
|
385
|
+
style={{
|
|
386
|
+
...styles.submitBtn,
|
|
387
|
+
opacity: submitting ? 0.5 : 1,
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
{submitting ? 'spawning...' : isPlanner ? 'Start Planning' : 'Spawn Agent'}
|
|
391
|
+
</button>
|
|
392
|
+
</form>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const styles = {
|
|
398
|
+
title: {
|
|
399
|
+
fontSize: 11, fontWeight: 600, color: 'var(--text-dim)',
|
|
400
|
+
textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 14,
|
|
401
|
+
},
|
|
402
|
+
label: {
|
|
403
|
+
fontSize: 11, color: 'var(--text-dim)',
|
|
404
|
+
marginBottom: 4, marginTop: 12,
|
|
405
|
+
textTransform: 'uppercase', letterSpacing: 1.5, fontWeight: 600,
|
|
406
|
+
},
|
|
407
|
+
roleGrid: {
|
|
408
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4,
|
|
409
|
+
},
|
|
410
|
+
roleBtn: {
|
|
411
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
412
|
+
borderRadius: 2, padding: '6px 4px',
|
|
413
|
+
color: 'var(--text-primary)', fontSize: 11, cursor: 'pointer',
|
|
414
|
+
fontFamily: 'var(--font)',
|
|
415
|
+
transition: 'color 0.1s, border-color 0.1s',
|
|
416
|
+
},
|
|
417
|
+
roleBtnActive: {
|
|
418
|
+
borderColor: 'var(--accent)',
|
|
419
|
+
color: 'var(--text-bright)',
|
|
420
|
+
},
|
|
421
|
+
roleDesc: {
|
|
422
|
+
fontSize: 10, color: 'var(--text-dim)', marginTop: 4, fontStyle: 'italic',
|
|
423
|
+
},
|
|
424
|
+
permGrid: {
|
|
425
|
+
display: 'flex', flexDirection: 'column', gap: 4,
|
|
426
|
+
},
|
|
427
|
+
permBtn: {
|
|
428
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
429
|
+
padding: '8px 10px', width: '100%',
|
|
430
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
431
|
+
borderRadius: 2, cursor: 'pointer', textAlign: 'left',
|
|
432
|
+
fontFamily: 'var(--font)',
|
|
433
|
+
},
|
|
434
|
+
permBtnActive: {
|
|
435
|
+
borderColor: 'var(--accent)',
|
|
436
|
+
},
|
|
437
|
+
permIcon: {
|
|
438
|
+
fontSize: 14, fontWeight: 700, color: 'var(--accent)',
|
|
439
|
+
width: 18, textAlign: 'center', flexShrink: 0,
|
|
440
|
+
},
|
|
441
|
+
permLabel: {
|
|
442
|
+
fontSize: 11, color: 'var(--text-bright)', fontWeight: 600,
|
|
443
|
+
},
|
|
444
|
+
permDesc: {
|
|
445
|
+
fontSize: 10, color: 'var(--text-dim)',
|
|
446
|
+
},
|
|
447
|
+
advancedToggle: {
|
|
448
|
+
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
449
|
+
fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
450
|
+
padding: '8px 0', marginTop: 8,
|
|
451
|
+
},
|
|
452
|
+
providerBtn: {
|
|
453
|
+
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
|
|
454
|
+
padding: '8px 10px',
|
|
455
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
456
|
+
borderRadius: 2, cursor: 'pointer', textAlign: 'left',
|
|
457
|
+
fontFamily: 'var(--font)',
|
|
458
|
+
},
|
|
459
|
+
providerModels: {
|
|
460
|
+
fontSize: 10, color: 'var(--text-dim)', marginLeft: 6,
|
|
461
|
+
},
|
|
462
|
+
connectBox: {
|
|
463
|
+
padding: '8px 10px', margin: '2px 0 4px',
|
|
464
|
+
background: 'var(--bg-base)', border: '1px solid var(--border)',
|
|
465
|
+
borderRadius: 2,
|
|
466
|
+
},
|
|
467
|
+
connectLabel: {
|
|
468
|
+
fontSize: 11, color: 'var(--text-primary)', fontWeight: 600, marginBottom: 4,
|
|
469
|
+
},
|
|
470
|
+
connectCode: {
|
|
471
|
+
display: 'block', padding: '6px 8px',
|
|
472
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2,
|
|
473
|
+
fontSize: 11, color: 'var(--accent)', wordBreak: 'break-all',
|
|
474
|
+
},
|
|
475
|
+
connectHint: {
|
|
476
|
+
fontSize: 10, color: 'var(--text-dim)', marginTop: 4,
|
|
477
|
+
},
|
|
478
|
+
connectSaveBtn: {
|
|
479
|
+
padding: '6px 12px', flexShrink: 0,
|
|
480
|
+
background: 'transparent', border: '1px solid var(--accent)',
|
|
481
|
+
borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
|
|
482
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
483
|
+
},
|
|
484
|
+
connectCancel: {
|
|
485
|
+
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
486
|
+
fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
487
|
+
padding: '4px 0', marginTop: 4,
|
|
488
|
+
},
|
|
489
|
+
input: {
|
|
490
|
+
width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
491
|
+
borderRadius: 2, padding: '6px 8px',
|
|
492
|
+
color: 'var(--text-primary)', fontSize: 12, outline: 'none',
|
|
493
|
+
fontFamily: 'var(--font)',
|
|
494
|
+
},
|
|
495
|
+
textarea: {
|
|
496
|
+
width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
497
|
+
borderRadius: 2, padding: '6px 8px',
|
|
498
|
+
color: 'var(--text-primary)', fontSize: 12, outline: 'none',
|
|
499
|
+
fontFamily: 'var(--font)', resize: 'vertical',
|
|
500
|
+
},
|
|
501
|
+
hint: {
|
|
502
|
+
fontSize: 10, color: 'var(--text-muted)', marginTop: 3,
|
|
503
|
+
},
|
|
504
|
+
error: {
|
|
505
|
+
color: 'var(--red)', fontSize: 11, marginTop: 8,
|
|
506
|
+
},
|
|
507
|
+
submitBtn: {
|
|
508
|
+
width: '100%', marginTop: 14, padding: '8px',
|
|
509
|
+
background: 'transparent', border: '1px solid var(--accent)',
|
|
510
|
+
borderRadius: 2,
|
|
511
|
+
color: 'var(--accent)', fontSize: 12, fontWeight: 600,
|
|
512
|
+
fontFamily: 'var(--font)',
|
|
513
|
+
cursor: 'pointer',
|
|
514
|
+
},
|
|
515
|
+
};
|