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,1067 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import { Map, MessageSquare, Columns3, Clock, Brain, Mic, Check, Keyboard, AlertCircle, Loader2, CheckCircle2, XCircle, ArrowLeft, ArrowRight, Rocket, RotateCcw } from 'lucide-react'
5
+ import { useSettings } from '@/app/settings-provider'
6
+ import { useTheme } from '@/app/providers'
7
+ import { THEMES } from '@/lib/themes'
8
+ import type { ThemeId } from '@/lib/themes'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Accent color presets (same as settings page)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const ACCENT_PRESETS = [
15
+ { label: 'Gold', value: '#F5C518' },
16
+ { label: 'Blue', value: '#3B82F6' },
17
+ { label: 'Green', value: '#22C55E' },
18
+ { label: 'Red', value: '#EF4444' },
19
+ { label: 'Orange', value: '#F97316' },
20
+ { label: 'Purple', value: '#A855F7' },
21
+ { label: 'Pink', value: '#EC4899' },
22
+ { label: 'Teal', value: '#14B8A6' },
23
+ { label: 'Cyan', value: '#06B6D4' },
24
+ { label: 'Indigo', value: '#6366F1' },
25
+ { label: 'Rose', value: '#F43F5E' },
26
+ { label: 'Lime', value: '#84CC16' },
27
+ ]
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Feature cards for overview step
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const FEATURES = [
34
+ { icon: Map, name: 'Agent Map', desc: 'Visual org chart of all your AI agents' },
35
+ { icon: MessageSquare, name: 'Chat', desc: 'Direct conversations with any agent' },
36
+ { icon: Columns3, name: 'Kanban', desc: 'Task board for agent work management' },
37
+ { icon: Clock, name: 'Crons', desc: 'Scheduled jobs with status monitoring' },
38
+ { icon: Brain, name: 'Memory', desc: 'Shared context and knowledge files' },
39
+ ]
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function getInitials(name: string): string {
46
+ if (!name.trim()) return '??'
47
+ const parts = name.trim().split(/\s+/)
48
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
49
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Types for system check
54
+ // ---------------------------------------------------------------------------
55
+
56
+ interface SystemCheckAgent {
57
+ id: string
58
+ name: string
59
+ emoji: string
60
+ title: string
61
+ }
62
+
63
+ type CheckStatus = 'loading' | 'ok' | 'error'
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Component
67
+ // ---------------------------------------------------------------------------
68
+
69
+ interface OnboardingWizardProps {
70
+ forceOpen?: boolean
71
+ onClose?: () => void
72
+ }
73
+
74
+ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps) {
75
+ const {
76
+ settings,
77
+ setPortalName,
78
+ setPortalSubtitle,
79
+ setOperatorName,
80
+ setAccentColor,
81
+ } = useSettings()
82
+ const { theme, setTheme } = useTheme()
83
+
84
+ const [visible, setVisible] = useState(false)
85
+ const [step, setStep] = useState(0)
86
+
87
+ // Local input values
88
+ const [localName, setLocalName] = useState('')
89
+ const [localSubtitle, setLocalSubtitle] = useState('')
90
+ const [localOperator, setLocalOperator] = useState('')
91
+
92
+ // System check state
93
+ const [agentsStatus, setAgentsStatus] = useState<CheckStatus>('loading')
94
+ const [cronsStatus, setCronsStatus] = useState<CheckStatus>('loading')
95
+ const [agents, setAgents] = useState<SystemCheckAgent[]>([])
96
+ const [agentsError, setAgentsError] = useState<string | null>(null)
97
+ const [cronsError, setCronsError] = useState<string | null>(null)
98
+
99
+ // First-run detection
100
+ useEffect(() => {
101
+ if (forceOpen) {
102
+ setLocalName(settings.portalName ?? '')
103
+ setLocalSubtitle(settings.portalSubtitle ?? '')
104
+ setLocalOperator(settings.operatorName ?? '')
105
+ setVisible(true)
106
+ return
107
+ }
108
+ if (typeof window !== 'undefined' && !localStorage.getItem('clawport-onboarded')) {
109
+ setVisible(true)
110
+ }
111
+ }, [forceOpen]) // eslint-disable-line react-hooks/exhaustive-deps
112
+
113
+ // Run system checks when we reach the system check step
114
+ useEffect(() => {
115
+ if (visible && step === 1) {
116
+ runSystemChecks()
117
+ }
118
+ }, [visible, step]) // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ function runSystemChecks() {
121
+ setAgentsStatus('loading')
122
+ setCronsStatus('loading')
123
+ setAgentsError(null)
124
+ setCronsError(null)
125
+
126
+ // Check agents
127
+ fetch('/api/agents')
128
+ .then(r => {
129
+ if (!r.ok) throw new Error(`HTTP ${r.status}`)
130
+ return r.json()
131
+ })
132
+ .then((data: unknown) => {
133
+ if (Array.isArray(data) && data.length > 0) {
134
+ setAgents(data.map((a: Record<string, unknown>) => ({
135
+ id: String(a.id ?? ''),
136
+ name: String(a.name ?? ''),
137
+ emoji: String(a.emoji ?? ''),
138
+ title: String(a.title ?? ''),
139
+ })))
140
+ setAgentsStatus('ok')
141
+ } else {
142
+ setAgentsError('No agents found. Check your agents.json or workspace config.')
143
+ setAgentsStatus('error')
144
+ }
145
+ })
146
+ .catch(() => {
147
+ setAgentsError('Could not reach agent registry. Is the server running?')
148
+ setAgentsStatus('error')
149
+ })
150
+
151
+ // Check crons (validates gateway + openclaw binary)
152
+ fetch('/api/crons')
153
+ .then(r => {
154
+ if (!r.ok) throw new Error(`HTTP ${r.status}`)
155
+ return r.json()
156
+ })
157
+ .then(() => {
158
+ setCronsStatus('ok')
159
+ })
160
+ .catch(() => {
161
+ setCronsError('Could not reach OpenClaw gateway. Run: openclaw gateway run')
162
+ setCronsStatus('error')
163
+ })
164
+ }
165
+
166
+ const TOTAL_STEPS = 7
167
+
168
+ const handleNext = useCallback(() => {
169
+ // Commit values on step 2 (naming step)
170
+ if (step === 2) {
171
+ setPortalName(localName || null)
172
+ setPortalSubtitle(localSubtitle || null)
173
+ setOperatorName(localOperator || null)
174
+ }
175
+
176
+ if (step < TOTAL_STEPS - 1) {
177
+ setStep(step + 1)
178
+ } else {
179
+ if (!forceOpen) {
180
+ localStorage.setItem('clawport-onboarded', '1')
181
+ }
182
+ setVisible(false)
183
+ onClose?.()
184
+ }
185
+ }, [step, localName, localSubtitle, localOperator, forceOpen, onClose, setPortalName, setPortalSubtitle, setOperatorName])
186
+
187
+ const handleBack = useCallback(() => {
188
+ if (step > 0) setStep(step - 1)
189
+ }, [step])
190
+
191
+ if (!visible) return null
192
+
193
+ const systemAllOk = agentsStatus === 'ok' && cronsStatus === 'ok'
194
+ const systemLoading = agentsStatus === 'loading' || cronsStatus === 'loading'
195
+
196
+ return (
197
+ <div
198
+ style={{
199
+ position: 'fixed',
200
+ inset: 0,
201
+ zIndex: 9999,
202
+ display: 'flex',
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ background: 'rgba(0,0,0,0.6)',
206
+ backdropFilter: 'blur(12px)',
207
+ WebkitBackdropFilter: 'blur(12px)',
208
+ }}
209
+ >
210
+ <div
211
+ className="animate-fade-in"
212
+ style={{
213
+ width: '100%',
214
+ maxWidth: 520,
215
+ margin: '0 var(--space-4)',
216
+ background: 'var(--material-regular)',
217
+ borderRadius: 'var(--radius-lg)',
218
+ border: '1px solid var(--separator)',
219
+ boxShadow: '0 24px 48px rgba(0,0,0,0.3)',
220
+ overflow: 'hidden',
221
+ display: 'flex',
222
+ flexDirection: 'column',
223
+ maxHeight: '90vh',
224
+ }}
225
+ >
226
+ {/* Step indicator dots */}
227
+ <div style={{
228
+ display: 'flex',
229
+ justifyContent: 'center',
230
+ gap: 8,
231
+ padding: 'var(--space-4) var(--space-4) 0',
232
+ }}>
233
+ {Array.from({ length: TOTAL_STEPS }).map((_, i) => (
234
+ <div
235
+ key={i}
236
+ style={{
237
+ width: i === step ? 24 : 8,
238
+ height: 8,
239
+ borderRadius: 4,
240
+ background: i === step ? 'var(--accent)' : i < step ? 'var(--accent)' : 'var(--fill-tertiary)',
241
+ opacity: i < step ? 0.5 : 1,
242
+ transition: 'all 200ms var(--ease-smooth)',
243
+ }}
244
+ />
245
+ ))}
246
+ </div>
247
+
248
+ {/* Step content */}
249
+ <div style={{
250
+ padding: 'var(--space-5) var(--space-5) var(--space-4)',
251
+ overflowY: 'auto',
252
+ flex: 1,
253
+ }}>
254
+ {/* Step 0: Welcome */}
255
+ {step === 0 && (
256
+ <div key="step-0" className="animate-fade-in" style={{ textAlign: 'center' }}>
257
+ <div style={{
258
+ fontSize: 56,
259
+ marginBottom: 'var(--space-3)',
260
+ lineHeight: 1,
261
+ }}>
262
+ {settings.portalEmoji ?? '\ud83e\udd9e'}
263
+ </div>
264
+ <h2 style={{
265
+ fontSize: 'var(--text-large-title)',
266
+ fontWeight: 'var(--weight-bold)',
267
+ letterSpacing: 'var(--tracking-tight)',
268
+ color: 'var(--text-primary)',
269
+ marginBottom: 'var(--space-2)',
270
+ }}>
271
+ Welcome to ClawPort
272
+ </h2>
273
+ <p style={{
274
+ fontSize: 'var(--text-body)',
275
+ color: 'var(--text-secondary)',
276
+ lineHeight: 'var(--leading-relaxed)',
277
+ maxWidth: 400,
278
+ margin: '0 auto',
279
+ marginBottom: 'var(--space-5)',
280
+ }}>
281
+ A visual command centre for your AI agent team.
282
+ Built to give you direct, real-time access to every agent
283
+ in your OpenClaw workspace.
284
+ </p>
285
+
286
+ <div style={{
287
+ display: 'flex',
288
+ flexDirection: 'column',
289
+ gap: 'var(--space-2)',
290
+ textAlign: 'left',
291
+ }}>
292
+ {[
293
+ { emoji: '🗺️', title: 'Map & Chat', desc: 'Interactive agent org chart with direct messaging' },
294
+ { emoji: '⚡', title: 'Monitor', desc: 'Cron jobs, shared memory, and task management' },
295
+ { emoji: '🎨', title: 'Personalize', desc: 'Five themes, accent colors, and custom branding' },
296
+ ].map(item => (
297
+ <div
298
+ key={item.title}
299
+ style={{
300
+ display: 'flex',
301
+ alignItems: 'center',
302
+ gap: 'var(--space-3)',
303
+ padding: 'var(--space-3) var(--space-4)',
304
+ borderRadius: 'var(--radius-md)',
305
+ background: 'var(--fill-quaternary)',
306
+ border: '1px solid var(--separator)',
307
+ }}
308
+ >
309
+ <span style={{ fontSize: 22, lineHeight: 1, flexShrink: 0 }}>{item.emoji}</span>
310
+ <div>
311
+ <div style={{
312
+ fontSize: 'var(--text-subheadline)',
313
+ fontWeight: 'var(--weight-semibold)',
314
+ color: 'var(--text-primary)',
315
+ }}>
316
+ {item.title}
317
+ </div>
318
+ <div style={{
319
+ fontSize: 'var(--text-caption1)',
320
+ color: 'var(--text-tertiary)',
321
+ lineHeight: 'var(--leading-normal)',
322
+ }}>
323
+ {item.desc}
324
+ </div>
325
+ </div>
326
+ </div>
327
+ ))}
328
+ </div>
329
+
330
+ <p style={{
331
+ fontSize: 'var(--text-caption1)',
332
+ color: 'var(--text-quaternary)',
333
+ marginTop: 'var(--space-4)',
334
+ }}>
335
+ Built by John Rice with Jarvis (OpenClaw AI)
336
+ </p>
337
+ </div>
338
+ )}
339
+
340
+ {/* Step 1: System Check */}
341
+ {step === 1 && (
342
+ <div key="step-1" className="animate-fade-in">
343
+ <h2 style={{
344
+ fontSize: 'var(--text-title1)',
345
+ fontWeight: 'var(--weight-bold)',
346
+ letterSpacing: 'var(--tracking-tight)',
347
+ color: 'var(--text-primary)',
348
+ marginBottom: 'var(--space-1)',
349
+ }}>
350
+ System Check
351
+ </h2>
352
+ <p style={{
353
+ fontSize: 'var(--text-subheadline)',
354
+ color: 'var(--text-tertiary)',
355
+ marginBottom: 'var(--space-4)',
356
+ }}>
357
+ Verifying your OpenClaw connection...
358
+ </p>
359
+
360
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
361
+ {/* Agent registry check */}
362
+ <div style={{
363
+ padding: 'var(--space-3) var(--space-4)',
364
+ borderRadius: 'var(--radius-md)',
365
+ background: 'var(--fill-quaternary)',
366
+ border: `1px solid ${agentsStatus === 'error' ? 'var(--system-red)' : 'var(--separator)'}`,
367
+ display: 'flex',
368
+ alignItems: 'center',
369
+ gap: 'var(--space-3)',
370
+ }}>
371
+ {agentsStatus === 'loading' && <Loader2 size={18} style={{ color: 'var(--text-tertiary)', animation: 'spin 1s linear infinite' }} />}
372
+ {agentsStatus === 'ok' && <CheckCircle2 size={18} style={{ color: 'var(--system-green)' }} />}
373
+ {agentsStatus === 'error' && <XCircle size={18} style={{ color: 'var(--system-red)' }} />}
374
+ <div style={{ flex: 1, minWidth: 0 }}>
375
+ <div style={{
376
+ fontSize: 'var(--text-subheadline)',
377
+ fontWeight: 'var(--weight-medium)',
378
+ color: 'var(--text-primary)',
379
+ }}>
380
+ Agent Registry
381
+ </div>
382
+ {agentsStatus === 'ok' && (
383
+ <div style={{ fontSize: 'var(--text-caption1)', color: 'var(--text-tertiary)' }}>
384
+ {agents.length} agent{agents.length !== 1 ? 's' : ''} found
385
+ </div>
386
+ )}
387
+ {agentsError && (
388
+ <div style={{ fontSize: 'var(--text-caption1)', color: 'var(--system-red)' }}>
389
+ {agentsError}
390
+ </div>
391
+ )}
392
+ </div>
393
+ </div>
394
+
395
+ {/* Gateway check */}
396
+ <div style={{
397
+ padding: 'var(--space-3) var(--space-4)',
398
+ borderRadius: 'var(--radius-md)',
399
+ background: 'var(--fill-quaternary)',
400
+ border: `1px solid ${cronsStatus === 'error' ? 'var(--system-red)' : 'var(--separator)'}`,
401
+ display: 'flex',
402
+ alignItems: 'center',
403
+ gap: 'var(--space-3)',
404
+ }}>
405
+ {cronsStatus === 'loading' && <Loader2 size={18} style={{ color: 'var(--text-tertiary)', animation: 'spin 1s linear infinite' }} />}
406
+ {cronsStatus === 'ok' && <CheckCircle2 size={18} style={{ color: 'var(--system-green)' }} />}
407
+ {cronsStatus === 'error' && <XCircle size={18} style={{ color: 'var(--system-red)' }} />}
408
+ <div style={{ flex: 1, minWidth: 0 }}>
409
+ <div style={{
410
+ fontSize: 'var(--text-subheadline)',
411
+ fontWeight: 'var(--weight-medium)',
412
+ color: 'var(--text-primary)',
413
+ }}>
414
+ OpenClaw Gateway
415
+ </div>
416
+ {cronsStatus === 'ok' && (
417
+ <div style={{ fontSize: 'var(--text-caption1)', color: 'var(--text-tertiary)' }}>
418
+ Connected at localhost:18789
419
+ </div>
420
+ )}
421
+ {cronsError && (
422
+ <div style={{ fontSize: 'var(--text-caption1)', color: 'var(--system-red)' }}>
423
+ {cronsError}
424
+ </div>
425
+ )}
426
+ </div>
427
+ </div>
428
+ </div>
429
+
430
+ {/* Agent roster */}
431
+ {agentsStatus === 'ok' && agents.length > 0 && (
432
+ <div style={{ marginTop: 'var(--space-4)' }}>
433
+ <div style={{
434
+ fontSize: 'var(--text-caption2)',
435
+ color: 'var(--text-quaternary)',
436
+ textTransform: 'uppercase',
437
+ letterSpacing: '0.06em',
438
+ fontWeight: 600,
439
+ marginBottom: 'var(--space-2)',
440
+ }}>
441
+ Your Agent Team
442
+ </div>
443
+ <div style={{
444
+ padding: 'var(--space-2)',
445
+ borderRadius: 'var(--radius-md)',
446
+ background: 'var(--fill-quaternary)',
447
+ border: '1px solid var(--separator)',
448
+ display: 'flex',
449
+ flexWrap: 'wrap',
450
+ gap: 'var(--space-2)',
451
+ }}>
452
+ {agents.map(a => (
453
+ <div
454
+ key={a.id}
455
+ style={{
456
+ display: 'flex',
457
+ alignItems: 'center',
458
+ gap: 6,
459
+ padding: '4px 10px',
460
+ borderRadius: 'var(--radius-sm)',
461
+ background: 'var(--material-thin)',
462
+ border: '1px solid var(--separator)',
463
+ fontSize: 'var(--text-caption1)',
464
+ }}
465
+ >
466
+ <span>{a.emoji}</span>
467
+ <span style={{ color: 'var(--text-primary)', fontWeight: 'var(--weight-medium)' }}>{a.name}</span>
468
+ </div>
469
+ ))}
470
+ </div>
471
+ <p style={{
472
+ fontSize: 'var(--text-caption1)',
473
+ color: 'var(--text-tertiary)',
474
+ marginTop: 'var(--space-2)',
475
+ }}>
476
+ Does this look like your team? If not, check your <code style={{
477
+ fontSize: 'var(--text-caption2)',
478
+ background: 'var(--code-bg)',
479
+ padding: '1px 4px',
480
+ borderRadius: 3,
481
+ color: 'var(--code-text)',
482
+ }}>agents.json</code> configuration.
483
+ </p>
484
+ </div>
485
+ )}
486
+
487
+ {/* Error help */}
488
+ {!systemLoading && !systemAllOk && (
489
+ <div style={{
490
+ marginTop: 'var(--space-4)',
491
+ padding: 'var(--space-3)',
492
+ borderRadius: 'var(--radius-md)',
493
+ background: 'rgba(255,69,58,0.08)',
494
+ border: '1px solid rgba(255,69,58,0.2)',
495
+ display: 'flex',
496
+ alignItems: 'flex-start',
497
+ gap: 'var(--space-3)',
498
+ }}>
499
+ <AlertCircle size={16} style={{ color: 'var(--system-red)', flexShrink: 0, marginTop: 2 }} />
500
+ <div style={{
501
+ fontSize: 'var(--text-caption1)',
502
+ color: 'var(--text-secondary)',
503
+ lineHeight: 'var(--leading-relaxed)',
504
+ }}>
505
+ Run <code style={{
506
+ fontSize: 'var(--text-caption2)',
507
+ background: 'var(--code-bg)',
508
+ padding: '1px 4px',
509
+ borderRadius: 3,
510
+ color: 'var(--code-text)',
511
+ }}>npm run setup</code> in your terminal to auto-detect and configure your environment.
512
+ You can continue setup and fix this later.
513
+ </div>
514
+ </div>
515
+ )}
516
+
517
+ {/* Retry button */}
518
+ {!systemLoading && !systemAllOk && (
519
+ <button
520
+ onClick={runSystemChecks}
521
+ style={{
522
+ marginTop: 'var(--space-3)',
523
+ padding: 'var(--space-2) var(--space-4)',
524
+ borderRadius: 'var(--radius-md)',
525
+ background: 'var(--fill-tertiary)',
526
+ color: 'var(--text-secondary)',
527
+ border: 'none',
528
+ cursor: 'pointer',
529
+ fontSize: 'var(--text-caption1)',
530
+ fontWeight: 'var(--weight-medium)',
531
+ display: 'inline-flex',
532
+ alignItems: 'center',
533
+ gap: 6,
534
+ }}
535
+ >
536
+ <RotateCcw size={16} />
537
+ Retry Checks
538
+ </button>
539
+ )}
540
+ </div>
541
+ )}
542
+
543
+ {/* Step 2: Name Your Dashboard */}
544
+ {step === 2 && (
545
+ <div key="step-2" className="animate-fade-in">
546
+ <h2 style={{
547
+ fontSize: 'var(--text-title1)',
548
+ fontWeight: 'var(--weight-bold)',
549
+ letterSpacing: 'var(--tracking-tight)',
550
+ color: 'var(--text-primary)',
551
+ marginBottom: 'var(--space-1)',
552
+ }}>
553
+ Name Your Dashboard
554
+ </h2>
555
+ <p style={{
556
+ fontSize: 'var(--text-subheadline)',
557
+ color: 'var(--text-tertiary)',
558
+ marginBottom: 'var(--space-5)',
559
+ }}>
560
+ Personalize your command centre.
561
+ </p>
562
+
563
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
564
+ <div>
565
+ <label style={{
566
+ display: 'block',
567
+ fontSize: 'var(--text-caption1)',
568
+ color: 'var(--text-tertiary)',
569
+ marginBottom: 'var(--space-1)',
570
+ }}>
571
+ Dashboard Name
572
+ </label>
573
+ <input
574
+ type="text"
575
+ className="apple-input"
576
+ placeholder="ClawPort"
577
+ value={localName}
578
+ onChange={e => setLocalName(e.target.value)}
579
+ autoFocus
580
+ style={{
581
+ width: '100%',
582
+ background: 'var(--bg-secondary)',
583
+ border: '1px solid var(--separator)',
584
+ }}
585
+ />
586
+ </div>
587
+
588
+ <div>
589
+ <label style={{
590
+ display: 'block',
591
+ fontSize: 'var(--text-caption1)',
592
+ color: 'var(--text-tertiary)',
593
+ marginBottom: 'var(--space-1)',
594
+ }}>
595
+ Subtitle
596
+ </label>
597
+ <input
598
+ type="text"
599
+ className="apple-input"
600
+ placeholder="Command Centre"
601
+ value={localSubtitle}
602
+ onChange={e => setLocalSubtitle(e.target.value)}
603
+ style={{
604
+ width: '100%',
605
+ background: 'var(--bg-secondary)',
606
+ border: '1px solid var(--separator)',
607
+ }}
608
+ />
609
+ </div>
610
+
611
+ <div>
612
+ <label style={{
613
+ display: 'block',
614
+ fontSize: 'var(--text-caption1)',
615
+ color: 'var(--text-tertiary)',
616
+ marginBottom: 'var(--space-1)',
617
+ }}>
618
+ Your Name
619
+ </label>
620
+ <input
621
+ type="text"
622
+ className="apple-input"
623
+ placeholder="Your Name"
624
+ value={localOperator}
625
+ onChange={e => setLocalOperator(e.target.value)}
626
+ style={{
627
+ width: '100%',
628
+ background: 'var(--bg-secondary)',
629
+ border: '1px solid var(--separator)',
630
+ }}
631
+ />
632
+ </div>
633
+ </div>
634
+
635
+ {/* Mini sidebar preview */}
636
+ <div style={{
637
+ marginTop: 'var(--space-4)',
638
+ padding: 'var(--space-3)',
639
+ borderRadius: 'var(--radius-md)',
640
+ background: 'var(--fill-quaternary)',
641
+ border: '1px solid var(--separator)',
642
+ }}>
643
+ <div style={{
644
+ fontSize: 'var(--text-caption2)',
645
+ color: 'var(--text-quaternary)',
646
+ textTransform: 'uppercase',
647
+ letterSpacing: '0.06em',
648
+ fontWeight: 600,
649
+ marginBottom: 'var(--space-2)',
650
+ }}>
651
+ Preview
652
+ </div>
653
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
654
+ <div style={{
655
+ width: 32,
656
+ height: 32,
657
+ borderRadius: 8,
658
+ background: settings.accentColor
659
+ ? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
660
+ : 'linear-gradient(135deg, #f5c518, #e8b800)',
661
+ display: 'flex',
662
+ alignItems: 'center',
663
+ justifyContent: 'center',
664
+ fontSize: 16,
665
+ flexShrink: 0,
666
+ }}>
667
+ {settings.portalEmoji ?? '\ud83e\udd9e'}
668
+ </div>
669
+ <div style={{ flex: 1, minWidth: 0 }}>
670
+ <div style={{
671
+ fontSize: 'var(--text-subheadline)',
672
+ fontWeight: 'var(--weight-bold)',
673
+ color: 'var(--text-primary)',
674
+ letterSpacing: 'var(--tracking-tight)',
675
+ overflow: 'hidden',
676
+ textOverflow: 'ellipsis',
677
+ whiteSpace: 'nowrap',
678
+ }}>
679
+ {localName || 'ClawPort'}
680
+ </div>
681
+ <div style={{
682
+ fontSize: 'var(--text-caption2)',
683
+ color: 'var(--text-tertiary)',
684
+ }}>
685
+ {localSubtitle || 'Command Centre'}
686
+ </div>
687
+ </div>
688
+ <div style={{
689
+ width: 28,
690
+ height: 28,
691
+ borderRadius: 7,
692
+ background: 'var(--accent-fill)',
693
+ display: 'flex',
694
+ alignItems: 'center',
695
+ justifyContent: 'center',
696
+ fontSize: 11,
697
+ fontWeight: 700,
698
+ color: 'var(--accent)',
699
+ flexShrink: 0,
700
+ letterSpacing: '-0.02em',
701
+ }}>
702
+ {getInitials(localOperator)}
703
+ </div>
704
+ </div>
705
+ </div>
706
+ </div>
707
+ )}
708
+
709
+ {/* Step 3: Theme */}
710
+ {step === 3 && (
711
+ <div key="step-3" className="animate-fade-in">
712
+ <h2 style={{
713
+ fontSize: 'var(--text-title2)',
714
+ fontWeight: 'var(--weight-bold)',
715
+ letterSpacing: 'var(--tracking-tight)',
716
+ color: 'var(--text-primary)',
717
+ marginBottom: 'var(--space-1)',
718
+ }}>
719
+ Choose a Theme
720
+ </h2>
721
+ <p style={{
722
+ fontSize: 'var(--text-subheadline)',
723
+ color: 'var(--text-tertiary)',
724
+ marginBottom: 'var(--space-4)',
725
+ }}>
726
+ Pick the look that suits you. This applies live.
727
+ </p>
728
+
729
+ <div style={{
730
+ display: 'grid',
731
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
732
+ gap: 'var(--space-3)',
733
+ }}>
734
+ {THEMES.map(t => {
735
+ const isActive = theme === t.id
736
+ return (
737
+ <button
738
+ key={t.id}
739
+ onClick={() => setTheme(t.id)}
740
+ style={{
741
+ display: 'flex',
742
+ flexDirection: 'column',
743
+ alignItems: 'center',
744
+ gap: 'var(--space-2)',
745
+ padding: 'var(--space-4) var(--space-3)',
746
+ borderRadius: 'var(--radius-md)',
747
+ background: 'var(--fill-quaternary)',
748
+ border: isActive ? '2px solid var(--accent)' : '2px solid var(--separator)',
749
+ cursor: 'pointer',
750
+ transition: 'all 150ms var(--ease-smooth)',
751
+ }}
752
+ >
753
+ <span style={{ fontSize: 28 }}>{t.emoji}</span>
754
+ <span style={{
755
+ fontSize: 'var(--text-footnote)',
756
+ fontWeight: isActive ? 'var(--weight-semibold)' : 'var(--weight-medium)',
757
+ color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
758
+ }}>
759
+ {t.label}
760
+ </span>
761
+ </button>
762
+ )
763
+ })}
764
+ </div>
765
+ </div>
766
+ )}
767
+
768
+ {/* Step 4: Accent Color */}
769
+ {step === 4 && (
770
+ <div key="step-4" className="animate-fade-in">
771
+ <h2 style={{
772
+ fontSize: 'var(--text-title2)',
773
+ fontWeight: 'var(--weight-bold)',
774
+ letterSpacing: 'var(--tracking-tight)',
775
+ color: 'var(--text-primary)',
776
+ marginBottom: 'var(--space-1)',
777
+ }}>
778
+ Accent Color
779
+ </h2>
780
+ <p style={{
781
+ fontSize: 'var(--text-subheadline)',
782
+ color: 'var(--text-tertiary)',
783
+ marginBottom: 'var(--space-4)',
784
+ }}>
785
+ Personalize with your favorite color.
786
+ </p>
787
+
788
+ <div style={{
789
+ display: 'grid',
790
+ gridTemplateColumns: 'repeat(6, 1fr)',
791
+ gap: 'var(--space-3)',
792
+ justifyItems: 'center',
793
+ }}>
794
+ {ACCENT_PRESETS.map(preset => {
795
+ const isActive = settings.accentColor === preset.value
796
+ return (
797
+ <button
798
+ key={preset.value}
799
+ onClick={() => setAccentColor(preset.value)}
800
+ aria-label={preset.label}
801
+ title={preset.label}
802
+ style={{
803
+ width: 40,
804
+ height: 40,
805
+ borderRadius: '50%',
806
+ background: preset.value,
807
+ border: 'none',
808
+ cursor: 'pointer',
809
+ display: 'flex',
810
+ alignItems: 'center',
811
+ justifyContent: 'center',
812
+ outline: isActive ? `3px solid ${preset.value}` : 'none',
813
+ outlineOffset: 3,
814
+ transition: 'all 100ms var(--ease-smooth)',
815
+ }}
816
+ >
817
+ {isActive && <Check size={18} color="#000" strokeWidth={3} />}
818
+ </button>
819
+ )
820
+ })}
821
+ </div>
822
+ </div>
823
+ )}
824
+
825
+ {/* Step 5: Voice Input */}
826
+ {step === 5 && (
827
+ <div key="step-5" className="animate-fade-in">
828
+ <h2 style={{
829
+ fontSize: 'var(--text-title2)',
830
+ fontWeight: 'var(--weight-bold)',
831
+ letterSpacing: 'var(--tracking-tight)',
832
+ color: 'var(--text-primary)',
833
+ marginBottom: 'var(--space-1)',
834
+ }}>
835
+ Voice Input
836
+ </h2>
837
+ <p style={{
838
+ fontSize: 'var(--text-subheadline)',
839
+ color: 'var(--text-tertiary)',
840
+ marginBottom: 'var(--space-4)',
841
+ lineHeight: 'var(--leading-relaxed)',
842
+ }}>
843
+ Talk to your agents using your system&apos;s built-in dictation.
844
+ No microphone setup needed in the browser.
845
+ </p>
846
+
847
+ <div style={{
848
+ display: 'flex',
849
+ flexDirection: 'column',
850
+ gap: 'var(--space-3)',
851
+ }}>
852
+ <div style={{
853
+ padding: 'var(--space-4)',
854
+ borderRadius: 'var(--radius-md)',
855
+ background: 'var(--fill-quaternary)',
856
+ border: '1px solid var(--separator)',
857
+ }}>
858
+ <div style={{
859
+ display: 'flex',
860
+ alignItems: 'center',
861
+ gap: 'var(--space-3)',
862
+ marginBottom: 'var(--space-3)',
863
+ }}>
864
+ <div style={{
865
+ width: 36,
866
+ height: 36,
867
+ borderRadius: 8,
868
+ background: 'var(--accent-fill)',
869
+ display: 'flex',
870
+ alignItems: 'center',
871
+ justifyContent: 'center',
872
+ flexShrink: 0,
873
+ }}>
874
+ <Keyboard size={18} style={{ color: 'var(--accent)' }} />
875
+ </div>
876
+ <div>
877
+ <div style={{
878
+ fontSize: 'var(--text-subheadline)',
879
+ fontWeight: 'var(--weight-semibold)',
880
+ color: 'var(--text-primary)',
881
+ }}>
882
+ macOS Dictation
883
+ </div>
884
+ <div style={{
885
+ fontSize: 'var(--text-caption1)',
886
+ color: 'var(--text-tertiary)',
887
+ }}>
888
+ Recommended
889
+ </div>
890
+ </div>
891
+ </div>
892
+
893
+ <div style={{
894
+ display: 'flex',
895
+ flexDirection: 'column',
896
+ gap: 'var(--space-2)',
897
+ fontSize: 'var(--text-footnote)',
898
+ color: 'var(--text-secondary)',
899
+ lineHeight: 'var(--leading-relaxed)',
900
+ }}>
901
+ <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
902
+ <span style={{ color: 'var(--accent)', fontWeight: 'var(--weight-semibold)', flexShrink: 0 }}>1.</span>
903
+ <span>Open <strong>System Settings &gt; Keyboard</strong></span>
904
+ </div>
905
+ <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
906
+ <span style={{ color: 'var(--accent)', fontWeight: 'var(--weight-semibold)', flexShrink: 0 }}>2.</span>
907
+ <span>Turn on <strong>Dictation</strong></span>
908
+ </div>
909
+ <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
910
+ <span style={{ color: 'var(--accent)', fontWeight: 'var(--weight-semibold)', flexShrink: 0 }}>3.</span>
911
+ <span>Click any chat input, press <strong>Fn Fn</strong> (double-tap), and start talking</span>
912
+ </div>
913
+ </div>
914
+ </div>
915
+
916
+ <div style={{
917
+ padding: 'var(--space-3)',
918
+ borderRadius: 'var(--radius-md)',
919
+ background: 'var(--fill-quaternary)',
920
+ border: '1px solid var(--separator)',
921
+ display: 'flex',
922
+ alignItems: 'flex-start',
923
+ gap: 'var(--space-3)',
924
+ }}>
925
+ <Mic size={16} style={{ color: 'var(--text-tertiary)', flexShrink: 0, marginTop: 2 }} />
926
+ <div style={{
927
+ fontSize: 'var(--text-caption1)',
928
+ color: 'var(--text-tertiary)',
929
+ lineHeight: 'var(--leading-relaxed)',
930
+ }}>
931
+ Your voice is converted to text by macOS, then sent as a regular message.
932
+ Agents respond just like they would to typed text. Works in any input field across the app.
933
+ </div>
934
+ </div>
935
+ </div>
936
+ </div>
937
+ )}
938
+
939
+ {/* Step 6: Overview */}
940
+ {step === 6 && (
941
+ <div key="step-6" className="animate-fade-in">
942
+ <h2 style={{
943
+ fontSize: 'var(--text-title2)',
944
+ fontWeight: 'var(--weight-bold)',
945
+ letterSpacing: 'var(--tracking-tight)',
946
+ color: 'var(--text-primary)',
947
+ marginBottom: 'var(--space-1)',
948
+ }}>
949
+ You&apos;re All Set
950
+ </h2>
951
+ <p style={{
952
+ fontSize: 'var(--text-subheadline)',
953
+ color: 'var(--text-tertiary)',
954
+ marginBottom: 'var(--space-4)',
955
+ }}>
956
+ Here&apos;s what you can do.
957
+ </p>
958
+
959
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
960
+ {FEATURES.map(f => {
961
+ const Icon = f.icon
962
+ return (
963
+ <div
964
+ key={f.name}
965
+ style={{
966
+ display: 'flex',
967
+ alignItems: 'center',
968
+ gap: 'var(--space-3)',
969
+ padding: 'var(--space-3)',
970
+ borderRadius: 'var(--radius-md)',
971
+ background: 'var(--fill-quaternary)',
972
+ border: '1px solid var(--separator)',
973
+ }}
974
+ >
975
+ <div style={{
976
+ width: 36,
977
+ height: 36,
978
+ borderRadius: 8,
979
+ background: 'var(--accent-fill)',
980
+ display: 'flex',
981
+ alignItems: 'center',
982
+ justifyContent: 'center',
983
+ flexShrink: 0,
984
+ }}>
985
+ <Icon size={18} style={{ color: 'var(--accent)' }} />
986
+ </div>
987
+ <div style={{ minWidth: 0 }}>
988
+ <div style={{
989
+ fontSize: 'var(--text-subheadline)',
990
+ fontWeight: 'var(--weight-semibold)',
991
+ color: 'var(--text-primary)',
992
+ }}>
993
+ {f.name}
994
+ </div>
995
+ <div style={{
996
+ fontSize: 'var(--text-caption1)',
997
+ color: 'var(--text-tertiary)',
998
+ }}>
999
+ {f.desc}
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+ )
1004
+ })}
1005
+ </div>
1006
+ </div>
1007
+ )}
1008
+ </div>
1009
+
1010
+ {/* Navigation buttons */}
1011
+ <div style={{
1012
+ display: 'flex',
1013
+ justifyContent: 'space-between',
1014
+ alignItems: 'center',
1015
+ padding: 'var(--space-3) var(--space-5) var(--space-5)',
1016
+ gap: 'var(--space-3)',
1017
+ }}>
1018
+ {step > 0 ? (
1019
+ <button
1020
+ onClick={handleBack}
1021
+ style={{
1022
+ padding: 'var(--space-2) var(--space-4)',
1023
+ borderRadius: 'var(--radius-md)',
1024
+ background: 'var(--fill-tertiary)',
1025
+ color: 'var(--text-secondary)',
1026
+ border: 'none',
1027
+ cursor: 'pointer',
1028
+ fontSize: 'var(--text-subheadline)',
1029
+ fontWeight: 'var(--weight-medium)',
1030
+ transition: 'all 150ms var(--ease-smooth)',
1031
+ display: 'inline-flex',
1032
+ alignItems: 'center',
1033
+ gap: 6,
1034
+ }}
1035
+ >
1036
+ <ArrowLeft size={16} />
1037
+ Back
1038
+ </button>
1039
+ ) : (
1040
+ <div />
1041
+ )}
1042
+ <button
1043
+ onClick={handleNext}
1044
+ disabled={step === 1 && systemLoading}
1045
+ style={{
1046
+ padding: 'var(--space-2) var(--space-6)',
1047
+ borderRadius: 'var(--radius-md)',
1048
+ background: step === 1 && systemLoading ? 'var(--fill-tertiary)' : 'var(--accent)',
1049
+ color: step === 1 && systemLoading ? 'var(--text-quaternary)' : 'var(--accent-contrast)',
1050
+ border: 'none',
1051
+ cursor: step === 1 && systemLoading ? 'wait' : 'pointer',
1052
+ fontSize: 'var(--text-subheadline)',
1053
+ fontWeight: 'var(--weight-semibold)',
1054
+ transition: 'all 150ms var(--ease-smooth)',
1055
+ display: 'inline-flex',
1056
+ alignItems: 'center',
1057
+ gap: 6,
1058
+ }}
1059
+ >
1060
+ {step === 0 ? 'Begin' : step === TOTAL_STEPS - 1 ? 'Get Started' : 'Next'}
1061
+ {step === TOTAL_STEPS - 1 ? <Rocket size={16} /> : <ArrowRight size={16} />}
1062
+ </button>
1063
+ </div>
1064
+ </div>
1065
+ </div>
1066
+ )
1067
+ }