groove-dev 0.12.5 → 0.12.7

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-B49YqEXS.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-D4-7VM-v.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,209 @@
1
+ // GROOVE GUI — Directory Picker
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useState, useEffect } from 'react';
5
+
6
+ export default function DirPicker({ onSelect, onClose, initial }) {
7
+ const [currentPath, setCurrentPath] = useState(initial || '');
8
+ const [dirs, setDirs] = useState([]);
9
+ const [parent, setParent] = useState(null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [error, setError] = useState('');
12
+
13
+ useEffect(() => {
14
+ browse(currentPath);
15
+ }, [currentPath]);
16
+
17
+ async function browse(path) {
18
+ setLoading(true);
19
+ setError('');
20
+ try {
21
+ const res = await fetch(`/api/browse?path=${encodeURIComponent(path)}`);
22
+ if (!res.ok) {
23
+ const err = await res.json().catch(() => ({}));
24
+ setError(err.error || 'Failed to browse');
25
+ setDirs([]);
26
+ return;
27
+ }
28
+ const data = await res.json();
29
+ setDirs(data.dirs);
30
+ setParent(data.parent);
31
+ } catch {
32
+ setError('Failed to connect');
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ }
37
+
38
+ const breadcrumbs = currentPath
39
+ ? ['root', ...currentPath.split('/')]
40
+ : ['root'];
41
+
42
+ function handleBreadcrumbClick(index) {
43
+ if (index === 0) {
44
+ setCurrentPath('');
45
+ } else {
46
+ const parts = currentPath.split('/');
47
+ setCurrentPath(parts.slice(0, index).join('/'));
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div style={styles.overlay} onClick={onClose}>
53
+ <div style={styles.modal} onClick={(e) => e.stopPropagation()}>
54
+ <div style={styles.header}>
55
+ <span style={styles.title}>SELECT DIRECTORY</span>
56
+ <button onClick={onClose} style={styles.closeBtn}>x</button>
57
+ </div>
58
+
59
+ {/* Breadcrumb */}
60
+ <div style={styles.breadcrumb}>
61
+ {breadcrumbs.map((part, i) => (
62
+ <span key={i}>
63
+ {i > 0 && <span style={styles.breadSep}>/</span>}
64
+ <button
65
+ onClick={() => handleBreadcrumbClick(i)}
66
+ style={{
67
+ ...styles.breadPart,
68
+ color: i === breadcrumbs.length - 1 ? 'var(--text-bright)' : 'var(--text-dim)',
69
+ }}
70
+ >
71
+ {part}
72
+ </button>
73
+ </span>
74
+ ))}
75
+ </div>
76
+
77
+ {/* Directory list */}
78
+ <div style={styles.list}>
79
+ {parent !== null && (
80
+ <button
81
+ onClick={() => setCurrentPath(parent)}
82
+ style={styles.dirRow}
83
+ >
84
+ <span style={styles.dirIcon}>..</span>
85
+ <span style={styles.dirName}>parent directory</span>
86
+ </button>
87
+ )}
88
+
89
+ {loading && <div style={styles.empty}>loading...</div>}
90
+ {error && <div style={styles.errorText}>{error}</div>}
91
+
92
+ {!loading && !error && dirs.length === 0 && (
93
+ <div style={styles.empty}>no subdirectories</div>
94
+ )}
95
+
96
+ {dirs.map((dir) => (
97
+ <button
98
+ key={dir.path}
99
+ onClick={() => setCurrentPath(dir.path)}
100
+ style={styles.dirRow}
101
+ >
102
+ <span style={styles.dirIcon}>{dir.hasChildren ? '+' : ' '}</span>
103
+ <span style={styles.dirName}>{dir.name}</span>
104
+ </button>
105
+ ))}
106
+ </div>
107
+
108
+ {/* Footer — select current */}
109
+ <div style={styles.footer}>
110
+ <div style={styles.selectedPath}>
111
+ {currentPath || '(project root)'}
112
+ </div>
113
+ <button
114
+ onClick={() => { onSelect(currentPath); onClose(); }}
115
+ style={styles.selectBtn}
116
+ >
117
+ Select
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ const styles = {
126
+ overlay: {
127
+ position: 'fixed', inset: 0, zIndex: 200,
128
+ background: 'rgba(0, 0, 0, 0.6)',
129
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
130
+ },
131
+ modal: {
132
+ width: 360, maxHeight: '70vh',
133
+ background: 'var(--bg-chrome)',
134
+ border: '1px solid var(--border)',
135
+ borderRadius: 4,
136
+ display: 'flex', flexDirection: 'column',
137
+ fontFamily: 'var(--font)',
138
+ },
139
+ header: {
140
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
141
+ padding: '10px 12px',
142
+ borderBottom: '1px solid var(--border)',
143
+ },
144
+ title: {
145
+ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)',
146
+ textTransform: 'uppercase', letterSpacing: 1.5,
147
+ },
148
+ closeBtn: {
149
+ background: 'none', border: 'none', color: 'var(--text-dim)',
150
+ fontSize: 14, cursor: 'pointer', fontFamily: 'var(--font)',
151
+ padding: '0 4px',
152
+ },
153
+ breadcrumb: {
154
+ padding: '8px 12px',
155
+ borderBottom: '1px solid var(--border)',
156
+ fontSize: 11,
157
+ },
158
+ breadSep: {
159
+ color: 'var(--text-muted)', margin: '0 2px',
160
+ },
161
+ breadPart: {
162
+ background: 'none', border: 'none', cursor: 'pointer',
163
+ fontFamily: 'var(--font)', fontSize: 11,
164
+ padding: 0,
165
+ },
166
+ list: {
167
+ flex: 1, overflowY: 'auto',
168
+ padding: '4px 0',
169
+ minHeight: 120, maxHeight: 300,
170
+ },
171
+ dirRow: {
172
+ display: 'flex', alignItems: 'center', gap: 6,
173
+ width: '100%', padding: '6px 12px',
174
+ background: 'none', border: 'none',
175
+ color: 'var(--text-primary)', fontSize: 12,
176
+ fontFamily: 'var(--font)', cursor: 'pointer',
177
+ textAlign: 'left',
178
+ },
179
+ dirIcon: {
180
+ color: 'var(--accent)', fontSize: 11, width: 12, textAlign: 'center',
181
+ flexShrink: 0,
182
+ },
183
+ dirName: {
184
+ flex: 1,
185
+ },
186
+ empty: {
187
+ padding: '16px 12px', color: 'var(--text-dim)', fontSize: 11,
188
+ textAlign: 'center',
189
+ },
190
+ errorText: {
191
+ padding: '16px 12px', color: 'var(--red)', fontSize: 11,
192
+ textAlign: 'center',
193
+ },
194
+ footer: {
195
+ display: 'flex', alignItems: 'center', gap: 8,
196
+ padding: '10px 12px',
197
+ borderTop: '1px solid var(--border)',
198
+ },
199
+ selectedPath: {
200
+ flex: 1, fontSize: 11, color: 'var(--text-dim)',
201
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
202
+ },
203
+ selectBtn: {
204
+ padding: '5px 14px',
205
+ background: 'transparent', border: '1px solid var(--accent)',
206
+ borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
207
+ fontFamily: 'var(--font)', cursor: 'pointer', flexShrink: 0,
208
+ },
209
+ };
@@ -3,6 +3,7 @@
3
3
 
4
4
  import React, { useState, useEffect } from 'react';
5
5
  import { useGrooveStore } from '../stores/groove';
6
+ import DirPicker from './DirPicker';
6
7
 
7
8
  const ROLE_PRESETS = [
8
9
  { id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'] },
@@ -39,6 +40,7 @@ export default function SpawnPanel() {
39
40
  const [connectingProvider, setConnectingProvider] = useState(null);
40
41
  const [apiKeyInput, setApiKeyInput] = useState('');
41
42
  const [keySaving, setKeySaving] = useState(false);
43
+ const [showDirPicker, setShowDirPicker] = useState(false);
42
44
 
43
45
  useEffect(() => {
44
46
  fetchProviders();
@@ -210,40 +212,51 @@ export default function SpawnPanel() {
210
212
  rows={3}
211
213
  />
212
214
 
213
- {/* Workspace picker — visible by default when workspaces detected */}
214
- {workspaces.length > 0 && (
215
- <>
216
- <div style={styles.label}>DIRECTORY</div>
217
- <div style={styles.wsRow}>
218
- <button
219
- type="button"
220
- onClick={() => setWorkingDir('')}
221
- style={{
222
- ...styles.wsBtn,
223
- ...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
224
- }}
225
- >
226
- project root
227
- </button>
228
- {workspaces.map((ws) => (
229
- <button
230
- key={ws.path}
231
- type="button"
232
- onClick={() => setWorkingDir(ws.path)}
233
- style={{
234
- ...styles.wsBtn,
235
- ...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
236
- }}
237
- title={`${ws.name} (${ws.files} files)`}
238
- >
239
- {ws.path}
240
- </button>
241
- ))}
242
- </div>
243
- <div style={styles.hint}>
244
- Agent spawns inside this directory and only sees this subtree
245
- </div>
246
- </>
215
+ {/* Directory picker */}
216
+ <div style={styles.label}>DIRECTORY</div>
217
+ <div style={styles.wsRow}>
218
+ <button
219
+ type="button"
220
+ onClick={() => setWorkingDir('')}
221
+ style={{
222
+ ...styles.wsBtn,
223
+ ...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
224
+ }}
225
+ >
226
+ project root
227
+ </button>
228
+ {workspaces.map((ws) => (
229
+ <button
230
+ key={ws.path}
231
+ type="button"
232
+ onClick={() => setWorkingDir(ws.path)}
233
+ style={{
234
+ ...styles.wsBtn,
235
+ ...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
236
+ }}
237
+ title={`${ws.name} (${ws.files} files)`}
238
+ >
239
+ {ws.path}
240
+ </button>
241
+ ))}
242
+ <button
243
+ type="button"
244
+ onClick={() => setShowDirPicker(true)}
245
+ style={styles.browseBtn}
246
+ >
247
+ Browse...
248
+ </button>
249
+ </div>
250
+ {workingDir && (
251
+ <div style={styles.hint}>{workingDir}</div>
252
+ )}
253
+
254
+ {showDirPicker && (
255
+ <DirPicker
256
+ initial={workingDir}
257
+ onSelect={(path) => setWorkingDir(path)}
258
+ onClose={() => setShowDirPicker(false)}
259
+ />
247
260
  )}
248
261
 
249
262
  {/* Permissions */}
@@ -279,18 +292,6 @@ export default function SpawnPanel() {
279
292
 
280
293
  {showAdvanced && (
281
294
  <>
282
- {/* Working directory — manual input for custom paths */}
283
- <div style={styles.label}>WORKING DIRECTORY</div>
284
- <input
285
- style={styles.input}
286
- placeholder="e.g. packages/frontend (default: project root)"
287
- value={workingDir}
288
- onChange={(e) => setWorkingDir(e.target.value)}
289
- />
290
- <div style={styles.hint}>
291
- Relative path — or use the directory buttons above
292
- </div>
293
-
294
295
  {/* Provider selector with connection flow */}
295
296
  <div style={styles.label}>PROVIDER</div>
296
297
  {providerList.map((p) => {
@@ -571,6 +572,12 @@ const styles = {
571
572
  fontFamily: 'var(--font)',
572
573
  transition: 'color 0.1s, border-color 0.1s',
573
574
  },
575
+ browseBtn: {
576
+ background: 'none', border: '1px dashed var(--border)',
577
+ borderRadius: 2, padding: '3px 8px',
578
+ color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
579
+ fontFamily: 'var(--font)',
580
+ },
574
581
  error: {
575
582
  color: 'var(--red)', fontSize: 11, marginTop: 8,
576
583
  },