groove-dev 0.22.2 → 0.22.4
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 +7 -0
- package/node_modules/@groove-dev/cli/src/commands/start.js +29 -1
- package/node_modules/@groove-dev/cli/src/setup.js +266 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BDyGhxDd.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BHDZqhzW.js +562 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -71
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-telemetry.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +17 -4
- package/package.json +1 -1
- package/packages/cli/src/commands/start.js +29 -1
- package/packages/cli/src/setup.js +266 -0
- package/packages/gui/dist/assets/index-BDyGhxDd.css +1 -0
- package/packages/gui/dist/assets/index-BHDZqhzW.js +562 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +144 -71
- package/packages/gui/src/components/agents/agent-node.jsx +1 -1
- package/packages/gui/src/components/agents/agent-panel.jsx +1 -1
- package/packages/gui/src/components/agents/agent-telemetry.jsx +2 -2
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +2 -2
- package/packages/gui/src/stores/groove.js +17 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-DtZ-1z8T.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-cKOfI2iD.js +0 -562
- package/packages/gui/dist/assets/index-DtZ-1z8T.css +0 -1
- package/packages/gui/dist/assets/index-cKOfI2iD.js +0 -562
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BHDZqhzW.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BDyGhxDd.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
3
3
|
import {
|
|
4
|
-
Send, Loader2, MessageSquare,
|
|
4
|
+
Send, Loader2, MessageSquare, ArrowRight,
|
|
5
5
|
FileEdit, Search, Terminal, CheckCircle2, AlertCircle,
|
|
6
|
-
RotateCw, Zap, Wrench, Eye,
|
|
7
|
-
ChevronDown,
|
|
6
|
+
RotateCw, Zap, Wrench, Eye, Code2, Bug,
|
|
7
|
+
ChevronDown, HelpCircle, Pencil,
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { useGrooveStore } from '../../stores/groove';
|
|
10
10
|
import { cn } from '../../lib/cn';
|
|
@@ -47,10 +47,14 @@ function FormattedText({ text }) {
|
|
|
47
47
|
{parts.map((part, i) => {
|
|
48
48
|
if (part.startsWith('```') && part.endsWith('```')) {
|
|
49
49
|
const code = part.slice(3, -3).replace(/^\w+\n/, '');
|
|
50
|
-
return
|
|
50
|
+
return (
|
|
51
|
+
<pre key={i} className="my-2.5 p-3.5 rounded-lg bg-[#0d1117] text-[12px] font-mono text-[#c9d1d9] overflow-x-auto whitespace-pre-wrap border border-white/[0.06] leading-relaxed">
|
|
52
|
+
{code}
|
|
53
|
+
</pre>
|
|
54
|
+
);
|
|
51
55
|
}
|
|
52
56
|
if (part.startsWith('`') && part.endsWith('`')) {
|
|
53
|
-
return <code key={i} className="px-1 py-
|
|
57
|
+
return <code key={i} className="px-1.5 py-0.5 rounded bg-accent/8 text-[12px] font-mono text-accent border border-accent/10">{part.slice(1, -1)}</code>;
|
|
54
58
|
}
|
|
55
59
|
return <span key={i}>{part.split(/(\*\*[^*]+\*\*)/g).map((s, j) =>
|
|
56
60
|
s.startsWith('**') && s.endsWith('**')
|
|
@@ -65,43 +69,57 @@ function FormattedText({ text }) {
|
|
|
65
69
|
// ── Message components ───────────────────────────────────────
|
|
66
70
|
|
|
67
71
|
function UserMessage({ msg }) {
|
|
72
|
+
const isQuery = msg.isQuery;
|
|
68
73
|
return (
|
|
69
74
|
<div className="flex justify-end pl-12">
|
|
70
75
|
<div className="max-w-[85%]">
|
|
76
|
+
{isQuery && (
|
|
77
|
+
<div className="flex items-center justify-end gap-1 mb-1">
|
|
78
|
+
<HelpCircle size={9} className="text-info" />
|
|
79
|
+
<span className="text-2xs text-info font-sans font-medium">Query</span>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
71
82
|
<div className={cn(
|
|
72
|
-
'px-
|
|
73
|
-
|
|
74
|
-
? 'bg-info/
|
|
75
|
-
: 'bg-accent/
|
|
83
|
+
'px-4 py-3 rounded-2xl rounded-br-md',
|
|
84
|
+
isQuery
|
|
85
|
+
? 'bg-info/10 border border-info/15'
|
|
86
|
+
: 'bg-accent/10 border border-accent/15',
|
|
76
87
|
)}>
|
|
77
|
-
<div className="text-[13px] font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
88
|
+
<div className="text-[13px] font-sans whitespace-pre-wrap break-words leading-relaxed text-text-0">
|
|
78
89
|
<FormattedText text={msg.text} />
|
|
79
90
|
</div>
|
|
80
91
|
</div>
|
|
81
|
-
<div className="text-
|
|
92
|
+
<div className="text-[10px] text-text-4 font-sans mt-1.5 text-right">{timeAgo(msg.timestamp)}</div>
|
|
82
93
|
</div>
|
|
83
94
|
</div>
|
|
84
95
|
);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
function AgentMessage({ msg }) {
|
|
98
|
+
function AgentMessage({ msg, agent }) {
|
|
88
99
|
return (
|
|
89
|
-
<div className="pr-
|
|
90
|
-
<div className="
|
|
100
|
+
<div className="pr-6">
|
|
101
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
102
|
+
<div className="w-5 h-5 rounded-md bg-accent/12 flex items-center justify-center flex-shrink-0">
|
|
103
|
+
<Code2 size={10} className="text-accent" />
|
|
104
|
+
</div>
|
|
105
|
+
<span className="text-2xs font-semibold text-text-1 font-sans">{agent?.name || 'Agent'}</span>
|
|
106
|
+
<span className="text-2xs text-text-4 font-sans">{agent?.role}</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="ml-7 px-4 py-3 rounded-2xl rounded-tl-md bg-surface-2/60 border border-border-subtle">
|
|
91
109
|
<div className="text-[13px] text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
92
110
|
<FormattedText text={msg.text} />
|
|
93
111
|
</div>
|
|
94
112
|
</div>
|
|
95
|
-
<div className="text-
|
|
113
|
+
<div className="text-[10px] text-text-4 font-sans mt-1.5 ml-7">{timeAgo(msg.timestamp)}</div>
|
|
96
114
|
</div>
|
|
97
115
|
);
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
function SystemMessage({ msg }) {
|
|
101
119
|
return (
|
|
102
|
-
<div className="flex items-center gap-3 py-
|
|
120
|
+
<div className="flex items-center gap-3 py-2">
|
|
103
121
|
<div className="flex-1 h-px bg-border-subtle" />
|
|
104
|
-
<span className="text-
|
|
122
|
+
<span className="text-[10px] text-text-4 font-sans flex-shrink-0 uppercase tracking-wide">{msg.text}</span>
|
|
105
123
|
<div className="flex-1 h-px bg-border-subtle" />
|
|
106
124
|
</div>
|
|
107
125
|
);
|
|
@@ -112,13 +130,15 @@ function SystemMessage({ msg }) {
|
|
|
112
130
|
function ActivityLine({ entry }) {
|
|
113
131
|
const meta = activityMeta(entry.text);
|
|
114
132
|
const Icon = meta.icon;
|
|
115
|
-
const display = entry.text?.length >
|
|
133
|
+
const display = entry.text?.length > 120 ? entry.text.slice(0, 120) + '...' : entry.text;
|
|
116
134
|
|
|
117
135
|
return (
|
|
118
|
-
<div className="flex items-center gap-2 py-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
<div className="flex items-center gap-2 py-0.5 group">
|
|
137
|
+
<div className="w-4 h-4 rounded flex items-center justify-center flex-shrink-0">
|
|
138
|
+
<Icon size={10} className={cn(meta.color, 'opacity-70')} />
|
|
139
|
+
</div>
|
|
140
|
+
<p className="text-[11px] text-text-3 font-sans truncate flex-1 min-w-0">{display}</p>
|
|
141
|
+
<span className="text-[10px] text-text-4 font-mono opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
|
122
142
|
{timeAgo(entry.timestamp)}
|
|
123
143
|
</span>
|
|
124
144
|
</div>
|
|
@@ -129,23 +149,23 @@ function ActivityGroup({ entries }) {
|
|
|
129
149
|
const [expanded, setExpanded] = useState(false);
|
|
130
150
|
|
|
131
151
|
if (entries.length === 1) {
|
|
132
|
-
return <ActivityLine entry={entries[0]}
|
|
152
|
+
return <div className="ml-7"><ActivityLine entry={entries[0]} /></div>;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
const visible = expanded ? entries :
|
|
136
|
-
const hiddenCount = entries.length -
|
|
155
|
+
const visible = expanded ? entries : entries.slice(0, 2);
|
|
156
|
+
const hiddenCount = entries.length - 2;
|
|
137
157
|
|
|
138
158
|
return (
|
|
139
|
-
<div className="
|
|
159
|
+
<div className="ml-7 py-1 pl-3 border-l border-border-subtle/50 space-y-px">
|
|
140
160
|
{visible.map((entry, i) => (
|
|
141
161
|
<ActivityLine key={i} entry={entry} />
|
|
142
162
|
))}
|
|
143
163
|
{!expanded && hiddenCount > 0 && (
|
|
144
164
|
<button
|
|
145
165
|
onClick={() => setExpanded(true)}
|
|
146
|
-
className="flex items-center gap-1 text-
|
|
166
|
+
className="flex items-center gap-1.5 text-[11px] text-text-4 hover:text-text-2 font-sans cursor-pointer py-0.5 ml-6"
|
|
147
167
|
>
|
|
148
|
-
<ChevronDown size={
|
|
168
|
+
<ChevronDown size={9} />
|
|
149
169
|
<span>{hiddenCount} more</span>
|
|
150
170
|
</button>
|
|
151
171
|
)}
|
|
@@ -163,28 +183,42 @@ function StreamingBar({ agent }) {
|
|
|
163
183
|
const isRecent = lastActivity && (Date.now() - lastActivity.timestamp) < 10000;
|
|
164
184
|
|
|
165
185
|
const display = isRecent && lastActivity.text
|
|
166
|
-
? (lastActivity.text.length >
|
|
186
|
+
? (lastActivity.text.length > 60 ? lastActivity.text.slice(0, 60) + '...' : lastActivity.text)
|
|
167
187
|
: null;
|
|
168
188
|
|
|
189
|
+
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
190
|
+
|
|
169
191
|
return (
|
|
170
|
-
<div className="flex items-center gap-
|
|
171
|
-
<div className="flex items-center gap-
|
|
172
|
-
<div className="relative flex items-center justify-center w-
|
|
173
|
-
<span className="absolute inset-0 rounded-full bg-accent/
|
|
192
|
+
<div className="flex items-center gap-3 px-4 h-8 border-b border-border-subtle bg-surface-1/80 flex-shrink-0">
|
|
193
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
194
|
+
<div className="relative flex items-center justify-center w-4 h-4">
|
|
195
|
+
<span className="absolute inset-0 rounded-full bg-accent/15 animate-ping" style={{ animationDuration: '2s' }} />
|
|
174
196
|
<span className="relative w-1.5 h-1.5 rounded-full bg-accent" />
|
|
175
197
|
</div>
|
|
176
198
|
{isRecent ? (
|
|
177
199
|
<>
|
|
178
|
-
<Icon size={
|
|
179
|
-
<span className="text-
|
|
200
|
+
<Icon size={10} className={cn(meta.color, 'flex-shrink-0')} />
|
|
201
|
+
<span className="text-[11px] text-text-2 font-sans truncate">{display}</span>
|
|
180
202
|
</>
|
|
181
203
|
) : (
|
|
182
|
-
<span className="text-
|
|
204
|
+
<span className="text-[11px] text-text-3 font-sans">Working...</span>
|
|
183
205
|
)}
|
|
184
206
|
</div>
|
|
185
|
-
<
|
|
186
|
-
{fmtTokens(agent.tokensUsed)}
|
|
187
|
-
|
|
207
|
+
<div className="flex items-center gap-3 flex-shrink-0">
|
|
208
|
+
<span className="text-[10px] text-text-4 font-mono">{fmtTokens(agent.tokensUsed)}</span>
|
|
209
|
+
<div className="flex items-center gap-1.5">
|
|
210
|
+
<div className="w-14 h-1 rounded-full bg-surface-4 overflow-hidden">
|
|
211
|
+
<div
|
|
212
|
+
className="h-full rounded-full transition-all duration-500"
|
|
213
|
+
style={{
|
|
214
|
+
width: `${ctxPct}%`,
|
|
215
|
+
background: ctxPct >= 75 ? 'var(--color-danger)' : ctxPct >= 50 ? 'var(--color-warning)' : 'var(--color-accent)',
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
<span className="text-[10px] text-text-4 font-mono w-7 text-right">{ctxPct}%</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
188
222
|
</div>
|
|
189
223
|
);
|
|
190
224
|
}
|
|
@@ -205,6 +239,7 @@ export function AgentFeed({ agent }) {
|
|
|
205
239
|
const queryAgent = useGrooveStore((s) => s.queryAgent);
|
|
206
240
|
|
|
207
241
|
const [input, setInput] = useState('');
|
|
242
|
+
const [mode, setMode] = useState('instruct'); // instruct | query
|
|
208
243
|
const [sending, setSending] = useState(false);
|
|
209
244
|
const scrollRef = useRef(null);
|
|
210
245
|
const inputRef = useRef(null);
|
|
@@ -271,8 +306,8 @@ export function AgentFeed({ agent }) {
|
|
|
271
306
|
setInput('');
|
|
272
307
|
setSending(true);
|
|
273
308
|
try {
|
|
274
|
-
if (
|
|
275
|
-
await queryAgent(agent.id, text
|
|
309
|
+
if (mode === 'query') {
|
|
310
|
+
await queryAgent(agent.id, text);
|
|
276
311
|
} else {
|
|
277
312
|
await instructAgent(agent.id, text);
|
|
278
313
|
}
|
|
@@ -288,7 +323,6 @@ export function AgentFeed({ agent }) {
|
|
|
288
323
|
}
|
|
289
324
|
}
|
|
290
325
|
|
|
291
|
-
const isQuery = input.startsWith('?');
|
|
292
326
|
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
293
327
|
|
|
294
328
|
return (
|
|
@@ -296,25 +330,27 @@ export function AgentFeed({ agent }) {
|
|
|
296
330
|
{isAlive && <StreamingBar agent={agent} />}
|
|
297
331
|
|
|
298
332
|
{/* Messages area */}
|
|
299
|
-
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-
|
|
333
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
|
|
300
334
|
{timeline.length === 0 && (
|
|
301
335
|
<div className="flex flex-col items-center justify-center h-full text-center py-8">
|
|
302
336
|
{isAlive ? (
|
|
303
337
|
<>
|
|
304
|
-
<div className="relative w-
|
|
305
|
-
<span className="absolute inset-0 rounded-full border border-accent/
|
|
338
|
+
<div className="relative w-12 h-12 mb-4">
|
|
339
|
+
<span className="absolute inset-0 rounded-full border border-accent/20 animate-ping" style={{ animationDuration: '2.5s' }} />
|
|
306
340
|
<span className="absolute inset-0 rounded-full bg-accent/6 flex items-center justify-center">
|
|
307
341
|
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
|
|
308
342
|
</span>
|
|
309
343
|
</div>
|
|
310
|
-
<p className="text-sm font-
|
|
311
|
-
<p className="text-xs text-text-3 font-sans mt-1
|
|
344
|
+
<p className="text-sm font-semibold text-text-0 font-sans">{agent.name}</p>
|
|
345
|
+
<p className="text-xs text-text-3 font-sans mt-1">Initializing session...</p>
|
|
312
346
|
</>
|
|
313
347
|
) : (
|
|
314
348
|
<>
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
349
|
+
<div className="w-10 h-10 rounded-xl bg-surface-3 flex items-center justify-center mb-3">
|
|
350
|
+
<MessageSquare size={18} className="text-text-4" />
|
|
351
|
+
</div>
|
|
352
|
+
<p className="text-sm font-semibold text-text-0 font-sans">{agent.name}</p>
|
|
353
|
+
<p className="text-xs text-text-3 font-sans mt-1">Session complete — send a message to continue</p>
|
|
318
354
|
</>
|
|
319
355
|
)}
|
|
320
356
|
</div>
|
|
@@ -328,53 +364,90 @@ export function AgentFeed({ agent }) {
|
|
|
328
364
|
return <AgentMessage key={`msg-${i}`} msg={item} agent={agent} />;
|
|
329
365
|
})}
|
|
330
366
|
{sending && (
|
|
331
|
-
<div className="
|
|
332
|
-
<div className="flex items-center
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
367
|
+
<div className="flex items-center gap-2 ml-7 py-2">
|
|
368
|
+
<div className="w-5 h-5 rounded-md bg-accent/12 flex items-center justify-center">
|
|
369
|
+
<Code2 size={10} className="text-accent" />
|
|
370
|
+
</div>
|
|
371
|
+
<div className="flex items-center gap-1.5 px-3 py-2 rounded-2xl bg-surface-2/60 border border-border-subtle">
|
|
372
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '0ms' }} />
|
|
373
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '200ms' }} />
|
|
374
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '400ms' }} />
|
|
336
375
|
</div>
|
|
337
376
|
</div>
|
|
338
377
|
)}
|
|
339
378
|
</div>
|
|
340
379
|
|
|
341
|
-
{/* Input */}
|
|
342
|
-
<div className="border-t border-border
|
|
343
|
-
|
|
380
|
+
{/* Input area */}
|
|
381
|
+
<div className="border-t border-border px-4 py-3 bg-surface-1/50 flex-shrink-0">
|
|
382
|
+
{/* Mode pills */}
|
|
383
|
+
<div className="flex items-center gap-1 mb-2">
|
|
384
|
+
<button
|
|
385
|
+
onClick={() => setMode('instruct')}
|
|
386
|
+
className={cn(
|
|
387
|
+
'flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-sans font-medium transition-colors cursor-pointer',
|
|
388
|
+
mode === 'instruct'
|
|
389
|
+
? 'bg-accent/12 text-accent border border-accent/20'
|
|
390
|
+
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
391
|
+
)}
|
|
392
|
+
>
|
|
393
|
+
<Pencil size={10} />
|
|
394
|
+
Instruct
|
|
395
|
+
</button>
|
|
396
|
+
<button
|
|
397
|
+
onClick={() => setMode('query')}
|
|
398
|
+
className={cn(
|
|
399
|
+
'flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-sans font-medium transition-colors cursor-pointer',
|
|
400
|
+
mode === 'query'
|
|
401
|
+
? 'bg-info/12 text-info border border-info/20'
|
|
402
|
+
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
403
|
+
)}
|
|
404
|
+
>
|
|
405
|
+
<HelpCircle size={10} />
|
|
406
|
+
Query
|
|
407
|
+
</button>
|
|
408
|
+
<span className="text-[10px] text-text-4 font-sans ml-auto">
|
|
409
|
+
{mode === 'query' ? 'Read-only — agent keeps working' : isAlive ? 'Directs the agent' : 'Continues the session'}
|
|
410
|
+
</span>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div className={cn(
|
|
414
|
+
'flex items-end gap-2 rounded-xl border bg-surface-0 p-1 transition-colors',
|
|
415
|
+
mode === 'query' ? 'border-info/20 focus-within:border-info/40' : 'border-border-subtle focus-within:border-accent/30',
|
|
416
|
+
)}>
|
|
344
417
|
<textarea
|
|
345
418
|
ref={inputRef}
|
|
346
419
|
value={input}
|
|
347
420
|
onChange={(e) => setInput(e.target.value)}
|
|
348
421
|
onKeyDown={onKeyDown}
|
|
349
|
-
placeholder={
|
|
422
|
+
placeholder={mode === 'query'
|
|
423
|
+
? 'Ask about this agent\'s work...'
|
|
424
|
+
: isAlive ? 'Send an instruction...' : 'Continue this session...'}
|
|
350
425
|
rows={1}
|
|
351
426
|
className={cn(
|
|
352
|
-
'flex-1 resize-none
|
|
353
|
-
'bg-
|
|
427
|
+
'flex-1 resize-none px-3 py-2 text-[13px]',
|
|
428
|
+
'bg-transparent text-text-0 font-sans',
|
|
354
429
|
'placeholder:text-text-4',
|
|
355
|
-
'focus:outline-none
|
|
356
|
-
'max-h-[
|
|
357
|
-
isQuery ? 'border-info/25 focus:ring-info/30' : 'border-border-subtle focus:ring-accent/30',
|
|
430
|
+
'focus:outline-none',
|
|
431
|
+
'max-h-[120px] min-h-[36px]',
|
|
358
432
|
)}
|
|
359
|
-
style={{ height: Math.min(Math.max(36, input.split('\n').length * 20),
|
|
433
|
+
style={{ height: Math.min(Math.max(36, input.split('\n').length * 20), 120) }}
|
|
360
434
|
/>
|
|
361
435
|
<button
|
|
362
436
|
onClick={handleSend}
|
|
363
437
|
disabled={!input.trim() || sending}
|
|
364
438
|
className={cn(
|
|
365
|
-
'w-
|
|
439
|
+
'w-9 h-9 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0 mb-px',
|
|
366
440
|
'disabled:opacity-15 disabled:cursor-not-allowed',
|
|
367
441
|
input.trim()
|
|
368
|
-
?
|
|
442
|
+
? mode === 'query'
|
|
443
|
+
? 'bg-info text-white hover:bg-info/85'
|
|
444
|
+
: 'bg-accent text-white hover:bg-accent/85'
|
|
369
445
|
: 'bg-transparent text-text-4',
|
|
370
446
|
)}
|
|
371
447
|
>
|
|
372
|
-
{sending ? <Loader2 size={
|
|
448
|
+
{sending ? <Loader2 size={15} className="animate-spin" /> : <Send size={15} />}
|
|
373
449
|
</button>
|
|
374
450
|
</div>
|
|
375
|
-
{isQuery && (
|
|
376
|
-
<div className="text-2xs text-info/70 font-sans mt-1 pl-1">Read-only query — agent keeps running</div>
|
|
377
|
-
)}
|
|
378
451
|
</div>
|
|
379
452
|
</div>
|
|
380
453
|
);
|
|
@@ -117,7 +117,7 @@ const STATUS_SHORT = {
|
|
|
117
117
|
const AgentNode = memo(({ data, selected }) => {
|
|
118
118
|
const { agent } = data;
|
|
119
119
|
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
120
|
-
const contextPct = Math.round((agent.
|
|
120
|
+
const contextPct = Math.round((agent.contextUsage || 0) * 100);
|
|
121
121
|
const sColor = statusColor(agent.status);
|
|
122
122
|
const tokens = agent.tokensUsed || 0;
|
|
123
123
|
const [hovered, setHovered] = useState(false);
|
|
@@ -85,7 +85,7 @@ export function AgentPanel() {
|
|
|
85
85
|
if (!agent) return null;
|
|
86
86
|
|
|
87
87
|
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
88
|
-
const ctxPct = Math.round((agent.
|
|
88
|
+
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
89
89
|
const spawned = agent.spawnedAt || agent.createdAt;
|
|
90
90
|
const uptime = spawned ? Math.floor((Date.now() - new Date(spawned).getTime()) / 1000) : 0;
|
|
91
91
|
const colors = roleColor(agent.role);
|
|
@@ -153,7 +153,7 @@ export function AgentTelemetry({ agent }) {
|
|
|
153
153
|
|
|
154
154
|
// Rough health score based on context usage and burn rate
|
|
155
155
|
const healthScore = useMemo(() => {
|
|
156
|
-
const ctx = agent.
|
|
156
|
+
const ctx = agent.contextUsage || 0;
|
|
157
157
|
let score = 100;
|
|
158
158
|
if (ctx > 90) score -= 50;
|
|
159
159
|
else if (ctx > 70) score -= 25;
|
|
@@ -163,7 +163,7 @@ export function AgentTelemetry({ agent }) {
|
|
|
163
163
|
if (agent.status === 'crashed') score = 10;
|
|
164
164
|
if (agent.status === 'completed') score = 95;
|
|
165
165
|
return Math.max(0, Math.min(100, score));
|
|
166
|
-
}, [agent.
|
|
166
|
+
}, [agent.contextUsage, agent.status, burnRate]);
|
|
167
167
|
|
|
168
168
|
return (
|
|
169
169
|
<div className="px-5 py-5 space-y-5 overflow-y-auto h-full">
|
|
@@ -48,8 +48,8 @@ export function FleetPanel({ agents }) {
|
|
|
48
48
|
<div
|
|
49
49
|
className="h-full rounded-full"
|
|
50
50
|
style={{
|
|
51
|
-
width: `${Math.min(a.
|
|
52
|
-
background: (a.
|
|
51
|
+
width: `${Math.min(a.contextUsage || 0, 100)}%`,
|
|
52
|
+
background: (a.contextUsage || 0) >= 80 ? 'var(--color-danger)' : (a.contextUsage || 0) >= 60 ? 'var(--color-warning)' : 'var(--color-success)',
|
|
53
53
|
}}
|
|
54
54
|
/>
|
|
55
55
|
</div>
|
|
@@ -118,7 +118,19 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
118
118
|
|
|
119
119
|
case 'agent:output': {
|
|
120
120
|
const { agentId, data } = msg;
|
|
121
|
-
const text = typeof data.data === 'string' ? data.data : (Array.isArray(data.data) ? data.data.filter((b) => b.type === 'text').map((b) => b.text).join('\n') :
|
|
121
|
+
const text = typeof data.data === 'string' ? data.data : (Array.isArray(data.data) ? data.data.filter((b) => b.type === 'text').map((b) => b.text).join('\n') : '');
|
|
122
|
+
|
|
123
|
+
// Update agent metrics in real-time (contextUsage, tokensUsed)
|
|
124
|
+
if (data.contextUsage !== undefined || data.tokensUsed !== undefined) {
|
|
125
|
+
const agents = get().agents.map((a) => {
|
|
126
|
+
if (a.id !== agentId) return a;
|
|
127
|
+
const updates = {};
|
|
128
|
+
if (data.contextUsage !== undefined) updates.contextUsage = data.contextUsage;
|
|
129
|
+
if (data.tokensUsed !== undefined) updates.tokensUsed = data.tokensUsed;
|
|
130
|
+
return { ...a, ...updates };
|
|
131
|
+
});
|
|
132
|
+
set({ agents });
|
|
133
|
+
}
|
|
122
134
|
|
|
123
135
|
// Promote assistant text and result responses to chat bubbles
|
|
124
136
|
if ((data.subtype === 'assistant' || data.type === 'result') && text && text.trim()) {
|
|
@@ -127,14 +139,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
127
139
|
history[agentId] = [...history[agentId].slice(-100), { from: 'agent', text: text.trim(), timestamp: Date.now() }];
|
|
128
140
|
set({ chatHistory: history });
|
|
129
141
|
persistJSON('groove:chatHistory', history);
|
|
130
|
-
} else {
|
|
131
|
-
//
|
|
142
|
+
} else if (text && text.trim()) {
|
|
143
|
+
// Non-empty activity goes to activity log
|
|
132
144
|
const log = { ...get().activityLog };
|
|
133
145
|
if (!log[agentId]) log[agentId] = [];
|
|
134
146
|
log[agentId] = [...log[agentId].slice(-200), {
|
|
135
147
|
timestamp: Date.now(),
|
|
136
|
-
text: text
|
|
148
|
+
text: text.trim(),
|
|
137
149
|
type: data.type,
|
|
150
|
+
subtype: data.subtype || null,
|
|
138
151
|
}];
|
|
139
152
|
set({ activityLog: log });
|
|
140
153
|
persistJSON('groove:activityLog', log);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. 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.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
// GROOVE CLI — start command
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
4
6
|
import { Daemon } from '@groove-dev/daemon';
|
|
5
7
|
import chalk from 'chalk';
|
|
8
|
+
import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
|
|
6
9
|
|
|
7
10
|
export async function start(options) {
|
|
11
|
+
const grooveDir = resolve(process.cwd(), '.groove');
|
|
12
|
+
const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
|
|
13
|
+
|
|
14
|
+
// ── First-run interactive wizard ────────────────────────────
|
|
15
|
+
let setupKeys = {};
|
|
16
|
+
if (isFirstRun) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await runSetupWizard();
|
|
19
|
+
setupKeys = result.keys || {};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// If stdin is not interactive (piped), skip wizard
|
|
22
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE') {
|
|
23
|
+
console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
|
|
24
|
+
} else {
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Start daemon ────────────────────────────────────────────
|
|
8
31
|
console.log(chalk.bold('GROOVE') + ' starting daemon...');
|
|
9
32
|
|
|
10
33
|
try {
|
|
@@ -15,7 +38,6 @@ export async function start(options) {
|
|
|
15
38
|
|
|
16
39
|
const shutdown = async () => {
|
|
17
40
|
console.log('\nShutting down...');
|
|
18
|
-
// Force exit after 3s if stop hangs
|
|
19
41
|
const forceTimer = setTimeout(() => process.exit(1), 3000);
|
|
20
42
|
forceTimer.unref();
|
|
21
43
|
try { await daemon.stop(); } catch { /* ignore */ }
|
|
@@ -26,6 +48,12 @@ export async function start(options) {
|
|
|
26
48
|
process.on('SIGTERM', shutdown);
|
|
27
49
|
|
|
28
50
|
await daemon.start();
|
|
51
|
+
|
|
52
|
+
// Save API keys from wizard (after daemon is running)
|
|
53
|
+
if (Object.keys(setupKeys).length > 0) {
|
|
54
|
+
await saveKeysViaDaemon(setupKeys, daemon.port);
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
console.log(chalk.green('Ready.'));
|
|
30
58
|
} catch (err) {
|
|
31
59
|
console.error(chalk.red('Failed to start:'), err.message);
|