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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "description": "Open-source agent orchestration layer for AI coding tools. GUI dashboard, multi-agent coordination, zero cold-start (Journalist), infinite sessions (adaptive context rotation), AI Project Manager, Quick Launch. Works with Claude Code, Codex, Gemini CLI, Ollama.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -4,7 +4,7 @@
4
4
  import express from 'express';
5
5
  import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
- import { existsSync, readFileSync } from 'fs';
7
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
8
8
  import { listProviders } from './providers/index.js';
9
9
  import { validateAgentConfig } from './validate.js';
10
10
 
@@ -365,6 +365,52 @@ export function createApi(app, daemon) {
365
365
  res.json(daemon.adaptive.getAllProfiles());
366
366
  });
367
367
 
368
+ // --- Directory Browser ---
369
+
370
+ app.get('/api/browse', (req, res) => {
371
+ const relPath = req.query.path || '';
372
+
373
+ // Security: no absolute paths, no traversal
374
+ if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
375
+ return res.status(400).json({ error: 'Invalid path' });
376
+ }
377
+
378
+ const fullPath = relPath ? resolve(daemon.projectDir, relPath) : daemon.projectDir;
379
+
380
+ // Must stay within project directory
381
+ if (!fullPath.startsWith(daemon.projectDir)) {
382
+ return res.status(400).json({ error: 'Path outside project' });
383
+ }
384
+
385
+ if (!existsSync(fullPath)) {
386
+ return res.status(404).json({ error: 'Directory not found' });
387
+ }
388
+
389
+ try {
390
+ const entries = readdirSync(fullPath, { withFileTypes: true })
391
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
392
+ .sort((a, b) => a.name.localeCompare(b.name))
393
+ .map((e) => {
394
+ const childPath = relPath ? `${relPath}/${e.name}` : e.name;
395
+ // Check if this dir has subdirectories (for expand arrow)
396
+ let hasChildren = false;
397
+ try {
398
+ hasChildren = readdirSync(resolve(fullPath, e.name), { withFileTypes: true })
399
+ .some((c) => c.isDirectory() && !c.name.startsWith('.') && c.name !== 'node_modules');
400
+ } catch { /* unreadable */ }
401
+ return { name: e.name, path: childPath, hasChildren };
402
+ });
403
+
404
+ res.json({
405
+ current: relPath || '.',
406
+ parent: relPath ? relPath.split('/').slice(0, -1).join('/') : null,
407
+ dirs: entries,
408
+ });
409
+ } catch (err) {
410
+ res.status(500).json({ error: err.message });
411
+ }
412
+ });
413
+
368
414
  // --- Codebase Indexer ---
369
415
 
370
416
  app.get('/api/indexer', (req, res) => {
@@ -25,9 +25,33 @@ export function isFirstRun(grooveDir) {
25
25
  export function printWelcome(port, host = '127.0.0.1', firstRun = false) {
26
26
  const providers = listProviders();
27
27
  const installed = providers.filter((p) => p.installed);
28
+ const notInstalled = providers.filter((p) => !p.installed);
28
29
 
29
30
  console.log('');
30
- console.log(' GROOVE');
31
+ console.log(' ┌─────────────────────────────────────┐');
32
+ console.log(' │ Welcome to GROOVE │');
33
+ console.log(' │ Agent orchestration for AI coding │');
34
+ console.log(' └─────────────────────────────────────┘');
35
+ console.log('');
36
+
37
+ if (installed.length > 0) {
38
+ console.log(` Providers (${installed.length} ready):`);
39
+ for (const p of installed) {
40
+ console.log(` ✓ ${p.name}`);
41
+ }
42
+ } else {
43
+ console.log(' No AI providers detected.');
44
+ console.log(' Install at least one: npm i -g @anthropic-ai/claude-code');
45
+ }
46
+
47
+ if (notInstalled.length > 0) {
48
+ console.log('');
49
+ console.log(' Available to install:');
50
+ for (const p of notInstalled) {
51
+ console.log(` · ${p.name.padEnd(18)} ${p.installCommand}`);
52
+ }
53
+ }
54
+
31
55
  console.log('');
32
56
 
33
57
  const isRemote = host !== '127.0.0.1';
@@ -72,18 +96,6 @@ export function printWelcome(port, host = '127.0.0.1', firstRun = false) {
72
96
 
73
97
  console.log(` Stop: groove stop (or Ctrl+C)`);
74
98
  console.log(` Docs: https://docs.groovedev.ai`);
75
-
76
- // Show providers only on first run or if none installed
77
- if (firstRun || installed.length === 0) {
78
- console.log('');
79
- if (installed.length > 0) {
80
- console.log(` Providers: ${installed.map((p) => p.name).join(', ')}`);
81
- } else {
82
- console.log(' No AI providers detected.');
83
- console.log(' Install one: npm i -g @anthropic-ai/claude-code');
84
- }
85
- }
86
-
87
99
  console.log('');
88
100
  }
89
101