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