groove-dev 0.27.124 → 0.27.126

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.
Files changed (33) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +122 -0
  4. package/node_modules/@groove-dev/daemon/src/preview.js +28 -5
  5. package/node_modules/@groove-dev/daemon/src/process.js +21 -0
  6. package/node_modules/@groove-dev/daemon/src/providers/local.js +19 -20
  7. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +66 -3
  8. package/node_modules/@groove-dev/gui/dist/assets/index-Do3uUrEW.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-BcmoHTm0.js → index-oPlKeRNb.js} +1749 -1749
  10. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  11. package/node_modules/@groove-dev/gui/package.json +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +66 -6
  13. package/node_modules/@groove-dev/gui/src/stores/groove.js +169 -0
  14. package/node_modules/@groove-dev/gui/src/views/agents.jsx +8 -10
  15. package/node_modules/@groove-dev/gui/src/views/models.jsx +580 -236
  16. package/package.json +1 -1
  17. package/packages/cli/package.json +1 -1
  18. package/packages/daemon/package.json +1 -1
  19. package/packages/daemon/src/api.js +122 -0
  20. package/packages/daemon/src/preview.js +28 -5
  21. package/packages/daemon/src/process.js +21 -0
  22. package/packages/daemon/src/providers/local.js +19 -20
  23. package/packages/daemon/src/providers/ollama.js +66 -3
  24. package/packages/gui/dist/assets/index-Do3uUrEW.css +1 -0
  25. package/packages/gui/dist/assets/{index-BcmoHTm0.js → index-oPlKeRNb.js} +1749 -1749
  26. package/packages/gui/dist/index.html +2 -2
  27. package/packages/gui/package.json +1 -1
  28. package/packages/gui/src/components/agents/spawn-wizard.jsx +66 -6
  29. package/packages/gui/src/stores/groove.js +169 -0
  30. package/packages/gui/src/views/agents.jsx +8 -10
  31. package/packages/gui/src/views/models.jsx +580 -236
  32. package/node_modules/@groove-dev/gui/dist/assets/index-DWI-g_Sm.css +0 -1
  33. package/packages/gui/dist/assets/index-DWI-g_Sm.css +0 -1
@@ -6,12 +6,12 @@
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-BcmoHTm0.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-oPlKeRNb.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-CFF1Lrnz.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-DWI-g_Sm.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Do3uUrEW.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.124",
3
+ "version": "0.27.126",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -83,7 +83,10 @@ export function SpawnWizard() {
83
83
  const [recommendations, setRecommendations] = useState([]);
84
84
  const [preflightDialog, setPreflightDialog] = useState(null);
85
85
  const [claudeAuth, setClaudeAuth] = useState(null);
86
+ const [ollamaInstalled, setOllamaInstalled] = useState([]);
87
+ const [ollamaServerRunning, setOllamaServerRunning] = useState(false);
86
88
  const federation = useGrooveStore((s) => s.federation);
89
+ const ollamaRunningModels = useGrooveStore((s) => s.ollamaRunningModels);
87
90
 
88
91
  const selectedRole = role || customRole;
89
92
  const selectedProvider = providers.find((p) => p.id === provider);
@@ -92,11 +95,14 @@ export function SpawnWizard() {
92
95
 
93
96
  useEffect(() => {
94
97
  if (open) {
98
+ const _presetProvider = detailPanel?.presetProvider || '';
99
+ const _presetModel = detailPanel?.presetModel || '';
100
+
95
101
  fetchProviders().then((data) => {
96
102
  const list = Array.isArray(data) ? data : data.providers || [];
97
103
  setProviders(list);
98
104
  const installed = list.filter((p) => p.authType === 'api-key' ? (p.installed && p.hasKey) : p.installed);
99
- if (installed.length > 0 && !provider) {
105
+ if (installed.length > 0 && !_presetProvider) {
100
106
  const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
101
107
  const best = priority.find((pid) => installed.some((p) => p.id === pid)) || installed[0].id;
102
108
  setProvider(best);
@@ -114,7 +120,9 @@ export function SpawnWizard() {
114
120
  api.get('/personalities').then((data) => {
115
121
  setPersonalities(Array.isArray(data) ? data : data.personalities || []);
116
122
  }).catch(() => {});
117
- setRole(''); setCustomRole(''); setName(''); setProvider(''); setModel(''); setPrompt('');
123
+ setRole(''); setCustomRole(''); setName('');
124
+ setProvider(_presetProvider); setModel(_presetModel);
125
+ setPrompt('');
118
126
  setSelectedSkills([]);
119
127
  setSelectedIntegrations([]);
120
128
  setIntegrationApproval('manual');
@@ -149,6 +157,16 @@ export function SpawnWizard() {
149
157
  }).catch(() => setClaudeAuth(null));
150
158
  }, [open, provider]);
151
159
 
160
+ useEffect(() => {
161
+ if (!open || provider !== 'ollama') { setOllamaInstalled([]); return; }
162
+ api.get('/providers/ollama/models').then((data) => {
163
+ setOllamaInstalled(data.installed || []);
164
+ }).catch(() => setOllamaInstalled([]));
165
+ api.post('/providers/ollama/check').then((data) => {
166
+ setOllamaServerRunning(data.serverRunning);
167
+ }).catch(() => setOllamaServerRunning(false));
168
+ }, [open, provider]);
169
+
152
170
  async function runSpawn() {
153
171
  setSpawning(true);
154
172
  try {
@@ -396,9 +414,32 @@ export function SpawnWizard() {
396
414
  className="w-full h-8 px-3 pr-8 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent disabled:opacity-40"
397
415
  >
398
416
  <option value="">Auto</option>
399
- {availableModels.map((m) => (
400
- <option key={m.id} value={m.id}>{m.name}</option>
401
- ))}
417
+ {provider === 'ollama' && ollamaInstalled.length > 0 ? (
418
+ <>
419
+ <optgroup label="Installed Models">
420
+ {ollamaInstalled.map((m) => {
421
+ const isRunning = ollamaRunningModels.some((r) => r.name === m.id);
422
+ return (
423
+ <option key={m.id} value={m.id}>
424
+ {m.name || m.id} ({m.size}){isRunning ? ' ● Running' : ''}
425
+ </option>
426
+ );
427
+ })}
428
+ </optgroup>
429
+ <optgroup label="Catalog">
430
+ {availableModels
431
+ .filter((m) => !ollamaInstalled.some((i) => i.id === m.id))
432
+ .map((m) => (
433
+ <option key={m.id} value={m.id}>{m.name} (not installed)</option>
434
+ ))
435
+ }
436
+ </optgroup>
437
+ </>
438
+ ) : (
439
+ availableModels.map((m) => (
440
+ <option key={m.id} value={m.id}>{m.name}</option>
441
+ ))
442
+ )}
402
443
  </select>
403
444
  <ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none" />
404
445
  </div>
@@ -406,7 +447,7 @@ export function SpawnWizard() {
406
447
  </div>
407
448
 
408
449
  {provider && selectedProvider && (
409
- <div className="text-2xs text-text-3 font-sans flex items-center gap-2">
450
+ <div className="text-2xs text-text-3 font-sans flex items-center gap-2 flex-wrap">
410
451
  {selectedProvider.authType === 'local' ? (
411
452
  <Badge variant="success">Local</Badge>
412
453
  ) : selectedProvider.authType === 'subscription' ? (
@@ -419,6 +460,25 @@ export function SpawnWizard() {
419
460
  </div>
420
461
  )}
421
462
 
463
+ {/* Ollama model status */}
464
+ {provider === 'ollama' && model && (
465
+ <div className="flex items-center gap-2 flex-wrap text-2xs font-sans">
466
+ {ollamaRunningModels.some((r) => r.name === model) ? (
467
+ <Badge variant="success" className="text-2xs gap-1">
468
+ <span className="w-1.5 h-1.5 rounded-full bg-success" />
469
+ Ready — running in memory
470
+ </Badge>
471
+ ) : ollamaInstalled.some((m) => m.id === model) ? (
472
+ <Badge variant="subtle" className="text-2xs">Will auto-start when agent spawns</Badge>
473
+ ) : (
474
+ <Badge variant="warning" className="text-2xs">Not installed — will pull first</Badge>
475
+ )}
476
+ {!ollamaServerRunning && (
477
+ <span className="text-warning">Server not running — will auto-start</span>
478
+ )}
479
+ </div>
480
+ )}
481
+
422
482
  {/* Claude Code Auth */}
423
483
  {claudeNotAuthed && (
424
484
  <div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3">
@@ -48,6 +48,13 @@ export const useGrooveStore = create((set, get) => ({
48
48
  // ── Providers ────────────────────────────────────────────
49
49
  _providerRefreshTick: 0,
50
50
 
51
+ // ── Local Models (Ollama) ─────────────────────────────────
52
+ ollamaStatus: { installed: false, serverRunning: false, hardware: null },
53
+ ollamaInstalledModels: [],
54
+ ollamaRunningModels: [],
55
+ ollamaCatalog: [],
56
+ ollamaPullProgress: {},
57
+
51
58
  // ── Federation ────────────────────────────────────────────
52
59
  federation: {
53
60
  peers: [],
@@ -572,6 +579,34 @@ export const useGrooveStore = create((set, get) => ({
572
579
  get().addChatMessage(msg.agentId, 'system', 'Agent is working — message will be delivered when it finishes.');
573
580
  break;
574
581
 
582
+ case 'ollama:pull:progress':
583
+ set({ ollamaPullProgress: { ...get().ollamaPullProgress, [msg.model]: { status: 'pulling', progress: msg.progress } } });
584
+ break;
585
+
586
+ case 'ollama:pull:complete': {
587
+ const pullProg = { ...get().ollamaPullProgress };
588
+ delete pullProg[msg.model];
589
+ set({ ollamaPullProgress: pullProg });
590
+ get().fetchOllamaStatus();
591
+ break;
592
+ }
593
+
594
+ case 'ollama:pull:error': {
595
+ const pullProg2 = { ...get().ollamaPullProgress };
596
+ delete pullProg2[msg.model];
597
+ set({ ollamaPullProgress: pullProg2 });
598
+ get().addToast('error', `Model pull failed: ${msg.error}`);
599
+ break;
600
+ }
601
+
602
+ case 'ollama:model:loaded':
603
+ get().fetchOllamaStatus();
604
+ break;
605
+
606
+ case 'ollama:model:unloaded':
607
+ get().fetchOllamaStatus();
608
+ break;
609
+
575
610
  case 'rotation:start':
576
611
  break;
577
612
 
@@ -2060,6 +2095,140 @@ export const useGrooveStore = create((set, get) => ({
2060
2095
  return api.get('/providers');
2061
2096
  },
2062
2097
 
2098
+ // ── Local Models (Ollama) ─────────────────────────────────
2099
+
2100
+ async fetchOllamaStatus() {
2101
+ try {
2102
+ const check = await api.post('/providers/ollama/check');
2103
+ const updates = {
2104
+ ollamaStatus: { installed: check.installed, serverRunning: check.serverRunning, hardware: check.hardware },
2105
+ };
2106
+ if (check.installed) {
2107
+ try {
2108
+ const models = await api.get('/providers/ollama/models');
2109
+ updates.ollamaInstalledModels = models.installed || [];
2110
+ updates.ollamaCatalog = models.catalog || [];
2111
+ } catch {}
2112
+ }
2113
+ if (check.serverRunning) {
2114
+ try {
2115
+ const running = await api.get('/providers/ollama/running');
2116
+ updates.ollamaRunningModels = running.models || [];
2117
+ } catch {
2118
+ updates.ollamaRunningModels = [];
2119
+ }
2120
+ } else {
2121
+ updates.ollamaRunningModels = [];
2122
+ }
2123
+ set(updates);
2124
+ return updates.ollamaStatus;
2125
+ } catch {
2126
+ return get().ollamaStatus;
2127
+ }
2128
+ },
2129
+
2130
+ async startOllamaServer() {
2131
+ try {
2132
+ const result = await api.post('/providers/ollama/serve');
2133
+ if (result.ok) {
2134
+ get().addToast('success', 'Ollama server started');
2135
+ await new Promise((r) => setTimeout(r, 2000));
2136
+ await get().fetchOllamaStatus();
2137
+ }
2138
+ return result;
2139
+ } catch (err) {
2140
+ get().addToast('error', 'Could not start server', err.message);
2141
+ throw err;
2142
+ }
2143
+ },
2144
+
2145
+ async stopOllamaServer() {
2146
+ try {
2147
+ const result = await api.post('/providers/ollama/stop');
2148
+ if (result.ok) {
2149
+ get().addToast('info', 'Ollama server stopped');
2150
+ set((s) => ({
2151
+ ollamaStatus: { ...s.ollamaStatus, serverRunning: false },
2152
+ ollamaRunningModels: [],
2153
+ }));
2154
+ }
2155
+ return result;
2156
+ } catch (err) {
2157
+ get().addToast('error', 'Stop failed', err.message);
2158
+ throw err;
2159
+ }
2160
+ },
2161
+
2162
+ async restartOllamaServer() {
2163
+ try {
2164
+ const result = await api.post('/providers/ollama/restart');
2165
+ if (result.ok) {
2166
+ get().addToast('success', 'Ollama server restarted');
2167
+ await new Promise((r) => setTimeout(r, 2000));
2168
+ await get().fetchOllamaStatus();
2169
+ }
2170
+ return result;
2171
+ } catch (err) {
2172
+ get().addToast('error', 'Restart failed', err.message);
2173
+ throw err;
2174
+ }
2175
+ },
2176
+
2177
+ async pullOllamaModel(modelId) {
2178
+ try {
2179
+ set((s) => ({ ollamaPullProgress: { ...s.ollamaPullProgress, [modelId]: { status: 'pulling', progress: '' } } }));
2180
+ await api.post('/providers/ollama/pull', { model: modelId });
2181
+ set((s) => {
2182
+ const progress = { ...s.ollamaPullProgress };
2183
+ delete progress[modelId];
2184
+ return { ollamaPullProgress: progress };
2185
+ });
2186
+ get().addToast('success', `${modelId} ready to use`);
2187
+ get().fetchOllamaStatus();
2188
+ } catch (err) {
2189
+ set((s) => {
2190
+ const progress = { ...s.ollamaPullProgress };
2191
+ delete progress[modelId];
2192
+ return { ollamaPullProgress: progress };
2193
+ });
2194
+ get().addToast('error', `Pull failed: ${err.message}`);
2195
+ }
2196
+ },
2197
+
2198
+ async deleteOllamaModel(modelId) {
2199
+ try {
2200
+ await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
2201
+ set((s) => ({ ollamaInstalledModels: s.ollamaInstalledModels.filter((m) => m.id !== modelId) }));
2202
+ get().addToast('success', `Removed ${modelId}`);
2203
+ } catch (err) {
2204
+ get().addToast('error', `Delete failed: ${err.message}`);
2205
+ }
2206
+ },
2207
+
2208
+ async loadOllamaModel(modelId) {
2209
+ try {
2210
+ await api.post('/providers/ollama/load', { model: modelId });
2211
+ get().addToast('success', `${modelId} loaded into memory`);
2212
+ get().fetchOllamaStatus();
2213
+ } catch (err) {
2214
+ get().addToast('error', `Could not load model: ${err.message}`);
2215
+ }
2216
+ },
2217
+
2218
+ async unloadOllamaModel(modelId) {
2219
+ try {
2220
+ await api.post('/providers/ollama/unload', { model: modelId });
2221
+ set((s) => ({ ollamaRunningModels: s.ollamaRunningModels.filter((m) => m.name !== modelId) }));
2222
+ get().addToast('info', `${modelId} unloaded`);
2223
+ } catch (err) {
2224
+ get().addToast('error', `Unload failed: ${err.message}`);
2225
+ }
2226
+ },
2227
+
2228
+ spawnFromModel(modelId) {
2229
+ get().openDetail({ type: 'spawn', presetProvider: 'ollama', presetModel: modelId });
2230
+ },
2231
+
2063
2232
  // ── Onboarding ────────────────────────────────────────────
2064
2233
 
2065
2234
  async fetchOnboardingStatus() {
@@ -53,14 +53,12 @@ function savePositions(teamId, positions) {
53
53
  try { localStorage.setItem(key, s); } catch { /* still over — give up silently */ }
54
54
  }
55
55
 
56
- function loadRoleLayout(teamId) {
57
- if (!teamId) return {};
58
- try { return JSON.parse(localStorage.getItem(`groove:roleLayout:${teamId}`) || '{}'); } catch { return {}; }
56
+ function loadRoleLayout() {
57
+ try { return JSON.parse(localStorage.getItem('groove:roleLayout') || '{}'); } catch { return {}; }
59
58
  }
60
59
 
61
- function saveRoleLayout(teamId, layout) {
62
- if (!teamId) return;
63
- const key = `groove:roleLayout:${teamId}`;
60
+ function saveRoleLayout(layout) {
61
+ const key = 'groove:roleLayout';
64
62
  const s = JSON.stringify(layout);
65
63
  try { localStorage.setItem(key, s); return; } catch { /* quota */ }
66
64
  if (!freeLocalStorage()) return;
@@ -367,7 +365,7 @@ function AgentTreeInner() {
367
365
  // Build nodes — positions are stable, data updates flow to node components
368
366
  const targetNodes = useMemo(() => {
369
367
  const saved = positionsRef.current;
370
- const roleLayout = loadRoleLayout(activeTeamId);
368
+ const roleLayout = loadRoleLayout();
371
369
  const runningCount = agents.filter((a) => a.status === 'running').length;
372
370
 
373
371
  const rootPosition = saved[ROOT_ID] || roleLayout[ROOT_ID] || { x: 0, y: 0 };
@@ -1589,7 +1587,7 @@ export default function AgentsView() {
1589
1587
  const positions = loadPositions(activeTeamId);
1590
1588
  const layout = {};
1591
1589
  const roleCounts = new Map();
1592
- teamAgents.forEach((agent) => {
1590
+ [...teamAgents].sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)).forEach((agent) => {
1593
1591
  const key = agent.name || agent.id;
1594
1592
  const pos = positions[key];
1595
1593
  if (!pos) return;
@@ -1600,13 +1598,13 @@ export default function AgentsView() {
1600
1598
  layout[roleKey] = pos;
1601
1599
  });
1602
1600
  if (positions[ROOT_ID]) layout[ROOT_ID] = positions[ROOT_ID];
1603
- saveRoleLayout(activeTeamId, layout);
1601
+ saveRoleLayout(layout);
1604
1602
  addToast('success', 'Layout saved', 'Future spawns will use these positions');
1605
1603
  }}
1606
1604
  className="absolute bottom-4 left-28 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
1607
1605
  >
1608
1606
  <LayoutGrid size={14} />
1609
- {Object.keys(loadRoleLayout(activeTeamId)).length > 0 ? 'Update Layout' : 'Save Layout'}
1607
+ {Object.keys(loadRoleLayout()).length > 0 ? 'Update Layout' : 'Save Layout'}
1610
1608
  </button>
1611
1609
  )}
1612
1610
  {!isLoading && teamAgents.length > 0 && !workspaceMode && (