groove-dev 0.27.142 → 0.27.144

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 (187) 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 +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useRef } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { Button } from '../ui/button';
5
5
  import { Badge } from '../ui/badge';
@@ -8,13 +8,16 @@ import { Dialog, DialogContent } from '../ui/dialog';
8
8
  import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
9
9
  import { Tooltip } from '../ui/tooltip';
10
10
  import { ScrollArea } from '../ui/scroll-area';
11
- import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, CheckCircle, AlertTriangle, ChevronRight, Wrench } from 'lucide-react';
11
+ import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2 } from 'lucide-react';
12
12
  import { cn } from '../../lib/cn';
13
13
 
14
+ const IS_APPLE = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || '');
15
+
14
16
  const RUNTIME_TYPES = [
15
17
  { value: 'ollama', label: 'Ollama' },
16
18
  { value: 'vllm', label: 'vLLM' },
17
19
  { value: 'llama-cpp', label: 'llama.cpp' },
20
+ { value: 'mlx', label: 'MLX', suffix: 'Apple Silicon', appleOnly: true },
18
21
  { value: 'tgi', label: 'TGI' },
19
22
  { value: 'openai-compatible', label: 'OpenAI Compatible' },
20
23
  ];
@@ -23,6 +26,7 @@ const DEFAULT_ENDPOINTS = {
23
26
  ollama: 'http://localhost:11434',
24
27
  vllm: 'http://localhost:8000',
25
28
  'llama-cpp': 'http://localhost:8080',
29
+ mlx: 'http://localhost:8080',
26
30
  tgi: 'http://localhost:8080',
27
31
  'openai-compatible': 'http://localhost:8000',
28
32
  };
@@ -64,8 +68,8 @@ function AddRuntimeDialog({ open, onOpenChange }) {
64
68
  <Select value={type} onValueChange={handleTypeChange}>
65
69
  <SelectTrigger placeholder="Select type" />
66
70
  <SelectContent>
67
- {RUNTIME_TYPES.map((rt) => (
68
- <SelectItem key={rt.value} value={rt.value}>{rt.label}</SelectItem>
71
+ {RUNTIME_TYPES.filter(rt => !rt.appleOnly || IS_APPLE).map((rt) => (
72
+ <SelectItem key={rt.value} value={rt.value}>{rt.label}{rt.suffix ? ` (${rt.suffix})` : ''}</SelectItem>
69
73
  ))}
70
74
  </SelectContent>
71
75
  </Select>
@@ -84,30 +88,54 @@ function AddRuntimeDialog({ open, onOpenChange }) {
84
88
  );
85
89
  }
86
90
 
87
- function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, testing }) {
91
+ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onStart, testing }) {
92
+ const online = runtime.status === 'connected';
93
+ const managed = !!(runtime._localModelId || runtime._mlxModelId || runtime.launchConfig || runtime.type === 'mlx' || runtime.type === 'llama-cpp');
88
94
  return (
89
95
  <button
90
96
  onClick={() => onSelect(runtime.id)}
91
97
  className={cn(
92
98
  'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded-sm',
93
- active ? 'bg-accent/8 text-text-0' : 'text-text-2 hover:bg-surface-3 hover:text-text-0',
99
+ active ? 'bg-accent/8 border border-accent/20' : 'border border-transparent text-text-2 hover:bg-surface-3 hover:text-text-0',
94
100
  )}
95
101
  >
96
102
  <span className={cn(
97
103
  'w-1.5 h-1.5 rounded-full flex-shrink-0',
98
- runtime.status === 'connected' ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
104
+ online ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
99
105
  )} />
100
106
  <div className="flex-1 min-w-0">
101
- <div className="text-xs font-sans font-medium truncate">{runtime.name}</div>
107
+ <div className={cn('text-xs font-sans font-medium truncate', active ? 'text-text-0' : '')}>
108
+ {RUNTIME_TYPES.find((t) => t.value === runtime.type)?.label || runtime.type}
109
+ </div>
102
110
  <div className="text-2xs text-text-4 flex items-center gap-1.5">
103
- <span className="font-mono">{runtime.type}</span>
104
- {runtime.status === 'connected' && <span className="text-success">Healthy</span>}
105
- {runtime.status === 'error' && <span className="text-danger">Unreachable</span>}
111
+ <span className={cn('font-sans', online ? 'text-success' : 'text-danger')}>
112
+ {online ? 'Online' : 'Offline'}
113
+ </span>
114
+ {runtime.latency != null && online && (
115
+ <span className="font-mono">{Math.round(runtime.latency)}ms</span>
116
+ )}
106
117
  </div>
107
118
  </div>
108
119
  <div className="flex items-center gap-0.5 flex-shrink-0">
109
- {runtime.latency != null && (
110
- <span className="text-2xs font-mono text-text-4 mr-1">{Math.round(runtime.latency)}ms</span>
120
+ {managed && online && (
121
+ <Tooltip content="Stop server">
122
+ <button
123
+ onClick={(e) => { e.stopPropagation(); onStop(runtime.id); }}
124
+ className="p-1 text-text-4 hover:text-danger transition-colors cursor-pointer"
125
+ >
126
+ <Square size={10} />
127
+ </button>
128
+ </Tooltip>
129
+ )}
130
+ {managed && !online && (
131
+ <Tooltip content="Start server">
132
+ <button
133
+ onClick={(e) => { e.stopPropagation(); onStart(runtime.id); }}
134
+ className="p-1 text-text-4 hover:text-success transition-colors cursor-pointer"
135
+ >
136
+ <Play size={10} />
137
+ </button>
138
+ </Tooltip>
111
139
  )}
112
140
  <Tooltip content="Test connection">
113
141
  <button
@@ -117,7 +145,7 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, testing }) {
117
145
  {testing === runtime.id ? <Loader2 size={11} className="animate-spin" /> : <RotateCcw size={11} />}
118
146
  </button>
119
147
  </Tooltip>
120
- <Tooltip content="Remove">
148
+ <Tooltip content="Remove runtime">
121
149
  <button
122
150
  onClick={(e) => { e.stopPropagation(); onRemove(runtime.id); }}
123
151
  className="p-1 text-text-4 hover:text-danger transition-colors cursor-pointer"
@@ -138,7 +166,8 @@ function formatSize(bytes) {
138
166
  }
139
167
 
140
168
  const BACKENDS = [
141
- { id: 'llama-cpp', label: 'llama.cpp', subtitle: 'CPU + GPU, auto-managed', recommended: true, autoLaunch: true },
169
+ ...(IS_APPLE ? [{ id: 'mlx', label: 'MLX', subtitle: 'Apple Silicon optimized', recommended: true, autoLaunch: true, appleOnly: true }] : []),
170
+ { id: 'llama-cpp', label: 'llama.cpp', subtitle: 'CPU + GPU, auto-managed', recommended: !IS_APPLE, autoLaunch: true },
142
171
  { id: 'vllm', label: 'vLLM', subtitle: 'GPU-optimized, guided setup', autoLaunch: false },
143
172
  { id: 'tgi', label: 'TGI', subtitle: 'HuggingFace, guided setup', autoLaunch: false },
144
173
  ];
@@ -160,6 +189,26 @@ function LaunchStatus({ phase, error }) {
160
189
  );
161
190
  }
162
191
 
192
+ function getIncompatibilityReason(modelType, backendId) {
193
+ if (modelType === 'gguf' && backendId === 'mlx') return 'GGUF model — MLX needs MLX-format weights';
194
+ if (modelType === 'gguf' && (backendId === 'vllm' || backendId === 'tgi')) return 'GGUF model — needs standard HuggingFace weights';
195
+ if (modelType === 'mlx' && backendId === 'llama-cpp') return 'MLX model — llama.cpp needs a GGUF file';
196
+ if (modelType === 'mlx' && (backendId === 'vllm' || backendId === 'tgi')) return 'MLX model — needs standard HuggingFace weights';
197
+ if (modelType === 'hf' && backendId === 'mlx') return 'HF model — MLX needs MLX-converted weights';
198
+ if (modelType === 'hf' && backendId === 'llama-cpp') return 'HF model — llama.cpp needs a GGUF file';
199
+ return 'Incompatible format';
200
+ }
201
+
202
+ function getBackendCompat(model, backends) {
203
+ if (!model) return backends.map((b) => ({ ...b, compatible: true, reason: null }));
204
+ const compat = model.compatibleBackends || (model.type === 'gguf' ? ['llama-cpp'] : model.type === 'mlx' ? ['mlx'] : ['vllm', 'tgi']);
205
+ return backends.map((b) => ({
206
+ ...b,
207
+ compatible: compat.includes(b.id),
208
+ reason: compat.includes(b.id) ? null : getIncompatibilityReason(model.type, b.id),
209
+ }));
210
+ }
211
+
163
212
  export function LaunchModel() {
164
213
  const localModels = useGrooveStore((s) => s.labLocalModels);
165
214
  const fetchLocalModels = useGrooveStore((s) => s.fetchLabLocalModels);
@@ -170,15 +219,58 @@ export function LaunchModel() {
170
219
  const launchPhase = useGrooveStore((s) => s.labLaunchPhase);
171
220
  const launchError = useGrooveStore((s) => s.labLaunchError);
172
221
  const launchLabAssistant = useGrooveStore((s) => s.launchLabAssistant);
222
+ const labAssistantAgentId = useGrooveStore((s) => s.labAssistantAgentId);
223
+ const labAssistantBackend = useGrooveStore((s) => s.labAssistantBackend);
224
+ const labAssistantMode = useGrooveStore((s) => s.labAssistantMode);
225
+ const setLabAssistantMode = useGrooveStore((s) => s.setLabAssistantMode);
226
+ const agents = useGrooveStore((s) => s.agents);
227
+ const runtimes = useGrooveStore((s) => s.labRuntimes);
228
+ const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
173
229
 
174
230
  const [selectedModel, setSelectedModel] = useState(null);
175
- const [selectedBackend, setSelectedBackend] = useState('llama-cpp');
231
+ const [selectedBackend, setSelectedBackend] = useState(IS_APPLE ? 'mlx' : 'llama-cpp');
176
232
  const [assistantLaunching, setAssistantLaunching] = useState(false);
233
+ const [suggestion, setSuggestion] = useState(null);
177
234
 
178
235
  useEffect(() => { fetchLocalModels(); checkLlama(); }, [fetchLocalModels, checkLlama]);
179
236
 
180
- const currentBackend = BACKENDS.find((b) => b.id === selectedBackend);
181
- const canLaunch = selectedModel && currentBackend?.autoLaunch && llamaInstalled && !launching;
237
+ const selectedModelObj = localModels.find((m) => m.id === selectedModel);
238
+ const backendsWithCompat = getBackendCompat(selectedModelObj, BACKENDS);
239
+ const currentBackend = backendsWithCompat.find((b) => b.id === selectedBackend);
240
+ const isCompatible = currentBackend?.compatible ?? true;
241
+
242
+ const backendReady = selectedBackend === 'mlx' || selectedBackend === 'llama-cpp' ? (selectedBackend === 'mlx' || llamaInstalled) : true;
243
+ const canLaunch = selectedModel && currentBackend?.autoLaunch && backendReady && !launching && isCompatible;
244
+
245
+ const assistantAgent = labAssistantAgentId ? agents.find((a) => a.id === labAssistantAgentId) : null;
246
+ const assistantRunning = assistantAgent?.status === 'running';
247
+ const assistantComplete = assistantAgent && assistantAgent.status !== 'running';
248
+ const hasActiveAssistant = !!(labAssistantAgentId && (assistantRunning || assistantComplete));
249
+
250
+ const activeRt = activeRuntime ? runtimes.find((r) => r.id === activeRuntime) : null;
251
+ const serverRunning = activeRt?.status === 'connected';
252
+
253
+ useEffect(() => {
254
+ if (!selectedModel || !selectedBackend || isCompatible) { setSuggestion(null); return; }
255
+ let cancelled = false;
256
+ fetch(`/api/lab/suggest-model?modelId=${encodeURIComponent(selectedModel)}&targetBackend=${selectedBackend}`)
257
+ .then((r) => r.ok ? r.json() : null)
258
+ .then((data) => { if (!cancelled) setSuggestion(data?.suggestion || null); })
259
+ .catch(() => { if (!cancelled) setSuggestion(null); });
260
+ return () => { cancelled = true; };
261
+ }, [selectedModel, selectedBackend, isCompatible]);
262
+
263
+ function handleModelChange(e) {
264
+ const id = e.target.value || null;
265
+ setSelectedModel(id);
266
+ if (!id) return;
267
+ const model = localModels.find((m) => m.id === id);
268
+ if (!model) return;
269
+ const compat = model.compatibleBackends || [];
270
+ const preferred = IS_APPLE ? ['mlx', 'llama-cpp', 'vllm', 'tgi'] : ['llama-cpp', 'vllm', 'tgi'];
271
+ const best = preferred.find((b) => compat.includes(b));
272
+ if (best) setSelectedBackend(best);
273
+ }
182
274
 
183
275
  function handleLaunch() {
184
276
  if (!canLaunch) return;
@@ -189,7 +281,8 @@ export function LaunchModel() {
189
281
  if (assistantLaunching) return;
190
282
  setAssistantLaunching(true);
191
283
  try {
192
- await launchLabAssistant(currentBackend.id);
284
+ const model = localModels.find((m) => m.id === selectedModel);
285
+ await launchLabAssistant(currentBackend.id, model || undefined);
193
286
  } finally {
194
287
  setAssistantLaunching(false);
195
288
  }
@@ -203,69 +296,77 @@ export function LaunchModel() {
203
296
  <div className="py-5 text-center">
204
297
  <HardDrive size={18} className="mx-auto text-text-4 mb-1.5" />
205
298
  <p className="text-xs text-text-3 font-sans">No downloaded models</p>
206
- <p className="text-2xs text-text-4 font-sans mt-0.5">Download GGUFs from the Models tab</p>
299
+ <p className="text-2xs text-text-4 font-sans mt-0.5">Download models from the Models tab</p>
207
300
  </div>
208
301
  ) : (
209
302
  <>
210
- <ScrollArea className="max-h-36">
211
- <div className="space-y-px">
212
- {localModels.map((m) => (
213
- <button
214
- key={m.id}
215
- onClick={() => setSelectedModel(m.id)}
216
- className={cn(
217
- 'w-full flex items-center gap-2 px-2.5 py-2 text-left transition-colors cursor-pointer rounded-sm',
218
- selectedModel === m.id ? 'bg-accent/8 text-text-0' : 'text-text-2 hover:bg-surface-3 hover:text-text-0',
219
- )}
220
- >
221
- <HardDrive size={11} className={cn('flex-shrink-0', selectedModel === m.id ? 'text-accent' : 'text-text-4')} />
222
- <div className="flex-1 min-w-0">
223
- <div className="text-xs font-sans font-medium truncate">
224
- {m.filename?.replace(/\.gguf$/i, '') || m.id}
225
- </div>
226
- <div className="text-2xs font-mono text-text-4 flex items-center gap-2">
227
- {m.quantization && <span>{m.quantization}</span>}
228
- {m.parameters && <span>{m.parameters}</span>}
229
- {m.sizeBytes && <span>{formatSize(m.sizeBytes)}</span>}
230
- </div>
231
- </div>
232
- {selectedModel === m.id && <ChevronRight size={11} className="text-accent flex-shrink-0" />}
233
- </button>
234
- ))}
235
- </div>
236
- </ScrollArea>
303
+ <div className="relative">
304
+ <select
305
+ value={selectedModel || ''}
306
+ onChange={handleModelChange}
307
+ className="w-full h-8 px-3 pr-8 text-xs 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"
308
+ >
309
+ <option value="">Select a model</option>
310
+ {localModels.map((m) => {
311
+ const label = m.filename?.replace(/\.gguf$/i, '') || m.id;
312
+ const tag = m.type === 'mlx' ? 'MLX' : m.type === 'hf' ? 'HF' : 'GGUF';
313
+ const meta = [tag, m.quantization, m.parameters, m.sizeBytes ? formatSize(m.sizeBytes) : null].filter(Boolean).join(' · ');
314
+ return (
315
+ <option key={m.id} value={m.id}>
316
+ {label}{meta ? ` (${meta})` : ''}
317
+ </option>
318
+ );
319
+ })}
320
+ </select>
321
+ <ChevronRight size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none rotate-90" />
322
+ </div>
237
323
 
238
324
  {selectedModel && (
239
325
  <div className="space-y-2">
240
326
  <span className="text-2xs font-semibold font-sans text-text-4 uppercase tracking-wider">Backend</span>
241
327
  <div className="space-y-px">
242
- {BACKENDS.map((b) => (
243
- <button
244
- key={b.id}
245
- onClick={() => setSelectedBackend(b.id)}
246
- className={cn(
247
- 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded-sm',
248
- selectedBackend === b.id ? 'bg-accent/8' : 'hover:bg-surface-3',
249
- )}
250
- >
251
- <span className={cn(
252
- 'w-2 h-2 rounded-full border-[1.5px] flex-shrink-0',
253
- selectedBackend === b.id ? 'border-accent bg-accent' : 'border-text-4',
254
- )} />
255
- <div className="flex-1 min-w-0">
256
- <div className="flex items-center gap-1.5">
257
- <span className={cn('text-xs font-sans font-medium', selectedBackend === b.id ? 'text-text-0' : 'text-text-2')}>
258
- {b.label}
259
- </span>
260
- {b.recommended && <Badge variant="success" className="text-2xs">Recommended</Badge>}
328
+ {backendsWithCompat.map((b) => (
329
+ <Tooltip key={b.id} content={b.reason} side="right">
330
+ <button
331
+ onClick={() => setSelectedBackend(b.id)}
332
+ className={cn(
333
+ 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded-sm',
334
+ selectedBackend === b.id ? 'bg-accent/8' : 'hover:bg-surface-3',
335
+ !b.compatible && 'opacity-40',
336
+ )}
337
+ >
338
+ <span className={cn(
339
+ 'w-2 h-2 rounded-full border-[1.5px] flex-shrink-0',
340
+ selectedBackend === b.id ? 'border-accent bg-accent' : 'border-text-4',
341
+ )} />
342
+ <div className="flex-1 min-w-0">
343
+ <div className="flex items-center gap-1.5">
344
+ <span className={cn('text-xs font-sans font-medium', selectedBackend === b.id ? 'text-text-0' : 'text-text-2')}>
345
+ {b.label}
346
+ </span>
347
+ {b.compatible && b.recommended && <Badge variant="success" className="text-2xs">Recommended</Badge>}
348
+ </div>
349
+ <div className="text-2xs text-text-4 font-sans">{b.subtitle}</div>
261
350
  </div>
262
- <div className="text-2xs text-text-4 font-sans">{b.subtitle}</div>
263
- </div>
264
- </button>
351
+ </button>
352
+ </Tooltip>
265
353
  ))}
266
354
  </div>
267
355
 
268
- {selectedBackend === 'llama-cpp' && (
356
+ {!isCompatible && (
357
+ <div className="px-2.5 py-2 bg-warning/8 rounded-sm space-y-1">
358
+ <p className="text-2xs text-warning font-sans">
359
+ {currentBackend?.reason}
360
+ </p>
361
+ {suggestion && (
362
+ <p className="text-2xs text-text-2 font-sans">
363
+ Try <span className="font-mono font-medium">{suggestion.repoId}</span> instead
364
+ </p>
365
+ )}
366
+ </div>
367
+ )}
368
+
369
+ {isCompatible && selectedBackend === 'llama-cpp' && (
269
370
  <div className="px-2.5">
270
371
  {llamaInstalled === null && (
271
372
  <div className="flex items-center gap-2 text-2xs text-text-3 font-sans">
@@ -294,43 +395,74 @@ export function LaunchModel() {
294
395
  </div>
295
396
  )}
296
397
 
297
- {!currentBackend?.autoLaunch && (
398
+ {isCompatible && !currentBackend?.autoLaunch && (
298
399
  <div className="space-y-2">
299
- <button
300
- onClick={handleLaunchAssistant}
301
- disabled={assistantLaunching}
302
- className={cn(
303
- 'w-full flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
304
- assistantLaunching ? 'bg-accent/20 text-accent' : 'bg-accent text-surface-0 hover:bg-accent/90',
305
- )}
306
- >
307
- {assistantLaunching
308
- ? <><Loader2 size={12} className="animate-spin" /> Starting Assistant...</>
309
- : <><Wrench size={12} /> Setup {currentBackend?.label} with Assistant</>
310
- }
311
- </button>
312
- <p className="text-2xs text-text-4 font-sans">
313
- An AI assistant will check your system and handle the installation, or start your server manually and add it as a Runtime below.
314
- </p>
400
+ {hasActiveAssistant && labAssistantBackend === selectedBackend ? (
401
+ <div className="space-y-2">
402
+ <div className={cn(
403
+ 'flex items-center gap-2 px-2.5 py-2 rounded-sm text-xs font-sans',
404
+ assistantRunning ? 'bg-accent/8 text-accent' : 'bg-success/8 text-success',
405
+ )}>
406
+ {assistantRunning ? (
407
+ <><Loader2 size={12} className="animate-spin" /> Assistant is setting up {currentBackend?.label}...</>
408
+ ) : (
409
+ <><CheckCircle size={12} /> Setup complete</>
410
+ )}
411
+ </div>
412
+ {!labAssistantMode && (
413
+ <button
414
+ onClick={() => setLabAssistantMode(true)}
415
+ className="w-full flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-sans font-medium text-text-1 bg-surface-3 hover:bg-surface-4 rounded-sm transition-colors cursor-pointer"
416
+ >
417
+ View Assistant
418
+ </button>
419
+ )}
420
+ </div>
421
+ ) : (
422
+ <>
423
+ <button
424
+ onClick={handleLaunchAssistant}
425
+ disabled={assistantLaunching}
426
+ className={cn(
427
+ 'w-full flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
428
+ assistantLaunching ? 'bg-accent/20 text-accent' : 'bg-accent text-surface-0 hover:bg-accent/90',
429
+ )}
430
+ >
431
+ {assistantLaunching
432
+ ? <><Loader2 size={12} className="animate-spin" /> Starting Assistant...</>
433
+ : <><Wrench size={12} /> Setup {currentBackend?.label} with Assistant</>
434
+ }
435
+ </button>
436
+ <p className="text-2xs text-text-4 font-sans">
437
+ An AI assistant will check your system and handle the installation.
438
+ </p>
439
+ </>
440
+ )}
315
441
  </div>
316
442
  )}
317
443
 
318
- {currentBackend?.autoLaunch && (
444
+ {isCompatible && currentBackend?.autoLaunch && (
319
445
  <div className="space-y-2">
320
- <button
321
- disabled={!canLaunch}
322
- onClick={handleLaunch}
323
- className={cn(
324
- 'w-full flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
325
- canLaunch ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
326
- )}
327
- >
328
- {launching ? (
329
- <><Loader2 size={12} className="animate-spin" /> Starting...</>
330
- ) : (
331
- <><Play size={12} /> Launch</>
332
- )}
333
- </button>
446
+ {serverRunning ? (
447
+ <div className="flex items-center gap-2 px-2.5 py-2 bg-success/8 rounded-sm text-xs font-sans text-success">
448
+ <CheckCircle size={12} /> Server Running
449
+ </div>
450
+ ) : (
451
+ <button
452
+ disabled={!canLaunch}
453
+ onClick={handleLaunch}
454
+ className={cn(
455
+ 'w-full flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
456
+ canLaunch ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
457
+ )}
458
+ >
459
+ {launching ? (
460
+ <><Loader2 size={12} className="animate-spin" /> Starting...</>
461
+ ) : (
462
+ <><Play size={12} /> Launch</>
463
+ )}
464
+ </button>
465
+ )}
334
466
  <LaunchStatus phase={launchPhase} error={launchError} />
335
467
  </div>
336
468
  )}
@@ -348,6 +480,8 @@ export function RuntimeConfig() {
348
480
  const setActiveRuntime = useGrooveStore((s) => s.setLabActiveRuntime);
349
481
  const testRuntime = useGrooveStore((s) => s.testLabRuntime);
350
482
  const removeRuntime = useGrooveStore((s) => s.removeLabRuntime);
483
+ const stopRuntime = useGrooveStore((s) => s.stopLabRuntime);
484
+ const startRuntime = useGrooveStore((s) => s.startLabRuntime);
351
485
 
352
486
  const [dialogOpen, setDialogOpen] = useState(false);
353
487
  const [testing, setTesting] = useState(null);
@@ -394,6 +528,8 @@ export function RuntimeConfig() {
394
528
  onSelect={setActiveRuntime}
395
529
  onTest={handleTest}
396
530
  onRemove={removeRuntime}
531
+ onStop={stopRuntime}
532
+ onStart={startRuntime}
397
533
  testing={testing}
398
534
  />
399
535
  ))}
@@ -405,3 +541,51 @@ export function RuntimeConfig() {
405
541
  </div>
406
542
  );
407
543
  }
544
+
545
+ export function RuntimeSection() {
546
+ const runtimes = useGrooveStore((s) => s.labRuntimes);
547
+ const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
548
+ const activeModel = useGrooveStore((s) => s.labActiveModel);
549
+ const [expanded, setExpanded] = useState(true);
550
+ const wasRunning = useRef(false);
551
+
552
+ const activeRt = activeRuntime ? runtimes.find((r) => r.id === activeRuntime) : null;
553
+ const serverRunning = activeRt?.status === 'connected';
554
+ const runtimeLabel = activeRt ? (RUNTIME_TYPES.find((t) => t.value === activeRt.type)?.label || activeRt.type) : null;
555
+
556
+ useEffect(() => {
557
+ if (serverRunning && !wasRunning.current) setExpanded(false);
558
+ wasRunning.current = serverRunning;
559
+ }, [serverRunning]);
560
+
561
+ if (!serverRunning || expanded) {
562
+ return (
563
+ <div className="space-y-5 [&>*]:pt-5 [&>*:first-child]:pt-0">
564
+ <LaunchModel />
565
+ <RuntimeConfig />
566
+ </div>
567
+ );
568
+ }
569
+
570
+ return (
571
+ <div className="space-y-2">
572
+ <div className="flex items-center gap-2.5 px-1 py-1.5">
573
+ <span className="w-2 h-2 rounded-full bg-success flex-shrink-0" />
574
+ <div className="flex-1 min-w-0">
575
+ <div className="text-xs font-sans font-medium text-text-0 truncate">{runtimeLabel}</div>
576
+ <div className="text-2xs text-text-4 font-sans truncate">
577
+ {activeModel || 'Ready'}{activeRt?.latency != null ? ` · ${Math.round(activeRt.latency)}ms` : ''}
578
+ </div>
579
+ </div>
580
+ <Tooltip content="Runtime settings">
581
+ <button
582
+ onClick={() => setExpanded(true)}
583
+ className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
584
+ >
585
+ <Settings2 size={13} />
586
+ </button>
587
+ </Tooltip>
588
+ </div>
589
+ </div>
590
+ );
591
+ }
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, Eye, BookOpen } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, BookOpen } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
  import { useGrooveStore } from '../../stores/groove';
@@ -18,7 +18,6 @@ const BASE_NAV_ITEMS = [
18
18
  ];
19
19
 
20
20
  const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
21
- const PREVIEW_NAV_ITEM = { id: 'preview', icon: Eye, label: 'Preview' };
22
21
 
23
22
  const UTIL_ITEMS = [
24
23
  { id: 'journalist', icon: Newspaper, label: 'Journalist', panel: true },
@@ -28,8 +27,7 @@ const UTIL_ITEMS = [
28
27
  export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel }) {
29
28
  const darwinTrafficLights = isElectron() && getPlatform() === 'darwin';
30
29
  const networkUnlocked = useGrooveStore((s) => s.networkUnlocked);
31
- const previewUrl = useGrooveStore((s) => s.previewState.url);
32
- let NAV_ITEMS = previewUrl ? [...BASE_NAV_ITEMS, PREVIEW_NAV_ITEM] : BASE_NAV_ITEMS;
30
+ let NAV_ITEMS = BASE_NAV_ITEMS;
33
31
  if (networkUnlocked) NAV_ITEMS = [...NAV_ITEMS, NETWORK_NAV_ITEM];
34
32
 
35
33
  return (
@@ -107,7 +107,9 @@ export function TerminalPanel({
107
107
  const startY = useRef(0);
108
108
  const startH = useRef(0);
109
109
 
110
- const activeAgent = useGrooveStore((s) => s.editorSelectedAgent);
110
+ const detailPanel = useGrooveStore((s) => s.detailPanel);
111
+ const workspaceAgentId = useGrooveStore((s) => s.workspaceAgentId);
112
+ const activeAgent = detailPanel?.type === 'agent' ? detailPanel.agentId : workspaceAgentId || null;
111
113
  const agents = useGrooveStore((s) => s.agents);
112
114
  const attachSnippet = useGrooveStore((s) => s.attachSnippet);
113
115
 
@@ -141,8 +143,8 @@ export function TerminalPanel({
141
143
  function sendToAgent(agentId) {
142
144
  if (!agentId || !selectedText?.trim()) return;
143
145
  setShowPicker(false);
144
- useGrooveStore.setState({ editorSelectedAgent: agentId });
145
146
  attachSnippet({ type: 'terminal', code: selectedText.trim() });
147
+ useGrooveStore.getState().selectAgent(agentId);
146
148
  }
147
149
 
148
150
  function handleSendClick() {