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
package/app/page.tsx ADDED
@@ -0,0 +1,817 @@
1
+ "use client"
2
+ import { useEffect, useState, useRef, useCallback } from "react"
3
+ import { useRouter } from "next/navigation"
4
+ import Link from "next/link"
5
+ import dynamic from "next/dynamic"
6
+ import type { Agent, CronJob } from "@/lib/types"
7
+ import { Skeleton } from "@/components/ui/skeleton"
8
+ import { Map as MapIcon, LayoutGrid, List, X, MessageSquare, User } from "lucide-react"
9
+ import { ErrorState } from "@/components/ErrorState"
10
+ import { AgentAvatar } from "@/components/AgentAvatar"
11
+ import { GridView } from "@/components/GridView"
12
+ import { FeedView } from "@/components/FeedView"
13
+
14
+ const OrgMap = dynamic(
15
+ () => import("@/components/ManorMap").then((m) => ({ default: m.OrgMap })),
16
+ {
17
+ ssr: false,
18
+ loading: () => (
19
+ <div className="flex items-center justify-center h-full">
20
+ <div className="flex flex-col items-center gap-3">
21
+ <Skeleton width={240} height={12} />
22
+ <Skeleton width={180} height={12} />
23
+ <Skeleton width={200} height={12} />
24
+ </div>
25
+ </div>
26
+ ),
27
+ },
28
+ )
29
+
30
+ const TOOL_ICONS: Record<string, string> = {
31
+ web_search: "\uD83D\uDD0D",
32
+ read: "\uD83D\uDCC1",
33
+ write: "\u270F\uFE0F",
34
+ exec: "\uD83D\uDCBB",
35
+ web_fetch: "\uD83C\uDF10",
36
+ message: "\uD83D\uDD14",
37
+ tts: "\uD83D\uDCAC",
38
+ edit: "\u2702\uFE0F",
39
+ sessions_spawn: "\uD83D\uDD04",
40
+ memory_search: "\uD83E\udDE0",
41
+ }
42
+
43
+ function StatusDot({ status }: { status: CronJob["status"] }) {
44
+ return (
45
+ <span
46
+ className={status === "error" ? "animate-error-pulse" : ""}
47
+ style={{
48
+ display: "inline-block",
49
+ width: 6,
50
+ height: 6,
51
+ borderRadius: "50%",
52
+ flexShrink: 0,
53
+ background:
54
+ status === "ok"
55
+ ? "var(--system-green)"
56
+ : status === "error"
57
+ ? "var(--system-red)"
58
+ : "var(--text-tertiary)",
59
+ }}
60
+ />
61
+ )
62
+ }
63
+
64
+ /* ──────────────────────────────────────────────
65
+ Loading skeleton for the map area
66
+ ────────────────────────────────────────────── */
67
+ function MapSkeleton() {
68
+ return (
69
+ <div
70
+ className="flex flex-col items-center justify-center h-full gap-6"
71
+ style={{ padding: "var(--space-8)" }}
72
+ >
73
+ {/* Fake root node */}
74
+ <Skeleton width={160} height={80} style={{ borderRadius: "var(--radius-md)" }} />
75
+ {/* Fake second row */}
76
+ <div className="flex gap-6">
77
+ {[1, 2, 3, 4].map((i) => (
78
+ <Skeleton
79
+ key={i}
80
+ width={140}
81
+ height={72}
82
+ style={{ borderRadius: "var(--radius-md)" }}
83
+ />
84
+ ))}
85
+ </div>
86
+ {/* Fake third row */}
87
+ <div className="flex gap-6">
88
+ {[1, 2, 3, 4, 5].map((i) => (
89
+ <Skeleton
90
+ key={i}
91
+ width={130}
92
+ height={64}
93
+ style={{ borderRadius: "var(--radius-md)" }}
94
+ />
95
+ ))}
96
+ </div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ type View = "map" | "grid" | "feed"
102
+
103
+ const VIEW_ICONS: Record<View, React.ComponentType<{ size: number }>> = {
104
+ map: MapIcon,
105
+ grid: LayoutGrid,
106
+ feed: List,
107
+ }
108
+
109
+ const VIEW_OPTIONS: { key: View; label: string }[] = [
110
+ { key: "map", label: "Map" },
111
+ { key: "grid", label: "Grid" },
112
+ { key: "feed", label: "Feed" },
113
+ ]
114
+
115
+ /* ──────────────────────────────────────────────
116
+ Main page
117
+ ────────────────────────────────────────────── */
118
+ export default function HomePage() {
119
+ const router = useRouter()
120
+ const [agents, setAgents] = useState<Agent[]>([])
121
+ const [crons, setCrons] = useState<CronJob[]>([])
122
+ const [selected, setSelected] = useState<Agent | null>(null)
123
+ const [loading, setLoading] = useState(true)
124
+ const [error, setError] = useState<string | null>(null)
125
+ const [view, setView] = useState<View>("map")
126
+ const closeRef = useRef<HTMLButtonElement>(null)
127
+
128
+ const loadData = useCallback(() => {
129
+ setLoading(true)
130
+ setError(null)
131
+ Promise.all([
132
+ fetch("/api/agents").then((r) => {
133
+ if (!r.ok) throw new Error("Failed to fetch agents")
134
+ return r.json()
135
+ }),
136
+ fetch("/api/crons").then((r) => {
137
+ if (!r.ok) throw new Error("Failed to fetch crons")
138
+ return r.json()
139
+ }),
140
+ ])
141
+ .then(([a, c]) => {
142
+ setAgents(a)
143
+ setCrons(c)
144
+ })
145
+ .catch((e) => setError(e.message))
146
+ .finally(() => setLoading(false))
147
+ }, [])
148
+
149
+ useEffect(() => {
150
+ loadData()
151
+ }, [loadData])
152
+
153
+ // Focus close button when panel opens
154
+ useEffect(() => {
155
+ if (selected && closeRef.current) {
156
+ closeRef.current.focus()
157
+ }
158
+ }, [selected])
159
+
160
+ // Keyboard: ESC closes panel
161
+ useEffect(() => {
162
+ function handleKeyDown(e: KeyboardEvent) {
163
+ if (e.key === "Escape" && selected) {
164
+ setSelected(null)
165
+ }
166
+ }
167
+ window.addEventListener("keydown", handleKeyDown)
168
+ return () => window.removeEventListener("keydown", handleKeyDown)
169
+ }, [selected])
170
+
171
+ const agentCrons = selected ? crons.filter((c) => c.agentId === selected.id) : []
172
+
173
+ // Find hierarchy info for the detail panel
174
+ const parentAgent = selected?.reportsTo
175
+ ? agents.find((a) => a.id === selected.reportsTo)
176
+ : null
177
+ const childAgents = selected
178
+ ? selected.directReports
179
+ .map((cid) => agents.find((a) => a.id === cid))
180
+ .filter(Boolean) as Agent[]
181
+ : []
182
+
183
+ if (error) {
184
+ return <ErrorState message={error} onRetry={loadData} />
185
+ }
186
+
187
+ return (
188
+ <div className="flex h-full relative" style={{ background: "var(--bg)" }}>
189
+ {/* ── Main content area ── */}
190
+ <div className="flex-1 h-full relative">
191
+ {loading ? (
192
+ <MapSkeleton />
193
+ ) : view === "map" ? (
194
+ <OrgMap
195
+ agents={agents}
196
+ crons={crons}
197
+ selectedId={selected?.id ?? null}
198
+ onNodeClick={setSelected}
199
+ />
200
+ ) : view === "grid" ? (
201
+ <GridView
202
+ agents={agents}
203
+ crons={crons}
204
+ selectedId={selected?.id ?? null}
205
+ onSelect={setSelected}
206
+ />
207
+ ) : (
208
+ <FeedView
209
+ agents={agents}
210
+ crons={crons}
211
+ selectedId={selected?.id ?? null}
212
+ onSelect={setSelected}
213
+ />
214
+ )}
215
+
216
+ {/* View switcher -- top left */}
217
+ <div
218
+ className="hidden md:flex"
219
+ style={{
220
+ position: "absolute",
221
+ top: "var(--space-4)",
222
+ left: "var(--space-4)",
223
+ zIndex: 10,
224
+ display: "flex",
225
+ alignItems: "center",
226
+ gap: 2,
227
+ padding: 3,
228
+ borderRadius: "var(--radius-sm)",
229
+ background: "var(--material-regular)",
230
+ backdropFilter: "blur(20px)",
231
+ WebkitBackdropFilter: "blur(20px)",
232
+ border: "1px solid var(--separator)",
233
+ }}
234
+ >
235
+ {VIEW_OPTIONS.map((opt) => {
236
+ const isActive = view === opt.key
237
+ const ViewIcon = VIEW_ICONS[opt.key]
238
+ return (
239
+ <button
240
+ key={opt.key}
241
+ onClick={() => setView(opt.key)}
242
+ className="focus-ring"
243
+ aria-label={`${opt.label} view`}
244
+ aria-pressed={isActive}
245
+ style={{
246
+ padding: "5px 14px",
247
+ borderRadius: "var(--radius-sm)",
248
+ fontSize: "var(--text-caption1)",
249
+ fontWeight: "var(--weight-medium)",
250
+ border: "none",
251
+ cursor: "pointer",
252
+ transition: "all 200ms var(--ease-smooth)",
253
+ display: "inline-flex",
254
+ alignItems: "center",
255
+ gap: 5,
256
+ ...(isActive
257
+ ? {
258
+ background: "var(--accent-fill)",
259
+ color: "var(--accent)",
260
+ boxShadow: "0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent)",
261
+ }
262
+ : {
263
+ background: "transparent",
264
+ color: "var(--text-secondary)",
265
+ }),
266
+ }}
267
+ >
268
+ <ViewIcon size={14} />
269
+ {opt.label}
270
+ </button>
271
+ )
272
+ })}
273
+ </div>
274
+
275
+ {/* Legend -- top right (map view only) */}
276
+ {view === "map" && (
277
+ <div
278
+ className="hidden md:flex"
279
+ style={{
280
+ position: "absolute",
281
+ top: "var(--space-4)",
282
+ right: "var(--space-4)",
283
+ zIndex: 10,
284
+ display: "flex",
285
+ alignItems: "center",
286
+ gap: "var(--space-4)",
287
+ padding: "var(--space-2) var(--space-3)",
288
+ borderRadius: "var(--radius-sm)",
289
+ background: "var(--material-regular)",
290
+ backdropFilter: "blur(20px)",
291
+ WebkitBackdropFilter: "blur(20px)",
292
+ border: "1px solid var(--separator)",
293
+ fontSize: "var(--text-caption2)",
294
+ color: "var(--text-tertiary)",
295
+ }}
296
+ >
297
+ <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
298
+ <span
299
+ style={{
300
+ width: 6,
301
+ height: 6,
302
+ borderRadius: "50%",
303
+ background: "var(--system-green)",
304
+ display: "inline-block",
305
+ }}
306
+ />
307
+ Healthy
308
+ </span>
309
+ <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
310
+ <span
311
+ style={{
312
+ width: 6,
313
+ height: 6,
314
+ borderRadius: "50%",
315
+ background: "var(--system-red)",
316
+ display: "inline-block",
317
+ }}
318
+ />
319
+ Errors
320
+ </span>
321
+ <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
322
+ <span
323
+ style={{
324
+ width: 6,
325
+ height: 6,
326
+ borderRadius: "50%",
327
+ background: "var(--text-tertiary)",
328
+ display: "inline-block",
329
+ }}
330
+ />
331
+ No crons
332
+ </span>
333
+ </div>
334
+ )}
335
+ </div>
336
+
337
+ {/* ── Mobile backdrop ── */}
338
+ {selected && (
339
+ <div
340
+ className="fixed inset-0 z-30 md:hidden backdrop-fade"
341
+ style={{ background: "rgba(0,0,0,0.5)" }}
342
+ onClick={() => setSelected(null)}
343
+ />
344
+ )}
345
+
346
+ {/* ── Detail panel ── */}
347
+ {selected && (
348
+ <div
349
+ className="panel-slide-in"
350
+ style={{
351
+ position: "absolute",
352
+ top: 0,
353
+ right: 0,
354
+ bottom: 0,
355
+ zIndex: 30,
356
+ }}
357
+ >
358
+ <div
359
+ className="h-full flex flex-col"
360
+ style={{
361
+ width: 380,
362
+ maxWidth: "100vw",
363
+ flexShrink: 0,
364
+ overflowY: "auto",
365
+ background: "var(--bg)",
366
+ boxShadow: "var(--shadow-overlay)",
367
+ }}
368
+ >
369
+ {/* ── Toolbar row ── */}
370
+ <div
371
+ style={{
372
+ position: "sticky",
373
+ top: 0,
374
+ zIndex: 10,
375
+ display: "flex",
376
+ alignItems: "center",
377
+ justifyContent: "flex-end",
378
+ padding: "var(--space-3) var(--space-4)",
379
+ background: "var(--bg)",
380
+ }}
381
+ >
382
+ <button
383
+ ref={closeRef}
384
+ onClick={() => setSelected(null)}
385
+ className="focus-ring"
386
+ aria-label="Close detail panel"
387
+ style={{
388
+ width: 30,
389
+ height: 30,
390
+ borderRadius: "50%",
391
+ display: "flex",
392
+ alignItems: "center",
393
+ justifyContent: "center",
394
+ background: "var(--fill-tertiary)",
395
+ color: "var(--text-secondary)",
396
+ border: "none",
397
+ cursor: "pointer",
398
+ transition: "all 150ms var(--ease-spring)",
399
+ }}
400
+ >
401
+ <X size={14} />
402
+ </button>
403
+ </div>
404
+
405
+ {/* ── Hero header ── */}
406
+ <div
407
+ style={{
408
+ display: "flex",
409
+ flexDirection: "column",
410
+ alignItems: "center",
411
+ textAlign: "center",
412
+ padding: "0 var(--space-6) var(--space-6)",
413
+ }}
414
+ >
415
+ {/* Agent avatar */}
416
+ <AgentAvatar
417
+ agent={selected}
418
+ size={72}
419
+ borderRadius={20}
420
+ style={{
421
+ border: `1px solid ${selected.color}40`,
422
+ marginBottom: "var(--space-3)",
423
+ boxShadow: `0 4px 20px ${selected.color}18`,
424
+ }}
425
+ />
426
+
427
+ <h2
428
+ style={{
429
+ fontSize: "var(--text-title2)",
430
+ fontWeight: "var(--weight-bold)",
431
+ letterSpacing: "var(--tracking-tight)",
432
+ color: "var(--text-primary)",
433
+ margin: 0,
434
+ lineHeight: "var(--leading-tight)",
435
+ }}
436
+ >
437
+ {selected.name}
438
+ </h2>
439
+
440
+ <p
441
+ style={{
442
+ fontSize: "var(--text-subheadline)",
443
+ fontWeight: "var(--weight-regular)",
444
+ color: selected.color,
445
+ margin: "2px 0 0",
446
+ opacity: 0.85,
447
+ }}
448
+ >
449
+ {selected.title}
450
+ </p>
451
+
452
+ {selected.description && (
453
+ <p
454
+ style={{
455
+ fontSize: "var(--text-footnote)",
456
+ lineHeight: "var(--leading-normal)",
457
+ color: "var(--text-secondary)",
458
+ margin: "var(--space-2) 0 0",
459
+ maxWidth: 280,
460
+ }}
461
+ >
462
+ {selected.description}
463
+ </p>
464
+ )}
465
+
466
+ {/* Quick action buttons */}
467
+ <div
468
+ style={{
469
+ display: "flex",
470
+ gap: "var(--space-2)",
471
+ marginTop: "var(--space-4)",
472
+ width: "100%",
473
+ maxWidth: 280,
474
+ }}
475
+ >
476
+ <button
477
+ onClick={() => router.push(`/chat/${selected.id}`)}
478
+ className="focus-ring btn-scale"
479
+ aria-label={`Open chat with ${selected.name}`}
480
+ style={{
481
+ flex: 1,
482
+ display: "flex",
483
+ alignItems: "center",
484
+ justifyContent: "center",
485
+ gap: "var(--space-2)",
486
+ padding: "var(--space-2) var(--space-3)",
487
+ borderRadius: "var(--radius-md)",
488
+ background: "var(--accent)",
489
+ color: "var(--accent-contrast)",
490
+ border: "none",
491
+ cursor: "pointer",
492
+ fontSize: "var(--text-subheadline)",
493
+ fontWeight: "var(--weight-semibold)",
494
+ transition: "all 150ms var(--ease-spring)",
495
+ }}
496
+ >
497
+ <MessageSquare size={16} />
498
+ Message
499
+ </button>
500
+ <Link
501
+ href={`/agents/${selected.id}`}
502
+ className="focus-ring btn-scale"
503
+ aria-label={`View full profile of ${selected.name}`}
504
+ style={{
505
+ flex: 1,
506
+ display: "flex",
507
+ alignItems: "center",
508
+ justifyContent: "center",
509
+ gap: "var(--space-2)",
510
+ padding: "var(--space-2) var(--space-3)",
511
+ borderRadius: "var(--radius-md)",
512
+ background: "var(--fill-tertiary)",
513
+ color: "var(--text-primary)",
514
+ border: "none",
515
+ cursor: "pointer",
516
+ fontSize: "var(--text-subheadline)",
517
+ fontWeight: "var(--weight-semibold)",
518
+ textDecoration: "none",
519
+ transition: "all 150ms var(--ease-spring)",
520
+ }}
521
+ >
522
+ <User size={16} />
523
+ Profile
524
+ </Link>
525
+ </div>
526
+ </div>
527
+
528
+ {/* ── Grouped sections ── */}
529
+ <div
530
+ style={{
531
+ display: "flex",
532
+ flexDirection: "column",
533
+ gap: "var(--space-8)",
534
+ padding: "var(--space-2) var(--space-4) var(--space-8)",
535
+ }}
536
+ >
537
+ {/* TOOLS */}
538
+ <div>
539
+ <div
540
+ style={{
541
+ fontSize: "var(--text-caption1)",
542
+ fontWeight: "var(--weight-semibold)",
543
+ letterSpacing: "var(--tracking-wide)",
544
+ textTransform: "uppercase",
545
+ color: "var(--text-tertiary)",
546
+ padding: "0 var(--space-4) var(--space-2)",
547
+ }}
548
+ >
549
+ Capabilities
550
+ </div>
551
+ <div
552
+ style={{
553
+ background: "var(--material-regular)",
554
+ borderRadius: "var(--radius-md)",
555
+ border: "1px solid var(--separator)",
556
+ overflow: "hidden",
557
+ }}
558
+ >
559
+ {selected.tools.map((t, idx) => (
560
+ <div
561
+ key={t}
562
+ style={{
563
+ display: "flex",
564
+ alignItems: "center",
565
+ gap: "var(--space-3)",
566
+ padding: "var(--space-3) var(--space-4)",
567
+ borderTop: idx > 0 ? "1px solid var(--separator)" : undefined,
568
+ }}
569
+ >
570
+ <span
571
+ style={{
572
+ width: 28,
573
+ height: 28,
574
+ borderRadius: 7,
575
+ background: "var(--fill-tertiary)",
576
+ display: "flex",
577
+ alignItems: "center",
578
+ justifyContent: "center",
579
+ fontSize: "var(--text-footnote)",
580
+ flexShrink: 0,
581
+ }}
582
+ >
583
+ {TOOL_ICONS[t] || "\u2699\uFE0F"}
584
+ </span>
585
+ <span
586
+ style={{
587
+ fontSize: "var(--text-body)",
588
+ color: "var(--text-primary)",
589
+ flex: 1,
590
+ }}
591
+ >
592
+ {t.replace(/_/g, " ")}
593
+ </span>
594
+ </div>
595
+ ))}
596
+ </div>
597
+ </div>
598
+
599
+ {/* HIERARCHY */}
600
+ {(parentAgent || childAgents.length > 0) && (
601
+ <div>
602
+ <div
603
+ style={{
604
+ fontSize: "var(--text-caption1)",
605
+ fontWeight: "var(--weight-semibold)",
606
+ letterSpacing: "var(--tracking-wide)",
607
+ textTransform: "uppercase",
608
+ color: "var(--text-tertiary)",
609
+ padding: "0 var(--space-4) var(--space-2)",
610
+ }}
611
+ >
612
+ Organization
613
+ </div>
614
+ <div
615
+ style={{
616
+ background: "var(--material-regular)",
617
+ borderRadius: "var(--radius-md)",
618
+ border: "1px solid var(--separator)",
619
+ overflow: "hidden",
620
+ }}
621
+ >
622
+ {parentAgent && (
623
+ <button
624
+ className="focus-ring"
625
+ aria-label={`Select ${parentAgent.name}`}
626
+ onClick={() => setSelected(parentAgent)}
627
+ style={{
628
+ display: "flex",
629
+ alignItems: "center",
630
+ gap: "var(--space-3)",
631
+ padding: "var(--space-3) var(--space-4)",
632
+ width: "100%",
633
+ background: "none",
634
+ border: "none",
635
+ cursor: "pointer",
636
+ textAlign: "left",
637
+ }}
638
+ >
639
+ <AgentAvatar agent={parentAgent} size={32} borderRadius={9} />
640
+ <div style={{ flex: 1, minWidth: 0 }}>
641
+ <div
642
+ style={{
643
+ fontSize: "var(--text-body)",
644
+ fontWeight: "var(--weight-medium)",
645
+ color: "var(--text-primary)",
646
+ whiteSpace: "nowrap",
647
+ overflow: "hidden",
648
+ textOverflow: "ellipsis",
649
+ }}
650
+ >
651
+ {parentAgent.name}
652
+ </div>
653
+ <div
654
+ style={{
655
+ fontSize: "var(--text-caption1)",
656
+ color: "var(--text-tertiary)",
657
+ }}
658
+ >
659
+ Reports to
660
+ </div>
661
+ </div>
662
+ <span
663
+ style={{
664
+ fontSize: "var(--text-body)",
665
+ color: "var(--text-quaternary)",
666
+ flexShrink: 0,
667
+ }}
668
+ >
669
+ &#x203A;
670
+ </span>
671
+ </button>
672
+ )}
673
+ {childAgents.map((c, idx) => (
674
+ <button
675
+ key={c.id}
676
+ className="focus-ring"
677
+ aria-label={`Select ${c.name}`}
678
+ onClick={() => setSelected(c)}
679
+ style={{
680
+ display: "flex",
681
+ alignItems: "center",
682
+ gap: "var(--space-3)",
683
+ padding: "var(--space-3) var(--space-4)",
684
+ width: "100%",
685
+ background: "none",
686
+ border: "none",
687
+ borderTop: (parentAgent || idx > 0) ? "1px solid var(--separator)" : undefined,
688
+ cursor: "pointer",
689
+ textAlign: "left",
690
+ }}
691
+ >
692
+ <AgentAvatar agent={c} size={32} borderRadius={9} />
693
+ <div style={{ flex: 1, minWidth: 0 }}>
694
+ <div
695
+ style={{
696
+ fontSize: "var(--text-body)",
697
+ fontWeight: "var(--weight-medium)",
698
+ color: "var(--text-primary)",
699
+ whiteSpace: "nowrap",
700
+ overflow: "hidden",
701
+ textOverflow: "ellipsis",
702
+ }}
703
+ >
704
+ {c.name}
705
+ </div>
706
+ <div
707
+ style={{
708
+ fontSize: "var(--text-caption1)",
709
+ color: "var(--text-tertiary)",
710
+ }}
711
+ >
712
+ Direct report
713
+ </div>
714
+ </div>
715
+ <span
716
+ style={{
717
+ fontSize: "var(--text-body)",
718
+ color: "var(--text-quaternary)",
719
+ flexShrink: 0,
720
+ }}
721
+ >
722
+ &#x203A;
723
+ </span>
724
+ </button>
725
+ ))}
726
+ </div>
727
+ </div>
728
+ )}
729
+
730
+ {/* CRONS */}
731
+ {agentCrons.length > 0 && (
732
+ <div>
733
+ <div
734
+ style={{
735
+ fontSize: "var(--text-caption1)",
736
+ fontWeight: "var(--weight-semibold)",
737
+ letterSpacing: "var(--tracking-wide)",
738
+ textTransform: "uppercase",
739
+ color: "var(--text-tertiary)",
740
+ padding: "0 var(--space-4) var(--space-2)",
741
+ }}
742
+ >
743
+ Scheduled Tasks
744
+ </div>
745
+ <div
746
+ style={{
747
+ background: "var(--material-regular)",
748
+ borderRadius: "var(--radius-md)",
749
+ border: "1px solid var(--separator)",
750
+ overflow: "hidden",
751
+ }}
752
+ >
753
+ {agentCrons.map((c, idx) => (
754
+ <div
755
+ key={c.id}
756
+ style={{
757
+ display: "flex",
758
+ alignItems: "center",
759
+ gap: "var(--space-3)",
760
+ padding: "var(--space-3) var(--space-4)",
761
+ borderTop: idx > 0 ? "1px solid var(--separator)" : undefined,
762
+ }}
763
+ >
764
+ <StatusDot status={c.status} />
765
+ <div style={{ flex: 1, minWidth: 0 }}>
766
+ <div
767
+ style={{
768
+ fontSize: "var(--text-body)",
769
+ fontWeight: "var(--weight-medium)",
770
+ color: "var(--text-primary)",
771
+ whiteSpace: "nowrap",
772
+ overflow: "hidden",
773
+ textOverflow: "ellipsis",
774
+ }}
775
+ >
776
+ {c.name}
777
+ </div>
778
+ {c.lastError && (
779
+ <div
780
+ style={{
781
+ fontSize: "var(--text-caption1)",
782
+ color: "var(--system-red)",
783
+ whiteSpace: "nowrap",
784
+ overflow: "hidden",
785
+ textOverflow: "ellipsis",
786
+ marginTop: 1,
787
+ }}
788
+ >
789
+ {c.lastError}
790
+ </div>
791
+ )}
792
+ </div>
793
+ <span
794
+ style={{
795
+ fontSize: "var(--text-caption1)",
796
+ fontFamily: "var(--font-mono)",
797
+ color: "var(--text-tertiary)",
798
+ flexShrink: 0,
799
+ background: "var(--fill-quaternary)",
800
+ padding: "2px 6px",
801
+ borderRadius: 4,
802
+ }}
803
+ >
804
+ {c.schedule}
805
+ </span>
806
+ </div>
807
+ ))}
808
+ </div>
809
+ </div>
810
+ )}
811
+ </div>
812
+ </div>
813
+ </div>
814
+ )}
815
+ </div>
816
+ )
817
+ }