@desplega.ai/agent-swarm 1.2.0 → 1.9.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.
Files changed (123) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.dockerignore +3 -0
  3. package/.env.docker.example +22 -1
  4. package/.env.example +17 -0
  5. package/.github/workflows/docker-publish.yml +92 -0
  6. package/CONTRIBUTING.md +270 -0
  7. package/DEPLOYMENT.md +391 -0
  8. package/Dockerfile.worker +29 -1
  9. package/FAQ.md +19 -0
  10. package/LICENSE +21 -0
  11. package/MCP.md +249 -0
  12. package/README.md +105 -185
  13. package/assets/agent-swarm-logo-orange.png +0 -0
  14. package/assets/agent-swarm-logo.png +0 -0
  15. package/assets/agent-swarm.png +0 -0
  16. package/deploy/docker-push.ts +30 -0
  17. package/docker-compose.example.yml +137 -0
  18. package/docker-entrypoint.sh +223 -7
  19. package/package.json +13 -4
  20. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  21. package/plugin/README.md +1 -0
  22. package/plugin/agents/.gitkeep +0 -0
  23. package/plugin/agents/codebase-analyzer.md +143 -0
  24. package/plugin/agents/codebase-locator.md +122 -0
  25. package/plugin/agents/codebase-pattern-finder.md +227 -0
  26. package/plugin/agents/web-search-researcher.md +109 -0
  27. package/plugin/commands/create-plan.md +415 -0
  28. package/plugin/commands/implement-plan.md +89 -0
  29. package/plugin/commands/research.md +200 -0
  30. package/plugin/commands/start-leader.md +101 -0
  31. package/plugin/commands/start-worker.md +56 -0
  32. package/plugin/commands/swarm-chat.md +78 -0
  33. package/plugin/commands/todos.md +66 -0
  34. package/plugin/commands/work-on-task.md +44 -0
  35. package/plugin/skills/.gitkeep +0 -0
  36. package/scripts/generate-mcp-docs.ts +415 -0
  37. package/slack-manifest.json +69 -0
  38. package/src/be/db.ts +1431 -25
  39. package/src/cli.tsx +135 -11
  40. package/src/commands/lead.ts +13 -0
  41. package/src/commands/runner.ts +255 -0
  42. package/src/commands/setup.tsx +5 -5
  43. package/src/commands/worker.ts +8 -220
  44. package/src/hooks/hook.ts +108 -14
  45. package/src/http.ts +361 -5
  46. package/src/prompts/base-prompt.ts +131 -0
  47. package/src/server.ts +56 -0
  48. package/src/slack/app.ts +73 -0
  49. package/src/slack/commands.ts +88 -0
  50. package/src/slack/handlers.ts +281 -0
  51. package/src/slack/index.ts +3 -0
  52. package/src/slack/responses.ts +175 -0
  53. package/src/slack/router.ts +170 -0
  54. package/src/slack/types.ts +20 -0
  55. package/src/slack/watcher.ts +119 -0
  56. package/src/tools/create-channel.ts +80 -0
  57. package/src/tools/get-tasks.ts +54 -21
  58. package/src/tools/join-swarm.ts +28 -4
  59. package/src/tools/list-channels.ts +37 -0
  60. package/src/tools/list-services.ts +110 -0
  61. package/src/tools/poll-task.ts +47 -3
  62. package/src/tools/post-message.ts +87 -0
  63. package/src/tools/read-messages.ts +192 -0
  64. package/src/tools/register-service.ts +118 -0
  65. package/src/tools/send-task.ts +80 -7
  66. package/src/tools/store-progress.ts +9 -3
  67. package/src/tools/task-action.ts +211 -0
  68. package/src/tools/unregister-service.ts +110 -0
  69. package/src/tools/update-profile.ts +105 -0
  70. package/src/tools/update-service-status.ts +118 -0
  71. package/src/types.ts +110 -3
  72. package/src/utils/pretty-print.ts +224 -0
  73. package/thoughts/shared/plans/.gitkeep +0 -0
  74. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  75. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  76. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  77. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  78. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  79. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  80. package/thoughts/shared/research/.gitkeep +0 -0
  81. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  82. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  83. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  84. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  85. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  86. package/tsconfig.json +3 -1
  87. package/ui/bun.lock +692 -0
  88. package/ui/index.html +22 -0
  89. package/ui/package.json +32 -0
  90. package/ui/pnpm-lock.yaml +3034 -0
  91. package/ui/postcss.config.js +6 -0
  92. package/ui/public/logo.png +0 -0
  93. package/ui/src/App.tsx +43 -0
  94. package/ui/src/components/ActivityFeed.tsx +415 -0
  95. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  96. package/ui/src/components/AgentsPanel.tsx +549 -0
  97. package/ui/src/components/ChatPanel.tsx +1820 -0
  98. package/ui/src/components/ConfigModal.tsx +232 -0
  99. package/ui/src/components/Dashboard.tsx +534 -0
  100. package/ui/src/components/Header.tsx +168 -0
  101. package/ui/src/components/ServicesPanel.tsx +612 -0
  102. package/ui/src/components/StatsBar.tsx +288 -0
  103. package/ui/src/components/StatusBadge.tsx +124 -0
  104. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  105. package/ui/src/components/TasksPanel.tsx +575 -0
  106. package/ui/src/hooks/queries.ts +170 -0
  107. package/ui/src/index.css +235 -0
  108. package/ui/src/lib/api.ts +161 -0
  109. package/ui/src/lib/config.ts +35 -0
  110. package/ui/src/lib/theme.ts +214 -0
  111. package/ui/src/lib/utils.ts +48 -0
  112. package/ui/src/main.tsx +32 -0
  113. package/ui/src/types/api.ts +164 -0
  114. package/ui/src/vite-env.d.ts +1 -0
  115. package/ui/tailwind.config.js +35 -0
  116. package/ui/tsconfig.json +31 -0
  117. package/ui/vite.config.ts +22 -0
  118. package/cc-plugin/README.md +0 -49
  119. package/cc-plugin/commands/setup-leader.md +0 -73
  120. package/cc-plugin/commands/start-worker.md +0 -64
  121. package/docker-compose.worker.yml +0 -35
  122. package/example-req-meta.json +0 -24
  123. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -0,0 +1,549 @@
1
+ import { useState, useMemo } from "react";
2
+ import Box from "@mui/joy/Box";
3
+ import Card from "@mui/joy/Card";
4
+ import Typography from "@mui/joy/Typography";
5
+ import Table from "@mui/joy/Table";
6
+ import Input from "@mui/joy/Input";
7
+ import Select from "@mui/joy/Select";
8
+ import Option from "@mui/joy/Option";
9
+ import { useColorScheme } from "@mui/joy/styles";
10
+ import { useAgents } from "../hooks/queries";
11
+ import StatusBadge from "./StatusBadge";
12
+ import type { AgentWithTasks, AgentStatus } from "../types/api";
13
+
14
+ function formatSmartTime(dateStr: string): string {
15
+ const date = new Date(dateStr);
16
+ const now = new Date();
17
+ const diffMs = now.getTime() - date.getTime();
18
+ const diffMins = Math.floor(diffMs / 60000);
19
+ const diffHours = Math.floor(diffMins / 60);
20
+
21
+ if (diffHours < 6) {
22
+ if (diffMins < 1) return "just now";
23
+ if (diffMins < 60) return `${diffMins}m ago`;
24
+ return `${diffHours}h ago`;
25
+ }
26
+
27
+ const isToday = date.toDateString() === now.toDateString();
28
+ if (isToday) {
29
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
30
+ }
31
+
32
+ return date.toLocaleDateString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
33
+ }
34
+
35
+ interface AgentsRowProps {
36
+ agent: AgentWithTasks;
37
+ selected: boolean;
38
+ onClick: () => void;
39
+ isDark: boolean;
40
+ }
41
+
42
+ function AgentRow({ agent, selected, onClick, isDark }: AgentsRowProps) {
43
+ const activeTasks = agent.tasks.filter(
44
+ (t) => t.status === "pending" || t.status === "in_progress"
45
+ ).length;
46
+
47
+ const isActive = agent.status === "busy";
48
+
49
+ const colors = {
50
+ amber: isDark ? "#F5A623" : "#D48806",
51
+ gold: isDark ? "#D4A574" : "#8B6914",
52
+ dormant: isDark ? "#6B5344" : "#A89A7C",
53
+ selectedBg: isDark ? "rgba(245, 166, 35, 0.08)" : "rgba(212, 136, 6, 0.08)",
54
+ amberGlow: isDark ? "0 0 10px rgba(245, 166, 35, 0.6)" : "0 0 8px rgba(212, 136, 6, 0.4)",
55
+ amberSoftBg: isDark ? "rgba(245, 166, 35, 0.1)" : "rgba(212, 136, 6, 0.1)",
56
+ amberBorder: isDark ? "rgba(245, 166, 35, 0.3)" : "rgba(212, 136, 6, 0.3)",
57
+ amberTextShadow: isDark ? "0 0 10px rgba(245, 166, 35, 0.5)" : "none",
58
+ };
59
+
60
+ return (
61
+ <tr
62
+ onClick={onClick}
63
+ style={{
64
+ cursor: "pointer",
65
+ backgroundColor: selected ? colors.selectedBg : undefined,
66
+ }}
67
+ className="row-hover"
68
+ >
69
+ <td>
70
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
71
+ {/* Agent status dot */}
72
+ <Box
73
+ sx={{
74
+ width: 8,
75
+ height: 8,
76
+ borderRadius: "50%",
77
+ bgcolor: isActive ? colors.amber : agent.status === "idle" ? colors.gold : colors.dormant,
78
+ boxShadow: isActive ? colors.amberGlow : "none",
79
+ animation: isActive ? "pulse-amber 2s ease-in-out infinite" : undefined,
80
+ }}
81
+ />
82
+ <Typography
83
+ sx={{
84
+ fontFamily: "code",
85
+ fontWeight: 600,
86
+ color: agent.isLead ? colors.amber : "text.primary",
87
+ whiteSpace: "nowrap",
88
+ }}
89
+ >
90
+ {agent.name}
91
+ </Typography>
92
+ {agent.isLead && (
93
+ <Typography
94
+ sx={{
95
+ fontFamily: "code",
96
+ fontSize: "0.6rem",
97
+ color: colors.amber,
98
+ textShadow: colors.amberTextShadow,
99
+ bgcolor: colors.amberSoftBg,
100
+ px: 0.75,
101
+ py: 0.25,
102
+ borderRadius: 1,
103
+ border: `1px solid ${colors.amberBorder}`,
104
+ }}
105
+ >
106
+ LEAD
107
+ </Typography>
108
+ )}
109
+ </Box>
110
+ </td>
111
+ <td>
112
+ <Typography
113
+ sx={{
114
+ fontFamily: "code",
115
+ fontSize: "0.75rem",
116
+ color: agent.role ? "text.secondary" : "text.tertiary",
117
+ whiteSpace: "nowrap",
118
+ overflow: "hidden",
119
+ textOverflow: "ellipsis",
120
+ maxWidth: 120,
121
+ }}
122
+ >
123
+ {agent.role || "—"}
124
+ </Typography>
125
+ </td>
126
+ <td>
127
+ <StatusBadge status={agent.status} />
128
+ </td>
129
+ <td>
130
+ <Typography
131
+ sx={{
132
+ fontFamily: "code",
133
+ fontSize: "0.8rem",
134
+ color: activeTasks > 0 ? colors.amber : "text.tertiary",
135
+ whiteSpace: "nowrap",
136
+ }}
137
+ >
138
+ {activeTasks}/{agent.tasks.length}
139
+ </Typography>
140
+ </td>
141
+ <td>
142
+ <Typography
143
+ sx={{
144
+ fontFamily: "code",
145
+ fontSize: "0.7rem",
146
+ color: "text.tertiary",
147
+ }}
148
+ >
149
+ {formatSmartTime(agent.lastUpdatedAt)}
150
+ </Typography>
151
+ </td>
152
+ </tr>
153
+ );
154
+ }
155
+
156
+ // Mobile card component
157
+ interface AgentCardProps {
158
+ agent: AgentWithTasks;
159
+ selected: boolean;
160
+ onClick: () => void;
161
+ isDark: boolean;
162
+ }
163
+
164
+ function AgentCard({ agent, selected, onClick, isDark }: AgentCardProps) {
165
+ const activeTasks = agent.tasks.filter(
166
+ (t) => t.status === "pending" || t.status === "in_progress"
167
+ ).length;
168
+
169
+ const isActive = agent.status === "busy";
170
+
171
+ const colors = {
172
+ amber: isDark ? "#F5A623" : "#D48806",
173
+ gold: isDark ? "#D4A574" : "#8B6914",
174
+ dormant: isDark ? "#6B5344" : "#A89A7C",
175
+ selectedBorder: isDark ? "#F5A623" : "#D48806",
176
+ amberGlow: isDark ? "0 0 10px rgba(245, 166, 35, 0.6)" : "0 0 8px rgba(212, 136, 6, 0.4)",
177
+ amberSoftBg: isDark ? "rgba(245, 166, 35, 0.1)" : "rgba(212, 136, 6, 0.1)",
178
+ amberBorder: isDark ? "rgba(245, 166, 35, 0.3)" : "rgba(212, 136, 6, 0.3)",
179
+ amberTextShadow: isDark ? "0 0 10px rgba(245, 166, 35, 0.5)" : "none",
180
+ };
181
+
182
+ return (
183
+ <Box
184
+ onClick={onClick}
185
+ sx={{
186
+ p: 2,
187
+ mb: 1,
188
+ borderRadius: "8px",
189
+ border: "1px solid",
190
+ borderColor: selected ? colors.selectedBorder : "neutral.outlinedBorder",
191
+ bgcolor: selected ? colors.amberSoftBg : "background.surface",
192
+ cursor: "pointer",
193
+ transition: "all 0.2s ease",
194
+ "&:active": {
195
+ bgcolor: colors.amberSoftBg,
196
+ },
197
+ }}
198
+ >
199
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 1 }}>
200
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1, flex: 1, minWidth: 0 }}>
201
+ <Box
202
+ sx={{
203
+ width: 10,
204
+ height: 10,
205
+ borderRadius: "50%",
206
+ bgcolor: isActive ? colors.amber : agent.status === "idle" ? colors.gold : colors.dormant,
207
+ boxShadow: isActive ? colors.amberGlow : "none",
208
+ flexShrink: 0,
209
+ }}
210
+ />
211
+ <Typography
212
+ sx={{
213
+ fontFamily: "code",
214
+ fontWeight: 600,
215
+ color: agent.isLead ? colors.amber : "text.primary",
216
+ whiteSpace: "nowrap",
217
+ overflow: "hidden",
218
+ textOverflow: "ellipsis",
219
+ }}
220
+ >
221
+ {agent.name}
222
+ </Typography>
223
+ {agent.isLead && (
224
+ <Typography
225
+ sx={{
226
+ fontFamily: "code",
227
+ fontSize: "0.55rem",
228
+ color: colors.amber,
229
+ textShadow: colors.amberTextShadow,
230
+ bgcolor: colors.amberSoftBg,
231
+ px: 0.5,
232
+ py: 0.2,
233
+ borderRadius: 0.5,
234
+ border: `1px solid ${colors.amberBorder}`,
235
+ flexShrink: 0,
236
+ }}
237
+ >
238
+ LEAD
239
+ </Typography>
240
+ )}
241
+ </Box>
242
+ <StatusBadge status={agent.status} />
243
+ </Box>
244
+ <Typography
245
+ sx={{
246
+ fontFamily: "code",
247
+ fontSize: "0.75rem",
248
+ color: "text.tertiary",
249
+ }}
250
+ >
251
+ {agent.role || "No role"} · {activeTasks}/{agent.tasks.length} tasks
252
+ </Typography>
253
+ </Box>
254
+ );
255
+ }
256
+
257
+ interface AgentsPanelProps {
258
+ selectedAgentId: string | null;
259
+ onSelectAgent: (agentId: string | null) => void;
260
+ statusFilter?: AgentStatus | "all";
261
+ onStatusFilterChange?: (status: AgentStatus | "all") => void;
262
+ }
263
+
264
+ export default function AgentsPanel({
265
+ selectedAgentId,
266
+ onSelectAgent,
267
+ statusFilter: controlledStatusFilter,
268
+ onStatusFilterChange,
269
+ }: AgentsPanelProps) {
270
+ const [searchQuery, setSearchQuery] = useState("");
271
+ const [internalStatusFilter, setInternalStatusFilter] = useState<AgentStatus | "all">("all");
272
+
273
+ // Use controlled or internal state
274
+ const statusFilter = controlledStatusFilter ?? internalStatusFilter;
275
+ const setStatusFilter = onStatusFilterChange ?? setInternalStatusFilter;
276
+
277
+ const { data: agents, isLoading } = useAgents();
278
+ const { mode } = useColorScheme();
279
+ const isDark = mode === "dark";
280
+
281
+ const colors = {
282
+ amber: isDark ? "#F5A623" : "#D48806",
283
+ amberGlow: isDark ? "0 0 8px rgba(245, 166, 35, 0.5)" : "0 0 6px rgba(212, 136, 6, 0.3)",
284
+ hoverBg: isDark ? "rgba(245, 166, 35, 0.05)" : "rgba(212, 136, 6, 0.05)",
285
+ hoverBorder: isDark ? "#4A3A2F" : "#D1C5B4",
286
+ amberInputGlow: isDark ? "0 0 10px rgba(245, 166, 35, 0.2)" : "0 0 8px rgba(212, 136, 6, 0.15)",
287
+ };
288
+
289
+ // Filter agents based on search and status
290
+ const filteredAgents = useMemo(() => {
291
+ if (!agents) return [];
292
+
293
+ return agents.filter((agent) => {
294
+ // Search filter
295
+ if (searchQuery.trim()) {
296
+ const query = searchQuery.toLowerCase();
297
+ if (!agent.name.toLowerCase().includes(query) &&
298
+ !agent.id.toLowerCase().includes(query)) {
299
+ return false;
300
+ }
301
+ }
302
+
303
+ // Status filter
304
+ if (statusFilter !== "all" && agent.status !== statusFilter) {
305
+ return false;
306
+ }
307
+
308
+ return true;
309
+ });
310
+ }, [agents, searchQuery, statusFilter]);
311
+
312
+ if (isLoading) {
313
+ return (
314
+ <Card
315
+ variant="outlined"
316
+ sx={{
317
+ p: 2,
318
+ height: "100%",
319
+ bgcolor: "background.surface",
320
+ borderColor: "neutral.outlinedBorder",
321
+ }}
322
+ >
323
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
324
+ Loading agents...
325
+ </Typography>
326
+ </Card>
327
+ );
328
+ }
329
+
330
+ if (!agents || agents.length === 0) {
331
+ return (
332
+ <Card
333
+ variant="outlined"
334
+ sx={{
335
+ p: 2,
336
+ height: "100%",
337
+ bgcolor: "background.surface",
338
+ borderColor: "neutral.outlinedBorder",
339
+ }}
340
+ >
341
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
342
+ No agents in the swarm
343
+ </Typography>
344
+ </Card>
345
+ );
346
+ }
347
+
348
+ return (
349
+ <Card
350
+ variant="outlined"
351
+ className="card-hover"
352
+ sx={{
353
+ p: 0,
354
+ overflow: "hidden",
355
+ height: "100%",
356
+ display: "flex",
357
+ flexDirection: "column",
358
+ bgcolor: "background.surface",
359
+ borderColor: "neutral.outlinedBorder",
360
+ }}
361
+ >
362
+ {/* Header */}
363
+ <Box
364
+ sx={{
365
+ px: { xs: 1.5, md: 2 },
366
+ py: 1.5,
367
+ borderBottom: "1px solid",
368
+ borderColor: "neutral.outlinedBorder",
369
+ bgcolor: "background.level1",
370
+ display: "flex",
371
+ flexDirection: { xs: "column", sm: "row" },
372
+ alignItems: { xs: "stretch", sm: "center" },
373
+ justifyContent: "space-between",
374
+ gap: 1.5,
375
+ }}
376
+ >
377
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
378
+ {/* Hex accent */}
379
+ <Box
380
+ sx={{
381
+ width: 8,
382
+ height: 10,
383
+ clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
384
+ bgcolor: colors.amber,
385
+ boxShadow: colors.amberGlow,
386
+ }}
387
+ />
388
+ <Typography
389
+ level="title-md"
390
+ sx={{
391
+ fontFamily: "display",
392
+ fontWeight: 600,
393
+ color: colors.amber,
394
+ letterSpacing: "0.03em",
395
+ fontSize: { xs: "0.9rem", md: "1rem" },
396
+ }}
397
+ >
398
+ AGENTS
399
+ </Typography>
400
+ <Typography
401
+ sx={{
402
+ fontFamily: "code",
403
+ fontSize: "0.7rem",
404
+ color: "text.tertiary",
405
+ }}
406
+ >
407
+ ({filteredAgents.length}/{agents.length})
408
+ </Typography>
409
+ </Box>
410
+
411
+ {/* Filters */}
412
+ <Box
413
+ sx={{
414
+ display: "flex",
415
+ flexDirection: { xs: "column", sm: "row" },
416
+ alignItems: { xs: "stretch", sm: "center" },
417
+ gap: 1,
418
+ }}
419
+ >
420
+ {/* Search */}
421
+ <Input
422
+ placeholder="Search..."
423
+ value={searchQuery}
424
+ onChange={(e) => setSearchQuery(e.target.value)}
425
+ size="sm"
426
+ sx={{
427
+ fontFamily: "code",
428
+ fontSize: "0.75rem",
429
+ minWidth: { xs: "100%", sm: 140 },
430
+ bgcolor: "background.surface",
431
+ borderColor: "neutral.outlinedBorder",
432
+ color: "text.primary",
433
+ "&:hover": {
434
+ borderColor: colors.hoverBorder,
435
+ },
436
+ "&:focus-within": {
437
+ borderColor: colors.amber,
438
+ boxShadow: colors.amberInputGlow,
439
+ },
440
+ }}
441
+ />
442
+
443
+ {/* Status Filter */}
444
+ <Select
445
+ value={statusFilter}
446
+ onChange={(_, value) => setStatusFilter(value as AgentStatus | "all")}
447
+ size="sm"
448
+ sx={{
449
+ fontFamily: "code",
450
+ fontSize: "0.75rem",
451
+ minWidth: { xs: "100%", sm: 100 },
452
+ bgcolor: "background.surface",
453
+ borderColor: "neutral.outlinedBorder",
454
+ color: "text.secondary",
455
+ "&:hover": {
456
+ borderColor: colors.amber,
457
+ },
458
+ "& .MuiSelect-indicator": {
459
+ color: "text.tertiary",
460
+ },
461
+ }}
462
+ >
463
+ <Option value="all">ALL</Option>
464
+ <Option value="idle">IDLE</Option>
465
+ <Option value="busy">BUSY</Option>
466
+ <Option value="offline">OFFLINE</Option>
467
+ </Select>
468
+ </Box>
469
+ </Box>
470
+
471
+ {/* Content */}
472
+ <Box sx={{ flex: 1, overflow: "auto" }}>
473
+ {filteredAgents.length === 0 ? (
474
+ <Box sx={{ p: 3, textAlign: "center" }}>
475
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
476
+ No agents match your filters
477
+ </Typography>
478
+ </Box>
479
+ ) : (
480
+ <>
481
+ {/* Desktop Table */}
482
+ <Box sx={{ display: { xs: "none", md: "block" } }}>
483
+ <Table
484
+ size="sm"
485
+ sx={{
486
+ "--TableCell-paddingY": "10px",
487
+ "--TableCell-paddingX": "12px",
488
+ "--TableCell-borderColor": "var(--joy-palette-neutral-outlinedBorder)",
489
+ "& thead th": {
490
+ bgcolor: "background.surface",
491
+ fontFamily: "code",
492
+ fontSize: "0.7rem",
493
+ letterSpacing: "0.05em",
494
+ color: "text.tertiary",
495
+ borderBottom: "1px solid",
496
+ borderColor: "neutral.outlinedBorder",
497
+ position: "sticky",
498
+ top: 0,
499
+ zIndex: 1,
500
+ },
501
+ "& tbody tr": {
502
+ transition: "background-color 0.2s ease",
503
+ },
504
+ "& tbody tr:hover": {
505
+ bgcolor: colors.hoverBg,
506
+ },
507
+ }}
508
+ >
509
+ <thead>
510
+ <tr>
511
+ <th>NAME</th>
512
+ <th style={{ width: "130px" }}>ROLE</th>
513
+ <th style={{ width: "90px" }}>STATUS</th>
514
+ <th style={{ width: "70px" }}>TASKS</th>
515
+ <th style={{ width: "100px" }}>UPDATED</th>
516
+ </tr>
517
+ </thead>
518
+ <tbody>
519
+ {filteredAgents.map((agent) => (
520
+ <AgentRow
521
+ key={agent.id}
522
+ agent={agent}
523
+ selected={selectedAgentId === agent.id}
524
+ onClick={() => onSelectAgent(selectedAgentId === agent.id ? null : agent.id)}
525
+ isDark={isDark}
526
+ />
527
+ ))}
528
+ </tbody>
529
+ </Table>
530
+ </Box>
531
+
532
+ {/* Mobile Cards */}
533
+ <Box sx={{ display: { xs: "block", md: "none" }, p: 1.5 }}>
534
+ {filteredAgents.map((agent) => (
535
+ <AgentCard
536
+ key={agent.id}
537
+ agent={agent}
538
+ selected={selectedAgentId === agent.id}
539
+ onClick={() => onSelectAgent(selectedAgentId === agent.id ? null : agent.id)}
540
+ isDark={isDark}
541
+ />
542
+ ))}
543
+ </Box>
544
+ </>
545
+ )}
546
+ </Box>
547
+ </Card>
548
+ );
549
+ }