groove-dev 0.27.152 → 0.27.154

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 (53) 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/journalist.js +52 -21
  4. package/node_modules/@groove-dev/daemon/src/keeper.js +37 -2
  5. package/node_modules/@groove-dev/daemon/src/llama-server.js +96 -3
  6. package/node_modules/@groove-dev/daemon/src/model-manager.js +52 -10
  7. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +16 -0
  8. package/node_modules/@groove-dev/daemon/src/routes/files.js +71 -2
  9. package/node_modules/@groove-dev/daemon/src/routes/providers.js +11 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +252 -44
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
  16. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +9 -2
  17. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +40 -3
  18. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +23 -8
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +8 -1
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
  21. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +24 -5
  22. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +13 -0
  23. package/node_modules/@groove-dev/gui/src/views/memory.jsx +87 -44
  24. package/node_modules/@groove-dev/gui/src/views/models.jsx +15 -2
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/daemon/package.json +1 -1
  28. package/packages/daemon/src/journalist.js +52 -21
  29. package/packages/daemon/src/keeper.js +37 -2
  30. package/packages/daemon/src/llama-server.js +96 -3
  31. package/packages/daemon/src/model-manager.js +52 -10
  32. package/packages/daemon/src/routes/coordination.js +16 -0
  33. package/packages/daemon/src/routes/files.js +71 -2
  34. package/packages/daemon/src/routes/providers.js +11 -0
  35. package/packages/gui/dist/assets/index-BTLb6zTD.js +1015 -0
  36. package/packages/gui/dist/assets/index-Diw6wDPU.css +1 -0
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/agent-feed.jsx +252 -44
  40. package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
  41. package/packages/gui/src/components/agents/spawn-wizard.jsx +9 -2
  42. package/packages/gui/src/components/editor/file-tree.jsx +40 -3
  43. package/packages/gui/src/components/lab/runtime-config.jsx +23 -8
  44. package/packages/gui/src/components/settings/quick-connect.jsx +8 -1
  45. package/packages/gui/src/stores/groove.js +9 -1
  46. package/packages/gui/src/stores/slices/agents-slice.js +24 -5
  47. package/packages/gui/src/stores/slices/providers-slice.js +13 -0
  48. package/packages/gui/src/views/memory.jsx +87 -44
  49. package/packages/gui/src/views/models.jsx +15 -2
  50. package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +0 -1
  51. package/node_modules/@groove-dev/gui/dist/assets/index-CReKPWhY.js +0 -1011
  52. package/packages/gui/dist/assets/index-CEkPsSAm.css +0 -1
  53. package/packages/gui/dist/assets/index-CReKPWhY.js +0 -1011
@@ -9,7 +9,7 @@ import { Dialog, DialogContent } from '../ui/dialog';
9
9
  import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
10
10
  import { Tooltip } from '../ui/tooltip';
11
11
  import { ScrollArea } from '../ui/scroll-area';
12
- import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2 } from 'lucide-react';
12
+ import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2, Download } from 'lucide-react';
13
13
  import { cn } from '../../lib/cn';
14
14
 
15
15
  const IS_APPLE = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || '');
@@ -229,9 +229,11 @@ export function LaunchModel() {
229
229
  const localModels = useGrooveStore((s) => s.labLocalModels);
230
230
  const fetchLocalModels = useGrooveStore((s) => s.fetchLabLocalModels);
231
231
  const checkLlama = useGrooveStore((s) => s.checkLlamaStatus);
232
+ const installLlama = useGrooveStore((s) => s.installLlamaServer);
232
233
  const launchModel = useGrooveStore((s) => s.launchLocalModel);
233
234
  const launching = useGrooveStore((s) => s.labLaunching);
234
235
  const llamaInstalled = useGrooveStore((s) => s.labLlamaInstalled);
236
+ const llamaInstalling = useGrooveStore((s) => s.labLlamaInstalling);
235
237
  const launchPhase = useGrooveStore((s) => s.labLaunchPhase);
236
238
  const launchError = useGrooveStore((s) => s.labLaunchError);
237
239
  const launchLabAssistant = useGrooveStore((s) => s.launchLabAssistant);
@@ -395,13 +397,26 @@ export function LaunchModel() {
395
397
  <div className="flex items-center gap-2 text-[11px] text-danger font-sans">
396
398
  <AlertTriangle size={10} /> llama-server not found
397
399
  </div>
398
- <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>
399
- <button
400
- onClick={checkLlama}
401
- className="flex items-center gap-1.5 text-[11px] font-sans text-accent hover:text-accent/80 transition-colors cursor-pointer"
402
- >
403
- <RotateCcw size={10} /> Recheck after install
404
- </button>
400
+ {llamaInstalling ? (
401
+ <div className="flex items-center gap-2 text-[11px] text-accent font-sans">
402
+ <Loader2 size={10} className="animate-spin" /> Installing llama-server...
403
+ </div>
404
+ ) : (
405
+ <div className="flex items-center gap-2">
406
+ <button
407
+ onClick={installLlama}
408
+ className="flex items-center gap-1.5 text-[11px] font-sans font-medium text-surface-0 bg-accent hover:bg-accent/90 px-2.5 py-1 rounded transition-colors cursor-pointer"
409
+ >
410
+ <Download size={10} /> Install
411
+ </button>
412
+ <button
413
+ onClick={checkLlama}
414
+ className="flex items-center gap-1.5 text-[11px] font-sans text-text-3 hover:text-text-2 transition-colors cursor-pointer"
415
+ >
416
+ <RotateCcw size={10} /> Recheck
417
+ </button>
418
+ </div>
419
+ )}
405
420
  </div>
406
421
  )}
407
422
  </div>
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useRef } from 'react';
2
+ import { useState, useRef, useEffect } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { AnimatePresence, motion } from 'framer-motion';
@@ -20,6 +20,13 @@ export function QuickConnect() {
20
20
  const [showWizard, setShowWizard] = useState(false);
21
21
  const wizardTunnelId = useRef(null);
22
22
 
23
+ useEffect(() => {
24
+ if (open) {
25
+ setShowWizard(false);
26
+ useGrooveStore.getState().fetchTunnels();
27
+ }
28
+ }, [open]);
29
+
23
30
  if (!open) return null;
24
31
 
25
32
  async function handleConnect(id) {
@@ -306,7 +306,15 @@ export const useGrooveStore = create((set, get) => ({
306
306
  const filePath = block.input?.file_path || block.input?.path;
307
307
  if (filePath && agentId === get().workspaceAgentId) {
308
308
  const relPath = filePath.replace(/^\/[^/]+.*?\/groove\//, '');
309
- get().openFile(relPath);
309
+ const hasActiveFile = get().editorActiveFile;
310
+ if (hasActiveFile) {
311
+ // Add to tabs without switching away from the user's current file
312
+ set((s) => ({
313
+ editorOpenTabs: s.editorOpenTabs.includes(relPath) ? s.editorOpenTabs : [...s.editorOpenTabs, relPath],
314
+ }));
315
+ } else {
316
+ get().openFile(relPath);
317
+ }
310
318
  }
311
319
  }
312
320
  }
@@ -95,12 +95,19 @@ export const createAgentsSlice = (set, get) => ({
95
95
 
96
96
  // ── Chat ──────────────────────────────────────────────────
97
97
 
98
- addChatMessage(agentId, from, text, isQuery = false) {
98
+ addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
99
99
  set((s) => {
100
100
  const history = { ...s.chatHistory };
101
101
  if (!history[agentId]) history[agentId] = [];
102
- history[agentId] = [...history[agentId].slice(-100), { from, text, timestamp: Date.now(), isQuery }];
103
- persistJSON('groove:chatHistory', history);
102
+ const msg = { from, text, timestamp: Date.now(), isQuery };
103
+ if (attachments?.length) msg.attachments = attachments;
104
+ history[agentId] = [...history[agentId].slice(-100), msg];
105
+ const forStorage = { ...history };
106
+ forStorage[agentId] = forStorage[agentId].map((m) => {
107
+ if (!m.attachments?.length) return m;
108
+ return { ...m, attachments: m.attachments.map(({ dataUrl, ...rest }) => rest) };
109
+ });
110
+ persistJSON('groove:chatHistory', forStorage);
104
111
  return { chatHistory: history };
105
112
  });
106
113
  },
@@ -120,7 +127,7 @@ export const createAgentsSlice = (set, get) => ({
120
127
  }
121
128
  },
122
129
 
123
- async instructAgent(id, message) {
130
+ async instructAgent(id, message, attachments = undefined) {
124
131
  // ── Keeper command interception ─────────────────────────
125
132
  const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
126
133
  if (keeperCmd) {
@@ -131,7 +138,7 @@ export const createAgentsSlice = (set, get) => ({
131
138
  }
132
139
  }
133
140
 
134
- get().addChatMessage(id, 'user', message, false);
141
+ get().addChatMessage(id, 'user', message, false, attachments);
135
142
  set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
136
143
 
137
144
  // Auto-attach active file context when in workspace mode
@@ -332,6 +339,18 @@ export const createAgentsSlice = (set, get) => ({
332
339
  }
333
340
  },
334
341
 
342
+ async moveKeeperItem(oldTag, newTag) {
343
+ try {
344
+ const item = await api.post('/keeper/move', { oldTag, newTag });
345
+ get().fetchKeeperItems();
346
+ get().addToast('success', `Moved #${oldTag} → #${item.tag}`);
347
+ return item;
348
+ } catch (err) {
349
+ get().addToast('error', 'Failed to move memory', err.message);
350
+ throw err;
351
+ }
352
+ },
353
+
335
354
  async getKeeperItem(tag) {
336
355
  try {
337
356
  return await api.get(`/keeper/${tag}`);
@@ -42,6 +42,7 @@ export const createProvidersSlice = (set, get) => ({
42
42
  labLocalModels: [],
43
43
  labLaunching: null,
44
44
  labLlamaInstalled: null,
45
+ labLlamaInstalling: false,
45
46
  labLaunchPhase: null,
46
47
  labLaunchError: null,
47
48
  labAssistantAgentId: localStorage.getItem('groove:labAssistantAgentId') || null,
@@ -356,6 +357,18 @@ export const createProvidersSlice = (set, get) => ({
356
357
  } catch { set({ labLlamaInstalled: false }); }
357
358
  },
358
359
 
360
+ async installLlamaServer() {
361
+ set({ labLlamaInstalling: true });
362
+ try {
363
+ await api.post('/llama/install');
364
+ set({ labLlamaInstalled: true, labLlamaInstalling: false });
365
+ get().addToast('success', 'llama-server installed successfully');
366
+ } catch (err) {
367
+ set({ labLlamaInstalling: false });
368
+ get().addToast('error', 'Install failed', err.message);
369
+ }
370
+ },
371
+
359
372
  async launchLocalModel(modelId) {
360
373
  set({ labLaunching: modelId, labLaunchPhase: 'starting', labLaunchError: null });
361
374
  try {
@@ -1,10 +1,10 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect, useRef } from 'react';
2
+ import { useState, useEffect, useRef, useCallback } from 'react';
3
3
  import { useGrooveStore } from '../stores/groove';
4
4
  import { Button } from '../components/ui/button';
5
5
  import { ScrollArea } from '../components/ui/scroll-area';
6
6
  import { Dialog, DialogContent } from '../components/ui/dialog';
7
- import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle } from 'lucide-react';
7
+ import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical } from 'lucide-react';
8
8
 
9
9
  const COMMANDS = [
10
10
  { cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
@@ -249,38 +249,71 @@ function InstructModal({ open, onOpenChange }) {
249
249
  );
250
250
  }
251
251
 
252
- function TreeGroup({ node, onSelect }) {
252
+ function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart, onDragOver, onDragLeave, onDrop }) {
253
+ return (
254
+ <div
255
+ draggable
256
+ onDragStart={(e) => { e.dataTransfer.setData('text/plain', tag); e.dataTransfer.effectAllowed = 'move'; onDragStart?.(tag); }}
257
+ onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(tag); }}
258
+ onDragLeave={() => onDragLeave?.()}
259
+ onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), tag); }}
260
+ onClick={() => onSelect({ tag })}
261
+ className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer group ${isDragOver ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
262
+ style={indent ? { paddingLeft: `${8 + indent * 16}px` } : undefined}
263
+ >
264
+ <GripVertical size={10} className="text-text-4 opacity-0 group-hover:opacity-50 flex-shrink-0 cursor-grab" />
265
+ <Hash size={11} className="text-text-4 flex-shrink-0" />
266
+ <span className="font-medium text-text-2 truncate">{label}</span>
267
+ {isDoc && <Sparkles size={9} className="text-purple flex-shrink-0" />}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
253
273
  const [expanded, setExpanded] = useState(true);
254
274
  const hasChildren = node.children && node.children.length > 0;
255
275
 
276
+ if (!hasChildren) {
277
+ return (
278
+ <TreeItem
279
+ tag={node.tag} label={node.tag} isDoc={node.type === 'doc'}
280
+ isDragOver={dragOverTag === node.tag}
281
+ onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
282
+ />
283
+ );
284
+ }
285
+
256
286
  return (
257
- <div>
287
+ <div
288
+ onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(node.tag); }}
289
+ onDragLeave={() => onDragLeave?.()}
290
+ onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), node.tag); }}
291
+ >
258
292
  <button
259
- onClick={() => hasChildren ? setExpanded(!expanded) : onSelect(node)}
260
- className="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-text-2 hover:bg-surface-2 transition-colors cursor-pointer"
293
+ onClick={() => setExpanded(!expanded)}
294
+ className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer ${dragOverTag === node.tag ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
261
295
  >
262
- {hasChildren ? (
263
- <ChevronRight size={12} className={`transition-transform ${expanded ? 'rotate-90' : ''}`} />
264
- ) : (
265
- <Hash size={12} className="text-text-4" />
266
- )}
267
- <FolderOpen size={12} className={hasChildren ? 'text-accent' : 'text-text-4'} />
268
- <span className="font-medium">{node.tag}</span>
269
- {hasChildren && (
270
- <span className="text-2xs text-text-4 ml-auto">{node.children.length}</span>
271
- )}
296
+ <ChevronRight size={12} className={`transition-transform text-text-4 ${expanded ? 'rotate-90' : ''}`} />
297
+ <FolderOpen size={12} className="text-accent" />
298
+ <span className="font-medium text-text-1">{node.tag}</span>
299
+ {!node.virtual && node.type === 'doc' && <Sparkles size={9} className="text-purple" />}
300
+ <span className="text-2xs text-text-4 ml-auto">{node.children.length}</span>
272
301
  </button>
273
- {expanded && hasChildren && (
274
- <div className="ml-4 mt-0.5 space-y-0.5">
302
+ {expanded && (
303
+ <div className="mt-0.5 space-y-0.5">
304
+ {!node.virtual && (
305
+ <TreeItem
306
+ tag={node.tag} label={node.tag} isDoc={node.type === 'doc'} indent={1}
307
+ isDragOver={false}
308
+ onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
309
+ />
310
+ )}
275
311
  {node.children.map((child) => (
276
- <button
277
- key={child.tag}
278
- onClick={() => onSelect(child)}
279
- className="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-text-3 hover:text-text-1 hover:bg-surface-2 transition-colors cursor-pointer"
280
- >
281
- <Hash size={10} className="text-text-4" />
282
- <span>{child.tag.split('/').pop()}</span>
283
- </button>
312
+ <TreeItem
313
+ key={child.tag} tag={child.tag} label={child.tag.split('/').pop()} isDoc={child.type === 'doc'} indent={1}
314
+ isDragOver={dragOverTag === child.tag}
315
+ onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
316
+ />
284
317
  ))}
285
318
  </div>
286
319
  )}
@@ -297,12 +330,15 @@ export default function MemoryView() {
297
330
  const saveKeeperItem = useGrooveStore((s) => s.saveKeeperItem);
298
331
  const updateKeeperItem = useGrooveStore((s) => s.updateKeeperItem);
299
332
  const deleteKeeperItem = useGrooveStore((s) => s.deleteKeeperItem);
333
+ const moveKeeperItem = useGrooveStore((s) => s.moveKeeperItem);
300
334
  const getKeeperItem = useGrooveStore((s) => s.getKeeperItem);
301
335
  const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
302
336
 
303
337
  const [search, setSearch] = useState('');
304
338
  const [viewMode, setViewMode] = useState('list');
305
339
  const [editorOpen, setEditorOpen] = useState(false);
340
+ const [dragOverTag, setDragOverTag] = useState(null);
341
+ const [draggingTag, setDraggingTag] = useState(null);
306
342
 
307
343
  useEffect(() => { fetchKeeperItems(); }, []);
308
344
 
@@ -344,8 +380,22 @@ export default function MemoryView() {
344
380
  await handleEdit(node);
345
381
  };
346
382
 
383
+ const handleDrop = useCallback(async (sourceTag, targetTag) => {
384
+ setDragOverTag(null);
385
+ setDraggingTag(null);
386
+ if (!sourceTag || !targetTag || sourceTag === targetTag) return;
387
+ // Don't drop onto self or own children
388
+ if (targetTag.startsWith(sourceTag + '/')) return;
389
+ const sourceName = sourceTag.split('/').pop();
390
+ const newTag = targetTag + '/' + sourceName;
391
+ if (sourceTag === newTag) return;
392
+ try {
393
+ await moveKeeperItem(sourceTag, newTag);
394
+ } catch { /* toast handles */ }
395
+ }, [moveKeeperItem]);
396
+
347
397
  return (
348
- <div className="flex-1 flex flex-col overflow-hidden">
398
+ <div className="flex flex-col h-full overflow-hidden">
349
399
  {/* Header */}
350
400
  <div className="flex-shrink-0 px-4 py-3 border-b border-border">
351
401
  <div className="flex items-center justify-between gap-3 mb-3">
@@ -394,7 +444,7 @@ export default function MemoryView() {
394
444
  </div>
395
445
 
396
446
  {/* Content */}
397
- <ScrollArea className="flex-1">
447
+ <ScrollArea className="flex-1 min-h-0">
398
448
  {keeperItems.length === 0 ? (
399
449
  <div className="flex flex-col items-center justify-center h-64 gap-3">
400
450
  <BookOpen size={32} className="text-text-4" />
@@ -414,24 +464,17 @@ export default function MemoryView() {
414
464
  </div>
415
465
  </div>
416
466
  ) : viewMode === 'tree' ? (
417
- <div className="p-3 space-y-1">
467
+ <div className="p-3 space-y-0.5" onDragOver={(e) => e.preventDefault()}>
418
468
  {keeperTree.map((node) => (
419
- <TreeGroup key={node.tag} node={node} onSelect={handleTreeSelect} />
469
+ <TreeGroup
470
+ key={node.tag} node={node} onSelect={handleTreeSelect}
471
+ dragOverTag={dragOverTag}
472
+ onDragStart={(tag) => setDraggingTag(tag)}
473
+ onDragOver={(tag) => { if (tag !== draggingTag) setDragOverTag(tag); }}
474
+ onDragLeave={() => setDragOverTag(null)}
475
+ onDrop={handleDrop}
476
+ />
420
477
  ))}
421
- {keeperItems
422
- .filter((item) => !item.tag.includes('/') && !keeperTree.some((t) => t.tag === item.tag && t.children?.length))
423
- .map((item) => (
424
- <button
425
- key={item.tag}
426
- onClick={() => handleEdit(item)}
427
- className="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-text-2 hover:bg-surface-2 transition-colors cursor-pointer"
428
- >
429
- <Hash size={12} className="text-text-4" />
430
- <span className="font-medium">{item.tag}</span>
431
- {item.type === 'doc' && <Sparkles size={10} className="text-purple" />}
432
- <span className="text-2xs text-text-4 ml-auto">{formatRelative(item.updatedAt)}</span>
433
- </button>
434
- ))}
435
478
  </div>
436
479
  ) : (
437
480
  <div className="p-3 space-y-2">
@@ -720,7 +720,7 @@ export default function ModelsView() {
720
720
  <Box size={28} className="text-text-4 mb-3" />
721
721
  <div className="text-sm font-mono font-semibold text-text-1 mb-1">No local models</div>
722
722
  <div className="text-xs font-mono text-text-3 text-center max-w-sm mb-5">
723
- Pull from Ollama or search HuggingFace for GGUF models to run locally.
723
+ Pull from Ollama or search HuggingFace for models to run locally.
724
724
  </div>
725
725
  <div className="flex gap-2">
726
726
  <Button variant="primary" onClick={() => {
@@ -855,7 +855,7 @@ export default function ModelsView() {
855
855
  </div>
856
856
  ) : searchResults.length === 0 ? (
857
857
  <div className="py-6 text-center">
858
- <div className="text-xs font-mono text-text-3">Search for GGUF models</div>
858
+ <div className="text-xs font-mono text-text-3">Search for models on HuggingFace</div>
859
859
  <div className="text-2xs font-mono text-text-4 mt-1">
860
860
  Try "qwen coder", "deepseek", "codestral", "llama"
861
861
  </div>
@@ -869,6 +869,19 @@ export default function ModelsView() {
869
869
  className="w-full text-left flex items-center gap-2 py-1.5 border-b border-border hover:bg-surface-2/50 transition-colors cursor-pointer"
870
870
  >
871
871
  <span className="text-xs font-mono font-semibold text-text-1 truncate flex-1">{r.name}</span>
872
+ {r.recommendedRuntimes?.length > 0 && (
873
+ <span className="flex gap-1 flex-shrink-0">
874
+ {r.recommendedRuntimes.map((rt) => (
875
+ <span key={rt} className={cn(
876
+ 'px-1.5 py-0.5 rounded text-[9px] font-mono font-medium leading-none',
877
+ rt === 'llama.cpp' && 'bg-blue-500/15 text-blue-400',
878
+ rt === 'vLLM' && 'bg-purple-500/15 text-purple-400',
879
+ rt === 'MLX' && 'bg-emerald-500/15 text-emerald-400',
880
+ rt === 'TGI' && 'bg-amber-500/15 text-amber-400',
881
+ )}>{rt}</span>
882
+ ))}
883
+ </span>
884
+ )}
872
885
  <span className="text-2xs font-mono text-text-4">{r.author}</span>
873
886
  <span className="text-2xs font-mono text-text-4 tabular-nums">{r.downloads?.toLocaleString()}</span>
874
887
  {expandedResult === r.id