rufloui 0.3.1
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/'1' +0 -0
- package/.env.example +46 -0
- package/CHANGELOG.md +87 -0
- package/CLAUDE.md +287 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/Webhooks) +0 -0
- package/docs/plans/2026-03-11-github-webhooks.md +957 -0
- package/docs/screenshot-swarm-monitor.png +0 -0
- package/frontend +0 -0
- package/index.html +13 -0
- package/package.json +56 -0
- package/public/vite.svg +4 -0
- package/src/backend/__tests__/webhook-github.test.ts +934 -0
- package/src/backend/jsonl-monitor.ts +430 -0
- package/src/backend/server.ts +2972 -0
- package/src/backend/telegram-bot.ts +511 -0
- package/src/backend/webhook-github.ts +350 -0
- package/src/frontend/App.tsx +461 -0
- package/src/frontend/api.ts +281 -0
- package/src/frontend/components/ErrorBoundary.tsx +98 -0
- package/src/frontend/components/Layout.tsx +431 -0
- package/src/frontend/components/ui/Button.tsx +111 -0
- package/src/frontend/components/ui/Card.tsx +51 -0
- package/src/frontend/components/ui/StatusBadge.tsx +60 -0
- package/src/frontend/main.tsx +63 -0
- package/src/frontend/pages/AgentVizPanel.tsx +428 -0
- package/src/frontend/pages/AgentsPanel.tsx +445 -0
- package/src/frontend/pages/ConfigPanel.tsx +661 -0
- package/src/frontend/pages/Dashboard.tsx +482 -0
- package/src/frontend/pages/HiveMindPanel.tsx +355 -0
- package/src/frontend/pages/HooksPanel.tsx +240 -0
- package/src/frontend/pages/LogsPanel.tsx +261 -0
- package/src/frontend/pages/MemoryPanel.tsx +444 -0
- package/src/frontend/pages/NeuralPanel.tsx +301 -0
- package/src/frontend/pages/PerformancePanel.tsx +198 -0
- package/src/frontend/pages/SessionsPanel.tsx +428 -0
- package/src/frontend/pages/SetupWizard.tsx +181 -0
- package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
- package/src/frontend/pages/SwarmPanel.tsx +322 -0
- package/src/frontend/pages/TasksPanel.tsx +535 -0
- package/src/frontend/pages/WebhooksPanel.tsx +335 -0
- package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
- package/src/frontend/store.ts +185 -0
- package/src/frontend/styles/global.css +113 -0
- package/src/frontend/test-setup.ts +1 -0
- package/src/frontend/tour/TourContext.tsx +161 -0
- package/src/frontend/tour/tourSteps.ts +181 -0
- package/src/frontend/tour/tourStyles.css +116 -0
- package/src/frontend/types.ts +239 -0
- package/src/frontend/utils/formatTime.test.ts +83 -0
- package/src/frontend/utils/formatTime.ts +23 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/{,+ +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { useStore } from '@/store'
|
|
5
|
+
import type { SwarmMonitorState, SwarmAgent } from '@/types'
|
|
6
|
+
|
|
7
|
+
interface AgentActivityEvent {
|
|
8
|
+
agentId: string
|
|
9
|
+
status: 'idle' | 'working' | 'error'
|
|
10
|
+
currentTask?: string
|
|
11
|
+
currentAction?: string
|
|
12
|
+
lastUpdate: string
|
|
13
|
+
tasksCompleted: number
|
|
14
|
+
errors: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
18
|
+
active: 'var(--accent-green)',
|
|
19
|
+
working: '#f59e0b',
|
|
20
|
+
healthy: 'var(--accent-green)',
|
|
21
|
+
idle: 'var(--accent-blue)',
|
|
22
|
+
error: 'var(--accent-red)',
|
|
23
|
+
ready: 'var(--accent-cyan)',
|
|
24
|
+
shutdown: 'var(--text-muted)',
|
|
25
|
+
inactive: 'var(--text-muted)',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STATUS_BG_COLORS: Record<string, string> = {
|
|
29
|
+
active: 'rgba(34, 197, 94, 0.12)',
|
|
30
|
+
working: 'rgba(245, 158, 11, 0.15)',
|
|
31
|
+
healthy: 'rgba(34, 197, 94, 0.10)',
|
|
32
|
+
idle: 'rgba(59, 130, 246, 0.08)',
|
|
33
|
+
error: 'rgba(239, 68, 68, 0.15)',
|
|
34
|
+
ready: 'rgba(6, 182, 212, 0.10)',
|
|
35
|
+
shutdown: 'rgba(100, 116, 139, 0.08)',
|
|
36
|
+
inactive: 'rgba(100, 116, 139, 0.08)',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATUS_BORDER_COLORS: Record<string, string> = {
|
|
40
|
+
active: 'rgba(34, 197, 94, 0.4)',
|
|
41
|
+
working: 'rgba(245, 158, 11, 0.5)',
|
|
42
|
+
healthy: 'rgba(34, 197, 94, 0.3)',
|
|
43
|
+
idle: 'rgba(59, 130, 246, 0.2)',
|
|
44
|
+
error: 'rgba(239, 68, 68, 0.5)',
|
|
45
|
+
ready: 'rgba(6, 182, 212, 0.3)',
|
|
46
|
+
shutdown: 'var(--border)',
|
|
47
|
+
inactive: 'var(--border)',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
51
|
+
coordinator: '#a78bfa',
|
|
52
|
+
coder: '#34d399',
|
|
53
|
+
researcher: '#60a5fa',
|
|
54
|
+
tester: '#fbbf24',
|
|
55
|
+
reviewer: '#f87171',
|
|
56
|
+
architect: '#f472b6',
|
|
57
|
+
analyst: '#38bdf8',
|
|
58
|
+
optimizer: '#c084fc',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Agent Output Modal ──────────────────────────────────────────────
|
|
62
|
+
function AgentOutputModal({ agent, onClose }: { agent: SwarmAgent; onClose: () => void }) {
|
|
63
|
+
const [lines, setLines] = useState<string[]>([])
|
|
64
|
+
const [loading, setLoading] = useState(true)
|
|
65
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
66
|
+
const autoScrollRef = useRef(true)
|
|
67
|
+
|
|
68
|
+
// Initial load
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
api.swarmMonitor.agentOutput(agent.id).then(res => {
|
|
71
|
+
setLines(res.lines || [])
|
|
72
|
+
setLoading(false)
|
|
73
|
+
}).catch(() => setLoading(false))
|
|
74
|
+
}, [agent.id])
|
|
75
|
+
|
|
76
|
+
// Listen for real-time output via WS
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
79
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
|
|
80
|
+
ws.onmessage = (event) => {
|
|
81
|
+
try {
|
|
82
|
+
const msg = JSON.parse(event.data) as { type: string; payload: { agentId: string; line: string } }
|
|
83
|
+
if (msg.type === 'agent:output' && msg.payload.agentId === agent.id) {
|
|
84
|
+
setLines(prev => [...prev, msg.payload.line])
|
|
85
|
+
}
|
|
86
|
+
} catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
return () => ws.close()
|
|
89
|
+
}, [agent.id])
|
|
90
|
+
|
|
91
|
+
// Auto-scroll to bottom
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (autoScrollRef.current && scrollRef.current) {
|
|
94
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
|
95
|
+
}
|
|
96
|
+
}, [lines])
|
|
97
|
+
|
|
98
|
+
const handleScroll = () => {
|
|
99
|
+
if (!scrollRef.current) return
|
|
100
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current
|
|
101
|
+
autoScrollRef.current = scrollHeight - scrollTop - clientHeight < 40
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const typeColor = TYPE_COLORS[agent.type] || 'var(--accent-cyan)'
|
|
105
|
+
const statusColor = STATUS_COLORS[agent.status] || 'var(--text-muted)'
|
|
106
|
+
|
|
107
|
+
function formatLine(line: string, idx: number) {
|
|
108
|
+
let color = 'var(--text-secondary)'
|
|
109
|
+
let prefix = ''
|
|
110
|
+
if (line.startsWith('[Tool]')) { color = 'var(--accent-cyan)'; prefix = 'tool' }
|
|
111
|
+
else if (line.startsWith('[Result]')) { color = 'var(--text-muted)'; prefix = 'result' }
|
|
112
|
+
else if (line.startsWith('[stderr]')) { color = 'var(--accent-red)'; prefix = 'err' }
|
|
113
|
+
else if (line.startsWith('[Done]')) { color = 'var(--accent-green)'; prefix = 'done' }
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div key={idx} style={{ display: 'flex', gap: 8, padding: '3px 0', borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
|
|
117
|
+
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 30, textAlign: 'right', flexShrink: 0, fontFamily: 'monospace' }}>
|
|
118
|
+
{idx + 1}
|
|
119
|
+
</span>
|
|
120
|
+
{prefix && (
|
|
121
|
+
<span style={{
|
|
122
|
+
color, fontSize: 10, fontWeight: 600, textTransform: 'uppercase',
|
|
123
|
+
width: 44, flexShrink: 0,
|
|
124
|
+
}}>
|
|
125
|
+
{prefix}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
<span style={{ color, wordBreak: 'break-word', whiteSpace: 'pre-wrap' }}>
|
|
129
|
+
{prefix ? line.slice(line.indexOf(']') + 2) : line}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return createPortal(
|
|
136
|
+
<div
|
|
137
|
+
onClick={onClose}
|
|
138
|
+
style={{
|
|
139
|
+
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 1000,
|
|
140
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
onClick={e => e.stopPropagation()}
|
|
145
|
+
style={{
|
|
146
|
+
width: '85vw', maxWidth: 960, height: '80vh',
|
|
147
|
+
background: 'var(--bg-primary)', border: '1px solid var(--border)',
|
|
148
|
+
borderRadius: 12, display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
149
|
+
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{/* Modal header */}
|
|
153
|
+
<div style={{
|
|
154
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
155
|
+
padding: '14px 20px', borderBottom: '1px solid var(--border)', background: 'var(--bg-secondary)',
|
|
156
|
+
}}>
|
|
157
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
158
|
+
<div style={{
|
|
159
|
+
width: 12, height: 12, borderRadius: '50%', background: statusColor,
|
|
160
|
+
boxShadow: agent.status === 'working' ? `0 0 8px ${statusColor}` : 'none',
|
|
161
|
+
animation: agent.status === 'working' ? 'pulse-glow 2s ease-in-out infinite' : 'none',
|
|
162
|
+
}} />
|
|
163
|
+
<span style={{
|
|
164
|
+
fontSize: 12, fontWeight: 600, padding: '2px 10px', borderRadius: 10,
|
|
165
|
+
background: `${typeColor}22`, color: typeColor, textTransform: 'uppercase',
|
|
166
|
+
}}>
|
|
167
|
+
{agent.type}
|
|
168
|
+
</span>
|
|
169
|
+
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Agent Output</span>
|
|
170
|
+
<span style={{ fontSize: 12, color: 'var(--text-muted)', fontFamily: 'monospace' }}>{agent.id}</span>
|
|
171
|
+
</div>
|
|
172
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
173
|
+
{(agent.status === 'working' || agent.status === 'active') && (
|
|
174
|
+
<span style={{
|
|
175
|
+
fontSize: 11, padding: '3px 10px', borderRadius: 10,
|
|
176
|
+
background: `${statusColor}22`, color: statusColor, fontWeight: 600,
|
|
177
|
+
animation: 'pulse-glow 2s ease-in-out infinite',
|
|
178
|
+
}}>
|
|
179
|
+
LIVE
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
<button
|
|
183
|
+
onClick={onClose}
|
|
184
|
+
style={{
|
|
185
|
+
background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 20,
|
|
186
|
+
cursor: 'pointer', padding: '0 4px', lineHeight: 1,
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
x
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Current action bar */}
|
|
195
|
+
{agent.currentAction && (
|
|
196
|
+
<div style={{
|
|
197
|
+
padding: '8px 20px', background: 'rgba(245, 158, 11, 0.08)',
|
|
198
|
+
borderBottom: '1px solid var(--border)', fontSize: 12,
|
|
199
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
200
|
+
}}>
|
|
201
|
+
<span style={{ color: '#f59e0b', fontWeight: 600 }}>Current:</span>
|
|
202
|
+
<span style={{ color: 'var(--text-secondary)', fontFamily: 'monospace' }}>{agent.currentAction}</span>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Output body */}
|
|
207
|
+
<div
|
|
208
|
+
ref={scrollRef}
|
|
209
|
+
onScroll={handleScroll}
|
|
210
|
+
style={{
|
|
211
|
+
flex: 1, overflow: 'auto', padding: '12px 20px',
|
|
212
|
+
fontFamily: 'monospace', fontSize: 12, lineHeight: 1.6,
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{loading ? (
|
|
216
|
+
<div style={{ color: 'var(--text-muted)', textAlign: 'center', padding: 40 }}>Loading output...</div>
|
|
217
|
+
) : lines.length === 0 ? (
|
|
218
|
+
<div style={{ color: 'var(--text-muted)', textAlign: 'center', padding: 40 }}>
|
|
219
|
+
No output yet. Output will appear here when the agent starts working on a task.
|
|
220
|
+
</div>
|
|
221
|
+
) : (
|
|
222
|
+
lines.map((line, i) => formatLine(line, i))
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Footer stats */}
|
|
227
|
+
<div style={{
|
|
228
|
+
display: 'flex', gap: 16, padding: '8px 20px', borderTop: '1px solid var(--border)',
|
|
229
|
+
background: 'var(--bg-secondary)', fontSize: 11, color: 'var(--text-muted)',
|
|
230
|
+
}}>
|
|
231
|
+
<span>Lines: {lines.length}</span>
|
|
232
|
+
<span>Tasks completed: {agent.tasks?.completed ?? 0}</span>
|
|
233
|
+
<span>Errors: {agent.tasks?.failed ?? agent.errors?.count ?? 0}</span>
|
|
234
|
+
<span style={{ marginLeft: 'auto' }}>Click outside or press X to close</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>,
|
|
238
|
+
document.body,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Agent Card ──────────────────────────────────────────────────────
|
|
243
|
+
function AgentCard({ agent, selected, onClick, onViewOutput }: {
|
|
244
|
+
agent: SwarmAgent; selected: boolean; onClick: () => void; onViewOutput: () => void
|
|
245
|
+
}) {
|
|
246
|
+
const color = STATUS_COLORS[agent.status] || 'var(--text-muted)'
|
|
247
|
+
const typeColor = TYPE_COLORS[agent.type] || 'var(--accent-cyan)'
|
|
248
|
+
const memPct = agent.memory ? Math.round((agent.memory.used / agent.memory.limit) * 100) : 0
|
|
249
|
+
const isWorking = agent.status === 'working'
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div
|
|
253
|
+
onClick={onClick}
|
|
254
|
+
className={isWorking ? 'agent-working-glow' : undefined}
|
|
255
|
+
style={{
|
|
256
|
+
padding: 14,
|
|
257
|
+
background: selected ? 'var(--bg-hover)' : (STATUS_BG_COLORS[agent.status] || 'var(--bg-secondary)'),
|
|
258
|
+
border: `1px solid ${selected ? 'var(--accent-blue)' : (STATUS_BORDER_COLORS[agent.status] || 'var(--border)')}`,
|
|
259
|
+
borderRadius: 'var(--radius)',
|
|
260
|
+
cursor: 'pointer',
|
|
261
|
+
transition: 'all var(--transition)',
|
|
262
|
+
minWidth: 220,
|
|
263
|
+
...(isWorking ? { boxShadow: '0 0 12px rgba(245, 158, 11, 0.25)' } : {}),
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
267
|
+
<div style={{
|
|
268
|
+
width: 10, height: 10, borderRadius: '50%', background: color, flexShrink: 0,
|
|
269
|
+
boxShadow: agent.status === 'active' || agent.status === 'working' ? `0 0 8px ${color}` : 'none',
|
|
270
|
+
animation: agent.status === 'active' || agent.status === 'working' ? 'pulse-glow 2s ease-in-out infinite' : 'none',
|
|
271
|
+
}} />
|
|
272
|
+
<span style={{
|
|
273
|
+
fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10,
|
|
274
|
+
background: `${typeColor}22`, color: typeColor, textTransform: 'uppercase',
|
|
275
|
+
}}>
|
|
276
|
+
{agent.type}
|
|
277
|
+
</span>
|
|
278
|
+
<span style={{ fontSize: 11, color: 'var(--text-muted)', marginLeft: 'auto' }}>
|
|
279
|
+
{agent.status}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
{/* Current action */}
|
|
283
|
+
{agent.currentAction && (
|
|
284
|
+
<div style={{
|
|
285
|
+
fontSize: 11, color: '#f59e0b', fontFamily: 'monospace', marginBottom: 6,
|
|
286
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
287
|
+
}}>
|
|
288
|
+
{agent.currentAction}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)', fontFamily: 'monospace', marginBottom: 6, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
292
|
+
{agent.id}
|
|
293
|
+
</div>
|
|
294
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
295
|
+
<div style={{ display: 'flex', gap: 12, fontSize: 11, color: 'var(--text-muted)' }}>
|
|
296
|
+
<span>Tasks: {agent.taskCount}</span>
|
|
297
|
+
<span>CPU: {agent.cpu ?? 0}%</span>
|
|
298
|
+
<span>Mem: {memPct}%</span>
|
|
299
|
+
</div>
|
|
300
|
+
<button
|
|
301
|
+
onClick={e => { e.stopPropagation(); onViewOutput() }}
|
|
302
|
+
style={{
|
|
303
|
+
fontSize: 10, padding: '2px 8px', background: 'rgba(255,255,255,0.08)',
|
|
304
|
+
border: '1px solid var(--border)', borderRadius: 4, color: 'var(--accent-cyan)',
|
|
305
|
+
cursor: 'pointer',
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
Output
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
{/* Memory bar */}
|
|
312
|
+
<div style={{ marginTop: 6, height: 3, background: 'var(--bg-primary)', borderRadius: 2, overflow: 'hidden' }}>
|
|
313
|
+
<div style={{ height: '100%', width: `${memPct}%`, background: memPct > 80 ? 'var(--accent-red)' : 'var(--accent-blue)', transition: 'width 0.3s' }} />
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function AgentDetail({ agent, onViewOutput }: { agent: SwarmAgent; onViewOutput: () => void }) {
|
|
320
|
+
const typeColor = TYPE_COLORS[agent.type] || 'var(--accent-cyan)'
|
|
321
|
+
const upHours = agent.uptime ? Math.floor(agent.uptime / 3600000) : 0
|
|
322
|
+
const upMins = agent.uptime ? Math.floor((agent.uptime % 3600000) / 60000) : 0
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div style={{ padding: 16, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
326
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
|
327
|
+
<div style={{
|
|
328
|
+
width: 14, height: 14, borderRadius: '50%',
|
|
329
|
+
background: STATUS_COLORS[agent.status] || 'var(--text-muted)',
|
|
330
|
+
}} />
|
|
331
|
+
<span style={{ fontSize: 16, fontWeight: 600, color: typeColor }}>{agent.type}</span>
|
|
332
|
+
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>({agent.status})</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div style={{ fontFamily: 'monospace', fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12 }}>{agent.id}</div>
|
|
335
|
+
|
|
336
|
+
{agent.currentAction && (
|
|
337
|
+
<div style={{
|
|
338
|
+
padding: '8px 10px', marginBottom: 12, borderRadius: 'var(--radius)',
|
|
339
|
+
background: 'rgba(245, 158, 11, 0.1)', border: '1px solid rgba(245, 158, 11, 0.3)',
|
|
340
|
+
fontSize: 12, fontFamily: 'monospace', color: '#f59e0b', wordBreak: 'break-word',
|
|
341
|
+
}}>
|
|
342
|
+
{agent.currentAction}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
<button
|
|
347
|
+
onClick={onViewOutput}
|
|
348
|
+
style={{
|
|
349
|
+
width: '100%', padding: '8px 0', marginBottom: 12, fontSize: 13, fontWeight: 600,
|
|
350
|
+
background: 'var(--accent-blue)', color: '#fff', border: 'none',
|
|
351
|
+
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
View Live Output
|
|
355
|
+
</button>
|
|
356
|
+
|
|
357
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
|
358
|
+
<StatBox label="Uptime" value={`${upHours}h ${upMins}m`} />
|
|
359
|
+
<StatBox label="Health" value={`${Math.round((agent.health ?? 1) * 100)}%`} color={agent.health >= 0.8 ? 'var(--accent-green)' : 'var(--accent-red)'} />
|
|
360
|
+
<StatBox label="CPU" value={`${agent.cpu ?? 0}%`} />
|
|
361
|
+
<StatBox label="Memory" value={`${agent.memory?.used ?? 0}/${agent.memory?.limit ?? 512} MB`} />
|
|
362
|
+
<StatBox label="Avg Latency" value={`${agent.latency?.avg ?? 0}ms`} />
|
|
363
|
+
<StatBox label="P99 Latency" value={`${agent.latency?.p99 ?? 0}ms`} />
|
|
364
|
+
<StatBox label="Tasks Active" value={String(agent.tasks?.active ?? 0)} />
|
|
365
|
+
<StatBox label="Tasks Completed" value={String(agent.tasks?.completed ?? 0)} />
|
|
366
|
+
<StatBox label="Tasks Failed" value={String(agent.tasks?.failed ?? 0)} color={agent.tasks?.failed ? 'var(--accent-red)' : undefined} />
|
|
367
|
+
<StatBox label="Errors" value={String(agent.errors?.count ?? 0)} color={agent.errors?.count ? 'var(--accent-red)' : undefined} />
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div style={{ marginTop: 12, fontSize: 11, color: 'var(--text-muted)' }}>
|
|
371
|
+
Created: {new Date(agent.createdAt).toLocaleString()}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function StatBox({ label, value, color }: { label: string; value: string; color?: string }) {
|
|
378
|
+
return (
|
|
379
|
+
<div style={{ padding: 8, background: 'var(--bg-primary)', borderRadius: 'var(--radius)' }}>
|
|
380
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginBottom: 2 }}>{label}</div>
|
|
381
|
+
<div style={{ fontSize: 14, fontWeight: 600, color: color || 'var(--text-primary)', fontFamily: 'monospace' }}>{value}</div>
|
|
382
|
+
</div>
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function ProgressBar({ value, label }: { value: number; label: string }) {
|
|
387
|
+
return (
|
|
388
|
+
<div>
|
|
389
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 12 }}>
|
|
390
|
+
<span style={{ color: 'var(--text-secondary)' }}>{label}</span>
|
|
391
|
+
<span style={{ color: 'var(--accent-cyan)', fontFamily: 'monospace' }}>{value}%</span>
|
|
392
|
+
</div>
|
|
393
|
+
<div style={{ height: 6, background: 'var(--bg-primary)', borderRadius: 3, overflow: 'hidden' }}>
|
|
394
|
+
<div style={{
|
|
395
|
+
height: '100%', borderRadius: 3, transition: 'width 0.5s',
|
|
396
|
+
width: `${value}%`,
|
|
397
|
+
background: value >= 80 ? 'var(--accent-green)' : value >= 40 ? 'var(--accent-blue)' : 'var(--accent-yellow)',
|
|
398
|
+
}} />
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default function SwarmMonitorPanel() {
|
|
405
|
+
const [data, setData] = useState<SwarmMonitorState | null>(null)
|
|
406
|
+
const [selectedAgent, setSelectedAgent] = useState<string | null>(null)
|
|
407
|
+
const [outputAgent, setOutputAgent] = useState<SwarmAgent | null>(null)
|
|
408
|
+
const [loading, setLoading] = useState(true)
|
|
409
|
+
const [autoRefresh, setAutoRefresh] = useState(true)
|
|
410
|
+
const [currentOnly, setCurrentOnly] = useState(true)
|
|
411
|
+
const [purging, setPurging] = useState(false)
|
|
412
|
+
const setSwarmMonitor = useStore(s => s.setSwarmMonitor)
|
|
413
|
+
|
|
414
|
+
const fetchData = useCallback(async () => {
|
|
415
|
+
try {
|
|
416
|
+
const snapshot = await api.swarmMonitor.snapshot(currentOnly) as SwarmMonitorState
|
|
417
|
+
setData(snapshot)
|
|
418
|
+
setSwarmMonitor(snapshot)
|
|
419
|
+
} catch {
|
|
420
|
+
/* ignore fetch errors during polling */
|
|
421
|
+
} finally {
|
|
422
|
+
setLoading(false)
|
|
423
|
+
}
|
|
424
|
+
}, [setSwarmMonitor, currentOnly])
|
|
425
|
+
|
|
426
|
+
const handlePurge = async () => {
|
|
427
|
+
setPurging(true)
|
|
428
|
+
try {
|
|
429
|
+
await api.swarmMonitor.purge()
|
|
430
|
+
await fetchData()
|
|
431
|
+
} catch { /* ignore */ }
|
|
432
|
+
setPurging(false)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Listen for real-time agent:activity WebSocket events
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
438
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
|
|
439
|
+
|
|
440
|
+
ws.onmessage = (event) => {
|
|
441
|
+
try {
|
|
442
|
+
const msg = JSON.parse(event.data) as { type: string; payload: unknown }
|
|
443
|
+
if (msg.type === 'agent:activity') {
|
|
444
|
+
const activity = msg.payload as AgentActivityEvent
|
|
445
|
+
setData(prev => {
|
|
446
|
+
if (!prev) return prev
|
|
447
|
+
const updated = prev.agents.map(agent => {
|
|
448
|
+
if (agent.id !== activity.agentId) return agent
|
|
449
|
+
return {
|
|
450
|
+
...agent,
|
|
451
|
+
status: activity.status as SwarmAgent['status'],
|
|
452
|
+
currentTask: activity.currentTask,
|
|
453
|
+
currentAction: activity.currentAction,
|
|
454
|
+
tasks: {
|
|
455
|
+
...(agent.tasks || { active: 0, queued: 0, completed: 0, failed: 0 }),
|
|
456
|
+
completed: activity.tasksCompleted,
|
|
457
|
+
failed: activity.errors,
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
return { ...prev, agents: updated }
|
|
462
|
+
})
|
|
463
|
+
// Update output modal agent if it's the same
|
|
464
|
+
setOutputAgent(prev => {
|
|
465
|
+
if (!prev || prev.id !== activity.agentId) return prev
|
|
466
|
+
return {
|
|
467
|
+
...prev,
|
|
468
|
+
status: activity.status as SwarmAgent['status'],
|
|
469
|
+
currentAction: activity.currentAction,
|
|
470
|
+
currentTask: activity.currentTask,
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
} catch { /* ignore bad messages */ }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return () => ws.close()
|
|
478
|
+
}, [])
|
|
479
|
+
|
|
480
|
+
useEffect(() => { fetchData() }, [fetchData])
|
|
481
|
+
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (!autoRefresh) return
|
|
484
|
+
const interval = setInterval(fetchData, 10000)
|
|
485
|
+
return () => clearInterval(interval)
|
|
486
|
+
}, [autoRefresh, fetchData])
|
|
487
|
+
|
|
488
|
+
const selected = data?.agents.find(a => a.id === selectedAgent) || null
|
|
489
|
+
|
|
490
|
+
if (loading) return (
|
|
491
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '60vh', gap: 16 }}>
|
|
492
|
+
<div style={{
|
|
493
|
+
width: 40, height: 40, border: '3px solid var(--border)', borderTopColor: 'var(--accent-blue)',
|
|
494
|
+
borderRadius: '50%', animation: 'spin 0.8s linear infinite',
|
|
495
|
+
}} />
|
|
496
|
+
<div style={{ fontSize: 15, color: 'var(--text-secondary)', fontWeight: 500 }}>Loading swarm data...</div>
|
|
497
|
+
</div>
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const noSwarm = !data || data.status === 'inactive' || data.status === 'shutdown'
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, height: '100%' }}>
|
|
504
|
+
{/* Output modal */}
|
|
505
|
+
{outputAgent && <AgentOutputModal agent={outputAgent} onClose={() => setOutputAgent(null)} />}
|
|
506
|
+
|
|
507
|
+
{/* Header */}
|
|
508
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
|
|
509
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
510
|
+
<h2 style={{ margin: 0, fontSize: 20, color: 'var(--text-primary)' }}>Swarm Monitor</h2>
|
|
511
|
+
{data && (
|
|
512
|
+
<span style={{
|
|
513
|
+
fontSize: 11, fontWeight: 600, padding: '3px 10px', borderRadius: 10,
|
|
514
|
+
background: `${STATUS_COLORS[data.status] || 'var(--text-muted)'}22`,
|
|
515
|
+
color: STATUS_COLORS[data.status] || 'var(--text-muted)',
|
|
516
|
+
textTransform: 'uppercase',
|
|
517
|
+
}}>
|
|
518
|
+
{data.status}
|
|
519
|
+
</span>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
523
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-secondary)', cursor: 'pointer' }}>
|
|
524
|
+
<input type="checkbox" checked={currentOnly} onChange={() => setCurrentOnly(!currentOnly)} />
|
|
525
|
+
Current swarm only
|
|
526
|
+
</label>
|
|
527
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-secondary)', cursor: 'pointer' }}>
|
|
528
|
+
<input type="checkbox" checked={autoRefresh} onChange={() => setAutoRefresh(!autoRefresh)} />
|
|
529
|
+
Auto-refresh
|
|
530
|
+
</label>
|
|
531
|
+
<button
|
|
532
|
+
onClick={handlePurge}
|
|
533
|
+
disabled={purging}
|
|
534
|
+
style={{
|
|
535
|
+
padding: '6px 14px', fontSize: 12, background: 'var(--accent-red)', color: '#fff',
|
|
536
|
+
border: 'none', borderRadius: 'var(--radius)', cursor: purging ? 'wait' : 'pointer',
|
|
537
|
+
opacity: purging ? 0.6 : 1,
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
{purging ? 'Purging...' : 'Purge All Agents'}
|
|
541
|
+
</button>
|
|
542
|
+
<button
|
|
543
|
+
onClick={fetchData}
|
|
544
|
+
style={{
|
|
545
|
+
padding: '6px 14px', fontSize: 12, background: 'var(--accent-blue)', color: '#fff',
|
|
546
|
+
border: 'none', borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
547
|
+
}}
|
|
548
|
+
>
|
|
549
|
+
Refresh
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{noSwarm ? (
|
|
555
|
+
<div style={{
|
|
556
|
+
padding: 40, textAlign: 'center', background: 'var(--bg-secondary)',
|
|
557
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
558
|
+
}}>
|
|
559
|
+
<div style={{ fontSize: 16, color: 'var(--text-secondary)', marginBottom: 8 }}>No Active Swarm</div>
|
|
560
|
+
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
|
|
561
|
+
Initialize a swarm from the Swarm panel to see live agent monitoring here.
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
) : (
|
|
565
|
+
<>
|
|
566
|
+
{/* Stats row */}
|
|
567
|
+
<div data-tour="monitor-status" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
|
|
568
|
+
<div style={{ padding: 14, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
569
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4 }}>Topology</div>
|
|
570
|
+
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--accent-cyan)' }}>{data?.topology}</div>
|
|
571
|
+
</div>
|
|
572
|
+
<div style={{ padding: 14, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
573
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4 }}>Strategy</div>
|
|
574
|
+
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--accent-purple, #a78bfa)' }}>{data?.strategy}</div>
|
|
575
|
+
</div>
|
|
576
|
+
<div style={{ padding: 14, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
577
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4 }}>Agents</div>
|
|
578
|
+
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>
|
|
579
|
+
{(data?.agentSummary.total || data?.agents.length) ?? 0}
|
|
580
|
+
<span style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 400, marginLeft: 6 }}>
|
|
581
|
+
({data?.agents.filter(a => a.status === 'active' || a.status === 'working').length || 0} active)
|
|
582
|
+
</span>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div style={{ padding: 14, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
586
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4 }}>Consensus</div>
|
|
587
|
+
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--accent-yellow)' }}>{data?.coordination.consensusRounds ?? 0} rounds</div>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
{/* Progress */}
|
|
592
|
+
{data && data.progress > 0 && (
|
|
593
|
+
<div style={{ padding: 14, background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' }}>
|
|
594
|
+
<ProgressBar value={data.progress} label={data.objective || 'Swarm Progress'} />
|
|
595
|
+
</div>
|
|
596
|
+
)}
|
|
597
|
+
|
|
598
|
+
{/* Main content: agents grid + detail */}
|
|
599
|
+
<div style={{ display: 'flex', gap: 16, flex: 1, minHeight: 0 }}>
|
|
600
|
+
{/* Agent grid */}
|
|
601
|
+
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
602
|
+
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 8 }}>
|
|
603
|
+
Agents ({data?.agents.length ?? 0})
|
|
604
|
+
</div>
|
|
605
|
+
<div data-tour="monitor-agents" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 10 }}>
|
|
606
|
+
{data?.agents.map(agent => (
|
|
607
|
+
<AgentCard
|
|
608
|
+
key={agent.id}
|
|
609
|
+
agent={agent}
|
|
610
|
+
selected={selectedAgent === agent.id}
|
|
611
|
+
onClick={() => setSelectedAgent(selectedAgent === agent.id ? null : agent.id)}
|
|
612
|
+
onViewOutput={() => setOutputAgent(agent)}
|
|
613
|
+
/>
|
|
614
|
+
))}
|
|
615
|
+
</div>
|
|
616
|
+
{(!data?.agents.length) && (
|
|
617
|
+
<div style={{ color: 'var(--text-muted)', fontSize: 13, textAlign: 'center', padding: 20 }}>
|
|
618
|
+
No agents deployed yet
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
{/* Detail panel */}
|
|
624
|
+
{selected && (
|
|
625
|
+
<div style={{ width: 320, flexShrink: 0, overflow: 'auto' }}>
|
|
626
|
+
<AgentDetail agent={selected} onViewOutput={() => setOutputAgent(selected)} />
|
|
627
|
+
</div>
|
|
628
|
+
)}
|
|
629
|
+
</div>
|
|
630
|
+
</>
|
|
631
|
+
)}
|
|
632
|
+
</div>
|
|
633
|
+
)
|
|
634
|
+
}
|