@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,575 @@
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 { useColorScheme } from "@mui/joy/styles";
11
+ import { useTasks, useAgents } from "../hooks/queries";
12
+ import StatusBadge from "./StatusBadge";
13
+ import type { TaskStatus, AgentTask } from "../types/api";
14
+
15
+ interface TasksPanelProps {
16
+ selectedTaskId: string | null;
17
+ onSelectTask: (taskId: string | null) => void;
18
+ preFilterAgentId?: string;
19
+ statusFilter?: TaskStatus | "all";
20
+ onStatusFilterChange?: (status: TaskStatus | "all") => void;
21
+ }
22
+
23
+ function getElapsedTime(task: AgentTask): string {
24
+ const start = new Date(task.createdAt).getTime();
25
+ const end = task.finishedAt ? new Date(task.finishedAt).getTime() : Date.now();
26
+ const elapsed = end - start;
27
+
28
+ const seconds = Math.floor(elapsed / 1000);
29
+ const minutes = Math.floor(seconds / 60);
30
+ const hours = Math.floor(minutes / 60);
31
+
32
+ if (hours > 0) {
33
+ return `${hours}h ${minutes % 60}m`;
34
+ } else if (minutes > 0) {
35
+ return `${minutes}m ${seconds % 60}s`;
36
+ }
37
+ return `${seconds}s`;
38
+ }
39
+
40
+ function formatSmartTime(dateStr: string): string {
41
+ const date = new Date(dateStr);
42
+ const now = new Date();
43
+ const diffMs = now.getTime() - date.getTime();
44
+ const diffMins = Math.floor(diffMs / 60000);
45
+ const diffHours = Math.floor(diffMins / 60);
46
+
47
+ // Less than 6 hours: relative time
48
+ if (diffHours < 6) {
49
+ if (diffMins < 1) return "just now";
50
+ if (diffMins < 60) return `${diffMins}m ago`;
51
+ return `${diffHours}h ago`;
52
+ }
53
+
54
+ // Same day: time only
55
+ const isToday = date.toDateString() === now.toDateString();
56
+ if (isToday) {
57
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
58
+ }
59
+
60
+ // Before today: full date
61
+ return date.toLocaleDateString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
62
+ }
63
+
64
+ // Mobile card component
65
+ interface TaskCardProps {
66
+ task: AgentTask;
67
+ selected: boolean;
68
+ onClick: () => void;
69
+ agentName?: string;
70
+ isDark: boolean;
71
+ }
72
+
73
+ function TaskCard({ task, selected, onClick, agentName, isDark }: TaskCardProps) {
74
+ const colors = {
75
+ amber: isDark ? "#F5A623" : "#D48806",
76
+ gold: isDark ? "#D4A574" : "#8B6914",
77
+ selectedBorder: isDark ? "#D4A574" : "#8B6914",
78
+ goldSoftBg: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
79
+ goldBorder: isDark ? "rgba(212, 165, 116, 0.3)" : "rgba(139, 105, 20, 0.25)",
80
+ };
81
+
82
+ return (
83
+ <Box
84
+ onClick={onClick}
85
+ sx={{
86
+ p: 2,
87
+ mb: 1,
88
+ borderRadius: "8px",
89
+ border: "1px solid",
90
+ borderColor: selected ? colors.selectedBorder : "neutral.outlinedBorder",
91
+ bgcolor: selected ? colors.goldSoftBg : "background.surface",
92
+ cursor: "pointer",
93
+ transition: "all 0.2s ease",
94
+ "&:active": {
95
+ bgcolor: colors.goldSoftBg,
96
+ },
97
+ }}
98
+ >
99
+ <Typography
100
+ sx={{
101
+ fontFamily: "code",
102
+ fontSize: "0.85rem",
103
+ fontWeight: 600,
104
+ color: "text.primary",
105
+ mb: 1,
106
+ overflow: "hidden",
107
+ textOverflow: "ellipsis",
108
+ display: "-webkit-box",
109
+ WebkitLineClamp: 2,
110
+ WebkitBoxOrient: "vertical",
111
+ }}
112
+ >
113
+ {task.task}
114
+ </Typography>
115
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 0.5 }}>
116
+ <StatusBadge status={task.status} />
117
+ <Typography sx={{ fontFamily: "code", fontSize: "0.7rem", color: "text.tertiary" }}>
118
+ {getElapsedTime(task)}
119
+ </Typography>
120
+ </Box>
121
+ {task.agentId && (
122
+ <Typography sx={{ fontFamily: "code", fontSize: "0.7rem", color: colors.amber, mt: 0.5 }}>
123
+ Agent: {agentName || task.agentId.slice(0, 8)}
124
+ </Typography>
125
+ )}
126
+ {task.tags && task.tags.length > 0 && (
127
+ <Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap", mt: 1 }}>
128
+ {task.tags.slice(0, 3).map((tag) => (
129
+ <Chip
130
+ key={tag}
131
+ size="sm"
132
+ variant="soft"
133
+ sx={{
134
+ fontFamily: "code",
135
+ fontSize: "0.55rem",
136
+ bgcolor: colors.goldSoftBg,
137
+ color: colors.gold,
138
+ border: `1px solid ${colors.goldBorder}`,
139
+ }}
140
+ >
141
+ {tag}
142
+ </Chip>
143
+ ))}
144
+ </Box>
145
+ )}
146
+ </Box>
147
+ );
148
+ }
149
+
150
+ export default function TasksPanel({
151
+ selectedTaskId,
152
+ onSelectTask,
153
+ preFilterAgentId,
154
+ statusFilter: controlledStatusFilter,
155
+ onStatusFilterChange,
156
+ }: TasksPanelProps) {
157
+ const [internalStatusFilter, setInternalStatusFilter] = useState<TaskStatus | "all">("all");
158
+ const [agentFilter, setAgentFilter] = useState<string | "all">(preFilterAgentId || "all");
159
+ const [searchQuery, setSearchQuery] = useState("");
160
+
161
+ // Use controlled or internal state
162
+ const statusFilter = controlledStatusFilter ?? internalStatusFilter;
163
+ const setStatusFilter = onStatusFilterChange ?? setInternalStatusFilter;
164
+
165
+ const { mode } = useColorScheme();
166
+ const isDark = mode === "dark";
167
+ const { data: agents } = useAgents();
168
+
169
+ const colors = {
170
+ gold: isDark ? "#D4A574" : "#8B6914",
171
+ goldGlow: isDark ? "0 0 8px rgba(212, 165, 116, 0.5)" : "0 0 6px rgba(139, 105, 20, 0.3)",
172
+ amber: isDark ? "#F5A623" : "#D48806",
173
+ amberGlow: isDark ? "0 0 10px rgba(245, 166, 35, 0.2)" : "0 0 8px rgba(212, 136, 6, 0.15)",
174
+ hoverBg: isDark ? "rgba(245, 166, 35, 0.03)" : "rgba(212, 136, 6, 0.03)",
175
+ hoverBorder: isDark ? "#4A3A2F" : "#D1C5B4",
176
+ };
177
+
178
+ // Build filters for API call
179
+ const filters = useMemo(() => {
180
+ const f: { status?: string; agentId?: string; search?: string } = {};
181
+ if (statusFilter !== "all") f.status = statusFilter;
182
+ if (agentFilter !== "all") f.agentId = agentFilter;
183
+ if (searchQuery.trim()) f.search = searchQuery.trim();
184
+ return Object.keys(f).length > 0 ? f : undefined;
185
+ }, [statusFilter, agentFilter, searchQuery]);
186
+
187
+ const { data: tasks, isLoading } = useTasks(filters);
188
+
189
+ // Create agent lookup
190
+ const agentMap = useMemo(() => {
191
+ const map = new Map<string, string>();
192
+ agents?.forEach((a) => map.set(a.id, a.name));
193
+ return map;
194
+ }, [agents]);
195
+
196
+ return (
197
+ <Card
198
+ variant="outlined"
199
+ className="card-hover"
200
+ sx={{
201
+ p: 0,
202
+ overflow: "hidden",
203
+ height: "100%",
204
+ display: "flex",
205
+ flexDirection: "column",
206
+ bgcolor: "background.surface",
207
+ borderColor: "neutral.outlinedBorder",
208
+ }}
209
+ >
210
+ {/* Header */}
211
+ <Box
212
+ sx={{
213
+ display: "flex",
214
+ flexDirection: { xs: "column", sm: "row" },
215
+ alignItems: { xs: "stretch", sm: "center" },
216
+ justifyContent: "space-between",
217
+ px: { xs: 1.5, md: 2 },
218
+ py: 1.5,
219
+ borderBottom: "1px solid",
220
+ borderColor: "neutral.outlinedBorder",
221
+ bgcolor: "background.level1",
222
+ gap: 1.5,
223
+ }}
224
+ >
225
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
226
+ {/* Hex accent */}
227
+ <Box
228
+ sx={{
229
+ width: 8,
230
+ height: 10,
231
+ clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
232
+ bgcolor: colors.gold,
233
+ boxShadow: colors.goldGlow,
234
+ }}
235
+ />
236
+ <Typography
237
+ level="title-md"
238
+ sx={{
239
+ fontFamily: "display",
240
+ fontWeight: 600,
241
+ color: colors.gold,
242
+ letterSpacing: "0.03em",
243
+ fontSize: { xs: "0.9rem", md: "1rem" },
244
+ }}
245
+ >
246
+ TASKS
247
+ </Typography>
248
+ <Typography
249
+ sx={{
250
+ fontFamily: "code",
251
+ fontSize: "0.7rem",
252
+ color: "text.tertiary",
253
+ }}
254
+ >
255
+ ({tasks?.length || 0})
256
+ </Typography>
257
+ </Box>
258
+
259
+ {/* Filters */}
260
+ <Box
261
+ sx={{
262
+ display: "flex",
263
+ flexDirection: { xs: "column", sm: "row" },
264
+ alignItems: { xs: "stretch", sm: "center" },
265
+ gap: 1,
266
+ }}
267
+ >
268
+ {/* Search */}
269
+ <Input
270
+ placeholder="Search tasks..."
271
+ value={searchQuery}
272
+ onChange={(e) => setSearchQuery(e.target.value)}
273
+ size="sm"
274
+ sx={{
275
+ fontFamily: "code",
276
+ fontSize: "0.75rem",
277
+ minWidth: { xs: "100%", sm: 180 },
278
+ bgcolor: "background.surface",
279
+ borderColor: "neutral.outlinedBorder",
280
+ color: "text.primary",
281
+ "&:hover": {
282
+ borderColor: colors.hoverBorder,
283
+ },
284
+ "&:focus-within": {
285
+ borderColor: colors.amber,
286
+ boxShadow: colors.amberGlow,
287
+ },
288
+ }}
289
+ />
290
+
291
+ {/* Agent Filter - hidden on mobile to save space */}
292
+ <Select
293
+ value={agentFilter}
294
+ onChange={(_, value) => setAgentFilter(value as string)}
295
+ size="sm"
296
+ sx={{
297
+ fontFamily: "code",
298
+ fontSize: "0.75rem",
299
+ minWidth: { xs: "100%", sm: 130 },
300
+ display: { xs: "none", sm: "flex" },
301
+ bgcolor: "background.surface",
302
+ borderColor: "neutral.outlinedBorder",
303
+ color: "text.secondary",
304
+ "&:hover": {
305
+ borderColor: colors.amber,
306
+ },
307
+ "& .MuiSelect-indicator": {
308
+ color: "text.tertiary",
309
+ },
310
+ }}
311
+ >
312
+ <Option value="all">ALL AGENTS</Option>
313
+ {agents?.map((agent) => (
314
+ <Option key={agent.id} value={agent.id}>
315
+ {agent.name}
316
+ </Option>
317
+ ))}
318
+ </Select>
319
+
320
+ {/* Status Filter */}
321
+ <Select
322
+ value={statusFilter}
323
+ onChange={(_, value) => setStatusFilter(value as TaskStatus | "all")}
324
+ size="sm"
325
+ sx={{
326
+ fontFamily: "code",
327
+ fontSize: "0.75rem",
328
+ minWidth: { xs: "100%", sm: 120 },
329
+ bgcolor: "background.surface",
330
+ borderColor: "neutral.outlinedBorder",
331
+ color: "text.secondary",
332
+ "&:hover": {
333
+ borderColor: colors.amber,
334
+ },
335
+ "& .MuiSelect-indicator": {
336
+ color: "text.tertiary",
337
+ },
338
+ }}
339
+ >
340
+ <Option value="all">ALL STATUS</Option>
341
+ <Option value="unassigned">UNASSIGNED</Option>
342
+ <Option value="offered">OFFERED</Option>
343
+ <Option value="pending">PENDING</Option>
344
+ <Option value="in_progress">IN PROGRESS</Option>
345
+ <Option value="completed">COMPLETED</Option>
346
+ <Option value="failed">FAILED</Option>
347
+ </Select>
348
+ </Box>
349
+ </Box>
350
+
351
+ {/* Content */}
352
+ <Box sx={{ flex: 1, overflow: "auto" }}>
353
+ {isLoading ? (
354
+ <Box sx={{ p: 3, textAlign: "center" }}>
355
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
356
+ Loading tasks...
357
+ </Typography>
358
+ </Box>
359
+ ) : !tasks || tasks.length === 0 ? (
360
+ <Box sx={{ p: 3, textAlign: "center" }}>
361
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
362
+ No tasks found
363
+ </Typography>
364
+ </Box>
365
+ ) : (
366
+ <>
367
+ {/* Desktop Table */}
368
+ <Box sx={{ display: { xs: "none", md: "block" } }}>
369
+ <Table
370
+ size="sm"
371
+ sx={{
372
+ "--TableCell-paddingY": "10px",
373
+ "--TableCell-paddingX": "12px",
374
+ "--TableCell-borderColor": "var(--joy-palette-neutral-outlinedBorder)",
375
+ tableLayout: "fixed",
376
+ width: "100%",
377
+ "& thead th": {
378
+ bgcolor: "background.surface",
379
+ fontFamily: "code",
380
+ fontSize: "0.7rem",
381
+ letterSpacing: "0.05em",
382
+ color: "text.tertiary",
383
+ borderBottom: "1px solid",
384
+ borderColor: "neutral.outlinedBorder",
385
+ position: "sticky",
386
+ top: 0,
387
+ zIndex: 1,
388
+ },
389
+ "& tbody tr": {
390
+ transition: "background-color 0.2s ease",
391
+ cursor: "pointer",
392
+ },
393
+ "& tbody tr:hover": {
394
+ bgcolor: colors.hoverBg,
395
+ },
396
+ }}
397
+ >
398
+ <thead>
399
+ <tr>
400
+ <th style={{ width: "30%" }}>TASK</th>
401
+ <th style={{ width: "10%" }}>AGENT</th>
402
+ <th style={{ width: "8%" }}>TYPE</th>
403
+ <th style={{ width: "12%" }}>TAGS</th>
404
+ <th style={{ width: "10%" }}>STATUS</th>
405
+ <th style={{ width: "12%" }}>PROGRESS</th>
406
+ <th style={{ width: "8%" }}>ELAPSED</th>
407
+ <th style={{ width: "10%" }}>UPDATED</th>
408
+ </tr>
409
+ </thead>
410
+ <tbody>
411
+ {tasks.slice(0, 50).map((task) => (
412
+ <tr
413
+ key={task.id}
414
+ onClick={() => onSelectTask(selectedTaskId === task.id ? null : task.id)}
415
+ >
416
+ <td>
417
+ <Typography
418
+ sx={{
419
+ fontFamily: "code",
420
+ fontSize: "0.8rem",
421
+ color: "text.primary",
422
+ overflow: "hidden",
423
+ textOverflow: "ellipsis",
424
+ whiteSpace: "nowrap",
425
+ }}
426
+ >
427
+ {task.task}
428
+ </Typography>
429
+ </td>
430
+ <td>
431
+ <Typography
432
+ sx={{
433
+ fontFamily: "code",
434
+ fontSize: "0.75rem",
435
+ color: task.agentId ? colors.amber : "text.tertiary",
436
+ overflow: "hidden",
437
+ textOverflow: "ellipsis",
438
+ whiteSpace: "nowrap",
439
+ }}
440
+ >
441
+ {task.agentId ? (agentMap.get(task.agentId) || task.agentId.slice(0, 8)) : "—"}
442
+ </Typography>
443
+ </td>
444
+ <td>
445
+ <Typography
446
+ sx={{
447
+ fontFamily: "code",
448
+ fontSize: "0.7rem",
449
+ color: task.taskType ? "text.secondary" : "text.tertiary",
450
+ overflow: "hidden",
451
+ textOverflow: "ellipsis",
452
+ whiteSpace: "nowrap",
453
+ }}
454
+ >
455
+ {task.taskType || "—"}
456
+ </Typography>
457
+ </td>
458
+ <td>
459
+ {task.tags && task.tags.length > 0 ? (
460
+ <Box sx={{ display: "flex", gap: 0.5, flexWrap: "nowrap", overflow: "hidden" }}>
461
+ {task.tags.slice(0, 2).map((tag) => (
462
+ <Chip
463
+ key={tag}
464
+ size="sm"
465
+ variant="soft"
466
+ sx={{
467
+ fontFamily: "code",
468
+ fontSize: "0.6rem",
469
+ bgcolor: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
470
+ color: colors.gold,
471
+ border: `1px solid ${isDark ? "rgba(212, 165, 116, 0.3)" : "rgba(139, 105, 20, 0.25)"}`,
472
+ }}
473
+ >
474
+ {tag}
475
+ </Chip>
476
+ ))}
477
+ {task.tags.length > 2 && (
478
+ <Typography sx={{ fontFamily: "code", fontSize: "0.6rem", color: "text.tertiary" }}>
479
+ +{task.tags.length - 2}
480
+ </Typography>
481
+ )}
482
+ </Box>
483
+ ) : (
484
+ <Typography sx={{ fontFamily: "code", fontSize: "0.7rem", color: "text.tertiary" }}>
485
+
486
+ </Typography>
487
+ )}
488
+ </td>
489
+ <td>
490
+ <StatusBadge status={task.status} />
491
+ </td>
492
+ <td>
493
+ <Typography
494
+ sx={{
495
+ fontFamily: "code",
496
+ fontSize: "0.7rem",
497
+ color: "text.tertiary",
498
+ overflow: "hidden",
499
+ textOverflow: "ellipsis",
500
+ whiteSpace: "nowrap",
501
+ }}
502
+ >
503
+ {task.progress || "—"}
504
+ </Typography>
505
+ </td>
506
+ <td>
507
+ <Typography
508
+ sx={{
509
+ fontFamily: "code",
510
+ fontSize: "0.7rem",
511
+ color: task.status === "in_progress" ? colors.amber : "text.tertiary",
512
+ }}
513
+ >
514
+ {getElapsedTime(task)}
515
+ </Typography>
516
+ </td>
517
+ <td>
518
+ <Typography
519
+ sx={{
520
+ fontFamily: "code",
521
+ fontSize: "0.7rem",
522
+ color: "text.tertiary",
523
+ }}
524
+ >
525
+ {formatSmartTime(task.lastUpdatedAt)}
526
+ </Typography>
527
+ </td>
528
+ </tr>
529
+ ))}
530
+ </tbody>
531
+ </Table>
532
+ </Box>
533
+
534
+ {/* Mobile Cards */}
535
+ <Box sx={{ display: { xs: "block", md: "none" }, p: 1.5 }}>
536
+ {tasks.slice(0, 50).map((task) => (
537
+ <TaskCard
538
+ key={task.id}
539
+ task={task}
540
+ selected={selectedTaskId === task.id}
541
+ onClick={() => onSelectTask(selectedTaskId === task.id ? null : task.id)}
542
+ agentName={task.agentId ? agentMap.get(task.agentId) : undefined}
543
+ isDark={isDark}
544
+ />
545
+ ))}
546
+ </Box>
547
+ </>
548
+ )}
549
+ </Box>
550
+
551
+ {/* Footer */}
552
+ {tasks && tasks.length > 50 && (
553
+ <Box
554
+ sx={{
555
+ p: 1.5,
556
+ textAlign: "center",
557
+ borderTop: "1px solid",
558
+ borderColor: "neutral.outlinedBorder",
559
+ bgcolor: "background.level1",
560
+ }}
561
+ >
562
+ <Typography
563
+ sx={{
564
+ fontFamily: "code",
565
+ fontSize: "0.7rem",
566
+ color: "text.tertiary",
567
+ }}
568
+ >
569
+ Showing 50 of {tasks.length} tasks
570
+ </Typography>
571
+ </Box>
572
+ )}
573
+ </Card>
574
+ );
575
+ }