groove-dev 0.27.88 → 0.27.91
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/CLAUDE.md +0 -11
- package/moe-training/client/parsers/claude-code.js +0 -2
- package/moe-training/client/session-attestation.js +2 -1
- package/moe-training/client/trajectory-capture.js +6 -0
- package/moe-training/test/client/parsers/claude-code.test.js +2 -2
- 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 +20 -10
- package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
- package/node_modules/@groove-dev/daemon/src/preview.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +34 -5
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +2 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-B_igwWvq.js → index-MLIZRMj1.js} +1734 -1734
- 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/agent-file-tree.jsx +51 -3
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +24 -10
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +7 -5
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
- package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -9
- 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 +20 -10
- package/packages/daemon/src/conversations.js +32 -6
- package/packages/daemon/src/preview.js +1 -1
- package/packages/daemon/src/process.js +34 -5
- package/packages/daemon/src/providers/claude-code.js +1 -1
- package/packages/daemon/src/providers/codex.js +1 -1
- package/packages/daemon/src/providers/gemini.js +1 -1
- package/packages/daemon/src/providers/grok.js +2 -2
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +1 -0
- package/packages/gui/dist/assets/{index-B_igwWvq.js → index-MLIZRMj1.js} +1734 -1734
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
- package/packages/gui/src/components/agents/workspace-mode.jsx +24 -10
- package/packages/gui/src/components/chat/chat-messages.jsx +7 -5
- package/packages/gui/src/components/chat/chat-view.jsx +2 -4
- package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
- package/packages/gui/src/stores/groove.js +3 -0
- package/packages/gui/src/views/agents.jsx +7 -9
- package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +0 -1
- package/packages/gui/dist/assets/index-BSqk8cbI.css +0 -1
- package/test/doomsday-clock/index.html +0 -55
- package/test/doomsday-clock/script.js +0 -66
- package/test/doomsday-clock/style.css +0 -315
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-MLIZRMj1.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D4vJ_1ET.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { api } from '../../lib/api';
|
|
6
|
-
import { ChevronRight, ChevronDown, File, Folder, FolderOpen, Clock } from 'lucide-react';
|
|
6
|
+
import { ChevronRight, ChevronDown, File, Folder, FolderOpen, Clock, FilePlus, FolderPlus, RefreshCw, ChevronsDownUp } from 'lucide-react';
|
|
7
7
|
import { ScrollArea } from '../ui/scroll-area';
|
|
8
8
|
|
|
9
9
|
const FILE_COLORS = {
|
|
@@ -81,6 +81,8 @@ export function AgentFileTree({ agentId }) {
|
|
|
81
81
|
const activityLog = useGrooveStore((s) => s.activityLog);
|
|
82
82
|
const openFile = useGrooveStore((s) => s.openFile);
|
|
83
83
|
const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
|
|
84
|
+
const createFile = useGrooveStore((s) => s.createFile);
|
|
85
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
84
86
|
|
|
85
87
|
const agent = agents.find((a) => a.id === agentId);
|
|
86
88
|
const scope = agent?.scope || [];
|
|
@@ -97,7 +99,7 @@ export function AgentFileTree({ agentId }) {
|
|
|
97
99
|
for (let i = log.length - 1; i >= 0; i--) {
|
|
98
100
|
const t = (log[i].text || '').toLowerCase();
|
|
99
101
|
if (!(t.includes('writ') || t.includes('edit') || t.includes('creat') || t.includes('read'))) continue;
|
|
100
|
-
const match = log[i].text.match(/(?:Write|Edit|Create|Read|wrote|editing|writing|reading)\S*\s+(\
|
|
102
|
+
const match = log[i].text.match(/(?:Write|Edit|Create|Read|wrote|editing|writing|reading)\S*\s+([\w./-]+\.[\w]+)/i);
|
|
101
103
|
if (!match) continue;
|
|
102
104
|
const path = match[1];
|
|
103
105
|
if (seen.has(path) || path.startsWith('.') || path.includes('node_modules')) continue;
|
|
@@ -180,8 +182,53 @@ export function AgentFileTree({ agentId }) {
|
|
|
180
182
|
openFile(path);
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
async function handleNewFile() {
|
|
186
|
+
const name = prompt('File name:');
|
|
187
|
+
if (!name?.trim()) return;
|
|
188
|
+
await createFile?.(name.trim());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function handleNewFolder() {
|
|
192
|
+
const name = prompt('Folder name:');
|
|
193
|
+
if (!name?.trim()) return;
|
|
194
|
+
try {
|
|
195
|
+
await api.post('/files/mkdir', { path: name.trim() });
|
|
196
|
+
addToast('success', `Created ${name.trim()}/`);
|
|
197
|
+
handleRefresh();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
addToast('error', 'Create folder failed', err.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleRefresh() {
|
|
204
|
+
fetchedRef.current = new Set();
|
|
205
|
+
setExpandedDirs(new Set());
|
|
206
|
+
setLoading(true);
|
|
207
|
+
fetchDir('').then((entries) => { setTreeData(entries || []); setLoading(false); });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleCollapseAll() {
|
|
211
|
+
setExpandedDirs(new Set());
|
|
212
|
+
}
|
|
213
|
+
|
|
183
214
|
return (
|
|
184
|
-
<
|
|
215
|
+
<div className="flex flex-col h-full">
|
|
216
|
+
<div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-border-subtle flex-shrink-0">
|
|
217
|
+
<span className="flex-1 text-2xs font-semibold text-text-3 uppercase tracking-wider px-1">Files</span>
|
|
218
|
+
<button onClick={handleNewFile} className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer" title="New file">
|
|
219
|
+
<FilePlus size={12} />
|
|
220
|
+
</button>
|
|
221
|
+
<button onClick={handleNewFolder} className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer" title="New folder">
|
|
222
|
+
<FolderPlus size={12} />
|
|
223
|
+
</button>
|
|
224
|
+
<button onClick={handleRefresh} className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer" title="Refresh">
|
|
225
|
+
<RefreshCw size={12} />
|
|
226
|
+
</button>
|
|
227
|
+
<button onClick={handleCollapseAll} className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer" title="Collapse all">
|
|
228
|
+
<ChevronsDownUp size={12} />
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
185
232
|
<div className="py-2">
|
|
186
233
|
{recentFiles.length > 0 && (
|
|
187
234
|
<div className="mb-3">
|
|
@@ -238,6 +285,7 @@ export function AgentFileTree({ agentId }) {
|
|
|
238
285
|
)}
|
|
239
286
|
</div>
|
|
240
287
|
</ScrollArea>
|
|
288
|
+
</div>
|
|
241
289
|
);
|
|
242
290
|
}
|
|
243
291
|
|
|
@@ -13,9 +13,10 @@ import { Tooltip } from '../ui/tooltip';
|
|
|
13
13
|
import { ScrollArea } from '../ui/scroll-area';
|
|
14
14
|
import { roleColor } from '../../lib/status';
|
|
15
15
|
import { fmtNum } from '../../lib/format';
|
|
16
|
+
import { MediaViewer, isMediaFile } from '../editor/media-viewer';
|
|
16
17
|
import {
|
|
17
18
|
X, Code2, MessageSquare, Activity, FileCode, GitCompareArrows,
|
|
18
|
-
ClipboardCheck, AlertTriangle, RefreshCw,
|
|
19
|
+
ClipboardCheck, AlertTriangle, RefreshCw, Users,
|
|
19
20
|
} from 'lucide-react';
|
|
20
21
|
|
|
21
22
|
const STATUS_VARIANT = {
|
|
@@ -73,7 +74,7 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
73
74
|
const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
|
|
74
75
|
|
|
75
76
|
return (
|
|
76
|
-
<div className="flex items-stretch h-
|
|
77
|
+
<div className="flex items-stretch h-8 bg-surface-2 border-b border-border-subtle flex-shrink-0">
|
|
77
78
|
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
|
|
78
79
|
{tabs.map((path) => {
|
|
79
80
|
const isActive = path === activeFile;
|
|
@@ -85,11 +86,11 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
85
86
|
<div
|
|
86
87
|
key={path}
|
|
87
88
|
className={cn(
|
|
88
|
-
'flex items-center gap-1.5 px-3 text-
|
|
89
|
+
'flex items-center gap-1.5 px-3 text-2xs font-sans cursor-pointer select-none',
|
|
89
90
|
'border-r border-white/5 transition-colors duration-75 flex-shrink-0',
|
|
90
91
|
isActive
|
|
91
|
-
? 'bg-surface-0 text-text-
|
|
92
|
-
: '
|
|
92
|
+
? 'bg-surface-0 text-text-1 border-b border-b-accent'
|
|
93
|
+
: 'text-text-4 hover:text-text-2 hover:bg-surface-3 border-b border-b-transparent',
|
|
93
94
|
)}
|
|
94
95
|
onClick={() => onSelect(path)}
|
|
95
96
|
>
|
|
@@ -99,7 +100,7 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
99
100
|
onClick={(e) => { e.stopPropagation(); onClose(path); }}
|
|
100
101
|
className="p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 transition-colors cursor-pointer ml-0.5"
|
|
101
102
|
>
|
|
102
|
-
<X size={
|
|
103
|
+
<X size={10} />
|
|
103
104
|
</button>
|
|
104
105
|
</div>
|
|
105
106
|
);
|
|
@@ -139,6 +140,7 @@ export function WorkspaceMode() {
|
|
|
139
140
|
const workspaceReviewMode = useGrooveStore((s) => s.workspaceReviewMode);
|
|
140
141
|
const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
|
|
141
142
|
const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
|
|
143
|
+
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
142
144
|
|
|
143
145
|
const editorFiles = useGrooveStore((s) => s.editorFiles);
|
|
144
146
|
const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
|
|
@@ -216,6 +218,7 @@ export function WorkspaceMode() {
|
|
|
216
218
|
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
217
219
|
const file = editorActiveFile ? editorFiles[editorActiveFile] : null;
|
|
218
220
|
const hasExternalChange = editorActiveFile && editorChangedFiles[editorActiveFile];
|
|
221
|
+
const isMedia = editorActiveFile && isMediaFile(editorActiveFile);
|
|
219
222
|
|
|
220
223
|
return (
|
|
221
224
|
<div className="flex h-full bg-surface-0">
|
|
@@ -235,7 +238,7 @@ export function WorkspaceMode() {
|
|
|
235
238
|
</div>
|
|
236
239
|
|
|
237
240
|
{/* Editor Area */}
|
|
238
|
-
<div className="flex-1 flex flex-col min-w-0">
|
|
241
|
+
<div className="flex-1 flex flex-col min-w-0 bg-[#1a1e25]">
|
|
239
242
|
{workspaceReviewMode ? (
|
|
240
243
|
<CodeReview agentId={agent.id} />
|
|
241
244
|
) : (
|
|
@@ -272,7 +275,7 @@ export function WorkspaceMode() {
|
|
|
272
275
|
)}
|
|
273
276
|
|
|
274
277
|
{!editorActiveFile && (
|
|
275
|
-
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans">
|
|
278
|
+
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans bg-[#1a1e25]">
|
|
276
279
|
<div className="text-center space-y-2">
|
|
277
280
|
<Code2 size={32} className="mx-auto" />
|
|
278
281
|
<p className="text-sm">Open a file from the tree</p>
|
|
@@ -281,11 +284,15 @@ export function WorkspaceMode() {
|
|
|
281
284
|
</div>
|
|
282
285
|
)}
|
|
283
286
|
|
|
284
|
-
{editorActiveFile &&
|
|
287
|
+
{editorActiveFile && isMedia && (
|
|
288
|
+
<MediaViewer path={editorActiveFile} />
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{editorActiveFile && diffMode && !isMedia && (
|
|
285
292
|
<DiffViewer filePath={editorActiveFile} />
|
|
286
293
|
)}
|
|
287
294
|
|
|
288
|
-
{editorActiveFile && !diffMode && file && (
|
|
295
|
+
{editorActiveFile && !diffMode && !isMedia && file && (
|
|
289
296
|
<CodeEditor
|
|
290
297
|
content={file.content}
|
|
291
298
|
language={file.language}
|
|
@@ -338,6 +345,13 @@ export function WorkspaceMode() {
|
|
|
338
345
|
>
|
|
339
346
|
<ClipboardCheck size={13} />
|
|
340
347
|
</button>
|
|
348
|
+
<button
|
|
349
|
+
onClick={() => setWorkspaceMode(false)}
|
|
350
|
+
className="flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors text-text-3 hover:text-text-1 hover:bg-surface-3"
|
|
351
|
+
title="Back to agent tree"
|
|
352
|
+
>
|
|
353
|
+
<Users size={13} />
|
|
354
|
+
</button>
|
|
341
355
|
</div>
|
|
342
356
|
|
|
343
357
|
{/* Tab switcher */}
|
|
@@ -259,13 +259,15 @@ function UserMessage({ msg }) {
|
|
|
259
259
|
|
|
260
260
|
function AssistantMessage({ msg, model, role }) {
|
|
261
261
|
const cleanText = stripEmojis(msg.text);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
: (model || 'Assistant');
|
|
262
|
+
const modelName = model || 'Assistant';
|
|
263
|
+
const roleLabel = role ? role.charAt(0).toUpperCase() + role.slice(1) : null;
|
|
265
264
|
return (
|
|
266
265
|
<div className="max-w-[85%]">
|
|
267
|
-
<div className="text-2xs
|
|
268
|
-
|
|
266
|
+
<div className="text-2xs font-sans mb-1 font-medium">
|
|
267
|
+
<span className="text-text-1">{modelName}</span>
|
|
268
|
+
{roleLabel && <span className="text-text-3 ml-1">{roleLabel}</span>}
|
|
269
|
+
</div>
|
|
270
|
+
<div className="border-l-2 border-accent pl-3.5 py-1">
|
|
269
271
|
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
270
272
|
<RenderedMarkdown text={cleanText} />
|
|
271
273
|
</div>
|
|
@@ -64,10 +64,8 @@ export function ChatView() {
|
|
|
64
64
|
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
65
65
|
|
|
66
66
|
const handleNewChat = useCallback(async (provider, model) => {
|
|
67
|
-
const p = provider || 'claude-code';
|
|
68
|
-
const m = model || 'claude-sonnet-4-6';
|
|
69
67
|
try {
|
|
70
|
-
await createConversation(
|
|
68
|
+
await createConversation(provider || null, model || null, 'api');
|
|
71
69
|
} catch { /* toast handles */ }
|
|
72
70
|
}, [createConversation]);
|
|
73
71
|
|
|
@@ -134,7 +132,7 @@ export function ChatView() {
|
|
|
134
132
|
<div className="flex h-full bg-surface-0">
|
|
135
133
|
{/* Conversation sidebar */}
|
|
136
134
|
<div className={cn(
|
|
137
|
-
'flex-shrink-0 border-r border-accent/12 bg-
|
|
135
|
+
'flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
|
|
138
136
|
sidebarCollapsed ? 'w-0' : 'w-64',
|
|
139
137
|
)}>
|
|
140
138
|
<ConversationList onNewChat={() => handleNewChat()} />
|
|
@@ -31,7 +31,7 @@ function groupByDate(conversations) {
|
|
|
31
31
|
function GroupLabel({ label }) {
|
|
32
32
|
return (
|
|
33
33
|
<div className="px-3 pt-4 pb-1.5">
|
|
34
|
-
<span className="text-2xs font-semibold text-
|
|
34
|
+
<span className="text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans">{label}</span>
|
|
35
35
|
</div>
|
|
36
36
|
);
|
|
37
37
|
}
|
|
@@ -45,23 +45,23 @@ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete
|
|
|
45
45
|
className={cn(
|
|
46
46
|
'w-full flex items-center gap-2 px-3 py-2 text-left rounded-md transition-colors cursor-pointer group',
|
|
47
47
|
isActive
|
|
48
|
-
? 'bg-
|
|
49
|
-
: 'text-
|
|
48
|
+
? 'bg-accent/10 text-text-0'
|
|
49
|
+
: 'text-text-2 hover:bg-surface-4 hover:text-text-1',
|
|
50
50
|
)}
|
|
51
51
|
>
|
|
52
|
-
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-
|
|
52
|
+
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-accent' : 'text-text-4 group-hover:text-text-3')} />
|
|
53
53
|
<div className="flex-1 min-w-0">
|
|
54
|
-
<div className="text-xs font-medium font-sans truncate
|
|
54
|
+
<div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
|
|
55
55
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
56
56
|
{conv.mode === 'agent'
|
|
57
|
-
? <Bot size={9} className="text-
|
|
58
|
-
: <Zap size={9} className="text-
|
|
57
|
+
? <Bot size={9} className="text-purple flex-shrink-0" />
|
|
58
|
+
: <Zap size={9} className="text-accent flex-shrink-0" />
|
|
59
59
|
}
|
|
60
60
|
{conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
|
|
61
|
-
<span className="text-2xs text-
|
|
61
|
+
<span className="text-2xs text-text-4 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
|
-
{conv.pinned && <Pin size={10} className="text-
|
|
64
|
+
{conv.pinned && <Pin size={10} className="text-accent flex-shrink-0" />}
|
|
65
65
|
</button>
|
|
66
66
|
</ContextMenuTrigger>
|
|
67
67
|
<ContextMenuContent>
|
|
@@ -125,9 +125,9 @@ export function ConversationList({ onNewChat }) {
|
|
|
125
125
|
<div className="flex-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
|
|
126
126
|
{conversations.length === 0 ? (
|
|
127
127
|
<div className="flex flex-col items-center justify-center py-16 text-center px-4">
|
|
128
|
-
<MessageCircle size={24} className="text-
|
|
129
|
-
<p className="text-xs text-
|
|
130
|
-
<p className="text-2xs text-
|
|
128
|
+
<MessageCircle size={24} className="text-text-4 mb-3" />
|
|
129
|
+
<p className="text-xs text-text-3 font-sans">No conversations yet</p>
|
|
130
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Start a new chat to begin</p>
|
|
131
131
|
</div>
|
|
132
132
|
) : (
|
|
133
133
|
<>
|
|
@@ -140,10 +140,10 @@ export function ConversationList({ onNewChat }) {
|
|
|
140
140
|
)}
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
|
-
<div className="p-3 border-t border-
|
|
143
|
+
<div className="p-3 border-t border-border-subtle">
|
|
144
144
|
<button
|
|
145
145
|
onClick={onNewChat}
|
|
146
|
-
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-
|
|
146
|
+
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
|
|
147
147
|
>
|
|
148
148
|
<Plus size={14} />
|
|
149
149
|
New Chat
|
|
@@ -2363,6 +2363,9 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2363
2363
|
setWorkspaceMode(on) {
|
|
2364
2364
|
set({ workspaceMode: on });
|
|
2365
2365
|
localStorage.setItem('groove:workspaceMode', String(on));
|
|
2366
|
+
if (on) {
|
|
2367
|
+
get().closeDetail();
|
|
2368
|
+
}
|
|
2366
2369
|
if (on && !get().workspaceAgentId) {
|
|
2367
2370
|
const teamAgents = get().agents.filter((a) => a.teamId === get().activeTeamId);
|
|
2368
2371
|
const selected = get().detailPanel?.type === 'agent' ? get().detailPanel.agentId : null;
|
|
@@ -854,7 +854,7 @@ export default function AgentsView() {
|
|
|
854
854
|
<EmptyState onPlanner={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
|
|
855
855
|
) : workspaceMode ? (
|
|
856
856
|
<WorkspaceMode />
|
|
857
|
-
) : showPreviewInAgents && previewState.url ? (
|
|
857
|
+
) : showPreviewInAgents && previewState.url && previewState.teamId === activeTeamId ? (
|
|
858
858
|
<PreviewWorkspace embedded />
|
|
859
859
|
) : (
|
|
860
860
|
<ReactFlowProvider key={activeTeamId}>
|
|
@@ -872,21 +872,19 @@ export default function AgentsView() {
|
|
|
872
872
|
Spawn
|
|
873
873
|
</button>
|
|
874
874
|
)}
|
|
875
|
-
{!isLoading && teamAgents.length > 0 && (
|
|
875
|
+
{!isLoading && teamAgents.length > 0 && !workspaceMode && (
|
|
876
876
|
<button
|
|
877
|
-
onClick={() => setWorkspaceMode(
|
|
877
|
+
onClick={() => setWorkspaceMode(true)}
|
|
878
878
|
className={cn(
|
|
879
879
|
'absolute bottom-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md text-xs font-semibold font-sans transition-colors cursor-pointer select-none shadow-lg shadow-black/10',
|
|
880
|
-
previewState.url &&
|
|
881
|
-
|
|
882
|
-
? 'bg-accent/15 text-accent hover:bg-accent/25'
|
|
883
|
-
: 'bg-purple/15 text-purple hover:bg-purple/25',
|
|
880
|
+
previewState.url && previewState.teamId === activeTeamId ? 'right-32' : 'right-4',
|
|
881
|
+
'bg-purple/15 text-purple hover:bg-purple/25',
|
|
884
882
|
)}
|
|
885
883
|
>
|
|
886
|
-
|
|
884
|
+
<Code2 size={14} /> Workspace
|
|
887
885
|
</button>
|
|
888
886
|
)}
|
|
889
|
-
{!isLoading && teamAgents.length > 0 && !workspaceMode && previewState.url && (
|
|
887
|
+
{!isLoading && teamAgents.length > 0 && !workspaceMode && previewState.url && previewState.teamId === activeTeamId && (
|
|
890
888
|
<button
|
|
891
889
|
onClick={togglePreviewInAgents}
|
|
892
890
|
className="absolute bottom-4 right-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-info/15 text-info text-xs font-semibold font-sans hover:bg-info/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.91",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -146,7 +146,10 @@ export function createApi(app, daemon) {
|
|
|
146
146
|
const team = daemon.teams.get(config.teamId);
|
|
147
147
|
if (team?.workingDir) config.workingDir = team.workingDir;
|
|
148
148
|
}
|
|
149
|
-
// Inherit configured
|
|
149
|
+
// Inherit configured defaults if the request didn't pick them
|
|
150
|
+
if (!config.provider && daemon.config?.defaultProvider) {
|
|
151
|
+
config.provider = daemon.config.defaultProvider;
|
|
152
|
+
}
|
|
150
153
|
if (!config.model && daemon.config?.defaultModel) {
|
|
151
154
|
config.model = daemon.config.defaultModel;
|
|
152
155
|
}
|
|
@@ -1112,8 +1115,8 @@ export function createApi(app, daemon) {
|
|
|
1112
1115
|
app.post('/api/conversations', async (req, res) => {
|
|
1113
1116
|
try {
|
|
1114
1117
|
const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
|
|
1115
|
-
if (
|
|
1116
|
-
return res.status(400).json({ error: 'provider
|
|
1118
|
+
if (provider && typeof provider !== 'string') {
|
|
1119
|
+
return res.status(400).json({ error: 'provider must be a string' });
|
|
1117
1120
|
}
|
|
1118
1121
|
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
1119
1122
|
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
@@ -4501,8 +4504,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4501
4504
|
}
|
|
4502
4505
|
|
|
4503
4506
|
daemon.config.defaultProvider = provider;
|
|
4507
|
+
daemon.config.defaultChatProvider = provider;
|
|
4504
4508
|
if (model && typeof model === 'string' && model.length <= 100) {
|
|
4505
4509
|
daemon.config.defaultModel = model.trim();
|
|
4510
|
+
daemon.config.defaultChatModel = model.trim();
|
|
4506
4511
|
}
|
|
4507
4512
|
const { saveConfig } = await import('./firstrun.js');
|
|
4508
4513
|
saveConfig(daemon.grooveDir, daemon.config);
|
|
@@ -4534,15 +4539,20 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4534
4539
|
saveConfig(daemon.grooveDir, daemon.config);
|
|
4535
4540
|
|
|
4536
4541
|
if (enabled) {
|
|
4537
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4538
|
-
const consent = new ConsentManager();
|
|
4539
4542
|
try {
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4544
|
+
const consent = new ConsentManager();
|
|
4545
|
+
try {
|
|
4546
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4547
|
+
} finally {
|
|
4548
|
+
consent.close();
|
|
4549
|
+
}
|
|
4550
|
+
await daemon._initTrajectoryCapture();
|
|
4551
|
+
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4552
|
+
} catch (e) {
|
|
4553
|
+
daemon.config.training_opt_in = false;
|
|
4554
|
+
return res.status(500).json({ error: 'Failed to enable data sharing', detail: e.message });
|
|
4543
4555
|
}
|
|
4544
|
-
await daemon._initTrajectoryCapture();
|
|
4545
|
-
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4546
4556
|
} else {
|
|
4547
4557
|
if (daemon.trajectoryCapture) {
|
|
4548
4558
|
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
@@ -55,6 +55,22 @@ export class ConversationManager {
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
_resolveAutoProviderModel(preferredProvider) {
|
|
59
|
+
const priority = ['claude-code', 'codex', 'gemini', 'grok', 'ollama'];
|
|
60
|
+
const candidates = preferredProvider ? [preferredProvider] : priority;
|
|
61
|
+
|
|
62
|
+
for (const pid of candidates) {
|
|
63
|
+
if (!isProviderInstalled(pid)) continue;
|
|
64
|
+
const p = getProvider(pid);
|
|
65
|
+
if (!p) continue;
|
|
66
|
+
const models = p.constructor.models || [];
|
|
67
|
+
const chatModel = models.find((m) => m.type !== 'image') || models[0];
|
|
68
|
+
if (chatModel) return { provider: pid, model: chatModel.id };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { provider: 'claude-code', model: 'claude-sonnet-4-6' };
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
async create(provider, model, title, mode = 'api', options = {}) {
|
|
59
75
|
if (!provider && this.daemon.config?.defaultChatProvider) {
|
|
60
76
|
provider = this.daemon.config.defaultChatProvider;
|
|
@@ -63,6 +79,12 @@ export class ConversationManager {
|
|
|
63
79
|
model = this.daemon.config.defaultChatModel;
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
if (!provider || !model) {
|
|
83
|
+
const resolved = this._resolveAutoProviderModel(provider);
|
|
84
|
+
if (!provider) provider = resolved.provider;
|
|
85
|
+
if (!model) model = resolved.model;
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
const id = randomUUID().slice(0, 12);
|
|
67
89
|
const now = new Date().toISOString();
|
|
68
90
|
|
|
@@ -328,12 +350,16 @@ export class ConversationManager {
|
|
|
328
350
|
let providerName = conv.provider;
|
|
329
351
|
|
|
330
352
|
if (!provider || !isProviderInstalled(conv.provider)) {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
if (!
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
353
|
+
const resolved = this._resolveAutoProviderModel(null);
|
|
354
|
+
provider = getProvider(resolved.provider);
|
|
355
|
+
if (!provider) throw new Error('No provider available for chat');
|
|
356
|
+
providerName = resolved.provider;
|
|
357
|
+
modelId = resolved.model;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!modelId) {
|
|
361
|
+
const resolved = this._resolveAutoProviderModel(providerName);
|
|
362
|
+
modelId = resolved.model;
|
|
337
363
|
}
|
|
338
364
|
|
|
339
365
|
// Build messages array for direct API call
|
|
@@ -191,7 +191,7 @@ export class PreviewService {
|
|
|
191
191
|
// expansion, and shell builtins work as the planner wrote them.
|
|
192
192
|
const proc = cpSpawn('bash', ['-lc', command], {
|
|
193
193
|
cwd: baseDir,
|
|
194
|
-
env: { ...process.env, FORCE_COLOR: '0', CI: '' },
|
|
194
|
+
env: { ...process.env, FORCE_COLOR: '0', CI: '', BROWSER: 'none' },
|
|
195
195
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
196
196
|
detached: false,
|
|
197
197
|
});
|
|
@@ -432,10 +432,17 @@ export class ProcessManager {
|
|
|
432
432
|
if (installed.length === 0) {
|
|
433
433
|
throw new Error('No AI providers installed. Install Claude Code, Gemini CLI, Codex, or Ollama first.');
|
|
434
434
|
}
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
// If a model is specified, find the provider that supports it
|
|
436
|
+
if (config.model && config.model !== 'auto') {
|
|
437
|
+
const match = installed.find((p) => p.models?.some((m) => m.id === config.model));
|
|
438
|
+
if (match) providerName = match.id;
|
|
439
|
+
}
|
|
440
|
+
// Fallback: priority-based selection
|
|
441
|
+
if (!providerName) {
|
|
442
|
+
const priority = ['claude-code', 'gemini', 'codex', 'local', 'ollama'];
|
|
443
|
+
const best = priority.find((p) => installed.some((i) => i.id === p)) || installed[0].id;
|
|
444
|
+
providerName = best;
|
|
445
|
+
}
|
|
439
446
|
}
|
|
440
447
|
|
|
441
448
|
const provider = getProvider(providerName);
|
|
@@ -1282,6 +1289,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1282
1289
|
}
|
|
1283
1290
|
}
|
|
1284
1291
|
|
|
1292
|
+
// Phase 2 spawns are async — _checkPhase2 may have consumed the pending
|
|
1293
|
+
// entry but the spawn() calls haven't resolved yet. Wait for them.
|
|
1294
|
+
if (this._phase2Spawning?.has(teamId)) return;
|
|
1295
|
+
|
|
1285
1296
|
const teamAgents = this.daemon.registry.getAll().filter((a) => a.teamId === teamId && a.role !== 'planner');
|
|
1286
1297
|
if (teamAgents.length === 0) return;
|
|
1287
1298
|
const terminal = new Set(['completed', 'crashed', 'stopped', 'killed']);
|
|
@@ -1367,6 +1378,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1367
1378
|
return files.length === 0;
|
|
1368
1379
|
});
|
|
1369
1380
|
|
|
1381
|
+
// Track that phase 2 spawns are in-flight for this team so
|
|
1382
|
+
// _checkPreviewReady doesn't race ahead of the async spawn calls.
|
|
1383
|
+
const groupTeamId = group.agents[0]?.teamId || this.daemon.teams.getDefault()?.id || null;
|
|
1384
|
+
if (!this._phase2Spawning) this._phase2Spawning = new Map();
|
|
1385
|
+
const spawnPromises = [];
|
|
1386
|
+
|
|
1370
1387
|
// Auto-spawn phase 2 agents — if phase 1 was idle, clear the prompt
|
|
1371
1388
|
// so QC also waits for instructions instead of auditing nothing
|
|
1372
1389
|
for (const config of group.agents) {
|
|
@@ -1406,7 +1423,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1406
1423
|
try {
|
|
1407
1424
|
const validated = validateAgentConfig(config);
|
|
1408
1425
|
if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
|
|
1409
|
-
this.spawn(validated).then((agent) => {
|
|
1426
|
+
const p = this.spawn(validated).then((agent) => {
|
|
1410
1427
|
this.daemon.broadcast({
|
|
1411
1428
|
type: 'phase2:spawned',
|
|
1412
1429
|
agentId: agent.id,
|
|
@@ -1422,6 +1439,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1422
1439
|
error: err.message,
|
|
1423
1440
|
});
|
|
1424
1441
|
});
|
|
1442
|
+
spawnPromises.push(p);
|
|
1425
1443
|
} catch (err) {
|
|
1426
1444
|
console.error(`[Groove] Phase 2 config invalid for ${config.role}: ${err.message}`);
|
|
1427
1445
|
this.daemon.broadcast({
|
|
@@ -1431,6 +1449,17 @@ For normal file edits within your scope, proceed without review.
|
|
|
1431
1449
|
});
|
|
1432
1450
|
}
|
|
1433
1451
|
}
|
|
1452
|
+
|
|
1453
|
+
// Mark this team as having phase 2 spawns in-flight. Cleared once
|
|
1454
|
+
// all spawn promises settle so _checkPreviewReady won't race ahead.
|
|
1455
|
+
if (spawnPromises.length > 0 && groupTeamId) {
|
|
1456
|
+
this._phase2Spawning.set(groupTeamId, (this._phase2Spawning.get(groupTeamId) || 0) + spawnPromises.length);
|
|
1457
|
+
Promise.allSettled(spawnPromises).then(() => {
|
|
1458
|
+
const remaining = (this._phase2Spawning.get(groupTeamId) || 0) - spawnPromises.length;
|
|
1459
|
+
if (remaining <= 0) this._phase2Spawning.delete(groupTeamId);
|
|
1460
|
+
else this._phase2Spawning.set(groupTeamId, remaining);
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1434
1463
|
}
|
|
1435
1464
|
}
|
|
1436
1465
|
}
|
|
@@ -282,7 +282,7 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
282
282
|
let finished = false;
|
|
283
283
|
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
284
284
|
const body = JSON.stringify({
|
|
285
|
-
model
|
|
285
|
+
model,
|
|
286
286
|
messages,
|
|
287
287
|
max_tokens: 8192,
|
|
288
288
|
stream: true,
|
|
@@ -163,7 +163,7 @@ export class CodexProvider extends Provider {
|
|
|
163
163
|
const effort = reasoningEffort || 'medium';
|
|
164
164
|
const verb = verbosity || 'medium';
|
|
165
165
|
const body = {
|
|
166
|
-
model
|
|
166
|
+
model,
|
|
167
167
|
input: previousResponseId ? [messages[messages.length - 1]] : messages,
|
|
168
168
|
stream: true,
|
|
169
169
|
reasoning: { effort },
|