groove-dev 0.27.147 → 0.27.149

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-BKbsE_hn.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-CReKPWhY.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.147",
3
+ "version": "0.27.149",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -91,6 +91,7 @@ function AddRuntimeDialog({ open, onOpenChange }) {
91
91
 
92
92
  function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onStart, testing }) {
93
93
  const online = runtime.status === 'connected';
94
+ const starting = runtime.status === 'starting';
94
95
  const managed = !!(runtime._localModelId || runtime._mlxModelId || runtime.launchConfig || runtime.type === 'mlx' || runtime.type === 'llama-cpp');
95
96
  return (
96
97
  <button
@@ -102,15 +103,15 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
102
103
  >
103
104
  <span className={cn(
104
105
  'w-1.5 h-1.5 rounded-full flex-shrink-0',
105
- online ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
106
+ starting ? 'bg-warning animate-pulse' : online ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
106
107
  )} />
107
108
  <div className="flex-1 min-w-0">
108
109
  <div className={cn('text-[11px] font-sans font-medium truncate', active ? 'text-text-0' : 'text-text-2')}>
109
110
  {RUNTIME_TYPES.find((t) => t.value === runtime.type)?.label || runtime.type}
110
111
  </div>
111
112
  <div className="text-[10px] text-text-4 flex items-center gap-1.5">
112
- <span className={cn('font-sans', online ? 'text-success' : 'text-danger')}>
113
- {online ? 'Online' : 'Offline'}
113
+ <span className={cn('font-sans', starting ? 'text-warning' : online ? 'text-success' : 'text-danger')}>
114
+ {starting ? 'Starting...' : online ? 'Online' : 'Offline'}
114
115
  </span>
115
116
  {runtime.latency != null && online && (
116
117
  <span className="font-mono">{Math.round(runtime.latency)}ms</span>
@@ -128,7 +129,14 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
128
129
  </button>
129
130
  </Tooltip>
130
131
  )}
131
- {managed && !online && (
132
+ {starting && (
133
+ <Tooltip content="Starting server...">
134
+ <span className="p-1 text-warning">
135
+ <Loader2 size={10} className="animate-spin" />
136
+ </span>
137
+ </Tooltip>
138
+ )}
139
+ {managed && !online && !starting && (
132
140
  <Tooltip content="Start server">
133
141
  <button
134
142
  onClick={(e) => { e.stopPropagation(); onStart(runtime.id); }}
@@ -26,7 +26,7 @@ export function StatusBar({
26
26
  const labRuntimes = useGrooveStore((s) => s.labRuntimes);
27
27
  const stopLabRuntime = useGrooveStore((s) => s.stopLabRuntime);
28
28
  const activeTunnels = savedTunnels.filter((t) => t.active);
29
- const runningRuntimes = (labRuntimes || []).filter((rt) => rt.status === 'connected');
29
+ const visibleRuntimes = (labRuntimes || []).filter((rt) => rt.status === 'connected' || rt.status === 'starting');
30
30
  const electron = isElectron();
31
31
 
32
32
  return (
@@ -113,26 +113,35 @@ export function StatusBar({
113
113
  <span>Federation</span>
114
114
  </button>
115
115
  )}
116
- {runningRuntimes.map((rt) => (
117
- <div key={rt.id} className="flex items-center gap-1">
118
- <button
119
- onClick={() => navigate('model-lab')}
120
- className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
121
- title={`${rt.name} running`}
122
- >
123
- <Cpu size={10} className="text-success" />
124
- <span>{rt.name}</span>
125
- <span className="w-1.5 h-1.5 rounded-full bg-success" />
126
- </button>
127
- <button
128
- onClick={() => stopLabRuntime(rt.id)}
129
- className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
130
- title={`Stop ${rt.name}`}
131
- >
132
- <Square size={8} />
133
- </button>
134
- </div>
135
- ))}
116
+ {visibleRuntimes.map((rt) => {
117
+ const starting = rt.status === 'starting';
118
+ return (
119
+ <div key={rt.id} className="flex items-center gap-1">
120
+ <button
121
+ onClick={() => navigate('model-lab')}
122
+ className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
123
+ title={`${rt.name} ${starting ? 'starting...' : 'running'}`}
124
+ >
125
+ <Cpu size={10} className={starting ? 'text-warning animate-pulse' : 'text-success'} />
126
+ <span>{rt.name}</span>
127
+ {starting ? (
128
+ <span className="w-1.5 h-1.5 rounded-full bg-warning animate-pulse" />
129
+ ) : (
130
+ <span className="w-1.5 h-1.5 rounded-full bg-success" />
131
+ )}
132
+ </button>
133
+ {!starting && (
134
+ <button
135
+ onClick={() => stopLabRuntime(rt.id)}
136
+ className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
137
+ title={`Stop ${rt.name}`}
138
+ >
139
+ <Square size={8} />
140
+ </button>
141
+ )}
142
+ </div>
143
+ );
144
+ })}
136
145
  </div>
137
146
 
138
147
  <div className="flex-1" />
@@ -396,12 +396,17 @@ export const createProvidersSlice = (set, get) => ({
396
396
 
397
397
  async startLabRuntime(id) {
398
398
  try {
399
- get().addToast('info', 'Starting server...');
399
+ const runtimes = get().labRuntimes.map((r) =>
400
+ r.id === id ? { ...r, status: 'starting' } : r,
401
+ );
402
+ set({ labRuntimes: runtimes });
403
+ persistJSON('groove:labRuntimes', runtimes);
400
404
  await api.post(`/lab/runtimes/${id}/start`);
401
405
  await get().fetchLabRuntimes();
402
406
  get().setLabActiveRuntime(id);
403
407
  get().addToast('success', 'Server started');
404
408
  } catch (err) {
409
+ await get().fetchLabRuntimes();
405
410
  get().addToast('error', 'Failed to start server', err.message);
406
411
  }
407
412
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.147",
3
+ "version": "0.27.149",
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.147",
3
+ "version": "0.27.149",
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.147",
3
+ "version": "0.27.149",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -175,6 +175,12 @@ export class ModelLab {
175
175
  const rt = this.runtimes.get(id);
176
176
  if (!rt) throw new Error('Runtime not found');
177
177
 
178
+ // Clear disconnected flag on start/reconnect
179
+ if (rt._disconnected) {
180
+ delete rt._disconnected;
181
+ this._saveRuntimes();
182
+ }
183
+
178
184
  // MLX runtimes — use built-in MLXServerManager
179
185
  if (rt.type === 'mlx' && !rt.launchConfig) {
180
186
  const modelId = rt._mlxModelId || this._deriveModelId(rt, 'MLX - ');
@@ -208,7 +214,16 @@ export class ModelLab {
208
214
  }
209
215
 
210
216
  // Generic launchConfig runtimes (vLLM, TGI, etc.)
211
- if (!rt.launchConfig) throw new Error('No launch config — use the assistant to set up this runtime first');
217
+ if (!rt.launchConfig) {
218
+ // Remote runtime — "start" means reconnect; verify it's reachable
219
+ const status = await this.getRuntimeStatus(rt);
220
+ if (status.online) {
221
+ this.daemon.broadcast({ type: 'lab:runtime:started', data: { id } });
222
+ this.daemon.audit.log('lab.runtime.start', { id, name: rt.name });
223
+ return rt;
224
+ }
225
+ throw new Error('Remote server is not reachable — start the server manually and try again');
226
+ }
212
227
  if (this._processes.has(id)) throw new Error('Server already running');
213
228
 
214
229
  const lc = rt.launchConfig;
@@ -268,12 +283,14 @@ export class ModelLab {
268
283
  const rt = this.runtimes.get(id);
269
284
  if (!rt) throw new Error('Runtime not found');
270
285
 
286
+ let killed = false;
287
+
271
288
  if (rt._localModelId) {
272
289
  const mm = this.daemon.modelManager;
273
290
  const ls = this.daemon.llamaServer;
274
291
  if (mm && ls) {
275
292
  const modelPath = mm.getModelPath(rt._localModelId);
276
- if (modelPath) await ls.stopServer(modelPath);
293
+ if (modelPath) { await ls.stopServer(modelPath); killed = true; }
277
294
  }
278
295
  }
279
296
 
@@ -281,7 +298,7 @@ export class ModelLab {
281
298
  const ms = this.daemon.mlxServer;
282
299
  if (ms) {
283
300
  const hfId = rt._mlxModelId.startsWith('mlx:') ? rt._mlxModelId.slice(4) : rt._mlxModelId;
284
- await ms.stopServer(hfId);
301
+ await ms.stopServer(hfId); killed = true;
285
302
  }
286
303
  }
287
304
 
@@ -293,6 +310,13 @@ export class ModelLab {
293
310
  try { proc.kill('SIGTERM'); } catch { clearTimeout(timeout); resolve(); }
294
311
  });
295
312
  this._processes.delete(id);
313
+ killed = true;
314
+ }
315
+
316
+ // Remote/external runtimes: mark as disconnected so status checks treat it as offline
317
+ if (!killed) {
318
+ rt._disconnected = true;
319
+ this._saveRuntimes();
296
320
  }
297
321
 
298
322
  this.daemon.broadcast({ type: 'lab:runtime:stopped', data: { id } });
@@ -370,6 +394,7 @@ export class ModelLab {
370
394
  }
371
395
 
372
396
  async getRuntimeStatus(rt) {
397
+ if (rt._disconnected) return { online: false, latency: null };
373
398
  try {
374
399
  const start = Date.now();
375
400
  const ep = localURL(rt.endpoint);
@@ -859,7 +859,9 @@ export class ProcessManager {
859
859
  }
860
860
 
861
861
  // Validate explicit model against provider's supported models
862
- if (config.model && config.model !== 'auto' && provider.constructor.models) {
862
+ // Skip validation for runtime: and gguf: models — they're resolved by getLoopConfig()
863
+ const skipModelValidation = config.model && (config.model.startsWith('runtime:') || config.model.startsWith('gguf:'));
864
+ if (config.model && config.model !== 'auto' && provider.constructor.models && !skipModelValidation) {
863
865
  const valid = provider.constructor.models.some(m => m.id === config.model);
864
866
  if (!valid) {
865
867
  const fallback = provider.constructor.models[0];
@@ -1128,7 +1130,7 @@ For normal file edits within your scope, proceed without review.
1128
1130
 
1129
1131
  const loop = new AgentLoop({ daemon: this.daemon, agent, loopConfig, logStream });
1130
1132
  this.handles.set(agent.id, { loop, logStream });
1131
- registry.update(agent.id, { status: 'running' });
1133
+ registry.update(agent.id, { status: 'running', model: loopConfig.model, apiBase: loopConfig.apiBase });
1132
1134
 
1133
1135
  // Record spawn lifecycle event
1134
1136
  if (this.daemon.timeline) {