groove-dev 0.22.2 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/gui/dist/assets/index-BDyGhxDd.css +1 -0
  3. package/node_modules/@groove-dev/gui/dist/assets/index-BHDZqhzW.js +562 -0
  4. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  5. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -71
  6. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +1 -1
  7. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +1 -1
  8. package/node_modules/@groove-dev/gui/src/components/agents/agent-telemetry.jsx +2 -2
  9. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +2 -2
  10. package/node_modules/@groove-dev/gui/src/stores/groove.js +17 -4
  11. package/package.json +1 -1
  12. package/packages/gui/dist/assets/index-BDyGhxDd.css +1 -0
  13. package/packages/gui/dist/assets/index-BHDZqhzW.js +562 -0
  14. package/packages/gui/dist/index.html +2 -2
  15. package/packages/gui/src/components/agents/agent-feed.jsx +144 -71
  16. package/packages/gui/src/components/agents/agent-node.jsx +1 -1
  17. package/packages/gui/src/components/agents/agent-panel.jsx +1 -1
  18. package/packages/gui/src/components/agents/agent-telemetry.jsx +2 -2
  19. package/packages/gui/src/components/dashboard/fleet-panel.jsx +2 -2
  20. package/packages/gui/src/stores/groove.js +17 -4
  21. package/node_modules/@groove-dev/gui/dist/assets/index-DtZ-1z8T.css +0 -1
  22. package/node_modules/@groove-dev/gui/dist/assets/index-cKOfI2iD.js +0 -562
  23. package/packages/gui/dist/assets/index-DtZ-1z8T.css +0 -1
  24. 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-cKOfI2iD.js"></script>
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-DtZ-1z8T.css">
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, HelpCircle, ArrowRight,
4
+ Send, Loader2, MessageSquare, ArrowRight,
5
5
  FileEdit, Search, Terminal, CheckCircle2, AlertCircle,
6
- RotateCw, Zap, Wrench, Eye, FileText, Code2, Bug,
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 <pre key={i} className="my-2 p-3 rounded-lg bg-black/30 text-[12px] font-mono text-text-1 overflow-x-auto whitespace-pre-wrap border border-white/[0.04] leading-relaxed">{code}</pre>;
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-px rounded bg-white/[0.06] text-[12px] font-mono text-accent">{part.slice(1, -1)}</code>;
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-3.5 py-2.5 rounded-lg rounded-br-sm',
73
- msg.isQuery
74
- ? 'bg-info/12 text-text-0'
75
- : 'bg-accent/12 text-text-0',
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-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
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-8">
90
- <div className="border-l-2 border-accent/30 pl-3.5 py-0.5">
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-2xs text-text-4 font-sans mt-1 pl-4">{timeAgo(msg.timestamp)}</div>
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-1">
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-2xs text-text-4 font-sans flex-shrink-0">{msg.text}</span>
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 > 100 ? entry.text.slice(0, 100) + '...' : entry.text;
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-px group">
119
- <Icon size={9} className={cn(meta.color, 'flex-shrink-0 opacity-60')} />
120
- <p className="text-2xs text-text-3 font-sans truncate flex-1 min-w-0">{display}</p>
121
- <span className="text-2xs text-text-4 font-mono opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
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 : [entries[0]];
136
- const hiddenCount = entries.length - 1;
155
+ const visible = expanded ? entries : entries.slice(0, 2);
156
+ const hiddenCount = entries.length - 2;
137
157
 
138
158
  return (
139
- <div className="space-y-px py-0.5 border-l border-border-subtle pl-3 ml-1">
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-2xs text-text-4 hover:text-text-2 font-sans cursor-pointer py-0.5"
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={8} />
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 > 50 ? lastActivity.text.slice(0, 50) + '...' : lastActivity.text)
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-2 px-4 h-7 border-b border-border-subtle bg-surface-0/50 flex-shrink-0">
171
- <div className="flex items-center gap-1.5 flex-1 min-w-0">
172
- <div className="relative flex items-center justify-center w-3.5 h-3.5">
173
- <span className="absolute inset-0 rounded-full bg-accent/20 animate-ping" style={{ animationDuration: '2s' }} />
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={9} className={cn(meta.color, 'flex-shrink-0')} />
179
- <span className="text-2xs text-text-2 font-sans truncate">{display}</span>
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-2xs text-text-3 font-sans">Processing...</span>
204
+ <span className="text-[11px] text-text-3 font-sans">Working...</span>
183
205
  )}
184
206
  </div>
185
- <span className="text-2xs text-text-4 font-mono flex-shrink-0">
186
- {fmtTokens(agent.tokensUsed)} tok
187
- </span>
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 (text.startsWith('?')) {
275
- await queryAgent(agent.id, text.slice(1).trim());
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-3 space-y-3">
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-10 h-10 mb-4">
305
- <span className="absolute inset-0 rounded-full border border-accent/25 animate-ping" style={{ animationDuration: '2.5s' }} />
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-medium text-text-1 font-sans">{agent.name} initializing</p>
311
- <p className="text-xs text-text-3 font-sans mt-1.5">Scanning workspace and loading context</p>
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
- <MessageSquare size={18} className="text-text-4 mb-3" />
316
- <p className="text-sm font-medium text-text-1 font-sans">Session complete</p>
317
- <p className="text-xs text-text-3 font-sans mt-1.5">Reply to continue with full context</p>
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="border-l-2 border-accent/20 pl-3.5 py-2">
332
- <div className="flex items-center gap-1.5">
333
- <span className="w-1 h-1 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '0ms' }} />
334
- <span className="w-1 h-1 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '150ms' }} />
335
- <span className="w-1 h-1 rounded-full bg-accent/60 animate-pulse" style={{ animationDelay: '300ms' }} />
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-subtle px-3 py-2.5 bg-surface-0/30 flex-shrink-0">
343
- <div className="flex items-end gap-2">
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={isAlive ? (isQuery ? 'Query (read-only)...' : 'Instruct agent...') : 'Continue conversation...'}
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 rounded-lg px-3 py-2 text-[13px]',
353
- 'bg-surface-0 border text-text-0 font-sans',
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 focus:ring-1',
356
- 'max-h-[100px] min-h-[36px]',
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), 100) }}
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-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
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
- ? 'bg-accent text-surface-0 hover:bg-accent/85'
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={14} className="animate-spin" /> : <Send size={14} />}
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.contextUsed || 0) * 100);
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.contextUsed || 0) * 100);
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.contextUsed || 0;
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.contextUsed, agent.status, burnRate]);
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.contextUsed || 0, 100)}%`,
52
- background: (a.contextUsed || 0) >= 80 ? 'var(--color-danger)' : (a.contextUsed || 0) >= 60 ? 'var(--color-warning)' : 'var(--color-success)',
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') : JSON.stringify(data.data));
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
- // Everything else goes to activity log
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);