groove-dev 0.27.145 → 0.27.147
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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +12 -6
- package/node_modules/@groove-dev/daemon/src/conversations.js +41 -10
- package/node_modules/@groove-dev/daemon/src/introducer.js +20 -0
- package/node_modules/@groove-dev/daemon/src/process.js +262 -15
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -3
- package/node_modules/@groove-dev/daemon/src/rotator.js +15 -3
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +43 -0
- package/node_modules/@groove-dev/daemon/templates/lab-general.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/llama-cpp-setup.json +12 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +1 -8
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +135 -13
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +21 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +6 -5
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +1 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +24 -1
- package/node_modules/@groove-dev/gui/src/components/ui/question-modal.jsx +107 -0
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +10 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +1 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +27 -22
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +12 -6
- package/packages/daemon/src/conversations.js +41 -10
- package/packages/daemon/src/introducer.js +20 -0
- package/packages/daemon/src/process.js +262 -15
- package/packages/daemon/src/providers/groove-network.js +1 -3
- package/packages/daemon/src/rotator.js +15 -3
- package/packages/daemon/src/routes/agents.js +43 -0
- package/packages/daemon/templates/lab-general.json +12 -0
- package/packages/daemon/templates/llama-cpp-setup.json +12 -0
- package/packages/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/packages/gui/dist/assets/index-CEkPsSAm.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/packages/gui/src/components/chat/chat-header.jsx +1 -8
- package/packages/gui/src/components/chat/chat-input.jsx +135 -13
- package/packages/gui/src/components/chat/chat-messages.jsx +21 -4
- package/packages/gui/src/components/chat/chat-view.jsx +6 -5
- package/packages/gui/src/components/chat/model-picker.jsx +3 -3
- package/packages/gui/src/components/lab/chat-playground.jsx +3 -3
- package/packages/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/packages/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/packages/gui/src/components/lab/parameter-panel.jsx +5 -5
- package/packages/gui/src/components/lab/runtime-config.jsx +1 -3
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/status-bar.jsx +24 -1
- package/packages/gui/src/components/ui/question-modal.jsx +107 -0
- package/packages/gui/src/components/ui/sheet.jsx +2 -2
- package/packages/gui/src/stores/groove.js +32 -2
- package/packages/gui/src/stores/slices/agents-slice.js +10 -1
- package/packages/gui/src/stores/slices/chat-slice.js +1 -0
- package/packages/gui/src/views/model-lab.jsx +27 -22
- package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +0 -1006
- package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +0 -1
- package/packages/gui/dist/assets/index-Bxc0gU06.js +0 -1006
- package/packages/gui/dist/assets/index-C0pztKBn.css +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
-
import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal } from 'lucide-react';
|
|
3
|
+
import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal, ChevronUp } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
|
-
import { formatModelName } from './model-picker';
|
|
5
|
+
import { formatModelName, isImageModel as checkImageModel, getTier, getContextSize, TIER_CONFIG } from './model-picker';
|
|
6
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
7
|
+
import { Badge } from '../ui/badge';
|
|
6
8
|
|
|
7
9
|
const EFFORT_OPTIONS = [
|
|
8
10
|
{ value: 'none', label: 'None' },
|
|
@@ -17,16 +19,37 @@ const VERBOSITY_OPTIONS = [
|
|
|
17
19
|
{ value: 'medium', label: 'Normal' },
|
|
18
20
|
];
|
|
19
21
|
|
|
20
|
-
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
|
|
22
|
+
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, currentProvider, onModelChange, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
|
|
21
23
|
const [input, setInput] = useState('');
|
|
22
24
|
const [inputHeight, setInputHeight] = useState(88);
|
|
25
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
23
26
|
const textareaRef = useRef(null);
|
|
24
27
|
const fileInputRef = useRef(null);
|
|
28
|
+
const modelPickerRef = useRef(null);
|
|
29
|
+
|
|
30
|
+
const [providers, setProviders] = useState([]);
|
|
31
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
fetchProviders().then((data) => {
|
|
35
|
+
if (Array.isArray(data)) setProviders(data);
|
|
36
|
+
else if (data?.providers) setProviders(data.providers);
|
|
37
|
+
}).catch(() => {});
|
|
38
|
+
}, [fetchProviders]);
|
|
25
39
|
|
|
26
40
|
useEffect(() => {
|
|
27
41
|
if (!disabled && textareaRef.current) textareaRef.current.focus();
|
|
28
42
|
}, [disabled]);
|
|
29
43
|
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!modelPickerOpen) return;
|
|
46
|
+
function handleClick(e) {
|
|
47
|
+
if (modelPickerRef.current && !modelPickerRef.current.contains(e.target)) setModelPickerOpen(false);
|
|
48
|
+
}
|
|
49
|
+
document.addEventListener('mousedown', handleClick);
|
|
50
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
51
|
+
}, [modelPickerOpen]);
|
|
52
|
+
|
|
30
53
|
function handleSend() {
|
|
31
54
|
const text = input.trim();
|
|
32
55
|
if (!text || sending || disabled) return;
|
|
@@ -160,17 +183,116 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
160
183
|
</button>
|
|
161
184
|
</div>
|
|
162
185
|
|
|
163
|
-
{
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
<div ref={modelPickerRef} className="relative">
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => setModelPickerOpen(!modelPickerOpen)}
|
|
189
|
+
className={cn(
|
|
190
|
+
'flex items-center gap-1 h-5 px-2 rounded text-2xs font-mono transition-colors cursor-pointer',
|
|
191
|
+
isImageModel
|
|
192
|
+
? 'bg-purple/8 text-purple hover:bg-purple/15'
|
|
193
|
+
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
170
196
|
{isImageModel && <ImageIcon size={9} />}
|
|
171
|
-
<span className="max-w-[
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
<span className="max-w-[120px] truncate">{currentModel ? formatModelName(currentModel) : 'Select model'}</span>
|
|
198
|
+
<ChevronUp size={10} className="text-text-4 flex-shrink-0" />
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
{modelPickerOpen && (() => {
|
|
202
|
+
const chatProviders = [];
|
|
203
|
+
const imageProviders = [];
|
|
204
|
+
for (const provider of providers) {
|
|
205
|
+
const models = provider.models || [];
|
|
206
|
+
const chat = [];
|
|
207
|
+
const img = [];
|
|
208
|
+
for (const m of models) {
|
|
209
|
+
const mid = typeof m === 'string' ? m : m.id || m.name;
|
|
210
|
+
const mtype = typeof m === 'object' ? m.type : undefined;
|
|
211
|
+
if (mtype === 'image' || checkImageModel(mid)) img.push(m);
|
|
212
|
+
else chat.push(m);
|
|
213
|
+
}
|
|
214
|
+
if (chat.length) chatProviders.push({ ...provider, models: chat });
|
|
215
|
+
if (img.length) imageProviders.push({ ...provider, models: img });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const renderModel = (provider, model) => {
|
|
219
|
+
const modelId = typeof model === 'string' ? model : model.id || model.name;
|
|
220
|
+
const modelName = typeof model === 'string' ? model : model.name || model.id;
|
|
221
|
+
const mtype = typeof model === 'object' ? model.type : undefined;
|
|
222
|
+
const isImg = mtype === 'image' || checkImageModel(modelId);
|
|
223
|
+
const tier = isImg ? null : getTier(modelId);
|
|
224
|
+
const tierCfg = tier ? TIER_CONFIG[tier] : null;
|
|
225
|
+
const TierIcon = tierCfg?.icon;
|
|
226
|
+
const isActive = currentModel === modelId && currentProvider === provider.id;
|
|
227
|
+
return (
|
|
228
|
+
<button
|
|
229
|
+
key={modelId}
|
|
230
|
+
onClick={() => {
|
|
231
|
+
onModelChange?.({ provider: provider.id, model: modelId });
|
|
232
|
+
setModelPickerOpen(false);
|
|
233
|
+
}}
|
|
234
|
+
className={cn(
|
|
235
|
+
'w-full flex items-center gap-3 px-3.5 py-2.5 text-left transition-colors cursor-pointer',
|
|
236
|
+
isActive ? 'bg-accent/10' : 'hover:bg-surface-3',
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
<div className="flex-1 min-w-0">
|
|
240
|
+
<div className="flex items-center gap-1.5">
|
|
241
|
+
{isImg && <ImageIcon size={11} className="text-purple flex-shrink-0" />}
|
|
242
|
+
<span className={cn('text-xs font-medium font-sans truncate', isActive ? 'text-accent' : 'text-text-0')}>{modelName}</span>
|
|
243
|
+
</div>
|
|
244
|
+
{!isImg && (
|
|
245
|
+
<div className="text-2xs text-text-4 font-sans mt-0.5">{getContextSize(modelId)} context</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
249
|
+
{isImg ? (
|
|
250
|
+
<Badge variant="purple" className="text-[9px]">
|
|
251
|
+
<ImageIcon size={8} /> Image
|
|
252
|
+
</Badge>
|
|
253
|
+
) : tierCfg && (
|
|
254
|
+
<Badge variant={tierCfg.variant} className="text-[9px]">
|
|
255
|
+
<TierIcon size={8} /> {tierCfg.label}
|
|
256
|
+
</Badge>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
</button>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div className="absolute bottom-full left-0 mb-1.5 w-80 max-h-[60vh] overflow-y-auto rounded-xl border border-border bg-surface-1 shadow-2xl z-50">
|
|
265
|
+
{providers.length === 0 && (
|
|
266
|
+
<div className="px-4 py-8 text-center text-xs text-text-3 font-sans">No providers available</div>
|
|
267
|
+
)}
|
|
268
|
+
{chatProviders.map((provider) => (
|
|
269
|
+
<div key={provider.id}>
|
|
270
|
+
<div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle sticky top-0 backdrop-blur-sm">
|
|
271
|
+
{provider.name || provider.id}
|
|
272
|
+
</div>
|
|
273
|
+
{provider.models.map((m) => renderModel(provider, m))}
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
{imageProviders.length > 0 && (
|
|
277
|
+
<>
|
|
278
|
+
<div className="px-3.5 py-2 text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans bg-surface-0 border-y border-border-subtle flex items-center gap-1.5 sticky top-0 backdrop-blur-sm">
|
|
279
|
+
<ImageIcon size={10} className="text-purple" />
|
|
280
|
+
Image Generation
|
|
281
|
+
</div>
|
|
282
|
+
{imageProviders.map((provider) => (
|
|
283
|
+
<div key={`img-${provider.id}`}>
|
|
284
|
+
<div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle">
|
|
285
|
+
{provider.name || provider.id}
|
|
286
|
+
</div>
|
|
287
|
+
{provider.models.map((m) => renderModel(provider, m))}
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
})()}
|
|
295
|
+
</div>
|
|
174
296
|
|
|
175
297
|
{isCodex && (
|
|
176
298
|
<>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw, Loader2 } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { timeAgo } from '../../lib/format';
|
|
6
6
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
@@ -435,7 +435,23 @@ function ApiTypingIndicator() {
|
|
|
435
435
|
);
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
|
|
438
|
+
function ToolCallIndicator({ tool }) {
|
|
439
|
+
return (
|
|
440
|
+
<div className="max-w-[85%] py-1">
|
|
441
|
+
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg bg-surface-3/50 border border-border-subtle/30">
|
|
442
|
+
<Loader2 size={13} className="text-accent animate-spin flex-shrink-0" />
|
|
443
|
+
<div className="flex-1 min-w-0">
|
|
444
|
+
<span className="text-xs font-semibold font-sans text-text-2">{tool.name}</span>
|
|
445
|
+
{tool.summary && (
|
|
446
|
+
<span className="text-xs font-mono text-text-4 ml-1.5 truncate">{tool.summary}</span>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function ChatMessages({ messages, isStreaming, model, mode, onImageReply, role, activeTool }) {
|
|
439
455
|
const scrollRef = useRef(null);
|
|
440
456
|
const isAtBottomRef = useRef(true);
|
|
441
457
|
|
|
@@ -453,7 +469,7 @@ export function ChatMessages({ messages, isStreaming, model, mode, onImageReply,
|
|
|
453
469
|
if (isAtBottomRef.current && scrollRef.current) {
|
|
454
470
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
455
471
|
}
|
|
456
|
-
}, [messages?.length, isStreaming]);
|
|
472
|
+
}, [messages?.length, isStreaming, activeTool]);
|
|
457
473
|
|
|
458
474
|
if (!messages || messages.length === 0) {
|
|
459
475
|
return (
|
|
@@ -472,7 +488,8 @@ export function ChatMessages({ messages, isStreaming, model, mode, onImageReply,
|
|
|
472
488
|
if (msg.from === 'system') return <SystemMessage key={i} msg={msg} />;
|
|
473
489
|
return <AssistantMessage key={i} msg={msg} model={model} role={role} />;
|
|
474
490
|
})}
|
|
475
|
-
{
|
|
491
|
+
{activeTool && <ToolCallIndicator tool={activeTool} />}
|
|
492
|
+
{isStreaming && !activeTool && (
|
|
476
493
|
mode === 'agent' ? (
|
|
477
494
|
<ThinkingIndicator className="py-1" />
|
|
478
495
|
) : (
|
|
@@ -33,6 +33,7 @@ export function ChatView() {
|
|
|
33
33
|
const conversationMessages = useGrooveStore((s) => s.conversationMessages);
|
|
34
34
|
const sendingMessage = useGrooveStore((s) => s.sendingMessage);
|
|
35
35
|
const streamingConversationId = useGrooveStore((s) => s.streamingConversationId);
|
|
36
|
+
const conversationActiveTools = useGrooveStore((s) => s.conversationActiveTools);
|
|
36
37
|
const createConversation = useGrooveStore((s) => s.createConversation);
|
|
37
38
|
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
38
39
|
const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
|
|
@@ -62,6 +63,7 @@ export function ChatView() {
|
|
|
62
63
|
const isCodexProvider = activeConversation?.provider === 'codex';
|
|
63
64
|
const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
|
|
64
65
|
const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
|
|
66
|
+
const activeTool = activeConversationId ? (conversationActiveTools?.[activeConversationId] || null) : null;
|
|
65
67
|
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
66
68
|
|
|
67
69
|
const handleNewChat = useCallback(async (provider, model) => {
|
|
@@ -126,10 +128,6 @@ export function ChatView() {
|
|
|
126
128
|
setConversationVerbosity(activeConversationId, verbosity);
|
|
127
129
|
}, [activeConversationId, setConversationVerbosity]);
|
|
128
130
|
|
|
129
|
-
const currentModel = activeConversation
|
|
130
|
-
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
131
|
-
: null;
|
|
132
|
-
|
|
133
131
|
return (
|
|
134
132
|
<div className="flex h-full bg-surface-0">
|
|
135
133
|
{/* Conversation sidebar */}
|
|
@@ -154,7 +152,7 @@ export function ChatView() {
|
|
|
154
152
|
|
|
155
153
|
{activeConversation ? (
|
|
156
154
|
<>
|
|
157
|
-
<ChatHeader conversation={activeConversation}
|
|
155
|
+
<ChatHeader conversation={activeConversation} role={activeRole} onRoleChange={handleRoleChange} sidebarCollapsed={sidebarCollapsed} />
|
|
158
156
|
<ChatMessages
|
|
159
157
|
messages={messages}
|
|
160
158
|
isStreaming={isStreaming}
|
|
@@ -162,6 +160,7 @@ export function ChatView() {
|
|
|
162
160
|
mode={activeConversation.mode || 'api'}
|
|
163
161
|
onImageReply={handleImageReply}
|
|
164
162
|
role={activeRole}
|
|
163
|
+
activeTool={activeTool}
|
|
165
164
|
/>
|
|
166
165
|
<ChatInput
|
|
167
166
|
onSend={handleSend}
|
|
@@ -171,6 +170,8 @@ export function ChatView() {
|
|
|
171
170
|
disabled={false}
|
|
172
171
|
isImageModel={currentModelIsImage}
|
|
173
172
|
currentModel={activeConversation.model}
|
|
173
|
+
currentProvider={activeConversation.provider}
|
|
174
|
+
onModelChange={handleModelChange}
|
|
174
175
|
replyContext={replyContext}
|
|
175
176
|
onClearReply={() => setReplyContext(null)}
|
|
176
177
|
role={activeRole}
|
|
@@ -15,13 +15,13 @@ export function formatModelName(id) {
|
|
|
15
15
|
.join(' ');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const TIER_CONFIG = {
|
|
18
|
+
export const TIER_CONFIG = {
|
|
19
19
|
frontier: { label: 'Frontier', variant: 'purple', icon: Sparkles },
|
|
20
20
|
mid: { label: 'Mid', variant: 'accent', icon: Zap },
|
|
21
21
|
fast: { label: 'Fast', variant: 'success', icon: Zap },
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
function getTier(model) {
|
|
24
|
+
export function getTier(model) {
|
|
25
25
|
const name = (model || '').toLowerCase();
|
|
26
26
|
if (name.includes('gpt-5.5') || name.includes('gpt-5.4-pro')) return 'frontier';
|
|
27
27
|
if (name.includes('gpt-5.4-mini') || name.includes('gpt-5-mini')) return 'mid';
|
|
@@ -32,7 +32,7 @@ function getTier(model) {
|
|
|
32
32
|
return 'fast';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function getContextSize(model) {
|
|
35
|
+
export function getContextSize(model) {
|
|
36
36
|
const name = (model || '').toLowerCase();
|
|
37
37
|
if (name.startsWith('gpt-5')) return '200k';
|
|
38
38
|
if (name.includes('opus') || name.includes('sonnet')) return '200k';
|
|
@@ -40,7 +40,7 @@ function UserMessage({ msg }) {
|
|
|
40
40
|
<div className="flex justify-end animate-chat-fade-in">
|
|
41
41
|
<div className="max-w-[80%]">
|
|
42
42
|
<div className="px-3.5 py-2 bg-accent/8 rounded rounded-br-none">
|
|
43
|
-
<p className="text-
|
|
43
|
+
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.content}</p>
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
@@ -60,7 +60,7 @@ function AssistantMessage({ msg, streaming }) {
|
|
|
60
60
|
</div>
|
|
61
61
|
{msg.reasoning && (
|
|
62
62
|
<div className="ml-5 mb-1.5 pl-3 border-l border-text-4/20 py-1">
|
|
63
|
-
<div className="text-
|
|
63
|
+
<div className="text-xs font-sans text-text-4 italic whitespace-pre-wrap break-words leading-relaxed">
|
|
64
64
|
{msg.reasoning}
|
|
65
65
|
{isReasoning && <span className="inline-block w-1 h-3 bg-text-4/40 ml-0.5 animate-pulse" />}
|
|
66
66
|
</div>
|
|
@@ -69,7 +69,7 @@ function AssistantMessage({ msg, streaming }) {
|
|
|
69
69
|
<div className="ml-5">
|
|
70
70
|
{msg.content ? (
|
|
71
71
|
<div className={cn(
|
|
72
|
-
'text-
|
|
72
|
+
'text-sm font-sans whitespace-pre-wrap break-words leading-relaxed',
|
|
73
73
|
msg.error ? 'text-danger' : 'text-text-1',
|
|
74
74
|
)}>
|
|
75
75
|
{msg.content}
|
|
@@ -307,7 +307,7 @@ export function LabAssistant() {
|
|
|
307
307
|
<span className="relative w-1.5 h-1.5 rounded-full bg-accent" />
|
|
308
308
|
</div>
|
|
309
309
|
<span className="text-2xs font-sans text-text-2">
|
|
310
|
-
Lab Assistant is setting up <span className="font-medium text-text-1">{backend?.toUpperCase()}</span
|
|
310
|
+
Lab Assistant {backend === 'lab-general' ? 'is active' : <>is setting up <span className="font-medium text-text-1">{backend?.toUpperCase()}</span></>}
|
|
311
311
|
</span>
|
|
312
312
|
<div className="flex-1" />
|
|
313
313
|
<Badge variant="success" className="text-2xs">running</Badge>
|
|
@@ -322,8 +322,14 @@ export function LabAssistant() {
|
|
|
322
322
|
<div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-3">
|
|
323
323
|
<FlaskConical size={22} className="text-accent" />
|
|
324
324
|
</div>
|
|
325
|
-
<p className="text-sm text-text-1 font-sans font-medium">
|
|
326
|
-
|
|
325
|
+
<p className="text-sm text-text-1 font-sans font-medium">
|
|
326
|
+
{backend === 'lab-general' ? 'Lab Assistant' : `Setting up ${backend?.toUpperCase()}`}
|
|
327
|
+
</p>
|
|
328
|
+
<p className="text-[13px] text-text-3 font-sans mt-1.5">
|
|
329
|
+
{backend === 'lab-general'
|
|
330
|
+
? 'Ask about runtime setup, model configs, context windows, prompts, or anything else...'
|
|
331
|
+
: 'The assistant is starting up and will begin configuring your runtime...'}
|
|
332
|
+
</p>
|
|
327
333
|
</div>
|
|
328
334
|
) : (
|
|
329
335
|
messages.map((msg, i) =>
|
|
@@ -115,6 +115,7 @@ function SparklineSection({ icon: Icon, label, value, unit, data, color = HEX.ac
|
|
|
115
115
|
export function MetricsPanel() {
|
|
116
116
|
const metrics = useGrooveStore((s) => s.labMetrics);
|
|
117
117
|
const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
|
|
118
|
+
const activeModel = useGrooveStore((s) => s.labActiveModel);
|
|
118
119
|
const activeSession = useGrooveStore((s) => s.labActiveSession);
|
|
119
120
|
const sessions = useGrooveStore((s) => s.labSessions);
|
|
120
121
|
|
|
@@ -276,11 +277,20 @@ export function MetricsPanel() {
|
|
|
276
277
|
</div>
|
|
277
278
|
|
|
278
279
|
{/* Attach to agent */}
|
|
279
|
-
{activeRuntime && (
|
|
280
|
+
{activeRuntime && activeModel && (
|
|
280
281
|
<>
|
|
281
282
|
<div className="h-px bg-border-subtle" />
|
|
282
|
-
<Tooltip content="
|
|
283
|
-
<button
|
|
283
|
+
<Tooltip content="Spawn a new agent using this runtime and model">
|
|
284
|
+
<button
|
|
285
|
+
onClick={() => {
|
|
286
|
+
useGrooveStore.getState().openDetail({
|
|
287
|
+
type: 'spawn',
|
|
288
|
+
presetProvider: 'local',
|
|
289
|
+
presetModel: `runtime:${activeRuntime}:${activeModel}`,
|
|
290
|
+
});
|
|
291
|
+
}}
|
|
292
|
+
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"
|
|
293
|
+
>
|
|
284
294
|
<Link size={11} /> Attach to Agent
|
|
285
295
|
</button>
|
|
286
296
|
</Tooltip>
|
|
@@ -72,8 +72,8 @@ function SeedInput({ value, onChange }) {
|
|
|
72
72
|
onChange(Math.floor(Math.random() * 2147483647));
|
|
73
73
|
}
|
|
74
74
|
return (
|
|
75
|
-
<div className="
|
|
76
|
-
<span className="text-[11px] text-text-2 font-sans">Seed</span>
|
|
75
|
+
<div className="pt-2">
|
|
76
|
+
<span className="text-[11px] text-text-2 font-sans block mb-2.5">Seed</span>
|
|
77
77
|
<div className="flex items-center gap-1.5">
|
|
78
78
|
<input
|
|
79
79
|
type="number"
|
|
@@ -118,8 +118,8 @@ function StopSequenceInput({ sequences, onChange }) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<div className="
|
|
122
|
-
<span className="text-[11px] text-text-2 font-sans">Stop Sequences</span>
|
|
121
|
+
<div className="pt-2">
|
|
122
|
+
<span className="text-[11px] text-text-2 font-sans block mb-2.5">Stop Sequences</span>
|
|
123
123
|
<div className="flex items-center gap-1.5">
|
|
124
124
|
<input
|
|
125
125
|
value={input}
|
|
@@ -138,7 +138,7 @@ function StopSequenceInput({ sequences, onChange }) {
|
|
|
138
138
|
</button>
|
|
139
139
|
</div>
|
|
140
140
|
{sequences.length > 0 && (
|
|
141
|
-
<div className="flex flex-wrap gap-1">
|
|
141
|
+
<div className="flex flex-wrap gap-1.5 mt-2.5">
|
|
142
142
|
{sequences.map((s, i) => (
|
|
143
143
|
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-surface-2 border border-border-subtle rounded text-[10px] font-mono text-text-2">
|
|
144
144
|
{s.length > 12 ? s.slice(0, 12) + '...' : s}
|
|
@@ -297,7 +297,7 @@ export function LaunchModel() {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
return (
|
|
300
|
-
<SidebarSection label="Launch Model"
|
|
300
|
+
<SidebarSection label="Launch Model">
|
|
301
301
|
{localModels.length === 0 ? (
|
|
302
302
|
<div className="py-6 text-center rounded-md bg-surface-1/50 border border-border-subtle">
|
|
303
303
|
<HardDrive size={16} className="mx-auto text-text-4 mb-2" />
|
|
@@ -494,8 +494,6 @@ export function RuntimeConfig() {
|
|
|
494
494
|
return (
|
|
495
495
|
<SidebarSection
|
|
496
496
|
label="Runtimes"
|
|
497
|
-
collapsible
|
|
498
|
-
defaultOpen={false}
|
|
499
497
|
action={
|
|
500
498
|
<Tooltip content="Add runtime">
|
|
501
499
|
<button
|
|
@@ -12,6 +12,7 @@ import { StatusBar } from './status-bar';
|
|
|
12
12
|
import { DetailPanel } from './detail-panel';
|
|
13
13
|
import { CommandPalette } from './command-palette';
|
|
14
14
|
import { ApprovalModal } from '../ui/approval-modal';
|
|
15
|
+
import { QuestionModal } from '../ui/question-modal';
|
|
15
16
|
import { QuickConnect } from '../settings/quick-connect';
|
|
16
17
|
|
|
17
18
|
import { TeamTabBar } from '../../views/agents';
|
|
@@ -119,6 +120,7 @@ export function AppShell({ children, detailContent, terminalContent }) {
|
|
|
119
120
|
<CommandPalette />
|
|
120
121
|
<QuickConnect />
|
|
121
122
|
<ApprovalModal />
|
|
123
|
+
<QuestionModal />
|
|
122
124
|
<ToastContainer />
|
|
123
125
|
</div>
|
|
124
126
|
</TooltipProvider>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug } from 'lucide-react';
|
|
2
|
+
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug, Cpu, Square } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { StatusDot } from '../ui/status-dot';
|
|
5
5
|
import { Badge } from '../ui/badge';
|
|
@@ -23,7 +23,10 @@ export function StatusBar({
|
|
|
23
23
|
const updateProgress = useGrooveStore((s) => s.updateProgress);
|
|
24
24
|
const setUpdateModalOpen = useGrooveStore((s) => s.setUpdateModalOpen);
|
|
25
25
|
const navigate = useGrooveStore((s) => s.setActiveView);
|
|
26
|
+
const labRuntimes = useGrooveStore((s) => s.labRuntimes);
|
|
27
|
+
const stopLabRuntime = useGrooveStore((s) => s.stopLabRuntime);
|
|
26
28
|
const activeTunnels = savedTunnels.filter((t) => t.active);
|
|
29
|
+
const runningRuntimes = (labRuntimes || []).filter((rt) => rt.status === 'connected');
|
|
27
30
|
const electron = isElectron();
|
|
28
31
|
|
|
29
32
|
return (
|
|
@@ -110,6 +113,26 @@ export function StatusBar({
|
|
|
110
113
|
<span>Federation</span>
|
|
111
114
|
</button>
|
|
112
115
|
)}
|
|
116
|
+
{runningRuntimes.map((rt) => (
|
|
117
|
+
<div key={rt.id} className="flex items-center gap-1">
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => navigate('model-lab')}
|
|
120
|
+
className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
|
|
121
|
+
title={`${rt.name} — running`}
|
|
122
|
+
>
|
|
123
|
+
<Cpu size={10} className="text-success" />
|
|
124
|
+
<span>{rt.name}</span>
|
|
125
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => stopLabRuntime(rt.id)}
|
|
129
|
+
className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
130
|
+
title={`Stop ${rt.name}`}
|
|
131
|
+
>
|
|
132
|
+
<Square size={8} />
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
113
136
|
</div>
|
|
114
137
|
|
|
115
138
|
<div className="flex-1" />
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
4
|
+
import { MessageCircleQuestion, Send } from 'lucide-react';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
7
|
+
|
|
8
|
+
export function QuestionModal() {
|
|
9
|
+
const pendingQuestions = useGrooveStore((s) => s.pendingQuestions);
|
|
10
|
+
const answerQuestion = useGrooveStore((s) => s.answerQuestion);
|
|
11
|
+
const [answers, setAnswers] = useState({});
|
|
12
|
+
|
|
13
|
+
if (!pendingQuestions?.length) return null;
|
|
14
|
+
|
|
15
|
+
function handleSubmit(q) {
|
|
16
|
+
const questionAnswers = {};
|
|
17
|
+
for (const qItem of q.questions) {
|
|
18
|
+
const key = qItem.question || qItem.header || `q${q.questions.indexOf(qItem)}`;
|
|
19
|
+
questionAnswers[key] = answers[`${q.agentId}:${key}`] || '';
|
|
20
|
+
}
|
|
21
|
+
answerQuestion(q.agentId, questionAnswers);
|
|
22
|
+
setAnswers((prev) => {
|
|
23
|
+
const next = { ...prev };
|
|
24
|
+
for (const key of Object.keys(next)) {
|
|
25
|
+
if (key.startsWith(q.agentId + ':')) delete next[key];
|
|
26
|
+
}
|
|
27
|
+
return next;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-lg flex flex-col gap-2 px-4">
|
|
33
|
+
<AnimatePresence>
|
|
34
|
+
{pendingQuestions.map((q) => (
|
|
35
|
+
<motion.div
|
|
36
|
+
key={q.id}
|
|
37
|
+
initial={{ y: 20, opacity: 0 }}
|
|
38
|
+
animate={{ y: 0, opacity: 1 }}
|
|
39
|
+
exit={{ y: 20, opacity: 0 }}
|
|
40
|
+
transition={{ duration: 0.2 }}
|
|
41
|
+
className="rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden"
|
|
42
|
+
>
|
|
43
|
+
<div className="px-4 py-3 flex items-start gap-3">
|
|
44
|
+
<MessageCircleQuestion size={16} className="text-accent shrink-0 mt-0.5" />
|
|
45
|
+
<div className="flex-1 min-w-0">
|
|
46
|
+
<p className="text-sm font-semibold text-text-0 font-sans">
|
|
47
|
+
{q.agentName || 'Agent'} has a question
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="px-4 pb-3 flex flex-col gap-2">
|
|
52
|
+
{q.questions.map((qItem, i) => {
|
|
53
|
+
const key = qItem.question || qItem.header || `q${i}`;
|
|
54
|
+
const inputKey = `${q.agentId}:${key}`;
|
|
55
|
+
return (
|
|
56
|
+
<div key={i}>
|
|
57
|
+
<p className="text-2xs text-text-2 font-sans mb-1">{qItem.question || key}</p>
|
|
58
|
+
{qItem.options?.length > 0 ? (
|
|
59
|
+
<div className="flex flex-wrap gap-1">
|
|
60
|
+
{qItem.options.map((opt) => {
|
|
61
|
+
const label = typeof opt === 'string' ? opt : opt.label;
|
|
62
|
+
const selected = answers[inputKey] === label;
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
key={label}
|
|
66
|
+
onClick={() => setAnswers((p) => ({ ...p, [inputKey]: label }))}
|
|
67
|
+
className={`px-2 py-1 text-2xs rounded border font-sans transition-colors ${
|
|
68
|
+
selected
|
|
69
|
+
? 'border-accent bg-accent/20 text-text-0'
|
|
70
|
+
: 'border-border-subtle bg-surface-1 text-text-2 hover:border-accent/50'
|
|
71
|
+
}`}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<input
|
|
80
|
+
type="text"
|
|
81
|
+
className="w-full px-2 py-1.5 text-xs rounded border border-border-subtle bg-surface-1 text-text-0 font-sans placeholder:text-text-3 focus:outline-none focus:border-accent"
|
|
82
|
+
placeholder="Type your answer..."
|
|
83
|
+
value={answers[inputKey] || ''}
|
|
84
|
+
onChange={(e) => setAnswers((p) => ({ ...p, [inputKey]: e.target.value }))}
|
|
85
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSubmit(q)}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
<div className="px-4 py-2.5 border-t border-border-subtle flex items-center justify-end">
|
|
93
|
+
<Button
|
|
94
|
+
size="sm"
|
|
95
|
+
variant="accent"
|
|
96
|
+
onClick={() => handleSubmit(q)}
|
|
97
|
+
>
|
|
98
|
+
<Send size={14} className="mr-1" />
|
|
99
|
+
Answer
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
</motion.div>
|
|
103
|
+
))}
|
|
104
|
+
</AnimatePresence>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -30,12 +30,12 @@ export function SheetContent({ children, className, title, side = 'right', width
|
|
|
30
30
|
<DialogPrimitive.Title className="text-base font-semibold text-text-0 font-sans">
|
|
31
31
|
{title}
|
|
32
32
|
</DialogPrimitive.Title>
|
|
33
|
-
<
|
|
33
|
+
<button
|
|
34
34
|
onClick={onClose}
|
|
35
35
|
className="p-1 rounded-md text-text-3 hover:text-text-0 hover:bg-surface-5 transition-colors cursor-pointer"
|
|
36
36
|
>
|
|
37
37
|
<X size={16} />
|
|
38
|
-
</
|
|
38
|
+
</button>
|
|
39
39
|
</div>
|
|
40
40
|
)}
|
|
41
41
|
<DialogPrimitive.Description className="sr-only">Panel</DialogPrimitive.Description>
|