groove-dev 0.27.144 → 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 (100) 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/conversations.js +18 -48
  4. package/node_modules/@groove-dev/daemon/src/routes/agents.js +6 -83
  5. package/node_modules/@groove-dev/gui/dist/assets/{index-BcoF6_eF.js → index-Bxc0gU06.js} +232 -238
  6. package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
  7. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  8. package/node_modules/@groove-dev/gui/package.json +1 -1
  9. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +80 -95
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  11. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
  14. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
  15. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +66 -65
  16. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  17. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +126 -127
  18. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  19. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  20. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +17 -0
  22. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +8 -1
  23. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +13 -14
  24. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +41 -10
  25. package/node_modules/@groove-dev/gui/src/views/models.jsx +57 -36
  26. package/node_modules/axios/CHANGELOG.md +260 -0
  27. package/node_modules/axios/README.md +595 -223
  28. package/node_modules/axios/dist/axios.js +1460 -1090
  29. package/node_modules/axios/dist/axios.js.map +1 -1
  30. package/node_modules/axios/dist/axios.min.js +3 -3
  31. package/node_modules/axios/dist/axios.min.js.map +1 -1
  32. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  33. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  34. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  35. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  36. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  37. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  38. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  39. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  40. package/node_modules/axios/index.d.cts +40 -41
  41. package/node_modules/axios/index.d.ts +151 -227
  42. package/node_modules/axios/index.js +2 -0
  43. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  44. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  45. package/node_modules/axios/lib/adapters/http.js +306 -58
  46. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  47. package/node_modules/axios/lib/core/Axios.js +7 -3
  48. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  49. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  50. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  51. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  52. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  53. package/node_modules/axios/lib/core/settle.js +7 -11
  54. package/node_modules/axios/lib/defaults/index.js +14 -9
  55. package/node_modules/axios/lib/env/data.js +1 -1
  56. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  57. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  58. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  59. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  60. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  61. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  62. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  63. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  64. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  65. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  66. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  67. package/node_modules/axios/lib/helpers/validator.js +3 -1
  68. package/node_modules/axios/lib/utils.js +33 -21
  69. package/node_modules/axios/package.json +17 -24
  70. package/node_modules/follow-redirects/README.md +7 -5
  71. package/node_modules/follow-redirects/index.js +24 -1
  72. package/node_modules/follow-redirects/package.json +1 -1
  73. package/package.json +1 -1
  74. package/packages/cli/package.json +1 -1
  75. package/packages/daemon/package.json +1 -1
  76. package/packages/daemon/src/conversations.js +18 -48
  77. package/packages/daemon/src/routes/agents.js +6 -83
  78. package/packages/gui/dist/assets/{index-BcoF6_eF.js → index-Bxc0gU06.js} +232 -238
  79. package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
  80. package/packages/gui/dist/index.html +2 -2
  81. package/packages/gui/package.json +1 -1
  82. package/packages/gui/src/components/agents/agent-feed.jsx +80 -95
  83. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  84. package/packages/gui/src/components/chat/chat-header.jsx +2 -0
  85. package/packages/gui/src/components/chat/chat-input.jsx +68 -66
  86. package/packages/gui/src/components/chat/chat-view.jsx +4 -8
  87. package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
  88. package/packages/gui/src/components/lab/parameter-panel.jsx +66 -65
  89. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  90. package/packages/gui/src/components/lab/runtime-config.jsx +126 -127
  91. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  92. package/packages/gui/src/components/ui/slider.jsx +8 -8
  93. package/packages/gui/src/lib/status.js +1 -0
  94. package/packages/gui/src/stores/groove.js +17 -0
  95. package/packages/gui/src/stores/slices/agents-slice.js +8 -1
  96. package/packages/gui/src/stores/slices/chat-slice.js +13 -14
  97. package/packages/gui/src/views/model-lab.jsx +41 -10
  98. package/packages/gui/src/views/models.jsx +57 -36
  99. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +0 -1
  100. package/packages/gui/dist/assets/index-Dd7qhiEd.css +0 -1
@@ -1,6 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
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';
@@ -95,8 +96,8 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
95
96
  <button
96
97
  onClick={() => onSelect(runtime.id)}
97
98
  className={cn(
98
- 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left transition-colors cursor-pointer rounded-sm',
99
- active ? 'bg-accent/8 border border-accent/20' : 'border border-transparent 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',
100
101
  )}
101
102
  >
102
103
  <span className={cn(
@@ -104,10 +105,10 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
104
105
  online ? 'bg-success' : runtime.status === 'error' ? 'bg-danger' : 'bg-text-4',
105
106
  )} />
106
107
  <div className="flex-1 min-w-0">
107
- <div className={cn('text-xs font-sans font-medium truncate', active ? 'text-text-0' : '')}>
108
+ <div className={cn('text-[11px] font-sans font-medium truncate', active ? 'text-text-0' : 'text-text-2')}>
108
109
  {RUNTIME_TYPES.find((t) => t.value === runtime.type)?.label || runtime.type}
109
110
  </div>
110
- <div className="text-2xs text-text-4 flex items-center gap-1.5">
111
+ <div className="text-[10px] text-text-4 flex items-center gap-1.5">
111
112
  <span className={cn('font-sans', online ? 'text-success' : 'text-danger')}>
112
113
  {online ? 'Online' : 'Offline'}
113
114
  </span>
@@ -116,7 +117,7 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
116
117
  )}
117
118
  </div>
118
119
  </div>
119
- <div className="flex items-center gap-0.5 flex-shrink-0">
120
+ <div className="flex items-center gap-px flex-shrink-0">
120
121
  {managed && online && (
121
122
  <Tooltip content="Stop server">
122
123
  <button
@@ -142,7 +143,7 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
142
143
  onClick={(e) => { e.stopPropagation(); onTest(runtime.id); }}
143
144
  className="p-1 text-text-4 hover:text-accent transition-colors cursor-pointer"
144
145
  >
145
- {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} />}
146
147
  </button>
147
148
  </Tooltip>
148
149
  <Tooltip content="Remove runtime">
@@ -150,7 +151,7 @@ function RuntimeItem({ runtime, active, onSelect, onTest, onRemove, onStop, onSt
150
151
  onClick={(e) => { e.stopPropagation(); onRemove(runtime.id); }}
151
152
  className="p-1 text-text-4 hover:text-danger transition-colors cursor-pointer"
152
153
  >
153
- <Trash2 size={11} />
154
+ <Trash2 size={10} />
154
155
  </button>
155
156
  </Tooltip>
156
157
  </div>
@@ -172,23 +173,30 @@ const BACKENDS = [
172
173
  { id: 'tgi', label: 'TGI', subtitle: 'HuggingFace, guided setup', autoLaunch: false },
173
174
  ];
174
175
 
175
- function LaunchStatus({ phase, error }) {
176
- 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
+ };
177
183
  return (
178
- <div className={cn(
179
- 'flex items-center gap-2 px-2.5 py-1.5 text-2xs font-sans rounded-sm',
180
- phase === 'ready' && 'bg-success/8 text-success',
181
- phase === 'error' && 'bg-danger/8 text-danger',
182
- (phase === 'starting' || phase === 'checking') && 'bg-accent/8 text-accent',
183
- )}>
184
- {phase === 'starting' && <><Loader2 size={11} className="animate-spin" /> Starting server...</>}
185
- {phase === 'checking' && <><Loader2 size={11} className="animate-spin" /> Checking...</>}
186
- {phase === 'ready' && <><CheckCircle size={11} /> Server ready</>}
187
- {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>
188
187
  </div>
189
188
  );
190
189
  }
191
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
+
192
200
  function getIncompatibilityReason(modelType, backendId) {
193
201
  if (modelType === 'gguf' && backendId === 'mlx') return 'GGUF model — MLX needs MLX-format weights';
194
202
  if (modelType === 'gguf' && (backendId === 'vllm' || backendId === 'tgi')) return 'GGUF model — needs standard HuggingFace weights';
@@ -289,22 +297,20 @@ export function LaunchModel() {
289
297
  }
290
298
 
291
299
  return (
292
- <div className="space-y-3">
293
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider">Launch Model</span>
294
-
300
+ <SidebarSection label="Launch Model" collapsible defaultOpen={false}>
295
301
  {localModels.length === 0 ? (
296
- <div className="py-5 text-center">
297
- <HardDrive size={18} className="mx-auto text-text-4 mb-1.5" />
298
- <p className="text-xs text-text-3 font-sans">No downloaded models</p>
299
- <p className="text-2xs text-text-4 font-sans mt-0.5">Download models 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>
300
306
  </div>
301
307
  ) : (
302
- <>
308
+ <div className="space-y-4">
303
309
  <div className="relative">
304
310
  <select
305
311
  value={selectedModel || ''}
306
312
  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"
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"
308
314
  >
309
315
  <option value="">Select a model</option>
310
316
  {localModels.map((m) => {
@@ -318,75 +324,73 @@ export function LaunchModel() {
318
324
  );
319
325
  })}
320
326
  </select>
321
- <ChevronRight size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none rotate-90" />
327
+ <ChevronRight size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none rotate-90" />
322
328
  </div>
323
329
 
324
330
  {selectedModel && (
325
- <div className="space-y-2">
326
- <span className="text-2xs font-semibold font-sans text-text-4 uppercase tracking-wider">Backend</span>
327
- <div className="space-y-px">
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>}
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>
348
357
  </div>
349
- <div className="text-2xs text-text-4 font-sans">{b.subtitle}</div>
350
- </div>
351
- </button>
352
- </Tooltip>
353
- ))}
358
+ </button>
359
+ </Tooltip>
360
+ ))}
361
+ </div>
354
362
  </div>
355
363
 
356
364
  {!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>
365
+ <StatusBanner variant="warning" icon={AlertTriangle}>
366
+ {currentBackend?.reason}
361
367
  {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>
368
+ <> — try <span className="font-mono font-medium">{suggestion.repoId}</span></>
365
369
  )}
366
- </div>
370
+ </StatusBanner>
367
371
  )}
368
372
 
369
373
  {isCompatible && selectedBackend === 'llama-cpp' && (
370
- <div className="px-2.5">
374
+ <div>
371
375
  {llamaInstalled === null && (
372
- <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">
373
377
  <Loader2 size={10} className="animate-spin" /> Checking llama-server...
374
378
  </div>
375
379
  )}
376
380
  {llamaInstalled === true && (
377
- <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">
378
382
  <CheckCircle size={10} /> llama-server found
379
383
  </div>
380
384
  )}
381
385
  {llamaInstalled === false && (
382
- <div className="space-y-1.5">
383
- <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">
384
388
  <AlertTriangle size={10} /> llama-server not found
385
389
  </div>
386
- <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>
387
391
  <button
388
392
  onClick={checkLlama}
389
- 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"
390
394
  >
391
395
  <RotateCcw size={10} /> Recheck after install
392
396
  </button>
@@ -399,44 +403,41 @@ export function LaunchModel() {
399
403
  <div className="space-y-2">
400
404
  {hasActiveAssistant && labAssistantBackend === selectedBackend ? (
401
405
  <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>
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
+ )}
412
413
  {!labAssistantMode && (
413
414
  <button
414
415
  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
+ 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"
416
417
  >
417
418
  View Assistant
418
419
  </button>
419
420
  )}
420
421
  </div>
421
422
  ) : (
422
- <>
423
+ <div className="space-y-2">
423
424
  <button
424
425
  onClick={handleLaunchAssistant}
425
426
  disabled={assistantLaunching}
426
427
  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
+ 'w-full flex items-center justify-center gap-1.5 h-8 text-[11px] font-sans font-medium rounded transition-colors cursor-pointer',
428
429
  assistantLaunching ? 'bg-accent/20 text-accent' : 'bg-accent text-surface-0 hover:bg-accent/90',
429
430
  )}
430
431
  >
431
432
  {assistantLaunching
432
- ? <><Loader2 size={12} className="animate-spin" /> Starting Assistant...</>
433
- : <><Wrench size={12} /> Setup {currentBackend?.label} with Assistant</>
433
+ ? <><Loader2 size={11} className="animate-spin" /> Starting Assistant...</>
434
+ : <><Wrench size={11} /> Setup {currentBackend?.label} with Assistant</>
434
435
  }
435
436
  </button>
436
- <p className="text-2xs text-text-4 font-sans">
437
+ <p className="text-[10px] text-text-4 font-sans text-center">
437
438
  An AI assistant will check your system and handle the installation.
438
439
  </p>
439
- </>
440
+ </div>
440
441
  )}
441
442
  </div>
442
443
  )}
@@ -444,22 +445,20 @@ export function LaunchModel() {
444
445
  {isCompatible && currentBackend?.autoLaunch && (
445
446
  <div className="space-y-2">
446
447
  {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>
448
+ <StatusBanner variant="success" icon={CheckCircle}>Server Running</StatusBanner>
450
449
  ) : (
451
450
  <button
452
451
  disabled={!canLaunch}
453
452
  onClick={handleLaunch}
454
453
  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',
454
+ 'w-full flex items-center justify-center gap-1.5 h-8 text-[11px] font-sans font-medium rounded transition-colors cursor-pointer',
456
455
  canLaunch ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
457
456
  )}
458
457
  >
459
458
  {launching ? (
460
- <><Loader2 size={12} className="animate-spin" /> Starting...</>
459
+ <><Loader2 size={11} className="animate-spin" /> Starting...</>
461
460
  ) : (
462
- <><Play size={12} /> Launch</>
461
+ <><Play size={11} /> Launch</>
463
462
  )}
464
463
  </button>
465
464
  )}
@@ -468,9 +467,9 @@ export function LaunchModel() {
468
467
  )}
469
468
  </div>
470
469
  )}
471
- </>
470
+ </div>
472
471
  )}
473
- </div>
472
+ </SidebarSection>
474
473
  );
475
474
  }
476
475
 
@@ -493,33 +492,35 @@ export function RuntimeConfig() {
493
492
  }
494
493
 
495
494
  return (
496
- <div className="space-y-2">
497
- <div className="flex items-center justify-between">
498
- <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={
499
500
  <Tooltip content="Add runtime">
500
501
  <button
501
502
  onClick={() => setDialogOpen(true)}
502
503
  className="p-1 text-text-4 hover:text-accent transition-colors cursor-pointer"
503
504
  >
504
- <Plus size={13} />
505
+ <Plus size={12} />
505
506
  </button>
506
507
  </Tooltip>
507
- </div>
508
-
508
+ }
509
+ >
509
510
  {runtimes.length === 0 ? (
510
- <div className="py-5 text-center">
511
- <WifiOff size={18} className="mx-auto text-text-4 mb-1.5" />
512
- <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>
513
514
  <button
514
515
  onClick={() => setDialogOpen(true)}
515
- 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"
516
517
  >
517
- <Plus size={11} /> Add Runtime
518
+ <Plus size={10} /> Add Runtime
518
519
  </button>
519
520
  </div>
520
521
  ) : (
521
522
  <ScrollArea className="max-h-48">
522
- <div className="space-y-px">
523
+ <div className="space-y-1 rounded-md bg-surface-1/50 border border-border-subtle p-2">
523
524
  {runtimes.map((rt) => (
524
525
  <RuntimeItem
525
526
  key={rt.id}
@@ -538,7 +539,7 @@ export function RuntimeConfig() {
538
539
  )}
539
540
 
540
541
  <AddRuntimeDialog open={dialogOpen} onOpenChange={setDialogOpen} />
541
- </div>
542
+ </SidebarSection>
542
543
  );
543
544
  }
544
545
 
@@ -560,7 +561,7 @@ export function RuntimeSection() {
560
561
 
561
562
  if (!serverRunning || expanded) {
562
563
  return (
563
- <div className="space-y-5 [&>*]:pt-5 [&>*:first-child]:pt-0">
564
+ <div className="space-y-6">
564
565
  <LaunchModel />
565
566
  <RuntimeConfig />
566
567
  </div>
@@ -568,24 +569,22 @@ export function RuntimeSection() {
568
569
  }
569
570
 
570
571
  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>
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` : ''}
579
578
  </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
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>
589
588
  </div>
590
589
  );
591
590
  }
@@ -25,7 +25,7 @@ export function SystemPromptEditor() {
25
25
  const systemPrompt = useGrooveStore((s) => s.labSystemPrompt);
26
26
  const setSystemPrompt = useGrooveStore((s) => s.setLabSystemPrompt);
27
27
  const themeKey = useGrooveStore((s) => s.editorTheme);
28
- const [collapsed, setCollapsed] = useState(false);
28
+ const [collapsed, setCollapsed] = useState(true);
29
29
  const containerRef = useRef(null);
30
30
  const viewRef = useRef(null);
31
31
  const themeCompartment = useRef(new Compartment());
@@ -87,20 +87,22 @@ export function SystemPromptEditor() {
87
87
  }, [systemPrompt]);
88
88
 
89
89
  return (
90
- <div className="space-y-1.5">
90
+ <div className="space-y-3">
91
91
  <button
92
92
  onClick={() => setCollapsed(!collapsed)}
93
- className="flex items-center gap-1.5 w-full cursor-pointer group"
93
+ className="flex items-center gap-1.5 w-full h-6 cursor-pointer group"
94
94
  >
95
95
  {collapsed ? (
96
- <ChevronRight size={11} className="text-text-4 group-hover:text-text-2 transition-colors" />
96
+ <ChevronRight size={10} className="text-text-4 group-hover:text-text-2 transition-colors flex-shrink-0" />
97
97
  ) : (
98
- <ChevronDown size={11} className="text-text-4 group-hover:text-text-2 transition-colors" />
98
+ <ChevronDown size={10} className="text-text-4 group-hover:text-text-2 transition-colors flex-shrink-0" />
99
99
  )}
100
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider group-hover:text-text-2 transition-colors">
100
+ <span className="text-[10px] font-semibold font-sans text-text-3 uppercase tracking-widest group-hover:text-text-2 transition-colors">
101
101
  System Prompt
102
102
  </span>
103
- <span className="text-2xs font-mono text-text-4 ml-auto">{charCount > 0 ? `${charCount}` : ''}</span>
103
+ {charCount > 0 && (
104
+ <span className="text-[10px] font-mono text-text-4 ml-auto tabular-nums">{charCount}</span>
105
+ )}
104
106
  </button>
105
107
 
106
108
  <div className={cn(
@@ -109,7 +111,7 @@ export function SystemPromptEditor() {
109
111
  )}>
110
112
  <div
111
113
  ref={containerRef}
112
- className="h-full border border-border-subtle rounded-sm overflow-hidden"
114
+ className="h-full rounded-md bg-surface-1/50 border border-border-subtle overflow-hidden"
113
115
  />
114
116
  </div>
115
117
  </div>
@@ -10,13 +10,13 @@ export function TuningSlider({
10
10
  const display = typeof fmt === 'function' ? fmt(value) : (typeof fmt === 'string' ? fmt : value);
11
11
 
12
12
  return (
13
- <div className={cn('group flex items-center gap-2 py-1.5', disabled && 'opacity-40 pointer-events-none', className)}>
14
- <span className="text-2xs text-text-2 font-sans w-20 shrink-0 truncate">{label}</span>
13
+ <div className={cn('group flex items-center gap-2.5 h-8', disabled && 'opacity-40 pointer-events-none', className)}>
14
+ <span className="text-[11px] text-text-2 font-sans w-[76px] shrink-0 truncate">{label}</span>
15
15
  <div className="relative flex-1 flex items-center h-5">
16
16
  <div className="absolute inset-y-0 flex items-center w-full pointer-events-none">
17
- <div className="w-full h-1 rounded-full bg-surface-5">
17
+ <div className="w-full h-[3px] rounded-full bg-surface-5">
18
18
  <div
19
- className="h-full rounded-full bg-accent/70 group-hover:bg-accent transition-colors"
19
+ className="h-full rounded-full bg-accent/60 group-hover:bg-accent transition-colors"
20
20
  style={{ width: `${pct}%` }}
21
21
  />
22
22
  </div>
@@ -30,20 +30,20 @@ export function TuningSlider({
30
30
  disabled={disabled}
31
31
  onChange={(e) => onChange(Number(e.target.value))}
32
32
  className="relative w-full h-5 appearance-none bg-transparent cursor-pointer
33
- [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
33
+ [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5
34
34
  [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent
35
35
  [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_var(--color-surface-1)]
36
- [&::-webkit-slider-thumb]:hover:bg-accent [&::-webkit-slider-thumb]:hover:shadow-[0_0_0_2px_var(--color-surface-1),0_0_8px_rgba(51,175,188,0.4)]
36
+ [&::-webkit-slider-thumb]:hover:shadow-[0_0_0_2px_var(--color-surface-1),0_0_6px_rgba(51,175,188,0.35)]
37
37
  [&::-webkit-slider-thumb]:active:scale-110
38
38
  [&::-webkit-slider-thumb]:transition-all
39
- [&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
39
+ [&::-moz-range-thumb]:w-2.5 [&::-moz-range-thumb]:h-2.5 [&::-moz-range-thumb]:rounded-full
40
40
  [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-none
41
41
  [&::-moz-range-thumb]:shadow-[0_0_0_2px_var(--color-surface-1)]
42
42
  [&::-moz-range-track]:bg-transparent
43
43
  disabled:cursor-not-allowed"
44
44
  />
45
45
  </div>
46
- <span className="text-2xs text-accent font-mono font-medium w-10 text-right shrink-0 tabular-nums">{display}</span>
46
+ <span className="text-[11px] text-accent font-mono font-medium w-9 text-right shrink-0 tabular-nums">{display}</span>
47
47
  </div>
48
48
  );
49
49
  }
@@ -45,6 +45,7 @@ export const ROLE_COLORS = {
45
45
  creative: NEUTRAL_ROLE,
46
46
  slides: NEUTRAL_ROLE,
47
47
  chat: NEUTRAL_ROLE,
48
+ research: NEUTRAL_ROLE,
48
49
  };
49
50
 
50
51
  export function roleColor(role) {
@@ -91,6 +91,11 @@ export const useGrooveStore = create((set, get) => ({
91
91
  if (data) set({ subscription: { ...get().subscription, ...data } });
92
92
  });
93
93
  }
94
+ if (window.groove?.update?.onUpdateAvailable) {
95
+ window.groove.update.onUpdateAvailable((data) => {
96
+ set({ updateProgress: { percent: 0, version: data.version } });
97
+ });
98
+ }
94
99
  if (window.groove?.update?.onUpdateProgress) {
95
100
  window.groove.update.onUpdateProgress((data) => {
96
101
  set({ updateProgress: data });
@@ -101,6 +106,18 @@ export const useGrooveStore = create((set, get) => ({
101
106
  set({ updateReady: data.version, updateModalOpen: true, updateProgress: null });
102
107
  });
103
108
  }
109
+ if (window.groove?.update?.getUpdateStatus) {
110
+ window.groove.update.getUpdateStatus().then((state) => {
111
+ if (!state) return;
112
+ if (state.downloaded) {
113
+ set({ updateReady: state.downloaded.version, updateProgress: null });
114
+ } else if (state.progress) {
115
+ set({ updateProgress: state.progress });
116
+ } else if (state.available) {
117
+ set({ updateProgress: { percent: 0, version: state.available.version } });
118
+ }
119
+ }).catch(() => {});
120
+ }
104
121
  };
105
122
 
106
123
  ws.onmessage = (event) => {
@@ -407,15 +407,22 @@ export const createAgentsSlice = (set, get) => ({
407
407
 
408
408
  case 'read': {
409
409
  if (tags.length === 0) { addSystemMsg('Usage: [read] #tag1 #tag2 ...'); return true; }
410
+ const userText = rest.replace(/#[\w/.-]+/g, '').trim();
410
411
  get().addChatMessage(agentId, 'user', message, false);
411
412
  const readBrief = await api.post('/keeper/pull', { tags });
412
413
  if (readBrief?.brief) {
414
+ const memoryBlock = `\n\n---\nContext from memories (${tags.map(t => '#' + t).join(', ')}):\n\n${readBrief.brief}`;
415
+ set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, agentId]) }));
413
416
  await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, {
414
- message: `Here is context from my tagged memories:\n\n${readBrief.brief}`,
417
+ message: userText ? `${userText}${memoryBlock}` : `Here is context from my tagged memories:\n\n${readBrief.brief}`,
415
418
  });
416
419
  addSystemMsg(`Sent ${tags.map(t => '#' + t).join(', ')} to agent`);
417
420
  } else {
418
421
  addSystemMsg(`No memories found for ${tags.map(t => '#' + t).join(', ')}`);
422
+ if (userText) {
423
+ set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, agentId]) }));
424
+ await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, { message: userText });
425
+ }
419
426
  }
420
427
  return true;
421
428
  }