groove-dev 0.27.143 → 0.27.145

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 (251) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +18 -48
  6. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  7. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  9. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  11. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  13. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  14. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  15. package/node_modules/@groove-dev/daemon/src/routes/agents.js +812 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  22. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  23. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  24. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  25. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  26. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  27. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  28. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  29. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  30. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  31. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  32. package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  33. package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
  34. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  35. package/node_modules/@groove-dev/gui/package.json +1 -1
  36. package/node_modules/@groove-dev/gui/src/{app.jsx → App.jsx} +0 -2
  37. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  39. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +210 -112
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  42. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  43. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  44. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  46. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  47. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  48. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
  49. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
  50. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  54. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  55. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  56. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  57. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  58. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  59. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  60. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
  61. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  62. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  63. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +200 -18
  64. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  65. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +335 -152
  66. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  67. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  68. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  69. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  70. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  71. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  73. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  74. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  75. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  76. package/node_modules/@groove-dev/gui/src/lib/status.js +25 -24
  77. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  78. package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -3144
  79. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +459 -0
  81. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  82. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +226 -0
  83. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  84. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  85. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  86. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  87. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  88. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  89. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  90. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  91. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  92. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  93. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +54 -12
  94. package/node_modules/@groove-dev/gui/src/views/models.jsx +419 -496
  95. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  96. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  97. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  98. package/node_modules/axios/CHANGELOG.md +260 -0
  99. package/node_modules/axios/README.md +595 -223
  100. package/node_modules/axios/dist/axios.js +1460 -1090
  101. package/node_modules/axios/dist/axios.js.map +1 -1
  102. package/node_modules/axios/dist/axios.min.js +3 -3
  103. package/node_modules/axios/dist/axios.min.js.map +1 -1
  104. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  105. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  106. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  107. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  108. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  109. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  110. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  111. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  112. package/node_modules/axios/index.d.cts +40 -41
  113. package/node_modules/axios/index.d.ts +151 -227
  114. package/node_modules/axios/index.js +2 -0
  115. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  116. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  117. package/node_modules/axios/lib/adapters/http.js +306 -58
  118. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  119. package/node_modules/axios/lib/core/Axios.js +7 -3
  120. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  121. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  122. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  123. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  124. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  125. package/node_modules/axios/lib/core/settle.js +7 -11
  126. package/node_modules/axios/lib/defaults/index.js +14 -9
  127. package/node_modules/axios/lib/env/data.js +1 -1
  128. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  129. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  130. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  131. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  132. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  133. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  134. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  135. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  136. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  137. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  138. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  139. package/node_modules/axios/lib/helpers/validator.js +3 -1
  140. package/node_modules/axios/lib/utils.js +33 -21
  141. package/node_modules/axios/package.json +17 -24
  142. package/node_modules/follow-redirects/README.md +7 -5
  143. package/node_modules/follow-redirects/index.js +24 -1
  144. package/node_modules/follow-redirects/package.json +1 -1
  145. package/package.json +1 -1
  146. package/packages/cli/package.json +1 -1
  147. package/packages/daemon/package.json +1 -1
  148. package/packages/daemon/src/api.js +1086 -6532
  149. package/packages/daemon/src/conversations.js +18 -48
  150. package/packages/daemon/src/gateways/manager.js +35 -1
  151. package/packages/daemon/src/index.js +3 -0
  152. package/packages/daemon/src/journalist.js +23 -13
  153. package/packages/daemon/src/mlx-server.js +365 -0
  154. package/packages/daemon/src/model-lab.js +308 -12
  155. package/packages/daemon/src/pm.js +1 -1
  156. package/packages/daemon/src/process.js +2 -2
  157. package/packages/daemon/src/providers/local.js +36 -8
  158. package/packages/daemon/src/registry.js +21 -5
  159. package/packages/daemon/src/routes/agents.js +812 -0
  160. package/packages/daemon/src/routes/coordination.js +318 -0
  161. package/packages/daemon/src/routes/files.js +751 -0
  162. package/packages/daemon/src/routes/integrations.js +485 -0
  163. package/packages/daemon/src/routes/network.js +1784 -0
  164. package/packages/daemon/src/routes/providers.js +755 -0
  165. package/packages/daemon/src/routes/schedules.js +110 -0
  166. package/packages/daemon/src/routes/teams.js +650 -0
  167. package/packages/daemon/src/scheduler.js +456 -24
  168. package/packages/daemon/src/teams.js +1 -1
  169. package/packages/daemon/src/validate.js +38 -1
  170. package/packages/daemon/templates/mlx-setup.json +12 -0
  171. package/packages/daemon/templates/tgi-setup.json +1 -1
  172. package/packages/daemon/templates/vllm-setup.json +1 -1
  173. package/packages/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  174. package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
  175. package/packages/gui/dist/index.html +2 -2
  176. package/packages/gui/package.json +1 -1
  177. package/packages/gui/src/{app.jsx → App.jsx} +0 -2
  178. package/packages/gui/src/app.css +35 -0
  179. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  180. package/packages/gui/src/components/agents/agent-feed.jsx +210 -112
  181. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  182. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  183. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  184. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  185. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  186. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  187. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  188. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  189. package/packages/gui/src/components/chat/chat-header.jsx +2 -0
  190. package/packages/gui/src/components/chat/chat-input.jsx +68 -66
  191. package/packages/gui/src/components/chat/chat-view.jsx +4 -8
  192. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  193. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  194. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  195. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  196. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  197. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  198. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  199. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  200. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  201. package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
  202. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  203. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  204. package/packages/gui/src/components/lab/parameter-panel.jsx +200 -18
  205. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  206. package/packages/gui/src/components/lab/runtime-config.jsx +335 -152
  207. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  208. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  209. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  210. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  211. package/packages/gui/src/components/network/network-health.jsx +2 -2
  212. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  213. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  214. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  215. package/packages/gui/src/components/ui/slider.jsx +8 -8
  216. package/packages/gui/src/lib/cron.js +64 -0
  217. package/packages/gui/src/lib/status.js +25 -24
  218. package/packages/gui/src/lib/theme-hex.js +1 -0
  219. package/packages/gui/src/stores/groove.js +51 -3144
  220. package/packages/gui/src/stores/helpers.js +10 -0
  221. package/packages/gui/src/stores/slices/agents-slice.js +459 -0
  222. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  223. package/packages/gui/src/stores/slices/chat-slice.js +226 -0
  224. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  225. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  226. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  227. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  228. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  229. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  230. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  231. package/packages/gui/src/views/agents.jsx +5 -5
  232. package/packages/gui/src/views/dashboard.jsx +12 -13
  233. package/packages/gui/src/views/marketplace.jsx +191 -3
  234. package/packages/gui/src/views/model-lab.jsx +54 -12
  235. package/packages/gui/src/views/models.jsx +419 -496
  236. package/packages/gui/src/views/network.jsx +3 -3
  237. package/packages/gui/src/views/settings.jsx +81 -94
  238. package/packages/gui/src/views/teams.jsx +40 -483
  239. package/SECURITY_SWEEP.md +0 -228
  240. package/TRAINING_DATA_v4.md +0 -6
  241. package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +0 -1
  242. package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +0 -984
  243. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -379
  244. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  245. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  246. package/packages/gui/dist/assets/index-CCVvAoQn.css +0 -1
  247. package/packages/gui/dist/assets/index-DGIv_TRm.js +0 -984
  248. package/packages/gui/src/components/agents/agent-chat.jsx +0 -379
  249. package/packages/gui/src/views/preview.jsx +0 -6
  250. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  251. package/test.py +0 -571
@@ -1,6 +1,7 @@
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
+ import { SidebarSection } from '../../views/model-lab';
4
5
  import { Button } from '../ui/button';
5
6
  import { Badge } from '../ui/badge';
6
7
  import { Input } from '../ui/input';
@@ -8,13 +9,16 @@ import { Dialog, DialogContent } from '../ui/dialog';
8
9
  import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
9
10
  import { Tooltip } from '../ui/tooltip';
10
11
  import { ScrollArea } from '../ui/scroll-area';
11
- import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, CheckCircle, AlertTriangle, ChevronRight, Wrench } from 'lucide-react';
12
+ import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2 } from 'lucide-react';
12
13
  import { cn } from '../../lib/cn';
13
14
 
15
+ const IS_APPLE = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || '');
16
+
14
17
  const RUNTIME_TYPES = [
15
18
  { value: 'ollama', label: 'Ollama' },
16
19
  { value: 'vllm', label: 'vLLM' },
17
20
  { value: 'llama-cpp', label: 'llama.cpp' },
21
+ { value: 'mlx', label: 'MLX', suffix: 'Apple Silicon', appleOnly: true },
18
22
  { value: 'tgi', label: 'TGI' },
19
23
  { value: 'openai-compatible', label: 'OpenAI Compatible' },
20
24
  ];
@@ -23,6 +27,7 @@ const DEFAULT_ENDPOINTS = {
23
27
  ollama: 'http://localhost:11434',
24
28
  vllm: 'http://localhost:8000',
25
29
  'llama-cpp': 'http://localhost:8080',
30
+ mlx: 'http://localhost:8080',
26
31
  tgi: 'http://localhost:8080',
27
32
  'openai-compatible': 'http://localhost:8000',
28
33
  };
@@ -64,8 +69,8 @@ function AddRuntimeDialog({ open, onOpenChange }) {
64
69
  <Select value={type} onValueChange={handleTypeChange}>
65
70
  <SelectTrigger placeholder="Select type" />
66
71
  <SelectContent>
67
- {RUNTIME_TYPES.map((rt) => (
68
- <SelectItem key={rt.value} value={rt.value}>{rt.label}</SelectItem>
72
+ {RUNTIME_TYPES.filter(rt => !rt.appleOnly || IS_APPLE).map((rt) => (
73
+ <SelectItem key={rt.value} value={rt.value}>{rt.label}{rt.suffix ? ` (${rt.suffix})` : ''}</SelectItem>
69
74
  ))}
70
75
  </SelectContent>
71
76
  </Select>
@@ -84,45 +89,69 @@ function AddRuntimeDialog({ open, onOpenChange }) {
84
89
  );
85
90
  }
86
91
 
87
- function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, testing }) {
92
+ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onStart, testing }) {
93
+ const online = runtime.status === 'connected';
94
+ const managed = !!(runtime._localModelId || runtime._mlxModelId || runtime.launchConfig || runtime.type === 'mlx' || runtime.type === 'llama-cpp');
88
95
  return (
89
96
  <button
90
97
  onClick={() => onSelect(runtime.id)}
91
98
  className={cn(
92
- '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
+ 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded',
100
+ active ? 'bg-accent/8 ring-1 ring-accent/20' : 'hover:bg-surface-2',
94
101
  )}
95
102
  >
96
103
  <span className={cn(
97
104
  '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',
105
+ online ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
99
106
  )} />
100
107
  <div className="flex-1 min-w-0">
101
- <div className="text-xs font-sans font-medium truncate">{runtime.name}</div>
102
- <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>}
108
+ <div className={cn('text-[11px] font-sans font-medium truncate', active ? 'text-text-0' : 'text-text-2')}>
109
+ {RUNTIME_TYPES.find((t) => t.value === runtime.type)?.label || runtime.type}
110
+ </div>
111
+ <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'}
114
+ </span>
115
+ {runtime.latency != null && online && (
116
+ <span className="font-mono">{Math.round(runtime.latency)}ms</span>
117
+ )}
106
118
  </div>
107
119
  </div>
108
- <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
+ <div className="flex items-center gap-px flex-shrink-0">
121
+ {managed && online && (
122
+ <Tooltip content="Stop server">
123
+ <button
124
+ onClick={(e) => { e.stopPropagation(); onStop(runtime.id); }}
125
+ className="p-1 text-text-4 hover:text-danger transition-colors cursor-pointer"
126
+ >
127
+ <Square size={10} />
128
+ </button>
129
+ </Tooltip>
130
+ )}
131
+ {managed && !online && (
132
+ <Tooltip content="Start server">
133
+ <button
134
+ onClick={(e) => { e.stopPropagation(); onStart(runtime.id); }}
135
+ className="p-1 text-text-4 hover:text-success transition-colors cursor-pointer"
136
+ >
137
+ <Play size={10} />
138
+ </button>
139
+ </Tooltip>
111
140
  )}
112
141
  <Tooltip content="Test connection">
113
142
  <button
114
143
  onClick={(e) => { e.stopPropagation(); onTest(runtime.id); }}
115
144
  className="p-1 text-text-4 hover:text-accent transition-colors cursor-pointer"
116
145
  >
117
- {testing === runtime.id ? <Loader2 size={11} className="animate-spin" /> : <RotateCcw size={11} />}
146
+ {testing === runtime.id ? <Loader2 size={10} className="animate-spin" /> : <RotateCcw size={10} />}
118
147
  </button>
119
148
  </Tooltip>
120
- <Tooltip content="Remove">
149
+ <Tooltip content="Remove runtime">
121
150
  <button
122
151
  onClick={(e) => { e.stopPropagation(); onRemove(runtime.id); }}
123
152
  className="p-1 text-text-4 hover:text-danger transition-colors cursor-pointer"
124
153
  >
125
- <Trash2 size={11} />
154
+ <Trash2 size={10} />
126
155
  </button>
127
156
  </Tooltip>
128
157
  </div>
@@ -138,28 +167,56 @@ function formatSize(bytes) {
138
167
  }
139
168
 
140
169
  const BACKENDS = [
141
- { id: 'llama-cpp', label: 'llama.cpp', subtitle: 'CPU + GPU, auto-managed', recommended: true, autoLaunch: true },
170
+ ...(IS_APPLE ? [{ id: 'mlx', label: 'MLX', subtitle: 'Apple Silicon optimized', recommended: true, autoLaunch: true, appleOnly: true }] : []),
171
+ { id: 'llama-cpp', label: 'llama.cpp', subtitle: 'CPU + GPU, auto-managed', recommended: !IS_APPLE, autoLaunch: true },
142
172
  { id: 'vllm', label: 'vLLM', subtitle: 'GPU-optimized, guided setup', autoLaunch: false },
143
173
  { id: 'tgi', label: 'TGI', subtitle: 'HuggingFace, guided setup', autoLaunch: false },
144
174
  ];
145
175
 
146
- function LaunchStatus({ phase, error }) {
147
- if (!phase) return null;
176
+ function StatusBanner({ variant, icon: Icon, children }) {
177
+ const styles = {
178
+ success: 'bg-success/8 text-success',
179
+ danger: 'bg-danger/8 text-danger',
180
+ accent: 'bg-accent/8 text-accent',
181
+ warning: 'bg-warning/8 text-warning',
182
+ };
148
183
  return (
149
- <div className={cn(
150
- 'flex items-center gap-2 px-2.5 py-1.5 text-2xs font-sans rounded-sm',
151
- phase === 'ready' && 'bg-success/8 text-success',
152
- phase === 'error' && 'bg-danger/8 text-danger',
153
- (phase === 'starting' || phase === 'checking') && 'bg-accent/8 text-accent',
154
- )}>
155
- {phase === 'starting' && <><Loader2 size={11} className="animate-spin" /> Starting server...</>}
156
- {phase === 'checking' && <><Loader2 size={11} className="animate-spin" /> Checking...</>}
157
- {phase === 'ready' && <><CheckCircle size={11} /> Server ready</>}
158
- {phase === 'error' && <><AlertTriangle size={11} /> {error || 'Launch failed'}</>}
184
+ <div className={cn('flex items-center gap-2 px-2.5 py-2 text-[11px] font-sans rounded', styles[variant])}>
185
+ <Icon size={11} className={variant === 'accent' ? 'animate-spin' : ''} />
186
+ <span>{children}</span>
159
187
  </div>
160
188
  );
161
189
  }
162
190
 
191
+ function LaunchStatus({ phase, error }) {
192
+ if (!phase) return null;
193
+ if (phase === 'starting') return <StatusBanner variant="accent" icon={Loader2}>Starting server...</StatusBanner>;
194
+ if (phase === 'checking') return <StatusBanner variant="accent" icon={Loader2}>Checking...</StatusBanner>;
195
+ if (phase === 'ready') return <StatusBanner variant="success" icon={CheckCircle}>Server ready</StatusBanner>;
196
+ if (phase === 'error') return <StatusBanner variant="danger" icon={AlertTriangle}>{error || 'Launch failed'}</StatusBanner>;
197
+ return null;
198
+ }
199
+
200
+ function getIncompatibilityReason(modelType, backendId) {
201
+ if (modelType === 'gguf' && backendId === 'mlx') return 'GGUF model — MLX needs MLX-format weights';
202
+ if (modelType === 'gguf' && (backendId === 'vllm' || backendId === 'tgi')) return 'GGUF model — needs standard HuggingFace weights';
203
+ if (modelType === 'mlx' && backendId === 'llama-cpp') return 'MLX model — llama.cpp needs a GGUF file';
204
+ if (modelType === 'mlx' && (backendId === 'vllm' || backendId === 'tgi')) return 'MLX model — needs standard HuggingFace weights';
205
+ if (modelType === 'hf' && backendId === 'mlx') return 'HF model — MLX needs MLX-converted weights';
206
+ if (modelType === 'hf' && backendId === 'llama-cpp') return 'HF model — llama.cpp needs a GGUF file';
207
+ return 'Incompatible format';
208
+ }
209
+
210
+ function getBackendCompat(model, backends) {
211
+ if (!model) return backends.map((b) => ({ ...b, compatible: true, reason: null }));
212
+ const compat = model.compatibleBackends || (model.type === 'gguf' ? ['llama-cpp'] : model.type === 'mlx' ? ['mlx'] : ['vllm', 'tgi']);
213
+ return backends.map((b) => ({
214
+ ...b,
215
+ compatible: compat.includes(b.id),
216
+ reason: compat.includes(b.id) ? null : getIncompatibilityReason(model.type, b.id),
217
+ }));
218
+ }
219
+
163
220
  export function LaunchModel() {
164
221
  const localModels = useGrooveStore((s) => s.labLocalModels);
165
222
  const fetchLocalModels = useGrooveStore((s) => s.fetchLabLocalModels);
@@ -170,15 +227,58 @@ export function LaunchModel() {
170
227
  const launchPhase = useGrooveStore((s) => s.labLaunchPhase);
171
228
  const launchError = useGrooveStore((s) => s.labLaunchError);
172
229
  const launchLabAssistant = useGrooveStore((s) => s.launchLabAssistant);
230
+ const labAssistantAgentId = useGrooveStore((s) => s.labAssistantAgentId);
231
+ const labAssistantBackend = useGrooveStore((s) => s.labAssistantBackend);
232
+ const labAssistantMode = useGrooveStore((s) => s.labAssistantMode);
233
+ const setLabAssistantMode = useGrooveStore((s) => s.setLabAssistantMode);
234
+ const agents = useGrooveStore((s) => s.agents);
235
+ const runtimes = useGrooveStore((s) => s.labRuntimes);
236
+ const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
173
237
 
174
238
  const [selectedModel, setSelectedModel] = useState(null);
175
- const [selectedBackend, setSelectedBackend] = useState('llama-cpp');
239
+ const [selectedBackend, setSelectedBackend] = useState(IS_APPLE ? 'mlx' : 'llama-cpp');
176
240
  const [assistantLaunching, setAssistantLaunching] = useState(false);
241
+ const [suggestion, setSuggestion] = useState(null);
177
242
 
178
243
  useEffect(() => { fetchLocalModels(); checkLlama(); }, [fetchLocalModels, checkLlama]);
179
244
 
180
- const currentBackend = BACKENDS.find((b) => b.id === selectedBackend);
181
- const canLaunch = selectedModel && currentBackend?.autoLaunch && llamaInstalled && !launching;
245
+ const selectedModelObj = localModels.find((m) => m.id === selectedModel);
246
+ const backendsWithCompat = getBackendCompat(selectedModelObj, BACKENDS);
247
+ const currentBackend = backendsWithCompat.find((b) => b.id === selectedBackend);
248
+ const isCompatible = currentBackend?.compatible ?? true;
249
+
250
+ const backendReady = selectedBackend === 'mlx' || selectedBackend === 'llama-cpp' ? (selectedBackend === 'mlx' || llamaInstalled) : true;
251
+ const canLaunch = selectedModel && currentBackend?.autoLaunch && backendReady && !launching && isCompatible;
252
+
253
+ const assistantAgent = labAssistantAgentId ? agents.find((a) => a.id === labAssistantAgentId) : null;
254
+ const assistantRunning = assistantAgent?.status === 'running';
255
+ const assistantComplete = assistantAgent && assistantAgent.status !== 'running';
256
+ const hasActiveAssistant = !!(labAssistantAgentId && (assistantRunning || assistantComplete));
257
+
258
+ const activeRt = activeRuntime ? runtimes.find((r) => r.id === activeRuntime) : null;
259
+ const serverRunning = activeRt?.status === 'connected';
260
+
261
+ useEffect(() => {
262
+ if (!selectedModel || !selectedBackend || isCompatible) { setSuggestion(null); return; }
263
+ let cancelled = false;
264
+ fetch(`/api/lab/suggest-model?modelId=${encodeURIComponent(selectedModel)}&targetBackend=${selectedBackend}`)
265
+ .then((r) => r.ok ? r.json() : null)
266
+ .then((data) => { if (!cancelled) setSuggestion(data?.suggestion || null); })
267
+ .catch(() => { if (!cancelled) setSuggestion(null); });
268
+ return () => { cancelled = true; };
269
+ }, [selectedModel, selectedBackend, isCompatible]);
270
+
271
+ function handleModelChange(e) {
272
+ const id = e.target.value || null;
273
+ setSelectedModel(id);
274
+ if (!id) return;
275
+ const model = localModels.find((m) => m.id === id);
276
+ if (!model) return;
277
+ const compat = model.compatibleBackends || [];
278
+ const preferred = IS_APPLE ? ['mlx', 'llama-cpp', 'vllm', 'tgi'] : ['llama-cpp', 'vllm', 'tgi'];
279
+ const best = preferred.find((b) => compat.includes(b));
280
+ if (best) setSelectedBackend(best);
281
+ }
182
282
 
183
283
  function handleLaunch() {
184
284
  if (!canLaunch) return;
@@ -189,103 +289,108 @@ export function LaunchModel() {
189
289
  if (assistantLaunching) return;
190
290
  setAssistantLaunching(true);
191
291
  try {
192
- await launchLabAssistant(currentBackend.id);
292
+ const model = localModels.find((m) => m.id === selectedModel);
293
+ await launchLabAssistant(currentBackend.id, model || undefined);
193
294
  } finally {
194
295
  setAssistantLaunching(false);
195
296
  }
196
297
  }
197
298
 
198
299
  return (
199
- <div className="space-y-3">
200
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider">Launch Model</span>
201
-
300
+ <SidebarSection label="Launch Model" collapsible defaultOpen={false}>
202
301
  {localModels.length === 0 ? (
203
- <div className="py-5 text-center">
204
- <HardDrive size={18} className="mx-auto text-text-4 mb-1.5" />
205
- <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>
302
+ <div className="py-6 text-center rounded-md bg-surface-1/50 border border-border-subtle">
303
+ <HardDrive size={16} className="mx-auto text-text-4 mb-2" />
304
+ <p className="text-[11px] text-text-3 font-sans">No downloaded models</p>
305
+ <p className="text-[10px] text-text-4 font-sans mt-0.5">Download models from the Models tab</p>
207
306
  </div>
208
307
  ) : (
209
- <>
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>
308
+ <div className="space-y-4">
309
+ <div className="relative">
310
+ <select
311
+ value={selectedModel || ''}
312
+ onChange={handleModelChange}
313
+ className="w-full h-9 px-2.5 pr-7 text-[11px] rounded bg-surface-1 border border-border text-text-0 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/50 transition-colors"
314
+ >
315
+ <option value="">Select a model</option>
316
+ {localModels.map((m) => {
317
+ const label = m.filename?.replace(/\.gguf$/i, '') || m.id;
318
+ const tag = m.type === 'mlx' ? 'MLX' : m.type === 'hf' ? 'HF' : 'GGUF';
319
+ const meta = [tag, m.quantization, m.parameters, m.sizeBytes ? formatSize(m.sizeBytes) : null].filter(Boolean).join(' · ');
320
+ return (
321
+ <option key={m.id} value={m.id}>
322
+ {label}{meta ? ` (${meta})` : ''}
323
+ </option>
324
+ );
325
+ })}
326
+ </select>
327
+ <ChevronRight size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none rotate-90" />
328
+ </div>
237
329
 
238
330
  {selectedModel && (
239
- <div className="space-y-2">
240
- <span className="text-2xs font-semibold font-sans text-text-4 uppercase tracking-wider">Backend</span>
241
- <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>}
261
- </div>
262
- <div className="text-2xs text-text-4 font-sans">{b.subtitle}</div>
263
- </div>
264
- </button>
265
- ))}
331
+ <div className="space-y-4">
332
+ <div className="space-y-2">
333
+ <span className="text-[10px] font-semibold font-sans text-text-4 uppercase tracking-widest">Backend</span>
334
+ <div className="space-y-1 rounded-md bg-surface-1/50 border border-border-subtle p-2">
335
+ {backendsWithCompat.map((b) => (
336
+ <Tooltip key={b.id} content={b.reason} side="right">
337
+ <button
338
+ onClick={() => setSelectedBackend(b.id)}
339
+ className={cn(
340
+ 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded',
341
+ selectedBackend === b.id ? 'bg-accent/10' : 'hover:bg-surface-3',
342
+ !b.compatible && 'opacity-40',
343
+ )}
344
+ >
345
+ <span className={cn(
346
+ 'w-2 h-2 rounded-full border-[1.5px] flex-shrink-0 transition-colors',
347
+ selectedBackend === b.id ? 'border-accent bg-accent' : 'border-text-4',
348
+ )} />
349
+ <div className="flex-1 min-w-0">
350
+ <div className="flex items-center gap-1.5">
351
+ <span className={cn('text-[11px] font-sans font-medium', selectedBackend === b.id ? 'text-text-0' : 'text-text-2')}>
352
+ {b.label}
353
+ </span>
354
+ {b.compatible && b.recommended && <Badge variant="success" className="text-[9px]">Recommended</Badge>}
355
+ </div>
356
+ <div className="text-[10px] text-text-4 font-sans">{b.subtitle}</div>
357
+ </div>
358
+ </button>
359
+ </Tooltip>
360
+ ))}
361
+ </div>
266
362
  </div>
267
363
 
268
- {selectedBackend === 'llama-cpp' && (
269
- <div className="px-2.5">
364
+ {!isCompatible && (
365
+ <StatusBanner variant="warning" icon={AlertTriangle}>
366
+ {currentBackend?.reason}
367
+ {suggestion && (
368
+ <> — try <span className="font-mono font-medium">{suggestion.repoId}</span></>
369
+ )}
370
+ </StatusBanner>
371
+ )}
372
+
373
+ {isCompatible && selectedBackend === 'llama-cpp' && (
374
+ <div>
270
375
  {llamaInstalled === null && (
271
- <div className="flex items-center gap-2 text-2xs text-text-3 font-sans">
376
+ <div className="flex items-center gap-2 text-[11px] text-text-3 font-sans">
272
377
  <Loader2 size={10} className="animate-spin" /> Checking llama-server...
273
378
  </div>
274
379
  )}
275
380
  {llamaInstalled === true && (
276
- <div className="flex items-center gap-2 text-2xs text-success font-sans">
381
+ <div className="flex items-center gap-2 text-[11px] text-success font-sans">
277
382
  <CheckCircle size={10} /> llama-server found
278
383
  </div>
279
384
  )}
280
385
  {llamaInstalled === false && (
281
- <div className="space-y-1.5">
282
- <div className="flex items-center gap-2 text-2xs text-danger font-sans">
386
+ <div className="space-y-2">
387
+ <div className="flex items-center gap-2 text-[11px] text-danger font-sans">
283
388
  <AlertTriangle size={10} /> llama-server not found
284
389
  </div>
285
- <code className="block text-2xs font-mono text-text-3 bg-surface-2 px-2 py-1 rounded-sm">brew install llama.cpp</code>
390
+ <code className="block text-[10px] font-mono text-text-3 bg-surface-2 px-2.5 py-1.5 rounded">brew install llama.cpp</code>
286
391
  <button
287
392
  onClick={checkLlama}
288
- className="flex items-center gap-1.5 text-2xs font-sans text-accent hover:text-accent/80 transition-colors cursor-pointer"
393
+ className="flex items-center gap-1.5 text-[11px] font-sans text-accent hover:text-accent/80 transition-colors cursor-pointer"
289
394
  >
290
395
  <RotateCcw size={10} /> Recheck after install
291
396
  </button>
@@ -294,51 +399,77 @@ export function LaunchModel() {
294
399
  </div>
295
400
  )}
296
401
 
297
- {!currentBackend?.autoLaunch && (
402
+ {isCompatible && !currentBackend?.autoLaunch && (
298
403
  <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>
404
+ {hasActiveAssistant && labAssistantBackend === selectedBackend ? (
405
+ <div className="space-y-2">
406
+ {assistantRunning ? (
407
+ <StatusBanner variant="accent" icon={Loader2}>
408
+ Assistant is setting up {currentBackend?.label}...
409
+ </StatusBanner>
410
+ ) : (
411
+ <StatusBanner variant="success" icon={CheckCircle}>Setup complete</StatusBanner>
412
+ )}
413
+ {!labAssistantMode && (
414
+ <button
415
+ onClick={() => setLabAssistantMode(true)}
416
+ className="w-full flex items-center justify-center gap-1.5 h-8 text-[11px] font-sans font-medium text-text-1 bg-surface-2 hover:bg-surface-3 rounded transition-colors cursor-pointer"
417
+ >
418
+ View Assistant
419
+ </button>
420
+ )}
421
+ </div>
422
+ ) : (
423
+ <div className="space-y-2">
424
+ <button
425
+ onClick={handleLaunchAssistant}
426
+ disabled={assistantLaunching}
427
+ className={cn(
428
+ 'w-full flex items-center justify-center gap-1.5 h-8 text-[11px] font-sans font-medium rounded transition-colors cursor-pointer',
429
+ assistantLaunching ? 'bg-accent/20 text-accent' : 'bg-accent text-surface-0 hover:bg-accent/90',
430
+ )}
431
+ >
432
+ {assistantLaunching
433
+ ? <><Loader2 size={11} className="animate-spin" /> Starting Assistant...</>
434
+ : <><Wrench size={11} /> Setup {currentBackend?.label} with Assistant</>
435
+ }
436
+ </button>
437
+ <p className="text-[10px] text-text-4 font-sans text-center">
438
+ An AI assistant will check your system and handle the installation.
439
+ </p>
440
+ </div>
441
+ )}
315
442
  </div>
316
443
  )}
317
444
 
318
- {currentBackend?.autoLaunch && (
445
+ {isCompatible && currentBackend?.autoLaunch && (
319
446
  <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>
447
+ {serverRunning ? (
448
+ <StatusBanner variant="success" icon={CheckCircle}>Server Running</StatusBanner>
449
+ ) : (
450
+ <button
451
+ disabled={!canLaunch}
452
+ onClick={handleLaunch}
453
+ className={cn(
454
+ 'w-full flex items-center justify-center gap-1.5 h-8 text-[11px] font-sans font-medium rounded transition-colors cursor-pointer',
455
+ canLaunch ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
456
+ )}
457
+ >
458
+ {launching ? (
459
+ <><Loader2 size={11} className="animate-spin" /> Starting...</>
460
+ ) : (
461
+ <><Play size={11} /> Launch</>
462
+ )}
463
+ </button>
464
+ )}
334
465
  <LaunchStatus phase={launchPhase} error={launchError} />
335
466
  </div>
336
467
  )}
337
468
  </div>
338
469
  )}
339
- </>
470
+ </div>
340
471
  )}
341
- </div>
472
+ </SidebarSection>
342
473
  );
343
474
  }
344
475
 
@@ -348,6 +479,8 @@ export function RuntimeConfig() {
348
479
  const setActiveRuntime = useGrooveStore((s) => s.setLabActiveRuntime);
349
480
  const testRuntime = useGrooveStore((s) => s.testLabRuntime);
350
481
  const removeRuntime = useGrooveStore((s) => s.removeLabRuntime);
482
+ const stopRuntime = useGrooveStore((s) => s.stopLabRuntime);
483
+ const startRuntime = useGrooveStore((s) => s.startLabRuntime);
351
484
 
352
485
  const [dialogOpen, setDialogOpen] = useState(false);
353
486
  const [testing, setTesting] = useState(null);
@@ -359,33 +492,35 @@ export function RuntimeConfig() {
359
492
  }
360
493
 
361
494
  return (
362
- <div className="space-y-2">
363
- <div className="flex items-center justify-between">
364
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider">Runtimes</span>
495
+ <SidebarSection
496
+ label="Runtimes"
497
+ collapsible
498
+ defaultOpen={false}
499
+ action={
365
500
  <Tooltip content="Add runtime">
366
501
  <button
367
502
  onClick={() => setDialogOpen(true)}
368
503
  className="p-1 text-text-4 hover:text-accent transition-colors cursor-pointer"
369
504
  >
370
- <Plus size={13} />
505
+ <Plus size={12} />
371
506
  </button>
372
507
  </Tooltip>
373
- </div>
374
-
508
+ }
509
+ >
375
510
  {runtimes.length === 0 ? (
376
- <div className="py-5 text-center">
377
- <WifiOff size={18} className="mx-auto text-text-4 mb-1.5" />
378
- <p className="text-xs text-text-3 font-sans">No runtimes configured</p>
511
+ <div className="py-6 text-center rounded-md bg-surface-1/50 border border-border-subtle">
512
+ <WifiOff size={16} className="mx-auto text-text-4 mb-2" />
513
+ <p className="text-[11px] text-text-3 font-sans">No runtimes configured</p>
379
514
  <button
380
515
  onClick={() => setDialogOpen(true)}
381
- className="mt-2 flex items-center gap-1 mx-auto px-3 py-1.5 text-2xs font-sans text-text-3 hover:text-text-1 transition-colors cursor-pointer"
516
+ className="mt-2 inline-flex items-center gap-1 px-2.5 py-1 text-[10px] font-sans text-accent hover:text-accent/80 transition-colors cursor-pointer"
382
517
  >
383
- <Plus size={11} /> Add Runtime
518
+ <Plus size={10} /> Add Runtime
384
519
  </button>
385
520
  </div>
386
521
  ) : (
387
522
  <ScrollArea className="max-h-48">
388
- <div className="space-y-px">
523
+ <div className="space-y-1 rounded-md bg-surface-1/50 border border-border-subtle p-2">
389
524
  {runtimes.map((rt) => (
390
525
  <RuntimeItem
391
526
  key={rt.id}
@@ -394,6 +529,8 @@ export function RuntimeConfig() {
394
529
  onSelect={setActiveRuntime}
395
530
  onTest={handleTest}
396
531
  onRemove={removeRuntime}
532
+ onStop={stopRuntime}
533
+ onStart={startRuntime}
397
534
  testing={testing}
398
535
  />
399
536
  ))}
@@ -402,6 +539,52 @@ export function RuntimeConfig() {
402
539
  )}
403
540
 
404
541
  <AddRuntimeDialog open={dialogOpen} onOpenChange={setDialogOpen} />
542
+ </SidebarSection>
543
+ );
544
+ }
545
+
546
+ export function RuntimeSection() {
547
+ const runtimes = useGrooveStore((s) => s.labRuntimes);
548
+ const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
549
+ const activeModel = useGrooveStore((s) => s.labActiveModel);
550
+ const [expanded, setExpanded] = useState(true);
551
+ const wasRunning = useRef(false);
552
+
553
+ const activeRt = activeRuntime ? runtimes.find((r) => r.id === activeRuntime) : null;
554
+ const serverRunning = activeRt?.status === 'connected';
555
+ const runtimeLabel = activeRt ? (RUNTIME_TYPES.find((t) => t.value === activeRt.type)?.label || activeRt.type) : null;
556
+
557
+ useEffect(() => {
558
+ if (serverRunning && !wasRunning.current) setExpanded(false);
559
+ wasRunning.current = serverRunning;
560
+ }, [serverRunning]);
561
+
562
+ if (!serverRunning || expanded) {
563
+ return (
564
+ <div className="space-y-6">
565
+ <LaunchModel />
566
+ <RuntimeConfig />
567
+ </div>
568
+ );
569
+ }
570
+
571
+ return (
572
+ <div className="flex items-center gap-2.5 px-2.5 py-2 rounded-md bg-surface-1/50 border border-border-subtle">
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-[11px] font-sans font-medium text-text-0 truncate">{runtimeLabel}</div>
576
+ <div className="text-[10px] 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={12} />
586
+ </button>
587
+ </Tooltip>
405
588
  </div>
406
589
  );
407
590
  }