@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,807 @@
1
+ import { useState, useCallback } from "react";
2
+ import Box from "@mui/joy/Box";
3
+ import Typography from "@mui/joy/Typography";
4
+ import IconButton from "@mui/joy/IconButton";
5
+ import Divider from "@mui/joy/Divider";
6
+ import Tooltip from "@mui/joy/Tooltip";
7
+ import Tabs from "@mui/joy/Tabs";
8
+ import TabList from "@mui/joy/TabList";
9
+ import Tab from "@mui/joy/Tab";
10
+ import TabPanel from "@mui/joy/TabPanel";
11
+ import Chip from "@mui/joy/Chip";
12
+ import { useColorScheme } from "@mui/joy/styles";
13
+ import { useTask, useAgents } from "../hooks/queries";
14
+ import { formatRelativeTime } from "../lib/utils";
15
+ import StatusBadge from "./StatusBadge";
16
+
17
+ interface TaskDetailPanelProps {
18
+ taskId: string;
19
+ onClose: () => void;
20
+ expanded?: boolean;
21
+ onToggleExpand?: () => void;
22
+ }
23
+
24
+ export default function TaskDetailPanel({
25
+ taskId,
26
+ onClose,
27
+ expanded = false,
28
+ onToggleExpand,
29
+ }: TaskDetailPanelProps) {
30
+ const { data: task, isLoading: taskLoading } = useTask(taskId);
31
+ const { data: agents } = useAgents();
32
+ const { mode } = useColorScheme();
33
+ const isDark = mode === "dark";
34
+ const [outputTab, setOutputTab] = useState<"output" | "error">("output");
35
+ const [copiedField, setCopiedField] = useState<"output" | "error" | null>(null);
36
+
37
+ const handleCopy = useCallback(async (content: string, field: "output" | "error") => {
38
+ try {
39
+ await navigator.clipboard.writeText(content);
40
+ setCopiedField(field);
41
+ setTimeout(() => setCopiedField(null), 2000);
42
+ } catch (err) {
43
+ console.error("Failed to copy:", err);
44
+ }
45
+ }, []);
46
+
47
+ const colors = {
48
+ amber: isDark ? "#F5A623" : "#D48806",
49
+ gold: isDark ? "#D4A574" : "#8B6914",
50
+ rust: isDark ? "#A85454" : "#B54242",
51
+ blue: "#3B82F6",
52
+ purple: isDark ? "#9370DB" : "#6B5B95",
53
+ warmGray: isDark ? "#C9B896" : "#8B7355",
54
+ tertiary: isDark ? "#8B7355" : "#6B5344",
55
+ closeBtn: isDark ? "#8B7355" : "#5C4A3D",
56
+ closeBtnHover: isDark ? "#FFF8E7" : "#1A130E",
57
+ goldGlow: isDark ? "0 0 8px rgba(212, 165, 116, 0.5)" : "0 0 6px rgba(139, 105, 20, 0.3)",
58
+ goldSoftBg: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
59
+ goldBorder: isDark ? "rgba(212, 165, 116, 0.3)" : "rgba(139, 105, 20, 0.25)",
60
+ hoverBg: isDark ? "rgba(245, 166, 35, 0.05)" : "rgba(212, 136, 6, 0.05)",
61
+ };
62
+
63
+ const getSourceColor = (source: string) => {
64
+ switch (source) {
65
+ case "mcp": return colors.amber;
66
+ case "slack": return colors.purple;
67
+ case "api": return colors.blue;
68
+ default: return colors.tertiary;
69
+ }
70
+ };
71
+
72
+ const agentName = agents?.find((a) => a.id === task?.agentId)?.name || task?.agentId?.slice(0, 8);
73
+
74
+ const getElapsedTime = () => {
75
+ if (!task) return "—";
76
+ const start = new Date(task.createdAt).getTime();
77
+ const end = task.finishedAt
78
+ ? new Date(task.finishedAt).getTime()
79
+ : Date.now();
80
+ const elapsed = end - start;
81
+
82
+ const seconds = Math.floor(elapsed / 1000);
83
+ const minutes = Math.floor(seconds / 60);
84
+ const hours = Math.floor(minutes / 60);
85
+
86
+ if (hours > 0) {
87
+ return `${hours}h ${minutes % 60}m`;
88
+ } else if (minutes > 0) {
89
+ return `${minutes}m ${seconds % 60}s`;
90
+ }
91
+ return `${seconds}s`;
92
+ };
93
+
94
+ const panelWidth = expanded ? "100%" : 450;
95
+
96
+ const loadingContent = (
97
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
98
+ Loading task...
99
+ </Typography>
100
+ );
101
+
102
+ const notFoundContent = (
103
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
104
+ Task not found
105
+ </Typography>
106
+ );
107
+
108
+ if (taskLoading || !task) {
109
+ return (
110
+ <Box
111
+ sx={{
112
+ position: { xs: "fixed", md: "relative" },
113
+ inset: { xs: 0, md: "auto" },
114
+ zIndex: { xs: 1300, md: "auto" },
115
+ width: { xs: "100%", md: panelWidth },
116
+ height: "100%",
117
+ bgcolor: "background.surface",
118
+ border: { xs: "none", md: "1px solid" },
119
+ borderColor: "neutral.outlinedBorder",
120
+ borderRadius: { xs: 0, md: "12px" },
121
+ p: { xs: 2, md: 3 },
122
+ overflow: "auto",
123
+ }}
124
+ >
125
+ {taskLoading ? loadingContent : notFoundContent}
126
+ </Box>
127
+ );
128
+ }
129
+
130
+ const progressLogs = task.logs?.filter((log) => log.eventType === "task_progress") || [];
131
+ const hasOutput = !!task.output;
132
+ const hasError = !!task.failureReason;
133
+ const hasBothOutputAndError = hasOutput && hasError;
134
+
135
+ // Details section - task info
136
+ const DetailsSection = ({ showProgress = true }: { showProgress?: boolean }) => (
137
+ <Box sx={{ p: { xs: 1.5, md: 2 }, display: "flex", flexDirection: "column", ...(showProgress ? {} : { height: "100%" }) }}>
138
+ {/* Info fields first */}
139
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5, flexShrink: 0, mb: 2 }}>
140
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
141
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
142
+ Status
143
+ </Typography>
144
+ <StatusBadge status={task.status} />
145
+ </Box>
146
+
147
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
148
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
149
+ Agent
150
+ </Typography>
151
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: task.agentId ? colors.amber : "text.tertiary" }}>
152
+ {task.agentId ? agentName : "Unassigned"}
153
+ </Typography>
154
+ </Box>
155
+
156
+ {task.source && (
157
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
158
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
159
+ Source
160
+ </Typography>
161
+ <Chip
162
+ size="sm"
163
+ variant="soft"
164
+ sx={{
165
+ fontFamily: "code",
166
+ fontSize: "0.65rem",
167
+ color: getSourceColor(task.source),
168
+ bgcolor: isDark ? "rgba(100, 100, 100, 0.15)" : "rgba(150, 150, 150, 0.12)",
169
+ textTransform: "uppercase",
170
+ }}
171
+ >
172
+ {task.source}
173
+ </Chip>
174
+ </Box>
175
+ )}
176
+
177
+ {task.taskType && (
178
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
179
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
180
+ Type
181
+ </Typography>
182
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: "text.secondary" }}>
183
+ {task.taskType}
184
+ </Typography>
185
+ </Box>
186
+ )}
187
+
188
+ {task.tags && task.tags.length > 0 && (
189
+ <Box>
190
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary", mb: 0.5 }}>
191
+ Tags
192
+ </Typography>
193
+ <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
194
+ {task.tags.map((tag) => (
195
+ <Chip
196
+ key={tag}
197
+ size="sm"
198
+ variant="soft"
199
+ sx={{
200
+ fontFamily: "code",
201
+ fontSize: "0.65rem",
202
+ bgcolor: colors.goldSoftBg,
203
+ color: colors.gold,
204
+ border: `1px solid ${colors.goldBorder}`,
205
+ }}
206
+ >
207
+ {tag}
208
+ </Chip>
209
+ ))}
210
+ </Box>
211
+ </Box>
212
+ )}
213
+
214
+ {task.priority !== undefined && task.priority !== 50 && (
215
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
216
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
217
+ Priority
218
+ </Typography>
219
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: task.priority > 50 ? colors.amber : "text.secondary" }}>
220
+ {task.priority}
221
+ </Typography>
222
+ </Box>
223
+ )}
224
+
225
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
226
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
227
+ Elapsed Time
228
+ </Typography>
229
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: "text.secondary" }}>
230
+ {getElapsedTime()}
231
+ </Typography>
232
+ </Box>
233
+
234
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
235
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
236
+ Created
237
+ </Typography>
238
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
239
+ {new Date(task.createdAt).toLocaleString()}
240
+ </Typography>
241
+ </Box>
242
+
243
+ {task.finishedAt && (
244
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
245
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
246
+ Finished
247
+ </Typography>
248
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
249
+ {new Date(task.finishedAt).toLocaleString()}
250
+ </Typography>
251
+ </Box>
252
+ )}
253
+ </Box>
254
+
255
+ {/* Task description */}
256
+ <Box
257
+ sx={{
258
+ p: 1.5,
259
+ mb: 2,
260
+ bgcolor: "background.level1",
261
+ borderRadius: 1,
262
+ border: "1px solid",
263
+ borderColor: "neutral.outlinedBorder",
264
+ flexShrink: 0,
265
+ }}
266
+ >
267
+ <Typography
268
+ sx={{
269
+ fontFamily: "code",
270
+ fontSize: "0.8rem",
271
+ color: "text.primary",
272
+ lineHeight: 1.5,
273
+ wordBreak: "break-word",
274
+ }}
275
+ >
276
+ {task.task}
277
+ </Typography>
278
+ </Box>
279
+
280
+ {/* Progress Logs - only in collapsed mode */}
281
+ {showProgress && progressLogs.length > 0 && (
282
+ <>
283
+ <Divider sx={{ my: 2, bgcolor: "neutral.outlinedBorder", flexShrink: 0 }} />
284
+ <Box sx={{ display: "flex", flexDirection: "column" }}>
285
+ <Typography
286
+ sx={{
287
+ fontFamily: "code",
288
+ fontSize: "0.7rem",
289
+ color: "text.tertiary",
290
+ letterSpacing: "0.05em",
291
+ mb: 1,
292
+ flexShrink: 0,
293
+ }}
294
+ >
295
+ PROGRESS ({progressLogs.length})
296
+ </Typography>
297
+ <Box
298
+ sx={{
299
+ display: "flex",
300
+ flexDirection: "column",
301
+ gap: 1,
302
+ }}
303
+ >
304
+ {progressLogs.map((log) => (
305
+ <Box
306
+ key={log.id}
307
+ sx={{
308
+ bgcolor: "background.level1",
309
+ p: 1.5,
310
+ borderRadius: 1,
311
+ border: "1px solid",
312
+ borderColor: "neutral.outlinedBorder",
313
+ flexShrink: 0,
314
+ }}
315
+ >
316
+ <Typography
317
+ sx={{
318
+ fontFamily: "code",
319
+ fontSize: "0.75rem",
320
+ color: "text.secondary",
321
+ mb: 0.5,
322
+ }}
323
+ >
324
+ {log.newValue || "Progress update"}
325
+ </Typography>
326
+ <Typography
327
+ sx={{
328
+ fontFamily: "code",
329
+ fontSize: "0.6rem",
330
+ color: "text.tertiary",
331
+ }}
332
+ >
333
+ {formatRelativeTime(log.createdAt)}
334
+ </Typography>
335
+ </Box>
336
+ ))}
337
+ </Box>
338
+ </Box>
339
+ </>
340
+ )}
341
+ </Box>
342
+ );
343
+
344
+ // Progress section for expanded view
345
+ const ProgressSection = () => (
346
+ <Box sx={{ p: 2, display: "flex", flexDirection: "column", height: "100%" }}>
347
+ <Typography
348
+ sx={{
349
+ fontFamily: "code",
350
+ fontSize: "0.7rem",
351
+ color: "text.tertiary",
352
+ letterSpacing: "0.05em",
353
+ mb: 1,
354
+ flexShrink: 0,
355
+ }}
356
+ >
357
+ PROGRESS ({progressLogs.length})
358
+ </Typography>
359
+ {progressLogs.length === 0 ? (
360
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
361
+ No progress updates
362
+ </Typography>
363
+ ) : (
364
+ <Box
365
+ sx={{
366
+ display: "flex",
367
+ flexDirection: "column",
368
+ gap: 1,
369
+ flex: 1,
370
+ overflow: "auto",
371
+ }}
372
+ >
373
+ {progressLogs.map((log) => (
374
+ <Box
375
+ key={log.id}
376
+ sx={{
377
+ bgcolor: "background.level1",
378
+ p: 1.5,
379
+ borderRadius: 1,
380
+ border: "1px solid",
381
+ borderColor: "neutral.outlinedBorder",
382
+ flexShrink: 0,
383
+ }}
384
+ >
385
+ <Typography
386
+ sx={{
387
+ fontFamily: "code",
388
+ fontSize: "0.75rem",
389
+ color: "text.secondary",
390
+ mb: 0.5,
391
+ }}
392
+ >
393
+ {log.newValue || "Progress update"}
394
+ </Typography>
395
+ <Typography
396
+ sx={{
397
+ fontFamily: "code",
398
+ fontSize: "0.6rem",
399
+ color: "text.tertiary",
400
+ }}
401
+ >
402
+ {formatRelativeTime(log.createdAt)}
403
+ </Typography>
404
+ </Box>
405
+ ))}
406
+ </Box>
407
+ )}
408
+ </Box>
409
+ );
410
+
411
+ // Copy button component
412
+ const CopyButton = ({ content, field }: { content: string; field: "output" | "error" }) => (
413
+ <Tooltip title={copiedField === field ? "Copied!" : "Copy to clipboard"} placement="left">
414
+ <IconButton
415
+ size="sm"
416
+ variant="plain"
417
+ onClick={() => handleCopy(content, field)}
418
+ sx={{
419
+ color: copiedField === field ? colors.amber : colors.closeBtn,
420
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
421
+ }}
422
+ >
423
+ {copiedField === field ? "✓" : "⧉"}
424
+ </IconButton>
425
+ </Tooltip>
426
+ );
427
+
428
+ // Output content
429
+ const OutputContent = () => (
430
+ <Box sx={{ flex: 1, overflow: "auto", p: 2, position: "relative" }}>
431
+ {hasOutput ? (
432
+ <>
433
+ <Box sx={{ position: "absolute", top: 8, right: 8, zIndex: 1 }}>
434
+ <CopyButton content={task.output!} field="output" />
435
+ </Box>
436
+ <Typography
437
+ sx={{
438
+ fontFamily: "code",
439
+ fontSize: "0.75rem",
440
+ color: colors.gold,
441
+ lineHeight: 1.6,
442
+ whiteSpace: "pre-wrap",
443
+ wordBreak: "break-word",
444
+ pr: 4,
445
+ }}
446
+ >
447
+ {task.output}
448
+ </Typography>
449
+ </>
450
+ ) : (
451
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
452
+ No output yet
453
+ </Typography>
454
+ )}
455
+ </Box>
456
+ );
457
+
458
+ // Error content
459
+ const ErrorContent = () => (
460
+ <Box sx={{ flex: 1, overflow: "auto", p: 2, position: "relative" }}>
461
+ <Box sx={{ position: "absolute", top: 8, right: 8, zIndex: 1 }}>
462
+ <CopyButton content={task.failureReason!} field="error" />
463
+ </Box>
464
+ <Typography
465
+ sx={{
466
+ fontFamily: "code",
467
+ fontSize: "0.75rem",
468
+ color: colors.rust,
469
+ lineHeight: 1.6,
470
+ whiteSpace: "pre-wrap",
471
+ wordBreak: "break-word",
472
+ pr: 4,
473
+ }}
474
+ >
475
+ {task.failureReason}
476
+ </Typography>
477
+ </Box>
478
+ );
479
+
480
+ // Get tab list styles with dynamic selected color
481
+ const getTabListStyles = (selectedColor: string, hasBorderTop = false) => ({
482
+ gap: 0.5,
483
+ bgcolor: "background.level1",
484
+ borderBottom: "1px solid",
485
+ borderColor: "neutral.outlinedBorder",
486
+ borderTop: hasBorderTop ? "1px solid" : "none",
487
+ px: 2,
488
+ pt: 1,
489
+ flexShrink: 0,
490
+ "& .MuiTab-root": {
491
+ fontFamily: "code",
492
+ fontSize: "0.75rem",
493
+ letterSpacing: "0.03em",
494
+ fontWeight: 600,
495
+ color: "text.tertiary",
496
+ bgcolor: "transparent",
497
+ border: "1px solid transparent",
498
+ borderBottom: "none",
499
+ borderRadius: "6px 6px 0 0",
500
+ px: 2,
501
+ py: 0.75,
502
+ minHeight: "auto",
503
+ transition: "all 0.2s ease",
504
+ "&:hover": {
505
+ color: "text.secondary",
506
+ bgcolor: colors.hoverBg,
507
+ },
508
+ "&.Mui-selected": {
509
+ color: selectedColor,
510
+ bgcolor: "background.surface",
511
+ borderColor: "neutral.outlinedBorder",
512
+ borderBottomColor: "background.surface",
513
+ marginBottom: "-1px",
514
+ },
515
+ },
516
+ });
517
+
518
+ // Output/Error section for expanded view (with tabs if both present)
519
+ const OutputSection = () => {
520
+ if (hasBothOutputAndError) {
521
+ return (
522
+ <Tabs
523
+ value={outputTab}
524
+ onChange={(_, value) => setOutputTab(value as "output" | "error")}
525
+ sx={{ display: "flex", flexDirection: "column", height: "100%" }}
526
+ >
527
+ <TabList sx={getTabListStyles(outputTab === "error" ? colors.rust : colors.gold)}>
528
+ <Tab value="output">OUTPUT</Tab>
529
+ <Tab value="error">ERROR</Tab>
530
+ </TabList>
531
+ <TabPanel value="output" sx={{ p: 0, flex: 1, overflow: "hidden" }}>
532
+ <OutputContent />
533
+ </TabPanel>
534
+ <TabPanel value="error" sx={{ p: 0, flex: 1, overflow: "hidden" }}>
535
+ <ErrorContent />
536
+ </TabPanel>
537
+ </Tabs>
538
+ );
539
+ }
540
+
541
+ if (hasError) {
542
+ return (
543
+ <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
544
+ <Box sx={{ px: 2, py: 1.5, bgcolor: "background.level1", flexShrink: 0 }}>
545
+ <Typography
546
+ sx={{
547
+ fontFamily: "code",
548
+ fontSize: "0.7rem",
549
+ color: colors.rust,
550
+ letterSpacing: "0.05em",
551
+ }}
552
+ >
553
+ ERROR
554
+ </Typography>
555
+ </Box>
556
+ <ErrorContent />
557
+ </Box>
558
+ );
559
+ }
560
+
561
+ return (
562
+ <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
563
+ <Box sx={{ px: 2, py: 1.5, bgcolor: "background.level1", flexShrink: 0 }}>
564
+ <Typography
565
+ sx={{
566
+ fontFamily: "code",
567
+ fontSize: "0.7rem",
568
+ color: "text.tertiary",
569
+ letterSpacing: "0.05em",
570
+ }}
571
+ >
572
+ OUTPUT
573
+ </Typography>
574
+ </Box>
575
+ <OutputContent />
576
+ </Box>
577
+ );
578
+ };
579
+
580
+ // Collapsed output section
581
+ const CollapsedOutputSection = () => {
582
+ if (hasOutput || hasError) {
583
+ if (hasBothOutputAndError) {
584
+ return (
585
+ <Tabs
586
+ value={outputTab}
587
+ onChange={(_, value) => setOutputTab(value as "output" | "error")}
588
+ sx={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}
589
+ >
590
+ <TabList sx={getTabListStyles(outputTab === "error" ? colors.rust : colors.gold, true)}>
591
+ <Tab value="output">OUTPUT</Tab>
592
+ <Tab value="error">ERROR</Tab>
593
+ </TabList>
594
+ <TabPanel value="output" sx={{ p: 0, flex: 1, overflow: "hidden" }}>
595
+ <OutputContent />
596
+ </TabPanel>
597
+ <TabPanel value="error" sx={{ p: 0, flex: 1, overflow: "hidden" }}>
598
+ <ErrorContent />
599
+ </TabPanel>
600
+ </Tabs>
601
+ );
602
+ }
603
+
604
+ if (hasError) {
605
+ return (
606
+ <Box sx={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}>
607
+ <Box sx={{ px: 2, py: 1.5, bgcolor: "background.level1", borderTop: "1px solid", borderColor: "neutral.outlinedBorder", flexShrink: 0 }}>
608
+ <Typography
609
+ sx={{
610
+ fontFamily: "code",
611
+ fontSize: "0.7rem",
612
+ color: colors.rust,
613
+ letterSpacing: "0.05em",
614
+ }}
615
+ >
616
+ ERROR
617
+ </Typography>
618
+ </Box>
619
+ <ErrorContent />
620
+ </Box>
621
+ );
622
+ }
623
+
624
+ return (
625
+ <Box sx={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}>
626
+ <Box sx={{ px: 2, py: 1.5, bgcolor: "background.level1", borderTop: "1px solid", borderColor: "neutral.outlinedBorder", flexShrink: 0 }}>
627
+ <Typography
628
+ sx={{
629
+ fontFamily: "code",
630
+ fontSize: "0.7rem",
631
+ color: "text.tertiary",
632
+ letterSpacing: "0.05em",
633
+ }}
634
+ >
635
+ OUTPUT
636
+ </Typography>
637
+ </Box>
638
+ <OutputContent />
639
+ </Box>
640
+ );
641
+ }
642
+
643
+ return null;
644
+ };
645
+
646
+ return (
647
+ <Box
648
+ sx={{
649
+ position: { xs: "fixed", md: "relative" },
650
+ inset: { xs: 0, md: "auto" },
651
+ zIndex: { xs: 1300, md: "auto" },
652
+ width: { xs: "100%", md: expanded ? "100%" : 450 },
653
+ height: { xs: "100%", md: "100%" },
654
+ bgcolor: "background.surface",
655
+ border: { xs: "none", md: "1px solid" },
656
+ borderColor: "neutral.outlinedBorder",
657
+ borderRadius: { xs: 0, md: "12px" },
658
+ display: "flex",
659
+ flexDirection: "column",
660
+ overflow: "hidden",
661
+ }}
662
+ >
663
+ {/* Header */}
664
+ <Box
665
+ sx={{
666
+ px: { xs: 1.5, md: 2 },
667
+ py: 1.5,
668
+ borderBottom: "1px solid",
669
+ borderColor: "neutral.outlinedBorder",
670
+ bgcolor: "background.level1",
671
+ display: "flex",
672
+ alignItems: "center",
673
+ justifyContent: "space-between",
674
+ borderRadius: { xs: 0, md: "12px 12px 0 0" },
675
+ flexShrink: 0,
676
+ }}
677
+ >
678
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
679
+ {/* Mobile back button */}
680
+ <IconButton
681
+ size="sm"
682
+ variant="plain"
683
+ onClick={onClose}
684
+ sx={{
685
+ display: { xs: "flex", md: "none" },
686
+ color: colors.closeBtn,
687
+ minWidth: 44,
688
+ minHeight: 44,
689
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
690
+ }}
691
+ >
692
+
693
+ </IconButton>
694
+ <Box
695
+ sx={{
696
+ width: 8,
697
+ height: 10,
698
+ clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
699
+ bgcolor: colors.gold,
700
+ boxShadow: colors.goldGlow,
701
+ display: { xs: "none", md: "block" },
702
+ }}
703
+ />
704
+ <Typography
705
+ sx={{
706
+ fontFamily: "display",
707
+ fontWeight: 600,
708
+ color: colors.gold,
709
+ letterSpacing: "0.03em",
710
+ fontSize: { xs: "0.9rem", md: "1rem" },
711
+ }}
712
+ >
713
+ TASK DETAILS
714
+ </Typography>
715
+ </Box>
716
+ {/* Desktop buttons - hidden on mobile */}
717
+ <Box sx={{ display: { xs: "none", md: "flex" }, alignItems: "center", gap: 0.5 }}>
718
+ {onToggleExpand && (
719
+ <Tooltip title={expanded ? "Collapse panel" : "Expand to full width"} placement="bottom">
720
+ <IconButton
721
+ size="sm"
722
+ variant="plain"
723
+ onClick={onToggleExpand}
724
+ sx={{
725
+ color: colors.closeBtn,
726
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
727
+ }}
728
+ >
729
+ {expanded ? "⊟" : "⊞"}
730
+ </IconButton>
731
+ </Tooltip>
732
+ )}
733
+ <Tooltip title="Close panel" placement="bottom">
734
+ <IconButton
735
+ size="sm"
736
+ variant="plain"
737
+ onClick={onClose}
738
+ sx={{
739
+ color: colors.closeBtn,
740
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
741
+ }}
742
+ >
743
+
744
+ </IconButton>
745
+ </Tooltip>
746
+ </Box>
747
+ </Box>
748
+
749
+ {/* Content */}
750
+ <Box
751
+ sx={{
752
+ flex: 1,
753
+ overflow: "hidden",
754
+ display: "flex",
755
+ flexDirection: expanded ? "row" : "column",
756
+ }}
757
+ >
758
+ {expanded ? (
759
+ <>
760
+ {/* Column 1: Details */}
761
+ <Box
762
+ sx={{
763
+ width: 350,
764
+ flexShrink: 0,
765
+ borderRight: "1px solid",
766
+ borderColor: "neutral.outlinedBorder",
767
+ overflow: "auto",
768
+ }}
769
+ >
770
+ <DetailsSection showProgress={false} />
771
+ </Box>
772
+ {/* Column 2: Progress */}
773
+ <Box
774
+ sx={{
775
+ width: 350,
776
+ flexShrink: 0,
777
+ borderRight: "1px solid",
778
+ borderColor: "neutral.outlinedBorder",
779
+ overflow: "hidden",
780
+ display: "flex",
781
+ flexDirection: "column",
782
+ }}
783
+ >
784
+ <ProgressSection />
785
+ </Box>
786
+ {/* Column 3: Output/Error */}
787
+ <Box
788
+ sx={{
789
+ flex: 1,
790
+ display: "flex",
791
+ flexDirection: "column",
792
+ overflow: "hidden",
793
+ }}
794
+ >
795
+ <OutputSection />
796
+ </Box>
797
+ </>
798
+ ) : (
799
+ <Box sx={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
800
+ <DetailsSection showProgress={true} />
801
+ <CollapsedOutputSection />
802
+ </Box>
803
+ )}
804
+ </Box>
805
+ </Box>
806
+ );
807
+ }