clawport-ui 0.1.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 (132) hide show
  1. package/.env.example +35 -0
  2. package/BRANDING.md +131 -0
  3. package/CLAUDE.md +252 -0
  4. package/README.md +262 -0
  5. package/SETUP.md +337 -0
  6. package/app/agents/[id]/page.tsx +727 -0
  7. package/app/api/agents/route.ts +12 -0
  8. package/app/api/chat/[id]/route.ts +139 -0
  9. package/app/api/cron-runs/route.ts +13 -0
  10. package/app/api/crons/route.ts +12 -0
  11. package/app/api/kanban/chat/[id]/route.ts +119 -0
  12. package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
  13. package/app/api/memory/route.ts +12 -0
  14. package/app/api/transcribe/route.ts +37 -0
  15. package/app/api/tts/route.ts +42 -0
  16. package/app/chat/[id]/page.tsx +10 -0
  17. package/app/chat/page.tsx +200 -0
  18. package/app/crons/page.tsx +870 -0
  19. package/app/docs/page.tsx +399 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +692 -0
  22. package/app/kanban/page.tsx +327 -0
  23. package/app/layout.tsx +45 -0
  24. package/app/memory/page.tsx +685 -0
  25. package/app/page.tsx +817 -0
  26. package/app/providers.tsx +37 -0
  27. package/app/settings/page.tsx +901 -0
  28. package/app/settings-provider.tsx +209 -0
  29. package/components/AgentAvatar.tsx +54 -0
  30. package/components/AgentNode.tsx +122 -0
  31. package/components/Breadcrumbs.tsx +126 -0
  32. package/components/DynamicFavicon.tsx +62 -0
  33. package/components/ErrorState.tsx +97 -0
  34. package/components/FeedView.tsx +494 -0
  35. package/components/GlobalSearch.tsx +571 -0
  36. package/components/GridView.tsx +532 -0
  37. package/components/ManorMap.tsx +157 -0
  38. package/components/MobileSidebar.tsx +251 -0
  39. package/components/NavLinks.tsx +271 -0
  40. package/components/OnboardingWizard.tsx +1067 -0
  41. package/components/Sidebar.tsx +115 -0
  42. package/components/ThemeToggle.tsx +108 -0
  43. package/components/chat/AgentList.tsx +537 -0
  44. package/components/chat/ConversationView.tsx +1047 -0
  45. package/components/chat/FileAttachment.tsx +140 -0
  46. package/components/chat/MediaPreview.tsx +111 -0
  47. package/components/chat/VoiceMessage.tsx +139 -0
  48. package/components/crons/PipelineGraph.tsx +327 -0
  49. package/components/crons/WeeklySchedule.tsx +630 -0
  50. package/components/docs/AgentsSection.tsx +209 -0
  51. package/components/docs/ApiReferenceSection.tsx +256 -0
  52. package/components/docs/ArchitectureSection.tsx +221 -0
  53. package/components/docs/ComponentsSection.tsx +253 -0
  54. package/components/docs/CronSystemSection.tsx +235 -0
  55. package/components/docs/DocSection.tsx +346 -0
  56. package/components/docs/GettingStartedSection.tsx +169 -0
  57. package/components/docs/ThemingSection.tsx +257 -0
  58. package/components/docs/TroubleshootingSection.tsx +200 -0
  59. package/components/kanban/AgentPicker.tsx +321 -0
  60. package/components/kanban/CreateTicketModal.tsx +333 -0
  61. package/components/kanban/KanbanBoard.tsx +70 -0
  62. package/components/kanban/KanbanColumn.tsx +166 -0
  63. package/components/kanban/TicketCard.tsx +245 -0
  64. package/components/kanban/TicketDetailPanel.tsx +850 -0
  65. package/components/ui/badge.tsx +48 -0
  66. package/components/ui/button.tsx +64 -0
  67. package/components/ui/card.tsx +92 -0
  68. package/components/ui/dialog.tsx +158 -0
  69. package/components/ui/scroll-area.tsx +58 -0
  70. package/components/ui/separator.tsx +28 -0
  71. package/components/ui/skeleton.tsx +27 -0
  72. package/components/ui/tabs.tsx +91 -0
  73. package/components/ui/tooltip.tsx +57 -0
  74. package/components.json +23 -0
  75. package/docs/API.md +648 -0
  76. package/docs/COMPONENTS.md +1059 -0
  77. package/docs/THEMING.md +795 -0
  78. package/lib/agents-registry.ts +35 -0
  79. package/lib/agents.json +282 -0
  80. package/lib/agents.test.ts +367 -0
  81. package/lib/agents.ts +32 -0
  82. package/lib/anthropic.test.ts +422 -0
  83. package/lib/anthropic.ts +220 -0
  84. package/lib/api-error.ts +16 -0
  85. package/lib/audio-recorder.test.ts +72 -0
  86. package/lib/audio-recorder.ts +169 -0
  87. package/lib/conversations.test.ts +331 -0
  88. package/lib/conversations.ts +117 -0
  89. package/lib/cron-pipelines.test.ts +69 -0
  90. package/lib/cron-pipelines.ts +58 -0
  91. package/lib/cron-runs.test.ts +118 -0
  92. package/lib/cron-runs.ts +67 -0
  93. package/lib/cron-utils.test.ts +222 -0
  94. package/lib/cron-utils.ts +160 -0
  95. package/lib/crons.test.ts +502 -0
  96. package/lib/crons.ts +114 -0
  97. package/lib/env.test.ts +44 -0
  98. package/lib/env.ts +14 -0
  99. package/lib/kanban/automation.test.ts +245 -0
  100. package/lib/kanban/automation.ts +143 -0
  101. package/lib/kanban/chat-store.test.ts +149 -0
  102. package/lib/kanban/chat-store.ts +81 -0
  103. package/lib/kanban/store.test.ts +238 -0
  104. package/lib/kanban/store.ts +98 -0
  105. package/lib/kanban/types.ts +50 -0
  106. package/lib/kanban/useAgentWork.ts +78 -0
  107. package/lib/memory.ts +45 -0
  108. package/lib/multimodal.test.ts +219 -0
  109. package/lib/multimodal.ts +68 -0
  110. package/lib/pipeline.integration.test.ts +343 -0
  111. package/lib/sanitize.ts +194 -0
  112. package/lib/settings.test.ts +137 -0
  113. package/lib/settings.ts +94 -0
  114. package/lib/styles.ts +24 -0
  115. package/lib/themes.ts +9 -0
  116. package/lib/transcribe.test.ts +141 -0
  117. package/lib/transcribe.ts +111 -0
  118. package/lib/types.ts +66 -0
  119. package/lib/utils.ts +6 -0
  120. package/lib/validation.test.ts +132 -0
  121. package/lib/validation.ts +80 -0
  122. package/next.config.ts +7 -0
  123. package/package.json +56 -0
  124. package/postcss.config.mjs +7 -0
  125. package/public/file.svg +1 -0
  126. package/public/globe.svg +1 -0
  127. package/public/next.svg +1 -0
  128. package/public/vercel.svg +1 -0
  129. package/public/window.svg +1 -0
  130. package/scripts/setup.mjs +215 -0
  131. package/tsconfig.json +34 -0
  132. package/vitest.config.ts +17 -0
@@ -0,0 +1,727 @@
1
+ "use client"
2
+ import { useEffect, useState, use, useCallback } from "react"
3
+ import Link from "next/link"
4
+ import { useRouter } from "next/navigation"
5
+ import type { Agent, CronJob } from "@/lib/types"
6
+ import { Skeleton } from "@/components/ui/skeleton"
7
+ import { ErrorState } from "@/components/ErrorState"
8
+ import { AgentAvatar } from "@/components/AgentAvatar"
9
+
10
+ const TOOL_ICONS: Record<string, string> = {
11
+ web_search: "\uD83D\uDD0D",
12
+ read: "\uD83D\uDCC1",
13
+ write: "\u270F\uFE0F",
14
+ exec: "\uD83D\uDCBB",
15
+ web_fetch: "\uD83C\uDF10",
16
+ message: "\uD83D\uDD14",
17
+ tts: "\uD83D\uDCAC",
18
+ edit: "\u2702\uFE0F",
19
+ sessions_spawn: "\uD83D\uDD04",
20
+ memory_search: "\uD83E\udDE0",
21
+ }
22
+
23
+ function StatusDot({ status }: { status: CronJob["status"] }) {
24
+ return (
25
+ <span
26
+ className={status === "error" ? "animate-error-pulse" : ""}
27
+ style={{
28
+ display: "inline-block",
29
+ width: 6,
30
+ height: 6,
31
+ borderRadius: "50%",
32
+ flexShrink: 0,
33
+ background:
34
+ status === "ok"
35
+ ? "var(--system-green)"
36
+ : status === "error"
37
+ ? "var(--system-red)"
38
+ : "var(--text-tertiary)",
39
+ }}
40
+ />
41
+ )
42
+ }
43
+
44
+ function SoulViewer({ content }: { content: string }) {
45
+ const [copied, setCopied] = useState(false)
46
+
47
+ const handleCopy = useCallback(() => {
48
+ navigator.clipboard.writeText(content).then(() => {
49
+ setCopied(true)
50
+ setTimeout(() => setCopied(false), 2000)
51
+ })
52
+ }, [content])
53
+
54
+ return (
55
+ <div
56
+ style={{
57
+ background: "var(--bg)",
58
+ borderRadius: "var(--radius-md)",
59
+ overflow: "hidden",
60
+ position: "relative",
61
+ }}
62
+ >
63
+ <pre
64
+ style={{
65
+ fontFamily: "var(--font-mono)",
66
+ fontSize: "var(--text-caption1)",
67
+ whiteSpace: "pre-wrap",
68
+ lineHeight: 1.6,
69
+ padding: "var(--space-4)",
70
+ color: "var(--text-secondary)",
71
+ margin: 0,
72
+ maxHeight: 400,
73
+ overflowY: "auto",
74
+ }}
75
+ >
76
+ {content}
77
+ </pre>
78
+ <div
79
+ style={{
80
+ display: "flex",
81
+ justifyContent: "flex-end",
82
+ gap: "var(--space-2)",
83
+ padding: "var(--space-2) var(--space-3)",
84
+ borderTop: "1px solid var(--separator)",
85
+ }}
86
+ >
87
+ <button
88
+ onClick={handleCopy}
89
+ className="focus-ring"
90
+ aria-label="Copy SOUL.md content"
91
+ style={{
92
+ background: "var(--fill-tertiary)",
93
+ color: "var(--text-secondary)",
94
+ border: "none",
95
+ borderRadius: "var(--radius-sm)",
96
+ padding: "var(--space-1) var(--space-3)",
97
+ fontSize: "var(--text-caption2)",
98
+ fontWeight: "var(--weight-medium)",
99
+ cursor: "pointer",
100
+ transition: "all 150ms var(--ease-spring)",
101
+ }}
102
+ >
103
+ {copied ? "Copied" : "Copy"}
104
+ </button>
105
+ </div>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ function CopyButton({ text, label }: { text: string; label: string }) {
111
+ const [copied, setCopied] = useState(false)
112
+
113
+ const handleCopy = useCallback(() => {
114
+ navigator.clipboard.writeText(text).then(() => {
115
+ setCopied(true)
116
+ setTimeout(() => setCopied(false), 2000)
117
+ })
118
+ }, [text])
119
+
120
+ return (
121
+ <button
122
+ onClick={handleCopy}
123
+ className="focus-ring"
124
+ aria-label={label}
125
+ style={{
126
+ background: "var(--fill-tertiary)",
127
+ color: "var(--text-secondary)",
128
+ border: "none",
129
+ borderRadius: "var(--radius-sm)",
130
+ padding: "var(--space-1) var(--space-2)",
131
+ fontSize: "var(--text-caption2)",
132
+ fontWeight: "var(--weight-medium)",
133
+ cursor: "pointer",
134
+ transition: "all 150ms var(--ease-spring)",
135
+ flexShrink: 0,
136
+ }}
137
+ >
138
+ {copied ? "Copied" : "Copy"}
139
+ </button>
140
+ )
141
+ }
142
+
143
+ /* ──────────────────────────────────────────────
144
+ Card wrapper with consistent styling
145
+ ────────────────────────────────────────────── */
146
+ function Card({
147
+ children,
148
+ className,
149
+ }: {
150
+ children: React.ReactNode
151
+ className?: string
152
+ }) {
153
+ return (
154
+ <div
155
+ className={className}
156
+ style={{
157
+ background: "var(--material-regular)",
158
+ border: "1px solid var(--separator)",
159
+ borderRadius: "var(--radius-lg)",
160
+ padding: "var(--space-5)",
161
+ boxShadow: "var(--shadow-card)",
162
+ }}
163
+ >
164
+ {children}
165
+ </div>
166
+ )
167
+ }
168
+
169
+ /* ──────────────────────────────────────────────
170
+ Loading skeleton for the detail page
171
+ ────────────────────────────────────────────── */
172
+ function DetailSkeleton() {
173
+ return (
174
+ <div className="h-full overflow-y-auto" style={{ background: "var(--bg)" }}>
175
+ {/* Header skeleton */}
176
+ <div
177
+ className="sticky top-0 z-10 px-6 py-4 flex items-center justify-between"
178
+ style={{
179
+ background: "var(--material-regular)",
180
+ borderBottom: "1px solid var(--separator)",
181
+ }}
182
+ >
183
+ <Skeleton width={80} height={16} />
184
+ <Skeleton width={100} height={36} style={{ borderRadius: "var(--radius-md)" }} />
185
+ </div>
186
+ <div
187
+ style={{
188
+ maxWidth: 720,
189
+ margin: "0 auto",
190
+ padding: "var(--space-8) var(--space-6)",
191
+ display: "flex",
192
+ flexDirection: "column",
193
+ gap: "var(--space-5)",
194
+ }}
195
+ >
196
+ {/* Hero skeleton */}
197
+ <div className="flex items-center gap-4">
198
+ <Skeleton
199
+ width={64}
200
+ height={64}
201
+ style={{ borderRadius: 16 }}
202
+ />
203
+ <div className="flex flex-col gap-2">
204
+ <Skeleton width={140} height={22} />
205
+ <Skeleton width={200} height={14} />
206
+ </div>
207
+ </div>
208
+ {/* Card skeletons */}
209
+ {[1, 2, 3].map((i) => (
210
+ <Skeleton
211
+ key={i}
212
+ height={120}
213
+ style={{
214
+ width: "100%",
215
+ borderRadius: "var(--radius-lg)",
216
+ }}
217
+ />
218
+ ))}
219
+ </div>
220
+ </div>
221
+ )
222
+ }
223
+
224
+ /* ──────────────────────────────────────────────
225
+ Agent Detail Page
226
+ ────────────────────────────────────────────── */
227
+ export default function AgentDetailPage({
228
+ params,
229
+ }: {
230
+ params: Promise<{ id: string }>
231
+ }) {
232
+ const { id } = use(params)
233
+ const router = useRouter()
234
+ const [agent, setAgent] = useState<Agent | null>(null)
235
+ const [allAgents, setAllAgents] = useState<Agent[]>([])
236
+ const [crons, setCrons] = useState<CronJob[]>([])
237
+ const [loading, setLoading] = useState(true)
238
+ const [error, setError] = useState<string | null>(null)
239
+
240
+ const loadData = useCallback(() => {
241
+ setLoading(true)
242
+ setError(null)
243
+ Promise.all([
244
+ fetch("/api/agents").then((r) => {
245
+ if (!r.ok) throw new Error("Failed to fetch agents")
246
+ return r.json()
247
+ }),
248
+ fetch("/api/crons").then((r) => {
249
+ if (!r.ok) throw new Error("Failed to fetch crons")
250
+ return r.json()
251
+ }),
252
+ ])
253
+ .then(([agents, c]) => {
254
+ setAllAgents(agents)
255
+ setAgent(agents.find((a: Agent) => a.id === id) || null)
256
+ setCrons(c.filter((cr: CronJob) => cr.agentId === id))
257
+ })
258
+ .catch((e) => setError(e.message))
259
+ .finally(() => setLoading(false))
260
+ }, [id])
261
+
262
+ useEffect(() => {
263
+ loadData()
264
+ }, [loadData])
265
+
266
+ if (loading) return <DetailSkeleton />
267
+ if (error) return <ErrorState message={error} onRetry={loadData} />
268
+ if (!agent) {
269
+ return (
270
+ <div
271
+ className="flex flex-col items-center justify-center h-full gap-3"
272
+ style={{ background: "var(--bg)" }}
273
+ >
274
+ <div
275
+ style={{
276
+ fontSize: "var(--text-headline)",
277
+ color: "var(--text-secondary)",
278
+ }}
279
+ >
280
+ Agent not found
281
+ </div>
282
+ <Link
283
+ href="/"
284
+ className="focus-ring"
285
+ style={{
286
+ color: "var(--system-blue)",
287
+ fontSize: "var(--text-body)",
288
+ }}
289
+ >
290
+ &larr; Back to Map
291
+ </Link>
292
+ </div>
293
+ )
294
+ }
295
+
296
+ const parent = agent.reportsTo
297
+ ? allAgents.find((a) => a.id === agent.reportsTo)
298
+ : null
299
+ const children = agent.directReports
300
+ .map((cid) => allAgents.find((a) => a.id === cid))
301
+ .filter(Boolean) as Agent[]
302
+
303
+ return (
304
+ <div className="h-full overflow-y-auto" style={{ background: "var(--bg)" }}>
305
+ {/* ── Sticky header ── */}
306
+ <div
307
+ className="sticky top-0 z-10"
308
+ style={{
309
+ background: "var(--material-regular)",
310
+ backdropFilter: "blur(20px) saturate(180%)",
311
+ WebkitBackdropFilter: "blur(20px) saturate(180%)",
312
+ borderBottom: "1px solid var(--separator)",
313
+ }}
314
+ >
315
+ {/* Color strip */}
316
+ <div style={{ height: 3, background: agent.color }} />
317
+
318
+ <div
319
+ className="flex items-center justify-between"
320
+ style={{ padding: "var(--space-3) var(--space-6)" }}
321
+ >
322
+ <Link
323
+ href="/"
324
+ className="focus-ring"
325
+ style={{
326
+ color: "var(--system-blue)",
327
+ fontSize: "var(--text-body)",
328
+ fontWeight: "var(--weight-medium)",
329
+ textDecoration: "none",
330
+ }}
331
+ >
332
+ &larr; Back to Map
333
+ </Link>
334
+ <button
335
+ onClick={() => router.push(`/chat/${agent.id}`)}
336
+ className="focus-ring"
337
+ aria-label={`Open chat with ${agent.name}`}
338
+ style={{
339
+ background: "var(--accent)",
340
+ color: "var(--accent-contrast)",
341
+ border: "none",
342
+ borderRadius: "var(--radius-md)",
343
+ padding: "var(--space-2) var(--space-5)",
344
+ fontSize: "var(--text-body)",
345
+ fontWeight: "var(--weight-semibold)",
346
+ cursor: "pointer",
347
+ transition: "all 150ms var(--ease-spring)",
348
+ }}
349
+ >
350
+ Open Chat &rarr;
351
+ </button>
352
+ </div>
353
+ </div>
354
+
355
+ {/* ── Content ── */}
356
+ <div
357
+ style={{
358
+ maxWidth: 720,
359
+ margin: "0 auto",
360
+ padding: "var(--space-8) var(--space-6)",
361
+ display: "flex",
362
+ flexDirection: "column",
363
+ gap: "var(--space-5)",
364
+ }}
365
+ >
366
+ {/* ── Hero section ── */}
367
+ <div className="flex items-start gap-4">
368
+ <AgentAvatar agent={agent} size={64} borderRadius={16} />
369
+ <div>
370
+ <h1
371
+ style={{
372
+ fontSize: "var(--text-title1)",
373
+ fontWeight: "var(--weight-bold)",
374
+ letterSpacing: "-0.5px",
375
+ color: "var(--text-primary)",
376
+ margin: 0,
377
+ lineHeight: 1.2,
378
+ }}
379
+ >
380
+ {agent.name}
381
+ </h1>
382
+ <p
383
+ style={{
384
+ fontSize: "var(--text-subheadline)",
385
+ color: "var(--text-secondary)",
386
+ margin: "2px 0 0",
387
+ }}
388
+ >
389
+ {agent.title}
390
+ </p>
391
+ {/* Color swatch */}
392
+ <div
393
+ style={{
394
+ display: "inline-block",
395
+ marginTop: "var(--space-2)",
396
+ width: 40,
397
+ height: 3,
398
+ borderRadius: 2,
399
+ background: agent.color,
400
+ }}
401
+ />
402
+ </div>
403
+ </div>
404
+
405
+ {/* ── About card ── */}
406
+ <Card>
407
+ <div className="section-header" style={{ marginBottom: "var(--space-3)" }}>
408
+ About
409
+ </div>
410
+ <p
411
+ style={{
412
+ fontSize: "var(--text-body)",
413
+ lineHeight: 1.65,
414
+ color: "var(--text-secondary)",
415
+ margin: 0,
416
+ }}
417
+ >
418
+ {agent.description}
419
+ </p>
420
+ </Card>
421
+
422
+ {/* ── Two-column: Tools + Hierarchy ── */}
423
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
424
+ {/* Tools card */}
425
+ <Card>
426
+ <div className="section-header" style={{ marginBottom: "var(--space-3)" }}>
427
+ Tools
428
+ </div>
429
+ <div className="flex flex-wrap gap-2">
430
+ {agent.tools.map((t) => (
431
+ <span
432
+ key={t}
433
+ style={{
434
+ display: "inline-flex",
435
+ alignItems: "center",
436
+ gap: 4,
437
+ background: "var(--fill-secondary)",
438
+ borderRadius: 8,
439
+ padding: "6px 12px",
440
+ fontSize: "var(--text-caption1)",
441
+ fontFamily: "var(--font-mono)",
442
+ color: "var(--text-secondary)",
443
+ }}
444
+ >
445
+ {TOOL_ICONS[t] && (
446
+ <span style={{ fontSize: "var(--text-caption2)" }}>
447
+ {TOOL_ICONS[t]}
448
+ </span>
449
+ )}
450
+ {t}
451
+ </span>
452
+ ))}
453
+ </div>
454
+ </Card>
455
+
456
+ {/* Hierarchy card */}
457
+ <Card>
458
+ <div className="section-header" style={{ marginBottom: "var(--space-3)" }}>
459
+ Hierarchy
460
+ </div>
461
+ {parent && (
462
+ <div style={{ marginBottom: "var(--space-3)" }}>
463
+ <div
464
+ style={{
465
+ fontSize: "var(--text-caption2)",
466
+ color: "var(--text-tertiary)",
467
+ marginBottom: 2,
468
+ }}
469
+ >
470
+ Reports to
471
+ </div>
472
+ <Link
473
+ href={`/agents/${parent.id}`}
474
+ className="focus-ring"
475
+ style={{
476
+ display: "inline-flex",
477
+ alignItems: "center",
478
+ gap: "var(--space-2)",
479
+ fontSize: "var(--text-body)",
480
+ fontWeight: "var(--weight-medium)",
481
+ color: "var(--system-blue)",
482
+ textDecoration: "none",
483
+ }}
484
+ >
485
+ <span>{parent.emoji}</span>
486
+ <span>{parent.name}</span>
487
+ <span style={{ color: "var(--text-tertiary)" }}>&rarr;</span>
488
+ </Link>
489
+ </div>
490
+ )}
491
+ {children.length > 0 && (
492
+ <div>
493
+ <div
494
+ style={{
495
+ fontSize: "var(--text-caption2)",
496
+ color: "var(--text-tertiary)",
497
+ marginBottom: 2,
498
+ }}
499
+ >
500
+ Direct reports ({children.length})
501
+ </div>
502
+ <div
503
+ style={{
504
+ display: "flex",
505
+ flexDirection: "column",
506
+ gap: 2,
507
+ }}
508
+ >
509
+ {children.map((c) => (
510
+ <Link
511
+ key={c.id}
512
+ href={`/agents/${c.id}`}
513
+ className="focus-ring"
514
+ style={{
515
+ display: "inline-flex",
516
+ alignItems: "center",
517
+ gap: "var(--space-2)",
518
+ fontSize: "var(--text-body)",
519
+ fontWeight: "var(--weight-medium)",
520
+ color: "var(--system-blue)",
521
+ textDecoration: "none",
522
+ padding: "2px 0",
523
+ }}
524
+ >
525
+ <span>{c.emoji}</span>
526
+ <span>{c.name}</span>
527
+ <span style={{ color: "var(--text-tertiary)" }}>&rarr;</span>
528
+ </Link>
529
+ ))}
530
+ </div>
531
+ </div>
532
+ )}
533
+ {!parent && children.length === 0 && (
534
+ <div
535
+ style={{
536
+ fontSize: "var(--text-footnote)",
537
+ color: "var(--text-tertiary)",
538
+ }}
539
+ >
540
+ No hierarchy connections
541
+ </div>
542
+ )}
543
+ </Card>
544
+ </div>
545
+
546
+ {/* ── SOUL.md card ── */}
547
+ {agent.soul && (
548
+ <Card>
549
+ <div className="section-header" style={{ marginBottom: "var(--space-3)" }}>
550
+ SOUL.md
551
+ </div>
552
+ <SoulViewer content={agent.soul} />
553
+ </Card>
554
+ )}
555
+
556
+ {/* ── Crons card ── */}
557
+ <Card>
558
+ <div
559
+ className="section-header"
560
+ style={{
561
+ marginBottom: "var(--space-3)",
562
+ display: "flex",
563
+ alignItems: "center",
564
+ justifyContent: "space-between",
565
+ }}
566
+ >
567
+ <span>Crons {crons.length > 0 && `(${crons.length})`}</span>
568
+ </div>
569
+ {crons.length === 0 ? (
570
+ <div
571
+ style={{
572
+ fontSize: "var(--text-footnote)",
573
+ color: "var(--text-tertiary)",
574
+ }}
575
+ >
576
+ No crons associated with this agent
577
+ </div>
578
+ ) : (
579
+ <div
580
+ style={{
581
+ borderRadius: "var(--radius-md)",
582
+ overflow: "hidden",
583
+ border: "1px solid var(--separator)",
584
+ }}
585
+ >
586
+ {crons.map((c, idx) => (
587
+ <div
588
+ key={c.id}
589
+ style={{
590
+ display: "flex",
591
+ alignItems: "center",
592
+ gap: "var(--space-2)",
593
+ minHeight: 44,
594
+ padding: "0 var(--space-3)",
595
+ borderTop: idx > 0 ? "1px solid var(--separator)" : undefined,
596
+ background:
597
+ c.status === "error" ? "rgba(255,69,58,0.06)" : undefined,
598
+ }}
599
+ >
600
+ <StatusDot status={c.status} />
601
+ <span
602
+ style={{
603
+ fontSize: "var(--text-body)",
604
+ fontFamily: "var(--font-mono)",
605
+ fontWeight: "var(--weight-medium)",
606
+ color: "var(--text-primary)",
607
+ flex: 1,
608
+ overflow: "hidden",
609
+ textOverflow: "ellipsis",
610
+ whiteSpace: "nowrap",
611
+ }}
612
+ >
613
+ {c.name}
614
+ </span>
615
+ <span
616
+ style={{
617
+ fontSize: "var(--text-caption1)",
618
+ fontFamily: "var(--font-mono)",
619
+ color: "var(--text-tertiary)",
620
+ flexShrink: 0,
621
+ }}
622
+ >
623
+ {c.schedule}
624
+ </span>
625
+ <span
626
+ style={{
627
+ fontSize: "var(--text-caption2)",
628
+ fontWeight: "var(--weight-medium)",
629
+ padding: "2px 8px",
630
+ borderRadius: 20,
631
+ flexShrink: 0,
632
+ background:
633
+ c.status === "ok"
634
+ ? "rgba(48,209,88,0.1)"
635
+ : c.status === "error"
636
+ ? "rgba(255,69,58,0.1)"
637
+ : "rgba(120,120,128,0.1)",
638
+ color:
639
+ c.status === "ok"
640
+ ? "var(--system-green)"
641
+ : c.status === "error"
642
+ ? "var(--system-red)"
643
+ : "var(--text-secondary)",
644
+ }}
645
+ >
646
+ {c.status}
647
+ </span>
648
+ </div>
649
+ ))}
650
+ </div>
651
+ )}
652
+ {crons.length > 0 && (
653
+ <div style={{ textAlign: "right", marginTop: "var(--space-3)" }}>
654
+ <Link
655
+ href="/crons"
656
+ className="focus-ring"
657
+ style={{
658
+ fontSize: "var(--text-footnote)",
659
+ color: "var(--system-blue)",
660
+ textDecoration: "none",
661
+ fontWeight: "var(--weight-medium)",
662
+ }}
663
+ >
664
+ View all crons &rarr;
665
+ </Link>
666
+ </div>
667
+ )}
668
+ </Card>
669
+
670
+ {/* ── Voice card ── */}
671
+ <Card>
672
+ <div className="section-header" style={{ marginBottom: "var(--space-3)" }}>
673
+ Voice
674
+ </div>
675
+ {agent.voiceId ? (
676
+ <div
677
+ style={{
678
+ display: "flex",
679
+ alignItems: "center",
680
+ gap: "var(--space-3)",
681
+ }}
682
+ >
683
+ <span
684
+ style={{
685
+ display: "inline-block",
686
+ padding: "2px 10px",
687
+ borderRadius: 20,
688
+ fontSize: "var(--text-caption1)",
689
+ fontWeight: "var(--weight-medium)",
690
+ background: "rgba(191,90,242,0.1)",
691
+ color: "var(--system-purple)",
692
+ border: "1px solid rgba(191,90,242,0.2)",
693
+ flexShrink: 0,
694
+ }}
695
+ >
696
+ ElevenLabs
697
+ </span>
698
+ <span
699
+ style={{
700
+ fontFamily: "var(--font-mono)",
701
+ fontSize: "var(--text-caption2)",
702
+ color: "var(--text-tertiary)",
703
+ flex: 1,
704
+ overflow: "hidden",
705
+ textOverflow: "ellipsis",
706
+ whiteSpace: "nowrap",
707
+ }}
708
+ >
709
+ {agent.voiceId}
710
+ </span>
711
+ <CopyButton text={agent.voiceId} label="Copy voice ID" />
712
+ </div>
713
+ ) : (
714
+ <div
715
+ style={{
716
+ fontSize: "var(--text-footnote)",
717
+ color: "var(--text-tertiary)",
718
+ }}
719
+ >
720
+ No voice configured
721
+ </div>
722
+ )}
723
+ </Card>
724
+ </div>
725
+ </div>
726
+ )
727
+ }