groove-dev 0.27.154 → 0.27.156

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.
@@ -6,7 +6,7 @@
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-BTLb6zTD.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-COQYX12F.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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.154",
3
+ "version": "0.27.156",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -106,10 +106,10 @@ function ContextMenu({ x, y, items, onClose }) {
106
106
  );
107
107
  }
108
108
 
109
- function downloadFile(path) {
109
+ function downloadFile(path, isDir) {
110
110
  const a = document.createElement('a');
111
111
  a.href = `/api/files/download?path=${encodeURIComponent(path)}`;
112
- a.download = path.split('/').pop();
112
+ a.download = isDir ? `${path.split('/').pop()}.zip` : path.split('/').pop();
113
113
  document.body.appendChild(a);
114
114
  a.click();
115
115
  a.remove();
@@ -138,8 +138,8 @@ function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextM
138
138
  onDragStartEntry(entry.path);
139
139
  }}
140
140
  onDragEnd={onDragEndEntry}
141
- onDragOver={isDir ? (e) => { e.preventDefault(); e.stopPropagation(); onSetDragOver(entry.path); } : undefined}
142
- onDrop={isDir ? (e) => onDropOnDir(entry.path, e) : undefined}
141
+ onDragOver={(e) => { e.preventDefault(); if (isDir) { e.stopPropagation(); onSetDragOver(entry.path); } }}
142
+ onDrop={(e) => onDropOnDir(isDir ? entry.path : (entry.path.includes('/') ? entry.path.split('/').slice(0, -1).join('/') : ''), e)}
143
143
  onClick={() => isDir ? onToggleDir(entry.path) : onOpen(entry.path)}
144
144
  onDoubleClick={handleCtxMenu}
145
145
  onContextMenu={handleCtxMenu}
@@ -500,11 +500,9 @@ export function AgentFileTree({ agentId, onCollapse }) {
500
500
  if (isDir) {
501
501
  items.push({ icon: FilePlus, label: 'New File', action: () => handleNewFileIn(entry.path) });
502
502
  items.push({ icon: FolderPlus, label: 'New Folder', action: () => handleNewFolderIn(entry.path) });
503
- items.push({ separator: true });
504
- } else {
505
- items.push({ icon: Download, label: 'Download', action: () => downloadFile(entry.path) });
506
- items.push({ separator: true });
507
503
  }
504
+ items.push({ icon: Download, label: isDir ? 'Download as ZIP' : 'Download', action: () => downloadFile(entry.path, isDir) });
505
+ items.push({ separator: true });
508
506
  items.push({ icon: Pencil, label: 'Rename', action: () => handleRename(entry) });
509
507
  items.push({ icon: Trash2, label: 'Delete', danger: true, action: () => handleDelete(entry) });
510
508
  return items;
@@ -533,7 +531,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
533
531
  )}
534
532
  </div>
535
533
  <ScrollArea className="flex-1 min-h-0">
536
- <div className="py-2">
534
+ <div
535
+ className="py-2 min-h-full"
536
+ onDragOver={(e) => { e.preventDefault(); if (dragState.draggingPath) setDragOverDir(null); }}
537
+ onDrop={(e) => handleDropOnDir('', e)}
538
+ >
537
539
  {inlineInput && (
538
540
  <InlineInput
539
541
  placeholder={inlineInput.type === 'file' ? 'filename.ext' : 'folder-name'}
@@ -595,11 +597,7 @@ export function AgentFileTree({ agentId, onCollapse }) {
595
597
  No files in scope
596
598
  </div>
597
599
  ) : (
598
- <div
599
- className="px-1"
600
- onDragOver={(e) => { e.preventDefault(); if (dragState.draggingPath) setDragOverDir(null); }}
601
- onDrop={(e) => handleDropOnDir('', e)}
602
- >
600
+ <div className="px-1">
603
601
  <div className="flex items-center gap-1.5 px-2 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider">
604
602
  <Folder size={10} />
605
603
  Scope
@@ -107,10 +107,10 @@ function GitDot({ status }) {
107
107
  return <span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', color)} />;
108
108
  }
109
109
 
110
- function downloadFile(path) {
110
+ function downloadFile(path, isDir) {
111
111
  const a = document.createElement('a');
112
112
  a.href = `/api/files/download?path=${encodeURIComponent(path)}`;
113
- a.download = path.split('/').pop();
113
+ a.download = isDir ? `${path.split('/').pop()}.zip` : path.split('/').pop();
114
114
  document.body.appendChild(a);
115
115
  a.click();
116
116
  a.remove();
@@ -140,8 +140,8 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
140
140
  onDragStartEntry(entry.path);
141
141
  }}
142
142
  onDragEnd={onDragEndEntry}
143
- onDragOver={isDir ? (e) => { e.preventDefault(); e.stopPropagation(); onSetDragOver(entry.path); } : undefined}
144
- onDrop={isDir ? (e) => onDropOnDir(entry.path, e) : undefined}
143
+ onDragOver={(e) => { e.preventDefault(); if (isDir) { e.stopPropagation(); onSetDragOver(entry.path); } }}
144
+ onDrop={(e) => onDropOnDir(isDir ? entry.path : (entry.path.includes('/') ? entry.path.split('/').slice(0, -1).join('/') : ''), e)}
145
145
  onClick={() => isDir ? onDirToggle(entry.path) : onFileClick(entry.path)}
146
146
  onDoubleClick={handleContextMenu}
147
147
  onContextMenu={handleContextMenu}
@@ -455,9 +455,7 @@ export function FileTree({ rootDir, onCollapse }) {
455
455
  }
456
456
 
457
457
  if (entry.name !== 'root') {
458
- if (!isDir) {
459
- items.push({ icon: Download, label: 'Download', action: () => downloadFile(entry.path) });
460
- }
458
+ items.push({ icon: Download, label: isDir ? 'Download as ZIP' : 'Download', action: () => downloadFile(entry.path, isDir) });
461
459
  if (items.length > 0) items.push({ separator: true });
462
460
  items.push({ icon: Pencil, label: 'Rename', action: () => handleRename(entry) });
463
461
  items.push({ icon: Trash2, label: 'Delete', danger: true, action: () => handleDelete(entry) });
@@ -527,7 +525,7 @@ export function FileTree({ rootDir, onCollapse }) {
527
525
  {/* Tree */}
528
526
  <ScrollArea className="flex-1">
529
527
  <div
530
- className="py-1"
528
+ className="py-1 min-h-full"
531
529
  onDragOver={(e) => { e.preventDefault(); if (dragState.draggingPath) setDragOverDir(null); }}
532
530
  onDrop={(e) => handleDropOnDir('', e)}
533
531
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.154",
3
+ "version": "0.27.156",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.154",
3
+ "version": "0.27.156",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.154",
3
+ "version": "0.27.156",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { resolve, sep, isAbsolute, basename } from 'path';
3
3
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, realpathSync } from 'fs';
4
- import { execFile, execFileSync } from 'child_process';
4
+ import { execFile, execFileSync, spawn } from 'child_process';
5
5
  import { homedir } from 'os';
6
6
  import { lookup as mimeLookup } from '../mimetypes.js';
7
7
 
@@ -331,15 +331,37 @@ export function registerFileRoutes(app, daemon) {
331
331
  }
332
332
  });
333
333
 
334
- // Download a file (serves raw with Content-Disposition)
334
+ // Download a file or folder (folders are streamed as zip)
335
335
  app.get('/api/files/download', (req, res) => {
336
336
  const relPath = req.query.path;
337
337
  const result = validateFilePath(relPath, getEditorRoot(daemon));
338
338
  if (result.error) return res.status(400).json({ error: result.error });
339
- if (!existsSync(result.fullPath)) return res.status(404).json({ error: 'File not found' });
339
+ if (!existsSync(result.fullPath)) return res.status(404).json({ error: 'Not found' });
340
340
 
341
341
  const stat = statSync(result.fullPath);
342
- if (stat.isDirectory()) return res.status(400).json({ error: 'Cannot download a directory' });
342
+
343
+ if (stat.isDirectory()) {
344
+ const folderName = basename(result.fullPath);
345
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(folderName)}.zip"`);
346
+ res.setHeader('Content-Type', 'application/zip');
347
+
348
+ const zipProc = spawn('zip', [
349
+ '-r', '-q', '-',
350
+ relPath,
351
+ '-x', `${relPath}/.git/*`,
352
+ '-x', `${relPath}/node_modules/*`,
353
+ ], {
354
+ cwd: getEditorRoot(daemon),
355
+ stdio: ['ignore', 'pipe', 'pipe'],
356
+ });
357
+
358
+ zipProc.stdout.pipe(res);
359
+ zipProc.stderr.on('data', () => {});
360
+ zipProc.on('error', () => {
361
+ if (!res.headersSent) res.status(500).json({ error: 'Failed to create zip' });
362
+ });
363
+ return;
364
+ }
343
365
 
344
366
  const name = basename(result.fullPath);
345
367
  const mime = mimeLookup(name) || 'application/octet-stream';
@@ -267,8 +267,6 @@ export class TunnelManager {
267
267
  let testResult;
268
268
  if (opts.skipTest && opts.testResult) {
269
269
  testResult = opts.testResult;
270
- } else if (config.lastConnected && opts.skipTest !== false) {
271
- testResult = { reachable: true, daemonRunning: true, grooveInstalled: true, remoteVersion: null };
272
270
  } else {
273
271
  testResult = await this.test(id);
274
272
  }