@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,534 @@
1
+ import Box from "@mui/joy/Box";
2
+ import Typography from "@mui/joy/Typography";
3
+ import IconButton from "@mui/joy/IconButton";
4
+ import Divider from "@mui/joy/Divider";
5
+ import Button from "@mui/joy/Button";
6
+ import Tooltip from "@mui/joy/Tooltip";
7
+ import Chip from "@mui/joy/Chip";
8
+ import { useColorScheme } from "@mui/joy/styles";
9
+ import { useAgent, useLogs } from "../hooks/queries";
10
+ import { formatRelativeTime } from "../lib/utils";
11
+ import StatusBadge from "./StatusBadge";
12
+ import type { AgentLog } from "../types/api";
13
+
14
+ interface AgentDetailPanelProps {
15
+ agentId: string;
16
+ onClose: () => void;
17
+ onGoToTasks: () => void;
18
+ expanded?: boolean;
19
+ onToggleExpand?: () => void;
20
+ }
21
+
22
+ export default function AgentDetailPanel({
23
+ agentId,
24
+ onClose,
25
+ onGoToTasks,
26
+ expanded = false,
27
+ onToggleExpand,
28
+ }: AgentDetailPanelProps) {
29
+ const { data: agent, isLoading: agentLoading } = useAgent(agentId);
30
+ const { data: logs, isLoading: logsLoading } = useLogs(20, agentId);
31
+ const { mode } = useColorScheme();
32
+ const isDark = mode === "dark";
33
+
34
+ const colors = {
35
+ amber: isDark ? "#F5A623" : "#D48806",
36
+ gold: isDark ? "#D4A574" : "#8B6914",
37
+ dormant: isDark ? "#6B5344" : "#A89A7C",
38
+ honey: isDark ? "#FFB84D" : "#B87300",
39
+ blue: "#3B82F6",
40
+ warmGray: isDark ? "#C9B896" : "#8B7355",
41
+ tertiary: isDark ? "#8B7355" : "#6B5344",
42
+ closeBtn: isDark ? "#8B7355" : "#5C4A3D",
43
+ closeBtnHover: isDark ? "#FFF8E7" : "#1A130E",
44
+ amberGlow: isDark ? "0 0 8px rgba(245, 166, 35, 0.5)" : "0 0 6px rgba(212, 136, 6, 0.3)",
45
+ amberSoftBg: isDark ? "rgba(245, 166, 35, 0.1)" : "rgba(212, 136, 6, 0.08)",
46
+ amberBorder: isDark ? "rgba(245, 166, 35, 0.3)" : "rgba(212, 136, 6, 0.25)",
47
+ hoverBg: isDark ? "rgba(245, 166, 35, 0.05)" : "rgba(212, 136, 6, 0.05)",
48
+ };
49
+
50
+ const getEventColor = (eventType: string) => {
51
+ switch (eventType) {
52
+ case "agent_joined":
53
+ return colors.amber;
54
+ case "agent_left":
55
+ return colors.dormant;
56
+ case "agent_status_change":
57
+ return colors.honey;
58
+ case "task_created":
59
+ return colors.blue;
60
+ case "task_status_change":
61
+ return colors.gold;
62
+ case "task_progress":
63
+ return colors.warmGray;
64
+ default:
65
+ return colors.tertiary;
66
+ }
67
+ };
68
+
69
+ const formatEventText = (log: AgentLog) => {
70
+ switch (log.eventType) {
71
+ case "agent_joined":
72
+ return "Joined the swarm";
73
+ case "agent_left":
74
+ return "Left the swarm";
75
+ case "agent_status_change":
76
+ return `Updated status to ${log.newValue}`;
77
+ case "task_created":
78
+ return "New task assigned";
79
+ case "task_status_change":
80
+ return `Task ${log.taskId?.slice(0, 8)}: updated status to ${log.newValue}`;
81
+ case "task_progress":
82
+ return `Progress: ${log.newValue}`;
83
+ default:
84
+ return log.eventType;
85
+ }
86
+ };
87
+
88
+ const panelWidth = expanded ? "100%" : 400;
89
+
90
+ const loadingContent = (
91
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
92
+ Loading agent...
93
+ </Typography>
94
+ );
95
+
96
+ const notFoundContent = (
97
+ <Typography sx={{ fontFamily: "code", color: "text.tertiary" }}>
98
+ Agent not found
99
+ </Typography>
100
+ );
101
+
102
+ if (agentLoading || !agent) {
103
+ return (
104
+ <Box
105
+ sx={{
106
+ position: { xs: "fixed", md: "relative" },
107
+ inset: { xs: 0, md: "auto" },
108
+ zIndex: { xs: 1300, md: "auto" },
109
+ width: { xs: "100%", md: panelWidth },
110
+ height: "100%",
111
+ bgcolor: "background.surface",
112
+ border: { xs: "none", md: "1px solid" },
113
+ borderColor: "neutral.outlinedBorder",
114
+ borderRadius: { xs: 0, md: "12px" },
115
+ p: { xs: 2, md: 3 },
116
+ overflow: "auto",
117
+ }}
118
+ >
119
+ {agentLoading ? loadingContent : notFoundContent}
120
+ </Box>
121
+ );
122
+ }
123
+
124
+ const activeTasks = agent.tasks?.filter(
125
+ (t) => t.status === "pending" || t.status === "in_progress"
126
+ ).length || 0;
127
+
128
+ // Info section component
129
+ const InfoSection = () => (
130
+ <Box sx={{ p: { xs: 1.5, md: 2 } }}>
131
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1.5, mb: 2 }}>
132
+ <Box
133
+ sx={{
134
+ width: 12,
135
+ height: 12,
136
+ borderRadius: "50%",
137
+ bgcolor: agent.status === "busy" ? colors.amber : agent.status === "idle" ? colors.gold : colors.dormant,
138
+ boxShadow: agent.status === "busy" ? colors.amberGlow : "none",
139
+ }}
140
+ />
141
+ <Typography
142
+ sx={{
143
+ fontFamily: "code",
144
+ fontWeight: 600,
145
+ fontSize: "1.1rem",
146
+ color: agent.isLead ? colors.amber : "text.primary",
147
+ whiteSpace: "nowrap",
148
+ }}
149
+ >
150
+ {agent.name}
151
+ </Typography>
152
+ {agent.isLead && (
153
+ <Typography
154
+ sx={{
155
+ fontFamily: "code",
156
+ fontSize: "0.6rem",
157
+ color: colors.amber,
158
+ bgcolor: colors.amberSoftBg,
159
+ px: 0.75,
160
+ py: 0.25,
161
+ borderRadius: 1,
162
+ border: `1px solid ${colors.amberBorder}`,
163
+ }}
164
+ >
165
+ LEAD
166
+ </Typography>
167
+ )}
168
+ </Box>
169
+
170
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
171
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
172
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
173
+ Status
174
+ </Typography>
175
+ <StatusBadge status={agent.status} />
176
+ </Box>
177
+
178
+ {agent.role && (
179
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
180
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
181
+ Role
182
+ </Typography>
183
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: "text.secondary" }}>
184
+ {agent.role}
185
+ </Typography>
186
+ </Box>
187
+ )}
188
+
189
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
190
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
191
+ Active Tasks
192
+ </Typography>
193
+ <Typography
194
+ sx={{
195
+ fontFamily: "code",
196
+ fontSize: "0.8rem",
197
+ color: activeTasks > 0 ? colors.amber : "text.secondary",
198
+ }}
199
+ >
200
+ {activeTasks}
201
+ </Typography>
202
+ </Box>
203
+
204
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
205
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
206
+ Total Tasks
207
+ </Typography>
208
+ <Typography sx={{ fontFamily: "code", fontSize: "0.8rem", color: "text.secondary" }}>
209
+ {agent.tasks?.length || 0}
210
+ </Typography>
211
+ </Box>
212
+
213
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
214
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
215
+ Last Update
216
+ </Typography>
217
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
218
+ {new Date(agent.lastUpdatedAt).toLocaleString()}
219
+ </Typography>
220
+ </Box>
221
+
222
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
223
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
224
+ Joined
225
+ </Typography>
226
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
227
+ {new Date(agent.createdAt).toLocaleString()}
228
+ </Typography>
229
+ </Box>
230
+
231
+ <Box>
232
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary", mb: 0.5 }}>
233
+ Capabilities
234
+ </Typography>
235
+ <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
236
+ {agent.capabilities && agent.capabilities?.map((cap) => (
237
+ <Chip
238
+ key={cap}
239
+ size="sm"
240
+ variant="soft"
241
+ sx={{
242
+ fontFamily: "code",
243
+ fontSize: "0.65rem",
244
+ bgcolor: colors.amberSoftBg,
245
+ color: colors.gold,
246
+ border: `1px solid ${colors.amberBorder}`,
247
+ }}
248
+ >
249
+ {cap}
250
+ </Chip>
251
+ ))}
252
+ {!agent.capabilities || agent.capabilities.length === 0 && (
253
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
254
+ No capabilities listed
255
+ </Typography>
256
+ )}
257
+ </Box>
258
+ </Box>
259
+
260
+ {agent.description && (
261
+ <Box>
262
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary", mb: 0.5 }}>
263
+ Description
264
+ </Typography>
265
+ <Box
266
+ sx={{
267
+ bgcolor: "background.level1",
268
+ borderRadius: 1,
269
+ p: 1,
270
+ border: "1px solid",
271
+ borderColor: "neutral.outlinedBorder",
272
+ maxHeight: 450,
273
+ overflow: "auto",
274
+ height: "auto",
275
+ whiteSpace: "pre-wrap",
276
+ wordWrap: "break-word",
277
+ }}
278
+ >
279
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.secondary" }}>
280
+ {agent.description}
281
+ </Typography>
282
+ </Box>
283
+ </Box>
284
+ )}
285
+ </Box>
286
+
287
+ <Button
288
+ variant="outlined"
289
+ size="sm"
290
+ onClick={onGoToTasks}
291
+ sx={{
292
+ mt: 2,
293
+ width: "100%",
294
+ fontFamily: "code",
295
+ fontSize: "0.75rem",
296
+ borderColor: "neutral.outlinedBorder",
297
+ color: "text.secondary",
298
+ "&:hover": {
299
+ borderColor: colors.amber,
300
+ color: colors.amber,
301
+ bgcolor: colors.hoverBg,
302
+ },
303
+ }}
304
+ >
305
+ VIEW TASKS →
306
+ </Button>
307
+ </Box>
308
+ );
309
+
310
+ // Activity section component
311
+ const ActivitySection = () => (
312
+ <>
313
+ <Box sx={{ px: 2, py: 1.5, bgcolor: "background.level1", borderTop: expanded ? "none" : "1px solid", borderColor: "neutral.outlinedBorder" }}>
314
+ <Typography
315
+ sx={{
316
+ fontFamily: "code",
317
+ fontSize: "0.7rem",
318
+ color: "text.tertiary",
319
+ letterSpacing: "0.05em",
320
+ }}
321
+ >
322
+ RECENT ACTIVITY
323
+ </Typography>
324
+ </Box>
325
+
326
+ <Box sx={{ flex: 1, overflow: "auto", p: 2 }}>
327
+ {logsLoading ? (
328
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
329
+ Loading activity...
330
+ </Typography>
331
+ ) : !logs || logs.length === 0 ? (
332
+ <Typography sx={{ fontFamily: "code", fontSize: "0.75rem", color: "text.tertiary" }}>
333
+ No recent activity
334
+ </Typography>
335
+ ) : (
336
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
337
+ {logs.map((log) => (
338
+ <Box
339
+ key={log.id}
340
+ sx={{
341
+ display: "flex",
342
+ gap: 1.5,
343
+ alignItems: "flex-start",
344
+ }}
345
+ >
346
+ <Box
347
+ sx={{
348
+ width: 8,
349
+ height: 8,
350
+ borderRadius: "50%",
351
+ bgcolor: getEventColor(log.eventType),
352
+ flexShrink: 0,
353
+ mt: 0.5,
354
+ }}
355
+ />
356
+ <Box sx={{ flex: 1, minWidth: 0 }}>
357
+ <Typography
358
+ sx={{
359
+ fontFamily: "code",
360
+ fontSize: "0.7rem",
361
+ color: "text.primary",
362
+ mb: 0.25,
363
+ }}
364
+ >
365
+ {formatEventText(log)}
366
+ </Typography>
367
+ <Typography
368
+ sx={{
369
+ fontFamily: "code",
370
+ fontSize: "0.6rem",
371
+ color: "text.tertiary",
372
+ }}
373
+ >
374
+ {formatRelativeTime(log.createdAt)}
375
+ </Typography>
376
+ </Box>
377
+ </Box>
378
+ ))}
379
+ </Box>
380
+ )}
381
+ </Box>
382
+ </>
383
+ );
384
+
385
+ return (
386
+ <Box
387
+ sx={{
388
+ position: { xs: "fixed", md: "relative" },
389
+ inset: { xs: 0, md: "auto" },
390
+ zIndex: { xs: 1300, md: "auto" },
391
+ width: { xs: "100%", md: expanded ? "100%" : 400 },
392
+ height: { xs: "100%", md: "100%" },
393
+ bgcolor: "background.surface",
394
+ border: { xs: "none", md: "1px solid" },
395
+ borderColor: "neutral.outlinedBorder",
396
+ borderRadius: { xs: 0, md: "12px" },
397
+ display: "flex",
398
+ flexDirection: "column",
399
+ overflow: "hidden",
400
+ }}
401
+ >
402
+ {/* Header */}
403
+ <Box
404
+ sx={{
405
+ px: { xs: 1.5, md: 2 },
406
+ py: 1.5,
407
+ borderBottom: "1px solid",
408
+ borderColor: "neutral.outlinedBorder",
409
+ bgcolor: "background.level1",
410
+ display: "flex",
411
+ alignItems: "center",
412
+ justifyContent: "space-between",
413
+ borderRadius: { xs: 0, md: "12px 12px 0 0" },
414
+ flexShrink: 0,
415
+ }}
416
+ >
417
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
418
+ {/* Mobile back button */}
419
+ <IconButton
420
+ size="sm"
421
+ variant="plain"
422
+ onClick={onClose}
423
+ sx={{
424
+ display: { xs: "flex", md: "none" },
425
+ color: colors.closeBtn,
426
+ minWidth: 44,
427
+ minHeight: 44,
428
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
429
+ }}
430
+ >
431
+
432
+ </IconButton>
433
+ <Box
434
+ sx={{
435
+ width: 8,
436
+ height: 10,
437
+ clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
438
+ bgcolor: colors.amber,
439
+ boxShadow: colors.amberGlow,
440
+ display: { xs: "none", md: "block" },
441
+ }}
442
+ />
443
+ <Typography
444
+ sx={{
445
+ fontFamily: "display",
446
+ fontWeight: 600,
447
+ color: colors.amber,
448
+ letterSpacing: "0.03em",
449
+ fontSize: { xs: "0.9rem", md: "1rem" },
450
+ }}
451
+ >
452
+ AGENT DETAILS
453
+ </Typography>
454
+ </Box>
455
+ {/* Desktop buttons - hidden on mobile */}
456
+ <Box sx={{ display: { xs: "none", md: "flex" }, alignItems: "center", gap: 0.5 }}>
457
+ {onToggleExpand && (
458
+ <Tooltip title={expanded ? "Collapse panel" : "Expand to full width"} placement="bottom">
459
+ <IconButton
460
+ size="sm"
461
+ variant="plain"
462
+ onClick={onToggleExpand}
463
+ sx={{
464
+ color: colors.closeBtn,
465
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
466
+ }}
467
+ >
468
+ {expanded ? "⊟" : "⊞"}
469
+ </IconButton>
470
+ </Tooltip>
471
+ )}
472
+ <Tooltip title="Close panel" placement="bottom">
473
+ <IconButton
474
+ size="sm"
475
+ variant="plain"
476
+ onClick={onClose}
477
+ sx={{
478
+ color: colors.closeBtn,
479
+ "&:hover": { color: colors.closeBtnHover, bgcolor: colors.hoverBg },
480
+ }}
481
+ >
482
+
483
+ </IconButton>
484
+ </Tooltip>
485
+ </Box>
486
+ </Box>
487
+
488
+ {/* Content - horizontal in expanded, vertical otherwise */}
489
+ <Box
490
+ sx={{
491
+ flex: 1,
492
+ overflow: "auto",
493
+ display: "flex",
494
+ flexDirection: expanded ? "row" : "column",
495
+ }}
496
+ >
497
+ {expanded ? (
498
+ <>
499
+ {/* Left side - Info */}
500
+ <Box
501
+ sx={{
502
+ minWidth: 220,
503
+ maxWidth: 500,
504
+ flexShrink: 0,
505
+ borderRight: "1px solid",
506
+ borderColor: "neutral.outlinedBorder",
507
+ overflow: "auto",
508
+ }}
509
+ >
510
+ <InfoSection />
511
+ </Box>
512
+ {/* Right side - Activity */}
513
+ <Box
514
+ sx={{
515
+ flex: 1,
516
+ display: "flex",
517
+ flexDirection: "column",
518
+ overflow: "hidden",
519
+ }}
520
+ >
521
+ <ActivitySection />
522
+ </Box>
523
+ </>
524
+ ) : (
525
+ <>
526
+ <InfoSection />
527
+ <Divider sx={{ bgcolor: "neutral.outlinedBorder" }} />
528
+ <ActivitySection />
529
+ </>
530
+ )}
531
+ </Box>
532
+ </Box>
533
+ );
534
+ }