groove-dev 0.8.0
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 +197 -0
- package/LICENSE +40 -0
- package/README.md +115 -0
- package/docs/GUI_DESIGN_SPEC.md +402 -0
- package/favicon.png +0 -0
- package/groove-logo-short.png +0 -0
- package/groove-logo.png +0 -0
- package/package.json +70 -0
- package/packages/cli/bin/groove.js +98 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/client.js +25 -0
- package/packages/cli/src/commands/agents.js +38 -0
- package/packages/cli/src/commands/approve.js +50 -0
- package/packages/cli/src/commands/config.js +35 -0
- package/packages/cli/src/commands/kill.js +15 -0
- package/packages/cli/src/commands/nuke.js +19 -0
- package/packages/cli/src/commands/providers.js +40 -0
- package/packages/cli/src/commands/rotate.js +16 -0
- package/packages/cli/src/commands/spawn.js +91 -0
- package/packages/cli/src/commands/start.js +31 -0
- package/packages/cli/src/commands/status.js +38 -0
- package/packages/cli/src/commands/stop.js +15 -0
- package/packages/cli/src/commands/team.js +77 -0
- package/packages/daemon/package.json +18 -0
- package/packages/daemon/src/adaptive.js +237 -0
- package/packages/daemon/src/api.js +533 -0
- package/packages/daemon/src/classifier.js +126 -0
- package/packages/daemon/src/credentials.js +121 -0
- package/packages/daemon/src/firstrun.js +93 -0
- package/packages/daemon/src/index.js +208 -0
- package/packages/daemon/src/introducer.js +238 -0
- package/packages/daemon/src/journalist.js +600 -0
- package/packages/daemon/src/lockmanager.js +58 -0
- package/packages/daemon/src/pm.js +108 -0
- package/packages/daemon/src/process.js +361 -0
- package/packages/daemon/src/providers/aider.js +72 -0
- package/packages/daemon/src/providers/base.js +38 -0
- package/packages/daemon/src/providers/claude-code.js +167 -0
- package/packages/daemon/src/providers/codex.js +68 -0
- package/packages/daemon/src/providers/gemini.js +62 -0
- package/packages/daemon/src/providers/index.js +38 -0
- package/packages/daemon/src/providers/ollama.js +94 -0
- package/packages/daemon/src/registry.js +89 -0
- package/packages/daemon/src/rotator.js +185 -0
- package/packages/daemon/src/router.js +132 -0
- package/packages/daemon/src/state.js +34 -0
- package/packages/daemon/src/supervisor.js +178 -0
- package/packages/daemon/src/teams.js +203 -0
- package/packages/daemon/src/terminal/base.js +27 -0
- package/packages/daemon/src/terminal/generic.js +27 -0
- package/packages/daemon/src/terminal/tmux.js +64 -0
- package/packages/daemon/src/tokentracker.js +124 -0
- package/packages/daemon/src/validate.js +122 -0
- package/packages/daemon/templates/api-builder.json +18 -0
- package/packages/daemon/templates/fullstack.json +18 -0
- package/packages/daemon/templates/monorepo.json +24 -0
- package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
- package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
- package/packages/gui/dist/favicon.png +0 -0
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/dist/index.html +13 -0
- package/packages/gui/index.html +12 -0
- package/packages/gui/package.json +22 -0
- package/packages/gui/public/favicon.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/App.jsx +215 -0
- package/packages/gui/src/components/AgentActions.jsx +347 -0
- package/packages/gui/src/components/AgentChat.jsx +479 -0
- package/packages/gui/src/components/AgentNode.jsx +117 -0
- package/packages/gui/src/components/AgentPanel.jsx +115 -0
- package/packages/gui/src/components/AgentStats.jsx +333 -0
- package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
- package/packages/gui/src/components/EmptyState.jsx +100 -0
- package/packages/gui/src/components/SpawnPanel.jsx +515 -0
- package/packages/gui/src/components/TeamSelector.jsx +162 -0
- package/packages/gui/src/main.jsx +9 -0
- package/packages/gui/src/stores/groove.js +247 -0
- package/packages/gui/src/theme.css +67 -0
- package/packages/gui/src/views/AgentTree.jsx +148 -0
- package/packages/gui/src/views/CommandCenter.jsx +620 -0
- package/packages/gui/src/views/JournalistFeed.jsx +149 -0
- package/packages/gui/vite.config.js +19 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// GROOVE GUI — Agent Chat Tab
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
5
|
+
import { useGrooveStore } from '../stores/groove';
|
|
6
|
+
|
|
7
|
+
export default function AgentChat({ agent }) {
|
|
8
|
+
const [input, setInput] = useState('');
|
|
9
|
+
const [status, setStatus] = useState(null);
|
|
10
|
+
const scrollRef = useRef();
|
|
11
|
+
|
|
12
|
+
const activityLog = useGrooveStore((s) => s.activityLog);
|
|
13
|
+
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
14
|
+
const queryAgent = useGrooveStore((s) => s.queryAgent);
|
|
15
|
+
const showStatus = useGrooveStore((s) => s.showStatus);
|
|
16
|
+
const chatHistory = useGrooveStore((s) => s.chatHistory);
|
|
17
|
+
|
|
18
|
+
const activity = activityLog[agent.id] || [];
|
|
19
|
+
const chats = chatHistory[agent.id] || [];
|
|
20
|
+
|
|
21
|
+
const timeline = buildTimeline(chats, activity);
|
|
22
|
+
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (scrollRef.current) {
|
|
26
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
27
|
+
}
|
|
28
|
+
}, [timeline.length]);
|
|
29
|
+
|
|
30
|
+
async function handleSubmit() {
|
|
31
|
+
const text = input.trim();
|
|
32
|
+
if (!text || status) return;
|
|
33
|
+
|
|
34
|
+
const isQuery = text.startsWith('?');
|
|
35
|
+
const message = isQuery ? text.slice(1).trim() : text;
|
|
36
|
+
if (!message) return;
|
|
37
|
+
|
|
38
|
+
setInput('');
|
|
39
|
+
|
|
40
|
+
if (isQuery && isAlive) {
|
|
41
|
+
// Query — one-shot read-only question, agent keeps running
|
|
42
|
+
setStatus('querying...');
|
|
43
|
+
try {
|
|
44
|
+
await queryAgent(agent.id, message);
|
|
45
|
+
} catch { /* handled in store */ }
|
|
46
|
+
setStatus(null);
|
|
47
|
+
} else {
|
|
48
|
+
// Instruct — works for both alive (rotation) and dead (continuation) agents
|
|
49
|
+
setStatus(isAlive ? 'sending...' : 'continuing...');
|
|
50
|
+
try {
|
|
51
|
+
await instructAgent(agent.id, message);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
showStatus(`failed: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
setStatus(null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleKeyDown(e) {
|
|
60
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
handleSubmit();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div style={styles.container}>
|
|
69
|
+
{/* Timeline */}
|
|
70
|
+
<div ref={scrollRef} style={styles.timeline}>
|
|
71
|
+
{timeline.length === 0 && (
|
|
72
|
+
<div style={styles.hint}>
|
|
73
|
+
{isAlive
|
|
74
|
+
? 'Type a message to instruct this agent. Prefix with ? to query without disrupting.'
|
|
75
|
+
: 'Agent finished. Reply to continue the conversation.'}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
{timeline.map((entry, i) => (
|
|
79
|
+
<div key={i} style={styles.entry}>
|
|
80
|
+
{entry.from === 'user' && (
|
|
81
|
+
<div style={styles.userMsg}>
|
|
82
|
+
<span style={styles.userLabel}>
|
|
83
|
+
{entry.isQuery ? '? you' : '> you'}
|
|
84
|
+
</span>
|
|
85
|
+
<div style={styles.userText}>{entry.text}</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
{entry.from === 'agent' && (
|
|
89
|
+
<div style={styles.agentMsg}>
|
|
90
|
+
<span style={styles.agentLabel}>{agent.name}</span>
|
|
91
|
+
<div style={styles.agentText}>
|
|
92
|
+
{/* Stream the latest agent message, show history instantly */}
|
|
93
|
+
{i === timeline.length - 1 && entry.from === 'agent' && Date.now() - entry.timestamp < 5000
|
|
94
|
+
? <StreamingText text={entry.text} />
|
|
95
|
+
: <FormattedText text={entry.text} />
|
|
96
|
+
}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
{entry.from === 'system' && (
|
|
101
|
+
<div style={styles.systemMsg}>{entry.text}</div>
|
|
102
|
+
)}
|
|
103
|
+
<span style={styles.time}>
|
|
104
|
+
{new Date(entry.timestamp).toLocaleTimeString()}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
{status && (
|
|
109
|
+
<div style={styles.statusMsg}>{status}</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Launch Team button — shown when planner completes */}
|
|
114
|
+
{agent.role === 'planner' && agent.status === 'completed' && (
|
|
115
|
+
<LaunchTeamButton showStatus={showStatus} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Input — always enabled */}
|
|
119
|
+
<div style={styles.inputRow}>
|
|
120
|
+
<input
|
|
121
|
+
style={styles.input}
|
|
122
|
+
value={input}
|
|
123
|
+
onChange={(e) => setInput(e.target.value)}
|
|
124
|
+
onKeyDown={handleKeyDown}
|
|
125
|
+
placeholder={isAlive ? 'message or ?query...' : 'reply to continue...'}
|
|
126
|
+
disabled={!!status}
|
|
127
|
+
spellCheck={false}
|
|
128
|
+
/>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={handleSubmit}
|
|
132
|
+
disabled={!!status || !input.trim()}
|
|
133
|
+
style={{
|
|
134
|
+
...styles.sendBtn,
|
|
135
|
+
opacity: (!!status || !input.trim()) ? 0.3 : 1,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
Send
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── STREAMING TEXT — reveals text progressively for latest agent message ──
|
|
146
|
+
|
|
147
|
+
function StreamingText({ text }) {
|
|
148
|
+
const [revealed, setRevealed] = useState(0);
|
|
149
|
+
const textRef = useRef(text);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
// Reset on new text
|
|
153
|
+
textRef.current = text;
|
|
154
|
+
setRevealed(0);
|
|
155
|
+
}, [text]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (revealed >= text.length) return;
|
|
159
|
+
// Reveal 2-4 chars at a time for a smooth streaming feel
|
|
160
|
+
const chunkSize = Math.random() > 0.7 ? 4 : 2;
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
setRevealed((r) => Math.min(r + chunkSize, text.length));
|
|
163
|
+
}, 12);
|
|
164
|
+
return () => clearTimeout(timer);
|
|
165
|
+
}, [revealed, text.length]);
|
|
166
|
+
|
|
167
|
+
const visibleText = text.slice(0, revealed);
|
|
168
|
+
const done = revealed >= text.length;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<FormattedText text={visibleText} />
|
|
173
|
+
{!done && <span style={styles.cursor}>|</span>}
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── LAUNCH TEAM BUTTON — one-click spawn from planner recommendation ──
|
|
179
|
+
|
|
180
|
+
function LaunchTeamButton({ showStatus }) {
|
|
181
|
+
const [team, setTeam] = useState(null);
|
|
182
|
+
const [launching, setLaunching] = useState(false);
|
|
183
|
+
const [launched, setLaunched] = useState(false);
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
fetch('/api/recommended-team')
|
|
187
|
+
.then((r) => r.json())
|
|
188
|
+
.then((d) => { if (d.exists && d.agents.length > 0) setTeam(d.agents); })
|
|
189
|
+
.catch(() => {});
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
async function handleLaunch() {
|
|
193
|
+
setLaunching(true);
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch('/api/recommended-team/launch', { method: 'POST' });
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
if (data.launched) {
|
|
198
|
+
showStatus(`Launched ${data.launched} agents`);
|
|
199
|
+
setLaunched(true);
|
|
200
|
+
} else {
|
|
201
|
+
showStatus(`Launch failed: ${data.error || 'unknown'}`);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
showStatus(`Launch failed: ${err.message}`);
|
|
205
|
+
}
|
|
206
|
+
setLaunching(false);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!team || launched) return null;
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div style={styles.launchBox}>
|
|
213
|
+
<div style={styles.launchHeader}>Recommended Team ({team.length} agents)</div>
|
|
214
|
+
<div style={styles.launchList}>
|
|
215
|
+
{team.map((a, i) => (
|
|
216
|
+
<div key={i} style={styles.launchAgent}>
|
|
217
|
+
<span style={styles.launchRole}>{a.role}</span>
|
|
218
|
+
<span style={styles.launchPrompt}>{(a.prompt || '').slice(0, 80)}{(a.prompt || '').length > 80 ? '...' : ''}</span>
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={handleLaunch}
|
|
225
|
+
disabled={launching}
|
|
226
|
+
style={{ ...styles.launchBtn, opacity: launching ? 0.5 : 1 }}
|
|
227
|
+
>
|
|
228
|
+
{launching ? 'Launching...' : 'Launch Team'}
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── FORMATTED TEXT — renders markdown-like agent output cleanly ──
|
|
235
|
+
|
|
236
|
+
function FormattedText({ text }) {
|
|
237
|
+
if (!text) return null;
|
|
238
|
+
const lines = text.split('\n');
|
|
239
|
+
|
|
240
|
+
return lines.map((line, i) => {
|
|
241
|
+
// Headers: ### or ## or #
|
|
242
|
+
if (/^#{1,3}\s/.test(line)) {
|
|
243
|
+
const content = line.replace(/^#{1,3}\s+/, '');
|
|
244
|
+
return <div key={i} style={{ fontWeight: 700, color: 'var(--text-bright)', marginTop: i > 0 ? 6 : 0, marginBottom: 2, fontSize: 11 }}>{renderInline(content)}</div>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Horizontal rules
|
|
248
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
249
|
+
return <div key={i} style={{ borderTop: '1px solid var(--border)', margin: '4px 0' }} />;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// List items: - or * or numbered
|
|
253
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
254
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
255
|
+
const content = line.replace(/^\s*[-*]\s+/, '');
|
|
256
|
+
return <div key={i} style={{ paddingLeft: 8 + indent * 6, position: 'relative' }}>
|
|
257
|
+
<span style={{ position: 'absolute', left: indent * 6, color: 'var(--text-dim)' }}>-</span>
|
|
258
|
+
{renderInline(content)}
|
|
259
|
+
</div>;
|
|
260
|
+
}
|
|
261
|
+
if (/^\s*\d+\.\s/.test(line)) {
|
|
262
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
263
|
+
const num = line.match(/(\d+)\./)[1];
|
|
264
|
+
const content = line.replace(/^\s*\d+\.\s+/, '');
|
|
265
|
+
return <div key={i} style={{ paddingLeft: 12 + indent * 6, position: 'relative' }}>
|
|
266
|
+
<span style={{ position: 'absolute', left: indent * 6, color: 'var(--text-dim)' }}>{num}.</span>
|
|
267
|
+
{renderInline(content)}
|
|
268
|
+
</div>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Empty lines
|
|
272
|
+
if (!line.trim()) return <div key={i} style={{ height: 4 }} />;
|
|
273
|
+
|
|
274
|
+
// Normal text
|
|
275
|
+
return <div key={i}>{renderInline(line)}</div>;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderInline(text) {
|
|
280
|
+
// Split on bold (**text**), code (`text`), and italic (*text*)
|
|
281
|
+
const parts = [];
|
|
282
|
+
let remaining = text;
|
|
283
|
+
let key = 0;
|
|
284
|
+
|
|
285
|
+
while (remaining.length > 0) {
|
|
286
|
+
// Bold: **text**
|
|
287
|
+
const boldMatch = remaining.match(/^(.*?)\*\*(.+?)\*\*(.*)/s);
|
|
288
|
+
if (boldMatch) {
|
|
289
|
+
if (boldMatch[1]) parts.push(<span key={key++}>{boldMatch[1]}</span>);
|
|
290
|
+
parts.push(<span key={key++} style={{ fontWeight: 700, color: 'var(--text-bright)' }}>{boldMatch[2]}</span>);
|
|
291
|
+
remaining = boldMatch[3];
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Inline code: `text`
|
|
296
|
+
const codeMatch = remaining.match(/^(.*?)`(.+?)`(.*)/s);
|
|
297
|
+
if (codeMatch) {
|
|
298
|
+
if (codeMatch[1]) parts.push(<span key={key++}>{codeMatch[1]}</span>);
|
|
299
|
+
parts.push(<span key={key++} style={{ background: 'var(--bg-base)', padding: '0 3px', borderRadius: 2, color: 'var(--accent)', fontSize: '0.95em' }}>{codeMatch[2]}</span>);
|
|
300
|
+
remaining = codeMatch[3];
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// No more patterns — emit rest as plain text
|
|
305
|
+
parts.push(<span key={key++}>{remaining}</span>);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return parts.length > 0 ? parts : text;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseActivityText(text) {
|
|
313
|
+
if (!text) return '';
|
|
314
|
+
// Try to parse stream-json entries and extract readable text
|
|
315
|
+
try {
|
|
316
|
+
const data = JSON.parse(text);
|
|
317
|
+
if (Array.isArray(data)) {
|
|
318
|
+
return data
|
|
319
|
+
.map((item) => {
|
|
320
|
+
if (item.type === 'text' && item.text) return item.text;
|
|
321
|
+
if (item.type === 'thinking' && item.thinking) return null; // skip thinking
|
|
322
|
+
if (item.type === 'tool_use') return null; // skip tool calls
|
|
323
|
+
return null;
|
|
324
|
+
})
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
.join('\n') || null;
|
|
327
|
+
}
|
|
328
|
+
if (data.type === 'text' && data.text) return data.text;
|
|
329
|
+
if (data.type === 'result' && data.result) return data.result;
|
|
330
|
+
return null;
|
|
331
|
+
} catch {
|
|
332
|
+
// Not JSON — return as-is if it's meaningful
|
|
333
|
+
if (text.length > 5) return text;
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildTimeline(chats, activity) {
|
|
339
|
+
const items = [];
|
|
340
|
+
|
|
341
|
+
for (const msg of chats) {
|
|
342
|
+
items.push({
|
|
343
|
+
timestamp: msg.timestamp,
|
|
344
|
+
from: msg.from,
|
|
345
|
+
text: msg.text,
|
|
346
|
+
isQuery: msg.isQuery,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Parse and add meaningful activity entries
|
|
351
|
+
for (const a of activity.slice(-30)) {
|
|
352
|
+
const parsed = parseActivityText(a.text);
|
|
353
|
+
if (!parsed) continue;
|
|
354
|
+
|
|
355
|
+
// Skip if we have a chat entry near this time from the agent
|
|
356
|
+
const hasChatNear = items.some((it) =>
|
|
357
|
+
Math.abs(it.timestamp - a.timestamp) < 2000 && it.from === 'agent'
|
|
358
|
+
);
|
|
359
|
+
if (!hasChatNear) {
|
|
360
|
+
items.push({
|
|
361
|
+
timestamp: a.timestamp,
|
|
362
|
+
from: 'agent',
|
|
363
|
+
text: parsed.slice(0, 500),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
items.sort((a, b) => a.timestamp - b.timestamp);
|
|
369
|
+
return items;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const styles = {
|
|
373
|
+
container: {
|
|
374
|
+
flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
375
|
+
},
|
|
376
|
+
timeline: {
|
|
377
|
+
flex: 1, overflowY: 'auto', padding: '10px 0',
|
|
378
|
+
},
|
|
379
|
+
hint: {
|
|
380
|
+
color: 'var(--text-dim)', fontSize: 11, padding: '20px 4px',
|
|
381
|
+
textAlign: 'center', lineHeight: 1.6,
|
|
382
|
+
},
|
|
383
|
+
entry: {
|
|
384
|
+
padding: '4px 0', position: 'relative',
|
|
385
|
+
},
|
|
386
|
+
userMsg: {
|
|
387
|
+
display: 'flex', flexDirection: 'column', gap: 2,
|
|
388
|
+
},
|
|
389
|
+
userLabel: {
|
|
390
|
+
fontSize: 10, fontWeight: 600, color: 'var(--accent)',
|
|
391
|
+
textTransform: 'uppercase', letterSpacing: 0.5,
|
|
392
|
+
},
|
|
393
|
+
userText: {
|
|
394
|
+
fontSize: 12, color: 'var(--text-bright)', lineHeight: 1.5,
|
|
395
|
+
padding: '4px 8px', background: 'var(--bg-surface)',
|
|
396
|
+
borderRadius: 2, border: '1px solid var(--border)',
|
|
397
|
+
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
398
|
+
},
|
|
399
|
+
agentMsg: {
|
|
400
|
+
display: 'flex', flexDirection: 'column', gap: 2,
|
|
401
|
+
},
|
|
402
|
+
agentLabel: {
|
|
403
|
+
fontSize: 10, fontWeight: 600, color: 'var(--green)',
|
|
404
|
+
textTransform: 'uppercase', letterSpacing: 0.5,
|
|
405
|
+
},
|
|
406
|
+
agentText: {
|
|
407
|
+
fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.5,
|
|
408
|
+
padding: '4px 8px', background: 'var(--bg-base)',
|
|
409
|
+
borderRadius: 2, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
410
|
+
},
|
|
411
|
+
systemMsg: {
|
|
412
|
+
fontSize: 10, color: 'var(--text-dim)', fontStyle: 'italic',
|
|
413
|
+
padding: '2px 0',
|
|
414
|
+
},
|
|
415
|
+
time: {
|
|
416
|
+
position: 'absolute', top: 4, right: 0,
|
|
417
|
+
fontSize: 9, color: 'var(--text-muted)',
|
|
418
|
+
},
|
|
419
|
+
statusMsg: {
|
|
420
|
+
fontSize: 11, color: 'var(--text-dim)', fontStyle: 'italic',
|
|
421
|
+
padding: '6px 0',
|
|
422
|
+
},
|
|
423
|
+
inputRow: {
|
|
424
|
+
display: 'flex', gap: 6, padding: '8px 0 0',
|
|
425
|
+
borderTop: '1px solid var(--border)',
|
|
426
|
+
},
|
|
427
|
+
input: {
|
|
428
|
+
flex: 1, background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
429
|
+
borderRadius: 2, padding: '8px 10px',
|
|
430
|
+
color: 'var(--text-primary)', fontSize: 12,
|
|
431
|
+
fontFamily: 'var(--font)', outline: 'none',
|
|
432
|
+
},
|
|
433
|
+
sendBtn: {
|
|
434
|
+
padding: '8px 14px',
|
|
435
|
+
background: 'transparent', border: '1px solid var(--accent)',
|
|
436
|
+
borderRadius: 2,
|
|
437
|
+
color: 'var(--accent)', fontSize: 11, fontWeight: 600,
|
|
438
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// Streaming cursor
|
|
442
|
+
cursor: {
|
|
443
|
+
color: 'var(--accent)', fontWeight: 400, animation: 'pulse 1s infinite',
|
|
444
|
+
marginLeft: 1,
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
// Launch team
|
|
448
|
+
launchBox: {
|
|
449
|
+
padding: '8px 0',
|
|
450
|
+
borderTop: '1px solid var(--border)',
|
|
451
|
+
flexShrink: 0,
|
|
452
|
+
},
|
|
453
|
+
launchHeader: {
|
|
454
|
+
fontSize: 10, fontWeight: 700, color: 'var(--text-bright)',
|
|
455
|
+
marginBottom: 6,
|
|
456
|
+
},
|
|
457
|
+
launchList: {
|
|
458
|
+
display: 'flex', flexDirection: 'column', gap: 3, marginBottom: 8,
|
|
459
|
+
},
|
|
460
|
+
launchAgent: {
|
|
461
|
+
display: 'flex', alignItems: 'baseline', gap: 6,
|
|
462
|
+
fontSize: 10, padding: '2px 0',
|
|
463
|
+
},
|
|
464
|
+
launchRole: {
|
|
465
|
+
fontWeight: 600, color: 'var(--accent)', minWidth: 60,
|
|
466
|
+
},
|
|
467
|
+
launchPrompt: {
|
|
468
|
+
color: 'var(--text-dim)', fontSize: 9,
|
|
469
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
470
|
+
flex: 1,
|
|
471
|
+
},
|
|
472
|
+
launchBtn: {
|
|
473
|
+
width: '100%', padding: '8px',
|
|
474
|
+
background: 'rgba(51, 175, 188, 0.1)', border: '1px solid var(--accent)',
|
|
475
|
+
color: 'var(--accent)', fontSize: 11, fontWeight: 700,
|
|
476
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
477
|
+
letterSpacing: 0.5,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// GROOVE GUI — Agent Node Component (Unity/n8n inspired)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Handle, Position } from '@xyflow/react';
|
|
6
|
+
|
|
7
|
+
const STATUS = {
|
|
8
|
+
running: { color: '#4ae168', label: 'LIVE' },
|
|
9
|
+
starting: { color: '#e5c07b', label: 'INIT' },
|
|
10
|
+
stopped: { color: '#5c6370', label: 'STOP' },
|
|
11
|
+
crashed: { color: '#e06c75', label: 'FAIL' },
|
|
12
|
+
completed: { color: '#33afbc', label: 'DONE' },
|
|
13
|
+
killed: { color: '#5c6370', label: 'KILL' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ROLE_COLORS = {
|
|
17
|
+
planner: '#c678dd',
|
|
18
|
+
backend: '#33afbc',
|
|
19
|
+
frontend: '#e5c07b',
|
|
20
|
+
fullstack: '#4ae168',
|
|
21
|
+
testing: '#61afef',
|
|
22
|
+
devops: '#d19a66',
|
|
23
|
+
docs: '#5c6370',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function AgentNode({ data }) {
|
|
27
|
+
const st = STATUS[data.status] || STATUS.stopped;
|
|
28
|
+
const alive = data.status === 'running' || data.status === 'starting';
|
|
29
|
+
const sel = data.selected;
|
|
30
|
+
const roleColor = ROLE_COLORS[data.role] || '#33afbc';
|
|
31
|
+
const ctx = Math.round((data.contextUsage || 0) * 100);
|
|
32
|
+
|
|
33
|
+
const tokens = data.tokensUsed > 0
|
|
34
|
+
? data.tokensUsed > 999 ? `${(data.tokensUsed / 1000).toFixed(1)}k` : `${data.tokensUsed}`
|
|
35
|
+
: '0';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div style={{
|
|
39
|
+
background: '#282c34',
|
|
40
|
+
border: sel ? '1px solid #33afbc' : '1px solid #3e4451',
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
width: 170,
|
|
43
|
+
cursor: 'pointer',
|
|
44
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace",
|
|
45
|
+
fontSize: 10,
|
|
46
|
+
overflow: 'hidden',
|
|
47
|
+
transition: 'border-color 0.2s',
|
|
48
|
+
}}>
|
|
49
|
+
{/* Target handle — circular port */}
|
|
50
|
+
<Handle type="target" position={Position.Top} style={{
|
|
51
|
+
background: '#282c34', border: `2px solid ${sel ? '#33afbc' : '#3e4451'}`,
|
|
52
|
+
width: 8, height: 8, borderRadius: '50%', top: -4,
|
|
53
|
+
}} />
|
|
54
|
+
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div style={{
|
|
57
|
+
padding: '8px 10px 6px',
|
|
58
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
59
|
+
}}>
|
|
60
|
+
<span style={{
|
|
61
|
+
color: '#e6e6e6', fontWeight: 700, fontSize: 11,
|
|
62
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
|
|
63
|
+
}}>
|
|
64
|
+
{data.name}
|
|
65
|
+
</span>
|
|
66
|
+
{/* Status dot */}
|
|
67
|
+
<div style={{
|
|
68
|
+
width: 6, height: 6, borderRadius: '50%', background: st.color, flexShrink: 0,
|
|
69
|
+
...(alive ? { animation: 'pulse 2s infinite' } : {}),
|
|
70
|
+
}} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Role badge */}
|
|
74
|
+
<div style={{ padding: '0 10px 6px' }}>
|
|
75
|
+
<span style={{
|
|
76
|
+
fontSize: 8, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1,
|
|
77
|
+
color: roleColor, background: roleColor + '18', padding: '2px 6px', borderRadius: 3,
|
|
78
|
+
}}>
|
|
79
|
+
{data.role}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Metrics — minimal */}
|
|
84
|
+
<div style={{
|
|
85
|
+
padding: '4px 10px 6px',
|
|
86
|
+
borderTop: '1px solid #2c313a',
|
|
87
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
88
|
+
}}>
|
|
89
|
+
<span style={{ color: '#8b929e', fontSize: 9 }}>
|
|
90
|
+
{tokens} <span style={{ color: '#5c6370' }}>tok</span>
|
|
91
|
+
</span>
|
|
92
|
+
<span style={{ color: '#8b929e', fontSize: 9 }}>
|
|
93
|
+
{ctx}% <span style={{ color: '#5c6370' }}>ctx</span>
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Activity bar for live agents */}
|
|
98
|
+
{alive && (
|
|
99
|
+
<div style={{
|
|
100
|
+
height: 2, background: '#1a1e25', overflow: 'hidden',
|
|
101
|
+
}}>
|
|
102
|
+
<div style={{
|
|
103
|
+
width: '200%', height: '100%',
|
|
104
|
+
background: `linear-gradient(90deg, transparent 25%, ${st.color}44 35%, ${st.color} 50%, ${st.color}44 65%, transparent 75%)`,
|
|
105
|
+
animation: 'neuralFlow 2s linear infinite',
|
|
106
|
+
}} />
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Source handle — circular port */}
|
|
111
|
+
<Handle type="source" position={Position.Bottom} style={{
|
|
112
|
+
background: '#282c34', border: `2px solid ${sel ? '#33afbc' : '#3e4451'}`,
|
|
113
|
+
width: 8, height: 8, borderRadius: '50%', bottom: -4,
|
|
114
|
+
}} />
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|