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,322 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback, useRef } from 'react'
|
|
2
|
+
import { useStore } from '@/store'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { Card } from '@/components/ui/Card'
|
|
5
|
+
import { Button } from '@/components/ui/Button'
|
|
6
|
+
import { StatusBadge } from '@/components/ui/StatusBadge'
|
|
7
|
+
import type { Agent, SwarmState } from '@/types'
|
|
8
|
+
|
|
9
|
+
type Topology = SwarmState['topology']
|
|
10
|
+
type Strategy = 'specialized' | 'generalist' | 'adaptive'
|
|
11
|
+
|
|
12
|
+
const TOPOLOGIES: Topology[] = ['hierarchical', 'mesh', 'star', 'ring', 'hierarchical-mesh']
|
|
13
|
+
const STRATEGIES: Strategy[] = ['specialized', 'generalist', 'adaptive']
|
|
14
|
+
|
|
15
|
+
const styles = {
|
|
16
|
+
page: { padding: 24, display: 'flex', flexDirection: 'column' as const, gap: 24 },
|
|
17
|
+
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
18
|
+
title: { fontSize: 24, fontWeight: 700, color: 'var(--text-primary)' },
|
|
19
|
+
form: { display: 'flex', gap: 16, alignItems: 'flex-end', flexWrap: 'wrap' as const },
|
|
20
|
+
field: { display: 'flex', flexDirection: 'column' as const, gap: 6 },
|
|
21
|
+
label: { fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase' as const, letterSpacing: '0.05em' },
|
|
22
|
+
select: { minWidth: 180, height: 38 },
|
|
23
|
+
input: { width: 100, height: 38 },
|
|
24
|
+
infoRow: { display: 'flex', gap: 16, flexWrap: 'wrap' as const },
|
|
25
|
+
infoBadge: { padding: '6px 14px', background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', fontSize: 13, color: 'var(--text-secondary)', display: 'flex', gap: 8, alignItems: 'center' },
|
|
26
|
+
infoValue: { color: 'var(--text-primary)', fontWeight: 600 },
|
|
27
|
+
svgContainer: { width: '100%', height: 600, background: 'var(--bg-primary)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border)', overflow: 'hidden' },
|
|
28
|
+
agentGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 12 },
|
|
29
|
+
agentCard: { padding: 16, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column' as const, gap: 8 },
|
|
30
|
+
agentName: { fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' },
|
|
31
|
+
agentType: { fontSize: 12, color: 'var(--text-muted)', fontFamily: 'monospace' },
|
|
32
|
+
agentMeta: { fontSize: 12, color: 'var(--text-secondary)' },
|
|
33
|
+
pulseDot: { width: 8, height: 8, borderRadius: '50%', background: 'var(--accent-green)', animation: 'pulse-glow 2s ease infinite', display: 'inline-block' },
|
|
34
|
+
emptyState: { textAlign: 'center' as const, padding: 48, color: 'var(--text-muted)' },
|
|
35
|
+
actions: { display: 'flex', gap: 12 },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function TopologyGraph({ topology, agents }: { topology: Topology; agents: Agent[] }) {
|
|
39
|
+
const count = Math.max(agents.length, 3)
|
|
40
|
+
const cx = 350
|
|
41
|
+
const cy = 315
|
|
42
|
+
const r = 180
|
|
43
|
+
|
|
44
|
+
const positions = agents.map((_, i) => {
|
|
45
|
+
if (topology === 'star') {
|
|
46
|
+
if (i === 0) return { x: cx, y: cy }
|
|
47
|
+
const angle = ((i - 1) / (count - 1)) * Math.PI * 2 - Math.PI / 2
|
|
48
|
+
return { x: cx + Math.cos(angle) * r, y: cy + Math.sin(angle) * r }
|
|
49
|
+
}
|
|
50
|
+
if (topology === 'hierarchical' || topology === 'hierarchical-mesh') {
|
|
51
|
+
const levels = Math.ceil(Math.log2(count + 1))
|
|
52
|
+
let level = 0
|
|
53
|
+
let idx = i
|
|
54
|
+
let nodesAtLevel = 1
|
|
55
|
+
let cumulative = 0
|
|
56
|
+
while (idx >= cumulative + nodesAtLevel && level < levels) {
|
|
57
|
+
cumulative += nodesAtLevel
|
|
58
|
+
nodesAtLevel *= 2
|
|
59
|
+
level++
|
|
60
|
+
}
|
|
61
|
+
const posInLevel = idx - cumulative
|
|
62
|
+
const spacing = 700 / (nodesAtLevel + 1)
|
|
63
|
+
return { x: spacing * (posInLevel + 1), y: 60 + level * 140 }
|
|
64
|
+
}
|
|
65
|
+
// ring or mesh: circular layout
|
|
66
|
+
const angle = (i / count) * Math.PI * 2 - Math.PI / 2
|
|
67
|
+
return { x: cx + Math.cos(angle) * r, y: cy + Math.sin(angle) * r }
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const edges: Array<[number, number]> = []
|
|
71
|
+
if (topology === 'mesh' || topology === 'hierarchical-mesh') {
|
|
72
|
+
for (let i = 0; i < positions.length; i++)
|
|
73
|
+
for (let j = i + 1; j < positions.length; j++) edges.push([i, j])
|
|
74
|
+
} else if (topology === 'star') {
|
|
75
|
+
for (let i = 1; i < positions.length; i++) edges.push([0, i])
|
|
76
|
+
} else if (topology === 'ring') {
|
|
77
|
+
for (let i = 0; i < positions.length; i++) edges.push([i, (i + 1) % positions.length])
|
|
78
|
+
} else if (topology === 'hierarchical') {
|
|
79
|
+
for (let i = 1; i < positions.length; i++) edges.push([Math.floor((i - 1) / 2), i])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const statusColor = (s: Agent['status']) =>
|
|
83
|
+
s === 'running' ? 'var(--accent-green)' : s === 'idle' ? 'var(--accent-blue)' : s === 'error' ? 'var(--accent-red)' : 'var(--text-muted)'
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<svg viewBox="0 0 700 630" style={{ width: '100%', height: '100%' }}>
|
|
87
|
+
<defs>
|
|
88
|
+
<linearGradient id="line-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
89
|
+
<stop offset="0%" stopColor="var(--accent-blue)" stopOpacity="0.4" />
|
|
90
|
+
<stop offset="100%" stopColor="var(--accent-cyan)" stopOpacity="0.4" />
|
|
91
|
+
</linearGradient>
|
|
92
|
+
</defs>
|
|
93
|
+
{edges.map(([a, b], i) => (
|
|
94
|
+
positions[a] && positions[b] ? (
|
|
95
|
+
<line key={i} x1={positions[a].x} y1={positions[a].y} x2={positions[b].x} y2={positions[b].y}
|
|
96
|
+
stroke="url(#line-grad)" strokeWidth={1.5} strokeDasharray="4 4">
|
|
97
|
+
<animate attributeName="stroke-dashoffset" values="8;0" dur="1.5s" repeatCount="indefinite" />
|
|
98
|
+
</line>
|
|
99
|
+
) : null
|
|
100
|
+
))}
|
|
101
|
+
{positions.map((pos, i) => (
|
|
102
|
+
<g key={agents[i]?.id || i}>
|
|
103
|
+
<circle cx={pos.x} cy={pos.y} r={42} fill="var(--bg-card)" stroke={statusColor(agents[i]?.status || 'idle')} strokeWidth={2} />
|
|
104
|
+
<text x={pos.x} y={pos.y - 4} textAnchor="middle" fill="var(--text-primary)" fontSize={12} fontWeight={600}>
|
|
105
|
+
{agents[i]?.name || `Agent ${i}`}
|
|
106
|
+
</text>
|
|
107
|
+
<text x={pos.x} y={pos.y + 12} textAnchor="middle" fill="var(--text-muted)" fontSize={10}>
|
|
108
|
+
{agents[i]?.type || 'agent'}
|
|
109
|
+
</text>
|
|
110
|
+
</g>
|
|
111
|
+
))}
|
|
112
|
+
</svg>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default function SwarmPanel() {
|
|
117
|
+
const { swarm, setSwarm, agents } = useStore()
|
|
118
|
+
const [topology, setTopology] = useState<Topology>('hierarchical')
|
|
119
|
+
const [maxAgents, setMaxAgents] = useState(8)
|
|
120
|
+
const [strategy, setStrategy] = useState<Strategy>('specialized')
|
|
121
|
+
const [loading, setLoading] = useState(false)
|
|
122
|
+
const [error, setError] = useState<string | null>(null)
|
|
123
|
+
const [confirmShutdown, setConfirmShutdown] = useState(false)
|
|
124
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
125
|
+
|
|
126
|
+
const fetchStatus = useCallback(async () => {
|
|
127
|
+
try {
|
|
128
|
+
const data = await api.swarm.status() as SwarmState & { status: string }
|
|
129
|
+
if (data.status === 'inactive' || data.status === 'shutdown') {
|
|
130
|
+
setSwarm(null)
|
|
131
|
+
} else {
|
|
132
|
+
setSwarm(data)
|
|
133
|
+
}
|
|
134
|
+
setError(null)
|
|
135
|
+
} catch {
|
|
136
|
+
/* swarm may not be active */
|
|
137
|
+
}
|
|
138
|
+
}, [setSwarm])
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
fetchStatus()
|
|
142
|
+
intervalRef.current = setInterval(fetchStatus, 5000)
|
|
143
|
+
return () => { if (intervalRef.current) clearInterval(intervalRef.current) }
|
|
144
|
+
}, [fetchStatus])
|
|
145
|
+
|
|
146
|
+
const handleInit = async () => {
|
|
147
|
+
setLoading(true)
|
|
148
|
+
setError(null)
|
|
149
|
+
try {
|
|
150
|
+
await api.swarm.init({ topology, maxAgents, strategy })
|
|
151
|
+
await fetchStatus()
|
|
152
|
+
} catch (e: unknown) {
|
|
153
|
+
setError(e instanceof Error ? e.message : 'Failed to initialize swarm')
|
|
154
|
+
} finally {
|
|
155
|
+
setLoading(false)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const handleShutdown = async () => {
|
|
160
|
+
setConfirmShutdown(false)
|
|
161
|
+
setLoading(true)
|
|
162
|
+
try {
|
|
163
|
+
await api.swarm.shutdown()
|
|
164
|
+
setSwarm(null)
|
|
165
|
+
} catch (e: unknown) {
|
|
166
|
+
setError(e instanceof Error ? e.message : 'Failed to shutdown swarm')
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isActive = swarm && swarm.status !== 'shutdown'
|
|
173
|
+
const swarmAgents = isActive ? (swarm.agents?.length ? swarm.agents : agents) : []
|
|
174
|
+
|
|
175
|
+
if (!isActive) {
|
|
176
|
+
return (
|
|
177
|
+
<div style={styles.page}>
|
|
178
|
+
<div style={styles.header}>
|
|
179
|
+
<span style={styles.title}>Swarm Management</span>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<Card>
|
|
183
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
184
|
+
<h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>Initialize New Swarm</h3>
|
|
185
|
+
{error && (
|
|
186
|
+
<div style={{ padding: '10px 14px', background: 'rgba(239,68,68,0.1)', border: '1px solid var(--accent-red)', borderRadius: 'var(--radius)', color: 'var(--accent-red)', fontSize: 13 }}>
|
|
187
|
+
{error}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
<div style={styles.form}>
|
|
191
|
+
<div style={styles.field} data-tour="swarm-topology">
|
|
192
|
+
<label style={styles.label}>Topology</label>
|
|
193
|
+
<select style={styles.select} value={topology} onChange={e => setTopology(e.target.value as Topology)}>
|
|
194
|
+
{TOPOLOGIES.map(t => <option key={t} value={t}>{t}</option>)}
|
|
195
|
+
</select>
|
|
196
|
+
</div>
|
|
197
|
+
<div style={styles.field}>
|
|
198
|
+
<label style={styles.label}>Max Agents</label>
|
|
199
|
+
<input type="number" min={1} max={15} style={styles.input} value={maxAgents}
|
|
200
|
+
onChange={e => setMaxAgents(Math.min(15, Math.max(1, Number(e.target.value))))} />
|
|
201
|
+
</div>
|
|
202
|
+
<div style={styles.field}>
|
|
203
|
+
<label style={styles.label}>Strategy</label>
|
|
204
|
+
<select style={styles.select} value={strategy} onChange={e => setStrategy(e.target.value as Strategy)}>
|
|
205
|
+
{STRATEGIES.map(s => <option key={s} value={s}>{s}</option>)}
|
|
206
|
+
</select>
|
|
207
|
+
</div>
|
|
208
|
+
<div data-tour="swarm-init">
|
|
209
|
+
<Button onClick={handleInit} disabled={loading}>
|
|
210
|
+
{loading ? 'Initializing...' : 'Initialize Swarm'}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
<Card>
|
|
218
|
+
<div style={{ ...styles.emptyState, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, padding: 28 }}>
|
|
219
|
+
<svg viewBox="0 0 120 120" width={100} height={100}>
|
|
220
|
+
{[0, 1, 2, 3, 4].map(i => {
|
|
221
|
+
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2
|
|
222
|
+
const x = 60 + Math.cos(angle) * 40
|
|
223
|
+
const y = 60 + Math.sin(angle) * 40
|
|
224
|
+
return (
|
|
225
|
+
<g key={i}>
|
|
226
|
+
<line x1={60} y1={60} x2={x} y2={y} stroke="var(--border)" strokeWidth={1} strokeDasharray="3 3" />
|
|
227
|
+
<circle cx={x} cy={y} r={10} fill="var(--bg-tertiary)" stroke="var(--border)" strokeWidth={1.5} />
|
|
228
|
+
</g>
|
|
229
|
+
)
|
|
230
|
+
})}
|
|
231
|
+
<circle cx={60} cy={60} r={12} fill="var(--bg-card)" stroke="var(--accent-blue)" strokeWidth={2} opacity={0.5} />
|
|
232
|
+
</svg>
|
|
233
|
+
<p style={{ fontSize: 14 }}>No active swarm. Configure and initialize one above.</p>
|
|
234
|
+
</div>
|
|
235
|
+
</Card>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div style={styles.page}>
|
|
242
|
+
<div style={styles.header}>
|
|
243
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
244
|
+
<span style={styles.title}>Swarm Management</span>
|
|
245
|
+
<span style={styles.pulseDot} />
|
|
246
|
+
</div>
|
|
247
|
+
<div style={styles.actions}>
|
|
248
|
+
<Button variant="danger" onClick={() => setConfirmShutdown(true)} disabled={loading}>
|
|
249
|
+
{loading ? 'Shutting down...' : 'Shutdown Swarm'}
|
|
250
|
+
</Button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{error && (
|
|
255
|
+
<div style={{ padding: '10px 14px', background: 'rgba(239,68,68,0.1)', border: '1px solid var(--accent-red)', borderRadius: 'var(--radius)', color: 'var(--accent-red)', fontSize: 13 }}>
|
|
256
|
+
{error}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
<div style={styles.infoRow}>
|
|
261
|
+
<div style={styles.infoBadge}>ID <span style={styles.infoValue}>{(swarm.id ?? '').slice(0, 8) || '—'}</span></div>
|
|
262
|
+
<div style={styles.infoBadge}>Topology <span style={styles.infoValue}>{swarm.topology}</span></div>
|
|
263
|
+
<div style={styles.infoBadge}>Strategy <span style={styles.infoValue}>{swarm.strategy}</span></div>
|
|
264
|
+
<div style={styles.infoBadge}>Status <StatusBadge status={swarm.status} /></div>
|
|
265
|
+
<div style={styles.infoBadge}>Agents <span style={styles.infoValue}>{swarm.activeAgents}/{swarm.maxAgents}</span></div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<Card>
|
|
269
|
+
<h3 data-tour="swarm-topology-view" style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 12 }}>Topology View</h3>
|
|
270
|
+
<div style={styles.svgContainer}>
|
|
271
|
+
<TopologyGraph topology={swarm.topology} agents={swarmAgents} />
|
|
272
|
+
</div>
|
|
273
|
+
</Card>
|
|
274
|
+
|
|
275
|
+
<Card>
|
|
276
|
+
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 12 }}>
|
|
277
|
+
Active Agents ({swarmAgents.length})
|
|
278
|
+
</h3>
|
|
279
|
+
{swarmAgents.length === 0 ? (
|
|
280
|
+
<p style={{ color: 'var(--text-muted)', fontSize: 13, padding: 16, textAlign: 'center' }}>No agents in swarm yet.</p>
|
|
281
|
+
) : (
|
|
282
|
+
<div style={styles.agentGrid}>
|
|
283
|
+
{swarmAgents.map(agent => (
|
|
284
|
+
<div key={agent.id} style={styles.agentCard}>
|
|
285
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
286
|
+
<span style={styles.agentName}>{agent.name}</span>
|
|
287
|
+
<StatusBadge status={agent.status} />
|
|
288
|
+
</div>
|
|
289
|
+
<span style={styles.agentType}>{agent.type}</span>
|
|
290
|
+
<span style={styles.agentMeta}>
|
|
291
|
+
Tasks: {agent.metrics?.tasksCompleted ?? 0}
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</Card>
|
|
298
|
+
|
|
299
|
+
{/* Shutdown Confirmation Modal */}
|
|
300
|
+
{confirmShutdown && (
|
|
301
|
+
<div onClick={() => setConfirmShutdown(false)} style={{
|
|
302
|
+
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex',
|
|
303
|
+
alignItems: 'center', justifyContent: 'center', zIndex: 9999,
|
|
304
|
+
}}>
|
|
305
|
+
<div onClick={e => e.stopPropagation()} style={{
|
|
306
|
+
background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)',
|
|
307
|
+
padding: 24, maxWidth: 400, width: '90%',
|
|
308
|
+
}}>
|
|
309
|
+
<h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 8 }}>Shutdown Swarm</h3>
|
|
310
|
+
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 20 }}>
|
|
311
|
+
Are you sure? All agents will be terminated and the swarm will be destroyed.
|
|
312
|
+
</p>
|
|
313
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
314
|
+
<Button variant="secondary" onClick={() => setConfirmShutdown(false)}>Cancel</Button>
|
|
315
|
+
<Button variant="danger" onClick={handleShutdown}>Shutdown</Button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
}
|