groove-dev 0.27.134 → 0.27.136

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 (85) hide show
  1. package/moe-training/client/domain-tagger.js +1 -1
  2. package/moe-training/scripts/retag-delegate-yield.js +303 -0
  3. package/moe-training/test/shared/envelope-schema.test.js +3 -3
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/adaptive.js +77 -0
  7. package/node_modules/@groove-dev/daemon/src/api.js +35 -5
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +28 -12
  9. package/node_modules/@groove-dev/daemon/src/model-lab.js +53 -76
  10. package/node_modules/@groove-dev/daemon/src/process.js +91 -2
  11. package/node_modules/@groove-dev/daemon/src/rotator.js +45 -3
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-Dozp69tK.js → index-BrZHF7pK.js} +1770 -1766
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DIfiwdKl.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +60 -18
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +42 -20
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +1 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +2 -22
  21. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +9 -9
  22. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +1 -1
  23. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +7 -0
  24. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +59 -51
  25. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +48 -48
  26. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +39 -38
  27. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +4 -5
  28. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +11 -11
  29. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +66 -62
  30. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +13 -13
  31. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  32. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +62 -22
  33. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +16 -17
  34. package/node_modules/@groove-dev/gui/src/components/ui/table-tree.jsx +38 -0
  35. package/node_modules/@groove-dev/gui/src/stores/groove.js +23 -9
  36. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -1
  37. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +101 -87
  38. package/node_modules/moe-training/client/domain-tagger.js +1 -1
  39. package/node_modules/moe-training/scripts/retag-delegate-yield.js +303 -0
  40. package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
  41. package/package.json +1 -1
  42. package/packages/cli/package.json +1 -1
  43. package/packages/daemon/package.json +1 -1
  44. package/packages/daemon/src/adaptive.js +77 -0
  45. package/packages/daemon/src/api.js +35 -5
  46. package/packages/daemon/src/journalist.js +28 -12
  47. package/packages/daemon/src/model-lab.js +53 -76
  48. package/packages/daemon/src/process.js +91 -2
  49. package/packages/daemon/src/rotator.js +45 -3
  50. package/packages/gui/dist/assets/{index-Dozp69tK.js → index-BrZHF7pK.js} +1770 -1766
  51. package/packages/gui/dist/assets/index-DIfiwdKl.css +1 -0
  52. package/packages/gui/dist/index.html +2 -2
  53. package/packages/gui/package.json +1 -1
  54. package/packages/gui/src/components/agents/agent-chat.jsx +60 -18
  55. package/packages/gui/src/components/agents/agent-feed.jsx +42 -20
  56. package/packages/gui/src/components/agents/agent-file-tree.jsx +1 -1
  57. package/packages/gui/src/components/agents/workspace-mode.jsx +1 -1
  58. package/packages/gui/src/components/chat/chat-messages.jsx +2 -22
  59. package/packages/gui/src/components/editor/code-editor.jsx +9 -9
  60. package/packages/gui/src/components/editor/file-tree.jsx +1 -1
  61. package/packages/gui/src/components/editor/terminal.jsx +7 -0
  62. package/packages/gui/src/components/lab/chat-playground.jsx +59 -51
  63. package/packages/gui/src/components/lab/lab-assistant.jsx +48 -48
  64. package/packages/gui/src/components/lab/metrics-panel.jsx +39 -38
  65. package/packages/gui/src/components/lab/parameter-panel.jsx +4 -5
  66. package/packages/gui/src/components/lab/preset-manager.jsx +11 -11
  67. package/packages/gui/src/components/lab/runtime-config.jsx +66 -62
  68. package/packages/gui/src/components/lab/system-prompt-editor.jsx +13 -13
  69. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  70. package/packages/gui/src/components/preview/preview-workspace.jsx +62 -22
  71. package/packages/gui/src/components/ui/slider.jsx +16 -17
  72. package/packages/gui/src/components/ui/table-tree.jsx +38 -0
  73. package/packages/gui/src/stores/groove.js +23 -9
  74. package/packages/gui/src/views/editor.jsx +1 -1
  75. package/packages/gui/src/views/model-lab.jsx +101 -87
  76. package/plan_files/DELEGATE_YIELD_TRAINING_TAGS.md +135 -0
  77. package/plan_files/session-quality-rotation-fixes.md +218 -0
  78. package/test.py +571 -0
  79. package/node_modules/@groove-dev/gui/dist/assets/index-BgQL4bNl.css +0 -1
  80. package/packages/gui/dist/assets/index-BgQL4bNl.css +0 -1
  81. /package/{AGENT_ORCHESTRATION.md → plan_files/AGENT_ORCHESTRATION.md} +0 -0
  82. /package/{DYNAMIC_LEAF_ARCH.md → plan_files/DYNAMIC_LEAF_ARCH.md} +0 -0
  83. /package/{EMBEDDING_DIAGNOSTIC.md → plan_files/EMBEDDING_DIAGNOSTIC.md} +0 -0
  84. /package/{EMBEDDING_SERVICE_BUILD_PLAN.md → plan_files/EMBEDDING_SERVICE_BUILD_PLAN.md} +0 -0
  85. /package/{MOE_TRAINING_PIPELINE.md → plan_files/MOE_TRAINING_PIPELINE.md} +0 -0
@@ -27,24 +27,24 @@ const LANGS = {
27
27
  const grooveHighlightStyle = HighlightStyle.define([
28
28
  { tag: t.keyword, color: '#b07fd5' },
29
29
  { tag: [t.name, t.deleted, t.character, t.macroName], color: '#d4d8e0' },
30
- { tag: [t.function(t.variableName), t.labelName], color: '#7ab0df' },
31
- { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#c4956a' },
30
+ { tag: [t.function(t.variableName), t.labelName], color: '#dcc9a0' },
31
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#d4a07a' },
32
32
  { tag: [t.definition(t.name), t.separator], color: '#bcc2cd' },
33
- { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#cda869' },
34
- { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.special(t.string)], color: '#73c991' },
33
+ { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#e0c589' },
34
+ { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.special(t.string)], color: '#89b4c4' },
35
35
  { tag: [t.meta, t.comment], color: '#6e7681', fontStyle: 'italic' },
36
36
  { tag: t.strong, fontWeight: 'bold' },
37
37
  { tag: t.emphasis, fontStyle: 'italic' },
38
38
  { tag: t.strikethrough, textDecoration: 'line-through' },
39
39
  { tag: t.link, color: '#7ab0df', textDecoration: 'underline' },
40
40
  { tag: t.heading, fontWeight: '400', color: '#bcc2cd' },
41
- { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c4956a' },
42
- { tag: [t.processingInstruction, t.string, t.inserted], color: '#73c991' },
41
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#d4a07a' },
42
+ { tag: [t.processingInstruction, t.string, t.inserted], color: '#95b2b8' },
43
43
  { tag: t.invalid, color: '#d4736e' },
44
- { tag: t.propertyName, color: '#7ab0df' },
44
+ { tag: t.propertyName, color: '#dcc9a0' },
45
45
  { tag: [t.tagName], color: '#d4736e' },
46
- { tag: t.attributeName, color: '#cda869' },
47
- { tag: t.attributeValue, color: '#73c991' },
46
+ { tag: t.attributeName, color: '#e0c589' },
47
+ { tag: t.attributeValue, color: '#95b2b8' },
48
48
  ]);
49
49
 
50
50
  // Custom theme overrides to match our design tokens
@@ -130,7 +130,7 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
130
130
  onDoubleClick={handleContextMenu}
131
131
  onContextMenu={handleContextMenu}
132
132
  className={cn(
133
- 'w-full flex items-center gap-1.5 py-[3px] text-[13px] font-sans cursor-pointer',
133
+ 'w-full flex items-center gap-1.5 py-[3px] text-xs font-sans cursor-pointer',
134
134
  'hover:bg-surface-5 transition-colors text-left select-none',
135
135
  isActive && 'bg-accent/10 text-text-0',
136
136
  !isActive && 'text-text-1',
@@ -29,6 +29,8 @@ function TerminalInstance({ tabId, visible, registerKill }) {
29
29
  const termIdRef = useRef(null);
30
30
  const handlerRef = useRef(null);
31
31
  const mountedRef = useRef(false);
32
+ const visibleRef = useRef(visible);
33
+ const lastSizeRef = useRef({ cols: 0, rows: 0 });
32
34
 
33
35
  useEffect(() => {
34
36
  registerKill?.(tabId, () => {
@@ -105,6 +107,9 @@ function TerminalInstance({ tabId, visible, registerKill }) {
105
107
  });
106
108
 
107
109
  term.onResize(({ cols, rows }) => {
110
+ if (cols === lastSizeRef.current.cols && rows === lastSizeRef.current.rows) return;
111
+ if (cols < 2 || rows < 2) return;
112
+ lastSizeRef.current = { cols, rows };
108
113
  const ws = useGrooveStore.getState().ws;
109
114
  if (ws?.readyState === WebSocket.OPEN && termIdRef.current) {
110
115
  ws.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows, cols }));
@@ -119,6 +124,7 @@ function TerminalInstance({ tabId, visible, registerKill }) {
119
124
  });
120
125
 
121
126
  const observer = new ResizeObserver(() => {
127
+ if (!visibleRef.current) return;
122
128
  requestAnimationFrame(() => { try { fitAddon.fit(); } catch {} });
123
129
  });
124
130
  observer.observe(containerRef.current);
@@ -135,6 +141,7 @@ function TerminalInstance({ tabId, visible, registerKill }) {
135
141
  }, []);
136
142
 
137
143
  useEffect(() => {
144
+ visibleRef.current = visible;
138
145
  if (visible && fitRef.current) {
139
146
  requestAnimationFrame(() => {
140
147
  try { fitRef.current.fit(); } catch {}
@@ -5,23 +5,23 @@ import { ScrollArea } from '../ui/scroll-area';
5
5
  import { Button } from '../ui/button';
6
6
  import { Tooltip } from '../ui/tooltip';
7
7
  import { cn } from '../../lib/cn';
8
- import { Send, Plus, ChevronDown, Loader2, Square, Clock, Zap, User, Bot } from 'lucide-react';
8
+ import { Send, Plus, ChevronDown, Square, Clock, Zap, Bot } from 'lucide-react';
9
9
 
10
10
  function MessageMetrics({ metrics }) {
11
11
  if (!metrics) return null;
12
12
  return (
13
- <div className="flex items-center gap-3 mt-1.5 pl-3">
13
+ <div className="flex items-center gap-3 mt-1 ml-5">
14
14
  {metrics.ttft != null && (
15
15
  <Tooltip content="Time to first token">
16
16
  <span className="text-2xs font-mono text-text-4 flex items-center gap-1">
17
- <Clock size={10} /> {Math.round(metrics.ttft)}ms
17
+ <Clock size={9} /> {Math.round(metrics.ttft)}ms
18
18
  </span>
19
19
  </Tooltip>
20
20
  )}
21
21
  {metrics.tokensPerSec != null && (
22
22
  <Tooltip content="Tokens per second">
23
23
  <span className="text-2xs font-mono text-text-4 flex items-center gap-1">
24
- <Zap size={10} /> {metrics.tokensPerSec.toFixed(1)} t/s
24
+ <Zap size={9} /> {metrics.tokensPerSec.toFixed(1)} t/s
25
25
  </span>
26
26
  </Tooltip>
27
27
  )}
@@ -37,9 +37,9 @@ function MessageMetrics({ metrics }) {
37
37
 
38
38
  function UserMessage({ msg }) {
39
39
  return (
40
- <div className="flex justify-end">
41
- <div className="max-w-[85%]">
42
- <div className="px-3 py-2 rounded-xl rounded-br-sm bg-accent/10 border border-accent/15">
40
+ <div className="flex justify-end animate-chat-fade-in">
41
+ <div className="max-w-[80%]">
42
+ <div className="px-3.5 py-2 bg-accent/8 rounded rounded-br-none">
43
43
  <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.content}</p>
44
44
  </div>
45
45
  </div>
@@ -51,35 +51,35 @@ function AssistantMessage({ msg, streaming }) {
51
51
  const isStreaming = streaming && !msg.content && !msg.reasoning && !msg.error;
52
52
  const isReasoning = streaming && msg.reasoning && !msg.content;
53
53
  return (
54
- <div>
55
- <div className="text-2xs text-text-3 font-sans mb-0.5 font-medium flex items-center gap-1">
56
- <Bot size={10} /> Assistant
54
+ <div className="animate-chat-fade-in">
55
+ <div className="flex items-center gap-1.5 mb-1">
56
+ <div className="w-4 h-4 rounded-sm bg-surface-4 flex items-center justify-center">
57
+ <Bot size={10} className="text-text-3" />
58
+ </div>
59
+ <span className="text-2xs text-text-3 font-sans font-medium">Assistant</span>
57
60
  </div>
58
61
  {msg.reasoning && (
59
- <div className="border-l-2 border-text-4/30 pl-3 py-0.5 mb-1">
60
- <div className="text-2xs font-sans text-text-3 italic whitespace-pre-wrap break-words leading-relaxed">
62
+ <div className="ml-5 mb-1.5 pl-3 border-l border-text-4/20 py-1">
63
+ <div className="text-2xs font-sans text-text-4 italic whitespace-pre-wrap break-words leading-relaxed">
61
64
  {msg.reasoning}
62
- {isReasoning && <span className="inline-block w-1 h-3 bg-text-4/50 ml-0.5 animate-pulse" />}
65
+ {isReasoning && <span className="inline-block w-1 h-3 bg-text-4/40 ml-0.5 animate-pulse" />}
63
66
  </div>
64
67
  </div>
65
68
  )}
66
- <div className={cn(
67
- 'border-l-2 pl-3 py-0.5',
68
- msg.error ? 'border-danger/40' : 'border-accent/40',
69
- )}>
69
+ <div className="ml-5">
70
70
  {msg.content ? (
71
71
  <div className={cn(
72
72
  'text-xs font-sans whitespace-pre-wrap break-words leading-relaxed',
73
73
  msg.error ? 'text-danger' : 'text-text-1',
74
74
  )}>
75
75
  {msg.content}
76
- {streaming && !msg.error && <span className="inline-block w-1.5 h-3.5 bg-accent/70 ml-0.5 animate-pulse" />}
76
+ {streaming && !msg.error && <span className="inline-block w-1.5 h-3.5 bg-accent/60 ml-0.5 animate-pulse" />}
77
77
  </div>
78
78
  ) : isStreaming ? (
79
- <div className="flex items-center gap-1 py-1">
80
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" />
81
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '150ms' }} />
82
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '300ms' }} />
79
+ <div className="flex items-center gap-1.5 py-1">
80
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" />
81
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '150ms' }} />
82
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '300ms' }} />
83
83
  </div>
84
84
  ) : null}
85
85
  </div>
@@ -99,19 +99,19 @@ function SessionSelector({ sessions, activeSession, onSelect, onNew }) {
99
99
  <div className="relative">
100
100
  <button
101
101
  onClick={() => setOpen(!open)}
102
- className="flex items-center gap-1.5 px-2 py-1 rounded-md text-2xs font-sans text-text-3 hover:text-text-1 hover:bg-surface-5/50 transition-colors cursor-pointer"
102
+ className="flex items-center gap-1 px-2 py-1 text-2xs font-sans text-text-3 hover:text-text-1 transition-colors cursor-pointer"
103
103
  >
104
104
  {label} <ChevronDown size={10} />
105
105
  </button>
106
106
  {open && (
107
- <div className="absolute top-full left-0 mt-1 z-20 min-w-40 bg-surface-1 border border-border rounded-md shadow-xl py-1">
107
+ <div className="absolute top-full left-0 mt-1 z-20 min-w-40 bg-surface-2 border border-border rounded py-1 shadow-xl">
108
108
  {sessions.map((sess, i) => (
109
109
  <button
110
110
  key={sess.id}
111
111
  onClick={() => { onSelect(sess.id); setOpen(false); }}
112
112
  className={cn(
113
113
  'w-full text-left px-3 py-1.5 text-xs font-sans cursor-pointer transition-colors',
114
- sess.id === activeSession ? 'text-accent bg-accent/10' : 'text-text-2 hover:bg-surface-5 hover:text-text-0',
114
+ sess.id === activeSession ? 'text-accent bg-accent/8' : 'text-text-2 hover:bg-surface-4 hover:text-text-0',
115
115
  )}
116
116
  >
117
117
  Session {i + 1}
@@ -165,11 +165,10 @@ export function ChatPlayground() {
165
165
  const canSend = input.trim() && activeRuntime && activeModel && !streaming;
166
166
 
167
167
  return (
168
- <div className="h-full flex flex-col bg-surface-0 rounded-lg border border-border overflow-hidden">
169
- {/* Header */}
170
- <div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle bg-surface-1">
171
- <div className="flex items-center gap-2">
172
- <span className="text-xs font-semibold font-sans text-text-1">Playground</span>
168
+ <div className="h-full flex flex-col">
169
+ {/* Toolbar */}
170
+ <div className="flex-shrink-0 flex items-center justify-between px-4 pb-2">
171
+ <div className="flex items-center gap-1">
173
172
  <SessionSelector
174
173
  sessions={sessions}
175
174
  activeSession={activeSession}
@@ -178,20 +177,25 @@ export function ChatPlayground() {
178
177
  />
179
178
  </div>
180
179
  <Tooltip content="New session">
181
- <Button variant="ghost" size="sm" onClick={newSession}>
182
- <Plus size={12} className="mr-1" /> New
183
- </Button>
180
+ <button
181
+ onClick={newSession}
182
+ className="flex items-center gap-1 px-2 py-1 text-2xs font-sans text-text-3 hover:text-text-1 transition-colors cursor-pointer"
183
+ >
184
+ <Plus size={11} /> New
185
+ </button>
184
186
  </Tooltip>
185
187
  </div>
186
188
 
187
189
  {/* Messages */}
188
190
  <ScrollArea ref={scrollRef} className="flex-1 min-h-0">
189
- <div className="px-4 py-4 space-y-4">
191
+ <div className="px-4 py-3 space-y-5">
190
192
  {messages.length === 0 ? (
191
- <div className="flex flex-col items-center justify-center py-16 text-center">
192
- <Bot size={32} className="text-text-4 mb-3" />
193
+ <div className="flex flex-col items-center justify-center py-20 text-center">
194
+ <div className="w-10 h-10 rounded bg-surface-2 flex items-center justify-center mb-3">
195
+ <Bot size={20} className="text-text-4" />
196
+ </div>
193
197
  <p className="text-sm text-text-2 font-sans font-medium">Start a conversation</p>
194
- <p className="text-xs text-text-3 font-sans mt-1">
198
+ <p className="text-xs text-text-4 font-sans mt-1">
195
199
  {!activeRuntime ? 'Select a runtime to get started' : !activeModel ? 'Select a model to get started' : 'Send a message to test your model'}
196
200
  </p>
197
201
  </div>
@@ -212,8 +216,8 @@ export function ChatPlayground() {
212
216
  </ScrollArea>
213
217
 
214
218
  {/* Input */}
215
- <div className="flex-shrink-0 px-3 py-3 border-t border-border-subtle bg-surface-1">
216
- <div className="flex items-end gap-2">
219
+ <div className="flex-shrink-0 px-4 py-3">
220
+ <div className="flex items-end gap-2 bg-surface-1 border border-border-subtle rounded-md p-1.5 focus-within:border-accent/30 transition-colors">
217
221
  <textarea
218
222
  ref={inputRef}
219
223
  value={input}
@@ -223,29 +227,33 @@ export function ChatPlayground() {
223
227
  disabled={!activeRuntime || !activeModel}
224
228
  rows={1}
225
229
  className={cn(
226
- 'flex-1 resize-none bg-surface-0 border border-border rounded-lg px-3 py-2',
230
+ 'flex-1 resize-none bg-transparent px-2 py-1.5',
227
231
  'text-xs text-text-0 font-sans placeholder:text-text-4',
228
- 'focus:outline-none focus:ring-1 focus:ring-accent',
232
+ 'focus:outline-none',
229
233
  'disabled:opacity-40 disabled:cursor-not-allowed',
230
- 'min-h-[36px] max-h-32',
234
+ 'min-h-[28px] max-h-32',
231
235
  )}
232
236
  style={{ height: 'auto', overflowY: input.split('\n').length > 4 ? 'auto' : 'hidden' }}
233
237
  onInput={(e) => { e.target.style.height = 'auto'; e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`; }}
234
238
  />
235
239
  {streaming ? (
236
- <Button variant="danger" size="icon" className="flex-shrink-0" onClick={() => {}}>
237
- <Square size={14} />
238
- </Button>
240
+ <button
241
+ onClick={() => useGrooveStore.getState().stopLabInference()}
242
+ className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm bg-danger/15 text-danger hover:bg-danger/25 transition-colors cursor-pointer"
243
+ >
244
+ <Square size={12} />
245
+ </button>
239
246
  ) : (
240
- <Button
241
- variant="primary"
242
- size="icon"
243
- className="flex-shrink-0"
247
+ <button
244
248
  disabled={!canSend}
245
249
  onClick={handleSend}
250
+ className={cn(
251
+ 'flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm transition-colors cursor-pointer',
252
+ canSend ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
253
+ )}
246
254
  >
247
- <Send size={14} />
248
- </Button>
255
+ <Send size={12} />
256
+ </button>
249
257
  )}
250
258
  </div>
251
259
  </div>
@@ -2,21 +2,20 @@
2
2
  import { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { ScrollArea } from '../ui/scroll-area';
5
- import { Button } from '../ui/button';
6
5
  import { Badge } from '../ui/badge';
7
6
  import { cn } from '../../lib/cn';
8
- import { Send, X, Bot, ArrowRight, Loader2 } from 'lucide-react';
7
+ import { Send, X, Bot, ArrowRight } from 'lucide-react';
9
8
 
10
9
  function AssistantMessage({ msg }) {
11
10
  return (
12
- <div>
13
- <div className="text-2xs text-text-3 font-sans mb-0.5 font-medium flex items-center gap-1">
14
- <Bot size={10} /> Lab Assistant
11
+ <div className="animate-chat-fade-in">
12
+ <div className="flex items-center gap-1.5 mb-1">
13
+ <div className="w-4 h-4 rounded-sm bg-surface-4 flex items-center justify-center">
14
+ <Bot size={10} className="text-text-3" />
15
+ </div>
16
+ <span className="text-2xs text-text-3 font-sans font-medium">Lab Assistant</span>
15
17
  </div>
16
- <div className={cn(
17
- 'border-l-2 pl-3 py-0.5',
18
- msg.error ? 'border-danger/40' : 'border-accent/40',
19
- )}>
18
+ <div className="ml-5">
20
19
  <div className={cn(
21
20
  'text-xs font-sans whitespace-pre-wrap break-words leading-relaxed',
22
21
  msg.error ? 'text-danger' : 'text-text-1',
@@ -30,9 +29,9 @@ function AssistantMessage({ msg }) {
30
29
 
31
30
  function UserMessage({ msg }) {
32
31
  return (
33
- <div className="flex justify-end">
34
- <div className="max-w-[85%]">
35
- <div className="px-3 py-2 rounded-xl rounded-br-sm bg-accent/10 border border-accent/15">
32
+ <div className="flex justify-end animate-chat-fade-in">
33
+ <div className="max-w-[80%]">
34
+ <div className="px-3.5 py-2 bg-accent/8 rounded rounded-br-none">
36
35
  <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
37
36
  </div>
38
37
  </div>
@@ -84,13 +83,12 @@ export function LabAssistant() {
84
83
  if (!agentId) return null;
85
84
 
86
85
  return (
87
- <div className="h-full flex flex-col bg-surface-0 rounded-lg border border-border overflow-hidden">
86
+ <div className="h-full flex flex-col">
88
87
  {/* Header */}
89
- <div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle bg-surface-1">
88
+ <div className="flex-shrink-0 flex items-center justify-between px-4 pb-2">
90
89
  <div className="flex items-center gap-2">
91
- <span className="text-xs font-semibold font-sans text-text-1">Lab Assistant</span>
92
90
  {backend && (
93
- <Badge variant="accent" className="text-2xs">{backend}</Badge>
91
+ <span className="text-2xs font-mono text-text-3">{backend}</span>
94
92
  )}
95
93
  {agent && (
96
94
  <Badge
@@ -103,7 +101,7 @@ export function LabAssistant() {
103
101
  </div>
104
102
  <button
105
103
  onClick={dismissLabAssistant}
106
- className="p-1 rounded text-text-4 hover:text-text-1 hover:bg-surface-5/50 transition-colors cursor-pointer"
104
+ className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
107
105
  >
108
106
  <X size={14} />
109
107
  </button>
@@ -111,12 +109,14 @@ export function LabAssistant() {
111
109
 
112
110
  {/* Messages */}
113
111
  <ScrollArea ref={scrollRef} className="flex-1 min-h-0">
114
- <div className="px-4 py-4 space-y-4">
112
+ <div className="px-4 py-3 space-y-5">
115
113
  {messages.length === 0 && !isThinking ? (
116
- <div className="flex flex-col items-center justify-center py-16 text-center">
117
- <Bot size={32} className="text-text-4 mb-3" />
114
+ <div className="flex flex-col items-center justify-center py-20 text-center">
115
+ <div className="w-10 h-10 rounded bg-surface-2 flex items-center justify-center mb-3">
116
+ <Bot size={20} className="text-text-4" />
117
+ </div>
118
118
  <p className="text-sm text-text-2 font-sans font-medium">Setting up {backend}</p>
119
- <p className="text-xs text-text-3 font-sans mt-1">The assistant is starting up...</p>
119
+ <p className="text-xs text-text-4 font-sans mt-1">The assistant is starting up...</p>
120
120
  </div>
121
121
  ) : (
122
122
  messages.map((msg, i) =>
@@ -128,18 +128,18 @@ export function LabAssistant() {
128
128
  )
129
129
  )}
130
130
 
131
- {/* Thinking indicator */}
132
131
  {isThinking && (
133
- <div>
134
- <div className="text-2xs text-text-3 font-sans mb-0.5 font-medium flex items-center gap-1">
135
- <Bot size={10} /> Lab Assistant
136
- </div>
137
- <div className="border-l-2 border-accent/40 pl-3 py-0.5">
138
- <div className="flex items-center gap-1 py-1">
139
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" />
140
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '150ms' }} />
141
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '300ms' }} />
132
+ <div className="animate-chat-fade-in">
133
+ <div className="flex items-center gap-1.5 mb-1">
134
+ <div className="w-4 h-4 rounded-sm bg-surface-4 flex items-center justify-center">
135
+ <Bot size={10} className="text-text-3" />
142
136
  </div>
137
+ <span className="text-2xs text-text-3 font-sans font-medium">Lab Assistant</span>
138
+ </div>
139
+ <div className="ml-5 flex items-center gap-1.5 py-1">
140
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" />
141
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '150ms' }} />
142
+ <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '300ms' }} />
143
143
  </div>
144
144
  </div>
145
145
  )}
@@ -148,23 +148,22 @@ export function LabAssistant() {
148
148
 
149
149
  {/* Completion banner */}
150
150
  {isComplete && messages.length > 0 && (
151
- <div className="flex-shrink-0 px-3 py-2 border-t border-border-subtle bg-success/5">
151
+ <div className="flex-shrink-0 px-4 py-2 bg-success/5 border-t border-success/10">
152
152
  <div className="flex items-center justify-between">
153
153
  <span className="text-xs font-sans text-success font-medium">Setup complete</span>
154
- <Button
155
- variant="primary"
156
- size="sm"
154
+ <button
157
155
  onClick={() => setLabAssistantMode(false)}
156
+ className="flex items-center gap-1 px-3 py-1.5 text-xs font-sans font-medium text-surface-0 bg-accent hover:bg-accent/90 rounded-sm transition-colors cursor-pointer"
158
157
  >
159
- <ArrowRight size={12} className="mr-1" /> Switch to Playground
160
- </Button>
158
+ <ArrowRight size={12} /> Playground
159
+ </button>
161
160
  </div>
162
161
  </div>
163
162
  )}
164
163
 
165
164
  {/* Input */}
166
- <div className="flex-shrink-0 px-3 py-3 border-t border-border-subtle bg-surface-1">
167
- <div className="flex items-end gap-2">
165
+ <div className="flex-shrink-0 px-4 py-3">
166
+ <div className="flex items-end gap-2 bg-surface-1 border border-border-subtle rounded-md p-1.5 focus-within:border-accent/30 transition-colors">
168
167
  <textarea
169
168
  ref={inputRef}
170
169
  value={input}
@@ -174,24 +173,25 @@ export function LabAssistant() {
174
173
  disabled={!isRunning}
175
174
  rows={1}
176
175
  className={cn(
177
- 'flex-1 resize-none bg-surface-0 border border-border rounded-lg px-3 py-2',
176
+ 'flex-1 resize-none bg-transparent px-2 py-1.5',
178
177
  'text-xs text-text-0 font-sans placeholder:text-text-4',
179
- 'focus:outline-none focus:ring-1 focus:ring-accent',
178
+ 'focus:outline-none',
180
179
  'disabled:opacity-40 disabled:cursor-not-allowed',
181
- 'min-h-[36px] max-h-32',
180
+ 'min-h-[28px] max-h-32',
182
181
  )}
183
182
  style={{ height: 'auto', overflowY: input.split('\n').length > 4 ? 'auto' : 'hidden' }}
184
183
  onInput={(e) => { e.target.style.height = 'auto'; e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`; }}
185
184
  />
186
- <Button
187
- variant="primary"
188
- size="icon"
189
- className="flex-shrink-0"
185
+ <button
190
186
  disabled={!input.trim() || !isRunning}
191
187
  onClick={handleSend}
188
+ className={cn(
189
+ 'flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm transition-colors cursor-pointer',
190
+ input.trim() && isRunning ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
191
+ )}
192
192
  >
193
- <Send size={14} />
194
- </Button>
193
+ <Send size={12} />
194
+ </button>
195
195
  </div>
196
196
  </div>
197
197
  </div>
@@ -1,8 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useGrooveStore } from '../../stores/groove';
3
- import { Button } from '../ui/button';
4
3
  import { Tooltip } from '../ui/tooltip';
5
- import { Badge } from '../ui/badge';
6
4
  import { cn } from '../../lib/cn';
7
5
  import { fmtNum } from '../../lib/format';
8
6
  import { HEX, hexAlpha } from '../../lib/theme-hex';
@@ -11,24 +9,24 @@ import { Zap, Clock, Cpu, Hash, Timer, Link } from 'lucide-react';
11
9
  function TtftGauge({ value }) {
12
10
  if (value == null) {
13
11
  return (
14
- <div className="text-center py-3">
15
- <div className="text-2xl font-mono font-bold text-text-4">—</div>
16
- <div className="text-2xs text-text-4 font-sans mt-0.5">TTFT</div>
12
+ <div className="text-center py-4">
13
+ <div className="text-2xl font-mono font-bold text-text-4 tabular-nums">--</div>
14
+ <div className="text-2xs text-text-4 font-sans mt-1">TTFT</div>
17
15
  </div>
18
16
  );
19
17
  }
20
- const color = value < 200 ? 'success' : value < 500 ? 'warning' : 'danger';
18
+ const color = value < 200 ? 'text-success' : value < 500 ? 'text-warning' : 'text-danger';
21
19
  return (
22
- <div className="text-center py-3">
23
- <div className={cn('text-2xl font-mono font-bold', `text-${color}`)}>
20
+ <div className="text-center py-4">
21
+ <div className={cn('text-2xl font-mono font-bold tabular-nums', color)}>
24
22
  {Math.round(value)}
25
23
  </div>
26
- <div className="text-2xs text-text-3 font-sans mt-0.5">ms TTFT</div>
24
+ <div className="text-2xs text-text-3 font-sans mt-1">ms TTFT</div>
27
25
  </div>
28
26
  );
29
27
  }
30
28
 
31
- function Sparkline({ data, width = 120, height = 28, color = HEX.accent }) {
29
+ function Sparkline({ data, width = 140, height = 32, color = HEX.accent }) {
32
30
  if (!data || data.length < 2) {
33
31
  return <div className="flex-shrink-0" style={{ width, height }} />;
34
32
  }
@@ -49,22 +47,22 @@ function Sparkline({ data, width = 120, height = 28, color = HEX.accent }) {
49
47
  const fillPoints = `0,${height} ${points} ${width},${height}`;
50
48
 
51
49
  return (
52
- <svg width={width} height={height} className="flex-shrink-0">
53
- <polyline points={fillPoints} fill={hexAlpha(color, 0.1)} stroke="none" />
54
- <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" strokeOpacity="0.8" />
50
+ <svg width={width} height={height} className="w-full">
51
+ <polyline points={fillPoints} fill={hexAlpha(color, 0.08)} stroke="none" />
52
+ <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" strokeOpacity="0.7" />
55
53
  </svg>
56
54
  );
57
55
  }
58
56
 
59
57
  function MetricRow({ icon: Icon, label, value, unit, tooltip }) {
60
58
  const content = (
61
- <div className="flex items-center justify-between py-1.5">
59
+ <div className="flex items-center justify-between py-2">
62
60
  <div className="flex items-center gap-2">
63
- <Icon size={12} className="text-text-3 flex-shrink-0" />
64
- <span className="text-xs text-text-2 font-sans">{label}</span>
61
+ <Icon size={11} className="text-text-4 flex-shrink-0" />
62
+ <span className="text-2xs text-text-3 font-sans">{label}</span>
65
63
  </div>
66
- <span className="text-xs font-mono font-medium text-text-0">
67
- {value != null ? value : ''}{unit && value != null ? <span className="text-text-3 ml-0.5">{unit}</span> : ''}
64
+ <span className="text-xs font-mono font-medium text-text-1 tabular-nums">
65
+ {value != null ? value : '--'}{unit && value != null ? <span className="text-text-4 ml-0.5 text-2xs">{unit}</span> : ''}
68
66
  </span>
69
67
  </div>
70
68
  );
@@ -76,30 +74,30 @@ export function MetricsPanel() {
76
74
  const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
77
75
 
78
76
  return (
79
- <div className="space-y-4">
80
- <span className="text-xs font-semibold font-sans text-text-2 uppercase tracking-wider">Metrics</span>
81
-
77
+ <div className="space-y-5">
82
78
  {/* TTFT Gauge */}
83
- <div className="bg-surface-1 rounded-lg border border-border-subtle px-4 py-2">
84
- <TtftGauge value={metrics.ttft} />
85
- </div>
79
+ <TtftGauge value={metrics.ttft} />
80
+
81
+ <div className="h-px bg-border-subtle" />
86
82
 
87
83
  {/* Tokens/sec with sparkline */}
88
- <div className="bg-surface-1 rounded-lg border border-border-subtle px-4 py-3 space-y-2">
84
+ <div className="space-y-2">
89
85
  <div className="flex items-center justify-between">
90
- <div className="flex items-center gap-2">
91
- <Zap size={12} className="text-accent" />
92
- <span className="text-xs text-text-2 font-sans">Tokens/sec</span>
86
+ <div className="flex items-center gap-1.5">
87
+ <Zap size={11} className="text-accent" />
88
+ <span className="text-2xs text-text-3 font-sans">Tokens/sec</span>
93
89
  </div>
94
- <span className="text-sm font-mono font-bold text-text-0">
95
- {metrics.tokensPerSec != null ? metrics.tokensPerSec.toFixed(1) : ''}
90
+ <span className="text-sm font-mono font-bold text-text-0 tabular-nums">
91
+ {metrics.tokensPerSec != null ? metrics.tokensPerSec.toFixed(1) : '--'}
96
92
  </span>
97
93
  </div>
98
- <Sparkline data={metrics.tokensPerSecHistory} width={180} height={28} />
94
+ <Sparkline data={metrics.tokensPerSecHistory} />
99
95
  </div>
100
96
 
101
- {/* Stats grid */}
102
- <div className="bg-surface-1 rounded-lg border border-border-subtle px-4 py-2 divide-y divide-border-subtle">
97
+ <div className="h-px bg-border-subtle" />
98
+
99
+ {/* Stats */}
100
+ <div className="space-y-0">
103
101
  <MetricRow
104
102
  icon={Cpu}
105
103
  label="Memory"
@@ -124,11 +122,14 @@ export function MetricsPanel() {
124
122
 
125
123
  {/* Attach to agent */}
126
124
  {activeRuntime && (
127
- <Tooltip content="Use current preset when spawning a new agent">
128
- <Button variant="outline" size="sm" className="w-full">
129
- <Link size={12} className="mr-1.5" /> Attach to Agent
130
- </Button>
131
- </Tooltip>
125
+ <>
126
+ <div className="h-px bg-border-subtle" />
127
+ <Tooltip content="Use current preset when spawning a new agent">
128
+ <button className="w-full flex items-center justify-center gap-1.5 px-3 py-2 text-2xs font-sans text-text-3 hover:text-text-1 border border-border-subtle rounded-sm hover:border-border transition-colors cursor-pointer">
129
+ <Link size={11} /> Attach to Agent
130
+ </button>
131
+ </Tooltip>
132
+ </>
132
133
  )}
133
134
  </div>
134
135
  );