groove-dev 0.14.1 → 0.15.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.
@@ -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-TcP3URUY.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CNCSwHwH.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
9
9
  </head>
10
10
  <body>
@@ -18,11 +18,14 @@ export default function AgentActions({ agent }) {
18
18
  const [editingPrompt, setEditingPrompt] = useState(false);
19
19
  const [selectedModel, setSelectedModel] = useState(agent.model || '');
20
20
  const [providerList, setProviderList] = useState([]);
21
+ const [installedSkills, setInstalledSkills] = useState([]);
22
+ const [showSkillPicker, setShowSkillPicker] = useState(false);
21
23
 
22
24
  const isAlive = agent.status === 'running' || agent.status === 'starting';
23
25
 
24
26
  useEffect(() => {
25
27
  fetch('/api/providers').then(r => r.json()).then(setProviderList).catch(() => {});
28
+ fetch('/api/skills/installed').then(r => r.json()).then(setInstalledSkills).catch(() => {});
26
29
  }, []);
27
30
 
28
31
  const currentProvider = providerList.find((p) => p.id === agent.provider);
@@ -67,6 +70,24 @@ export default function AgentActions({ agent }) {
67
70
  setConfirmDelete(false);
68
71
  }
69
72
 
73
+ async function handleAttachSkill(skillId) {
74
+ try {
75
+ await fetch(`/api/agents/${agent.id}/skills/${skillId}`, { method: 'POST' });
76
+ showStatus(`skill attached to ${agent.name}`);
77
+ } catch (err) {
78
+ showStatus(`attach failed: ${err.message}`);
79
+ }
80
+ }
81
+
82
+ async function handleDetachSkill(skillId) {
83
+ try {
84
+ await fetch(`/api/agents/${agent.id}/skills/${skillId}`, { method: 'DELETE' });
85
+ showStatus(`skill detached from ${agent.name}`);
86
+ } catch (err) {
87
+ showStatus(`detach failed: ${err.message}`);
88
+ }
89
+ }
90
+
70
91
  async function handleClone() {
71
92
  try {
72
93
  const newAgent = await spawnAgent({
@@ -75,6 +96,7 @@ export default function AgentActions({ agent }) {
75
96
  prompt: agent.prompt,
76
97
  provider: agent.provider,
77
98
  model: agent.model,
99
+ skills: agent.skills,
78
100
  });
79
101
  showStatus(`cloned as ${newAgent.name}`);
80
102
  } catch (err) {
@@ -119,6 +141,7 @@ export default function AgentActions({ agent }) {
119
141
  prompt: agent.prompt,
120
142
  provider: agent.provider,
121
143
  model: agent.model,
144
+ skills: agent.skills,
122
145
  });
123
146
  showStatus(`restarted as ${newAgent.name}`);
124
147
  } catch (err) {
@@ -232,6 +255,75 @@ export default function AgentActions({ agent }) {
232
255
  </div>
233
256
  )}
234
257
 
258
+ {/* Skills */}
259
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>
260
+ SKILLS ({(agent.skills || []).length})
261
+ </div>
262
+ {(agent.skills || []).length > 0 ? (
263
+ <div style={styles.skillsList}>
264
+ {(agent.skills || []).map((skillId) => {
265
+ const skill = installedSkills.find((s) => s.id === skillId);
266
+ return (
267
+ <div key={skillId} style={styles.skillRow}>
268
+ <span style={styles.skillRowIcon}>
269
+ {skill?.icon || skillId.charAt(0).toUpperCase()}
270
+ </span>
271
+ <div style={{ flex: 1, minWidth: 0 }}>
272
+ <div style={{ fontSize: 11, color: 'var(--text-primary)', fontWeight: 500 }}>
273
+ {skill?.name || skillId}
274
+ </div>
275
+ <div style={{ fontSize: 9, color: 'var(--text-muted)' }}>
276
+ {skill?.author || 'unknown'}
277
+ </div>
278
+ </div>
279
+ <button
280
+ onClick={() => handleDetachSkill(skillId)}
281
+ style={styles.detachBtn}
282
+ title="Detach skill"
283
+ >
284
+ {'\u2715'}
285
+ </button>
286
+ </div>
287
+ );
288
+ })}
289
+ </div>
290
+ ) : (
291
+ <div style={styles.noPrompt}>No skills attached</div>
292
+ )}
293
+ {!showSkillPicker && installedSkills.length > 0 && (
294
+ <button
295
+ onClick={() => setShowSkillPicker(true)}
296
+ style={{ ...styles.editBtn, marginTop: 6 }}
297
+ >
298
+ + Attach Skill
299
+ </button>
300
+ )}
301
+ {showSkillPicker && (
302
+ <div style={styles.skillPicker}>
303
+ {installedSkills
304
+ .filter((s) => !(agent.skills || []).includes(s.id))
305
+ .map((skill) => (
306
+ <button
307
+ key={skill.id}
308
+ onClick={() => { handleAttachSkill(skill.id); setShowSkillPicker(false); }}
309
+ style={styles.skillPickerItem}
310
+ >
311
+ <span style={styles.skillRowIcon}>
312
+ {skill.icon || skill.name.charAt(0)}
313
+ </span>
314
+ <div style={{ flex: 1 }}>
315
+ <div style={{ fontSize: 11, color: 'var(--text-primary)' }}>{skill.name}</div>
316
+ <div style={{ fontSize: 9, color: 'var(--text-muted)' }}>{skill.author}</div>
317
+ </div>
318
+ </button>
319
+ ))}
320
+ {installedSkills.filter((s) => !(agent.skills || []).includes(s.id)).length === 0 && (
321
+ <div style={{ fontSize: 10, color: 'var(--text-dim)', padding: 8 }}>All installed skills are already attached</div>
322
+ )}
323
+ <button onClick={() => setShowSkillPicker(false)} style={styles.cancelBtn}>cancel</button>
324
+ </div>
325
+ )}
326
+
235
327
  {/* Current config */}
236
328
  <div style={{ ...styles.sectionLabel, marginTop: 20 }}>CONFIGURATION</div>
237
329
  <ConfigRow label="ID" value={agent.id} />
@@ -346,4 +438,41 @@ const styles = {
346
438
  display: 'flex', gap: 8, padding: '3px 0',
347
439
  borderBottom: '1px solid var(--bg-surface)',
348
440
  },
441
+ skillsList: {
442
+ display: 'flex', flexDirection: 'column', gap: 3,
443
+ },
444
+ skillRow: {
445
+ display: 'flex', alignItems: 'center', gap: 8,
446
+ padding: '5px 8px',
447
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
448
+ borderRadius: 2,
449
+ },
450
+ skillRowIcon: {
451
+ width: 20, height: 20, borderRadius: 4,
452
+ background: 'var(--accent)',
453
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
454
+ fontSize: 9, fontWeight: 700, color: 'var(--bg-base)',
455
+ flexShrink: 0,
456
+ },
457
+ detachBtn: {
458
+ background: 'none', border: 'none',
459
+ color: 'var(--text-muted)', fontSize: 11,
460
+ cursor: 'pointer', fontFamily: 'var(--font)',
461
+ padding: '2px 4px', flexShrink: 0,
462
+ },
463
+ skillPicker: {
464
+ marginTop: 6, padding: 6,
465
+ background: 'var(--bg-base)', border: '1px solid var(--border)',
466
+ borderRadius: 2,
467
+ display: 'flex', flexDirection: 'column', gap: 2,
468
+ maxHeight: 180, overflowY: 'auto',
469
+ },
470
+ skillPickerItem: {
471
+ display: 'flex', alignItems: 'center', gap: 8,
472
+ padding: '5px 8px', width: '100%',
473
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
474
+ borderRadius: 2, cursor: 'pointer', textAlign: 'left',
475
+ fontFamily: 'var(--font)',
476
+ transition: 'border-color 0.1s',
477
+ },
349
478
  };
@@ -41,10 +41,13 @@ export default function SpawnPanel() {
41
41
  const [apiKeyInput, setApiKeyInput] = useState('');
42
42
  const [keySaving, setKeySaving] = useState(false);
43
43
  const [showDirPicker, setShowDirPicker] = useState(false);
44
+ const [installedSkills, setInstalledSkills] = useState([]);
45
+ const [selectedSkills, setSelectedSkills] = useState([]);
44
46
 
45
47
  useEffect(() => {
46
48
  fetchProviders();
47
49
  fetchWorkspaces();
50
+ fetchInstalledSkills();
48
51
  }, []);
49
52
 
50
53
  async function fetchProviders() {
@@ -62,6 +65,19 @@ export default function SpawnPanel() {
62
65
  } catch { /* ignore */ }
63
66
  }
64
67
 
68
+ async function fetchInstalledSkills() {
69
+ try {
70
+ const res = await fetch('/api/skills/installed');
71
+ setInstalledSkills(await res.json());
72
+ } catch { /* ignore */ }
73
+ }
74
+
75
+ function toggleSkill(skillId) {
76
+ setSelectedSkills((prev) =>
77
+ prev.includes(skillId) ? prev.filter((s) => s !== skillId) : [...prev, skillId]
78
+ );
79
+ }
80
+
65
81
  const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
66
82
  const effectiveScope = role === 'custom'
67
83
  ? scope
@@ -127,6 +143,7 @@ export default function SpawnPanel() {
127
143
  provider,
128
144
  permission,
129
145
  ...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
146
+ ...(selectedSkills.length > 0 ? { skills: selectedSkills } : {}),
130
147
  });
131
148
  closeDetail();
132
149
  } catch (err) {
@@ -281,6 +298,57 @@ export default function SpawnPanel() {
281
298
  ))}
282
299
  </div>
283
300
 
301
+ {/* Skills picker */}
302
+ {installedSkills.length > 0 && (
303
+ <>
304
+ <div style={styles.label}>SKILLS</div>
305
+ <div style={styles.skillsGrid}>
306
+ {installedSkills.map((skill) => {
307
+ const active = selectedSkills.includes(skill.id);
308
+ return (
309
+ <button
310
+ key={skill.id}
311
+ type="button"
312
+ onClick={() => toggleSkill(skill.id)}
313
+ style={{
314
+ ...styles.skillBtn,
315
+ borderColor: active ? 'var(--accent)' : 'var(--border)',
316
+ background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
317
+ }}
318
+ >
319
+ <span style={{
320
+ ...styles.skillIcon,
321
+ background: active ? 'var(--accent)' : 'var(--bg-active)',
322
+ color: active ? 'var(--bg-base)' : 'var(--text-dim)',
323
+ }}>
324
+ {skill.icon || skill.name.charAt(0)}
325
+ </span>
326
+ <div style={{ flex: 1, minWidth: 0 }}>
327
+ <div style={{
328
+ fontSize: 11, fontWeight: 600,
329
+ color: active ? 'var(--text-bright)' : 'var(--text-primary)',
330
+ }}>
331
+ {skill.name}
332
+ </div>
333
+ <div style={{ fontSize: 9, color: 'var(--text-muted)' }}>
334
+ {skill.author || 'local'}
335
+ </div>
336
+ </div>
337
+ {active && (
338
+ <span style={{ fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>{'\u2713'}</span>
339
+ )}
340
+ </button>
341
+ );
342
+ })}
343
+ </div>
344
+ {selectedSkills.length > 0 && (
345
+ <div style={styles.hint}>
346
+ {selectedSkills.length} skill{selectedSkills.length !== 1 ? 's' : ''} will be injected into this agent's context
347
+ </div>
348
+ )}
349
+ </>
350
+ )}
351
+
284
352
  {/* Advanced toggle */}
285
353
  <button
286
354
  type="button"
@@ -505,6 +573,22 @@ const styles = {
505
573
  permDesc: {
506
574
  fontSize: 10, color: 'var(--text-dim)',
507
575
  },
576
+ skillsGrid: {
577
+ display: 'flex', flexDirection: 'column', gap: 3,
578
+ },
579
+ skillBtn: {
580
+ display: 'flex', alignItems: 'center', gap: 8,
581
+ padding: '6px 8px', width: '100%',
582
+ border: '1px solid var(--border)',
583
+ borderRadius: 2, cursor: 'pointer', textAlign: 'left',
584
+ fontFamily: 'var(--font)',
585
+ transition: 'border-color 0.1s, background 0.1s',
586
+ },
587
+ skillIcon: {
588
+ width: 22, height: 22, borderRadius: 4,
589
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
590
+ fontSize: 10, fontWeight: 700, flexShrink: 0,
591
+ },
508
592
  advancedToggle: {
509
593
  background: 'none', border: 'none', color: 'var(--text-dim)',
510
594
  fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
@@ -653,7 +653,7 @@ const styles = {
653
653
 
654
654
  // Scroll area
655
655
  scrollArea: {
656
- flex: 1, overflowY: 'auto', padding: '0 20px 20px',
656
+ flex: 1, overflowY: 'auto', padding: '4px 20px 20px',
657
657
  },
658
658
 
659
659
  // Featured