groove-dev 0.25.4 → 0.25.5

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-DtW5ej1k.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-feaBOh4i.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useEffect } from 'react';
3
- import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight } from 'lucide-react';
3
+ import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { Avatar } from '../ui/avatar';
@@ -108,8 +108,10 @@ export function AgentChat({ agent }) {
108
108
 
109
109
  const [input, setInput] = useState('');
110
110
  const [sending, setSending] = useState(false);
111
+ const [attachedFiles, setAttachedFiles] = useState([]);
111
112
  const scrollRef = useRef(null);
112
113
  const inputRef = useRef(null);
114
+ const fileInputRef = useRef(null);
113
115
 
114
116
  useEffect(() => {
115
117
  if (scrollRef.current) {
@@ -117,10 +119,23 @@ export function AgentChat({ agent }) {
117
119
  }
118
120
  }, [chatHistory.length, activityLog.length]);
119
121
 
122
+ function handleFileSelect(e) {
123
+ const files = Array.from(e.target.files || []);
124
+ if (files.length === 0) return;
125
+ // Use webkitRelativePath or name — files from input have path info
126
+ const paths = files.map((f) => f.name);
127
+ setAttachedFiles((prev) => [...prev, ...paths]);
128
+ // Also add file paths to the prompt so the agent knows about them
129
+ const pathList = files.map((f) => f.name).join(', ');
130
+ setInput((prev) => prev + (prev ? '\n' : '') + `[Attached files: ${pathList}]`);
131
+ e.target.value = '';
132
+ }
133
+
120
134
  async function handleSend() {
121
135
  const text = input.trim();
122
136
  if (!text || sending) return;
123
137
  setInput('');
138
+ setAttachedFiles([]);
124
139
  setSending(true);
125
140
  try {
126
141
  if (text.startsWith('?')) {
@@ -192,6 +207,22 @@ export function AgentChat({ agent }) {
192
207
  </div>
193
208
 
194
209
  <div className="flex items-end gap-2">
210
+ {/* File import */}
211
+ <input
212
+ ref={fileInputRef}
213
+ type="file"
214
+ multiple
215
+ accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
216
+ onChange={handleFileSelect}
217
+ className="hidden"
218
+ />
219
+ <button
220
+ onClick={() => fileInputRef.current?.click()}
221
+ className="w-10 h-10 flex items-center justify-center rounded-xl text-text-4 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer"
222
+ title="Attach file"
223
+ >
224
+ <Paperclip size={16} />
225
+ </button>
195
226
  <textarea
196
227
  ref={inputRef}
197
228
  value={input}
@@ -11,7 +11,9 @@ import {
11
11
  Server, Monitor, Code2, TestTube, Cloud, FileText,
12
12
  Shield, Database, Megaphone, Calculator, UserCheck,
13
13
  Headphones, BarChart3, Rocket, ChevronDown, Pen, Presentation,
14
+ Sparkles,
14
15
  } from 'lucide-react';
16
+ import { api } from '../../lib/api';
15
17
 
16
18
  const ROLE_PRESETS = [
17
19
  { id: 'planner', label: 'Planner', desc: 'Plans the team and tasks', icon: Rocket, tier: 'Heavy' },
@@ -46,15 +48,20 @@ export function SpawnWizard() {
46
48
  const [model, setModel] = useState('');
47
49
  const [prompt, setPrompt] = useState('');
48
50
  const [providers, setProviders] = useState([]);
51
+ const [installedSkills, setInstalledSkills] = useState([]);
52
+ const [selectedSkills, setSelectedSkills] = useState([]);
49
53
  const [spawning, setSpawning] = useState(false);
50
54
 
51
55
  useEffect(() => {
52
56
  if (open) {
53
57
  fetchProviders().then((data) => {
54
- // API returns array directly
55
58
  setProviders(Array.isArray(data) ? data : data.providers || []);
56
59
  }).catch(() => {});
60
+ api.get('/skills/installed').then((data) => {
61
+ setInstalledSkills(Array.isArray(data) ? data : []);
62
+ }).catch(() => {});
57
63
  setRole(''); setCustomRole(''); setName(''); setProvider(''); setModel(''); setPrompt('');
64
+ setSelectedSkills([]);
58
65
  }
59
66
  }, [open, fetchProviders]);
60
67
 
@@ -69,10 +76,11 @@ export function SpawnWizard() {
69
76
  try {
70
77
  const config = {
71
78
  role: selectedRole,
72
- ...(name && { name: name.replace(/\s+/g, '-') }), // auto-slugify spaces
79
+ ...(name && { name: name.replace(/\s+/g, '-') }),
73
80
  ...(provider && { provider }),
74
81
  ...(model && { model }),
75
82
  ...(prompt && { prompt }),
83
+ ...(selectedSkills.length > 0 && { skills: selectedSkills }),
76
84
  };
77
85
  await spawnAgent(config);
78
86
  closeDetail();
@@ -202,6 +210,35 @@ export function SpawnWizard() {
202
210
  </div>
203
211
  )}
204
212
 
213
+ {/* Skills */}
214
+ {installedSkills.length > 0 && (
215
+ <div className="space-y-1.5">
216
+ <label className="text-xs font-medium text-text-2 font-sans">Skills</label>
217
+ <div className="flex flex-wrap gap-1.5">
218
+ {installedSkills.map((skill) => {
219
+ const active = selectedSkills.includes(skill.id);
220
+ return (
221
+ <button
222
+ key={skill.id}
223
+ onClick={() => setSelectedSkills((prev) =>
224
+ active ? prev.filter((s) => s !== skill.id) : [...prev, skill.id]
225
+ )}
226
+ className={cn(
227
+ 'inline-flex items-center gap-1 px-2 py-1 rounded text-2xs font-sans transition-colors cursor-pointer',
228
+ active
229
+ ? 'bg-accent/15 text-accent border border-accent/30'
230
+ : 'bg-surface-0 text-text-2 border border-border-subtle hover:border-border',
231
+ )}
232
+ >
233
+ <Sparkles size={10} />
234
+ {skill.name || skill.id}
235
+ </button>
236
+ );
237
+ })}
238
+ </div>
239
+ </div>
240
+ )}
241
+
205
242
  <Textarea
206
243
  label="Prompt (optional)"
207
244
  value={prompt}
@@ -216,6 +216,10 @@ function AgentTreeInner() {
216
216
  const occupied = new Set();
217
217
  const posKey = (x, y) => `${Math.round(x / 100)},${Math.round(y / 100)}`;
218
218
 
219
+ // Mark root node position as occupied
220
+ const rootPos = saved[ROOT_ID] || { x: 0, y: 0 };
221
+ occupied.add(posKey(rootPos.x, rootPos.y));
222
+
219
223
  // First pass: place agents with saved positions
220
224
  const pending = [];
221
225
  agents.forEach((agent, i) => {
@@ -239,7 +243,7 @@ function AgentTreeInner() {
239
243
  const col = index % MAX_PER_ROW;
240
244
  const totalInRow = Math.min(agents.length - row * MAX_PER_ROW, MAX_PER_ROW);
241
245
  const offsetX = -((totalInRow - 1) * NODE_X_GAP) / 2;
242
- let pos = { x: offsetX + col * NODE_X_GAP, y: 140 + row * NODE_Y_GAP };
246
+ let pos = { x: offsetX + col * NODE_X_GAP, y: NODE_Y_GAP + row * NODE_Y_GAP };
243
247
 
244
248
  // If position is occupied, shift down until we find empty space
245
249
  while (occupied.has(posKey(pos.x, pos.y))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.25.4",
3
+ "version": "0.25.5",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. 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.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",