@swarmclawai/swarmclaw 0.7.3 → 0.7.4

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 (147) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +3 -1
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -3,13 +3,18 @@
3
3
  import { useMemo, useState } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
- import type { ProviderType, Credential } from '@/types'
6
+ import type { ProviderType, Credential, GatewayProfile } from '@/types'
7
7
  import {
8
+ ONBOARDING_PATHS,
8
9
  SETUP_PROVIDERS,
9
- DEFAULT_AGENTS,
10
+ STARTER_KITS,
11
+ getDefaultModelForProvider,
12
+ type OnboardingPath,
10
13
  type SetupProvider,
14
+ type StarterKitAgentTemplate,
11
15
  } from '@/lib/setup-defaults'
12
16
 
17
+ type SetupStep = 'path' | 'providers' | 'connect' | 'agents' | 'done'
13
18
  type CheckState = 'idle' | 'checking' | 'ok' | 'error'
14
19
 
15
20
  interface ProviderCheckResponse {
@@ -17,6 +22,8 @@ interface ProviderCheckResponse {
17
22
  message: string
18
23
  normalizedEndpoint?: string
19
24
  recommendedModel?: string
25
+ errorCode?: string
26
+ deviceId?: string
20
27
  }
21
28
 
22
29
  interface SetupDoctorCheck {
@@ -39,10 +46,118 @@ interface SetupWizardProps {
39
46
  }
40
47
 
41
48
  interface ConfiguredProvider {
49
+ id: string
42
50
  provider: SetupProvider
51
+ name: string
52
+ credentialId: string | null
53
+ endpoint: string | null
54
+ defaultModel: string
55
+ gatewayProfileId: string | null
56
+ }
57
+
58
+ interface StarterDraftAgent {
59
+ id: string
60
+ templateId: string
61
+ name: string
62
+ description: string
63
+ systemPrompt: string
64
+ providerConfigId: string | null
65
+ provider: SetupProvider | null
66
+ model: string
43
67
  credentialId: string | null
44
- agentId: string
45
- agentName: string
68
+ apiEndpoint: string | null
69
+ gatewayProfileId: string | null
70
+ tools: string[]
71
+ capabilities: string[]
72
+ platformAssignScope: 'self' | 'all'
73
+ enabled: boolean
74
+ }
75
+
76
+ interface CreatedAgentSummary {
77
+ id: string
78
+ name: string
79
+ provider: SetupProvider
80
+ providerName: string
81
+ }
82
+
83
+ const STEP_ORDER: SetupStep[] = ['path', 'providers', 'agents', 'done']
84
+ const CONNECTOR_ICONS = [
85
+ { name: 'Discord', icon: 'D' },
86
+ { name: 'Slack', icon: 'S' },
87
+ { name: 'Telegram', icon: 'T' },
88
+ { name: 'WhatsApp', icon: 'W' },
89
+ ]
90
+
91
+ function stepIndex(step: SetupStep): number {
92
+ if (step === 'connect') return STEP_ORDER.indexOf('providers')
93
+ return STEP_ORDER.indexOf(step)
94
+ }
95
+
96
+ function defaultKitForPath(path: OnboardingPath): string {
97
+ if (path === 'manual') return 'blank_workspace'
98
+ return 'personal_assistant'
99
+ }
100
+
101
+ function applyIntentContext(prompt: string, intentText: string): string {
102
+ const trimmed = intentText.trim()
103
+ if (!trimmed) return prompt
104
+ return `${prompt}
105
+
106
+ Current user intent:
107
+ - ${trimmed}
108
+
109
+ Keep your help aligned to this intent unless the user changes direction.`
110
+ }
111
+
112
+ function formatAgentCount(count: number): string {
113
+ if (count === 0) return 'Blank'
114
+ if (count === 1) return '1 agent'
115
+ return `${count} agents`
116
+ }
117
+
118
+ function withHttpScheme(value: string): string {
119
+ return /^(https?|wss?):\/\//i.test(value) ? value : `http://${value}`
120
+ }
121
+
122
+ function parseProviderUrl(value: string | null | undefined): URL | null {
123
+ const trimmed = typeof value === 'string' ? value.trim() : ''
124
+ if (!trimmed) return null
125
+ try {
126
+ return new URL(withHttpScheme(trimmed))
127
+ } catch {
128
+ return null
129
+ }
130
+ }
131
+
132
+ function formatEndpointHost(value: string | null | undefined): string | null {
133
+ const parsed = parseProviderUrl(value)
134
+ if (!parsed) return null
135
+ return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
136
+ }
137
+
138
+ function isLocalOpenClawEndpoint(value: string | null | undefined): boolean {
139
+ const parsed = parseProviderUrl(value)
140
+ if (!parsed) return false
141
+ const host = parsed.hostname.trim().toLowerCase()
142
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
143
+ }
144
+
145
+ function resolveOpenClawPort(value: string | null | undefined): number {
146
+ const parsed = parseProviderUrl(value)
147
+ const port = parsed ? Number(parsed.port) : NaN
148
+ return Number.isFinite(port) && port > 0 ? port : 18789
149
+ }
150
+
151
+ function resolveOpenClawDashboardUrl(value: string | null | undefined): string {
152
+ const parsed = parseProviderUrl(value)
153
+ if (!parsed) return 'http://localhost:18789'
154
+ const next = new URL(parsed.toString())
155
+ if (next.protocol === 'wss:') next.protocol = 'https:'
156
+ if (next.protocol === 'ws:') next.protocol = 'http:'
157
+ next.pathname = ''
158
+ next.search = ''
159
+ next.hash = ''
160
+ return next.toString().replace(/\/+$/, '')
46
161
  }
47
162
 
48
163
  function SparkleIcon() {
@@ -99,83 +214,191 @@ function SkipLink({ onClick, label }: { onClick: () => void; label?: string }) {
99
214
  )
100
215
  }
101
216
 
102
- function ProviderBadge({ label }: { label?: string }) {
103
- if (!label) return null
104
- return (
105
- <span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
106
- {label}
107
- </span>
108
- )
109
- }
110
-
111
- function ConfiguredChips({ providers }: { providers: ConfiguredProvider[] }) {
217
+ function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[] }) {
112
218
  if (providers.length === 0) return null
113
219
  return (
114
220
  <div className="flex flex-wrap gap-2 justify-center mb-6">
115
221
  {providers.map((cp) => (
116
222
  <span
117
- key={cp.provider}
223
+ key={cp.id}
118
224
  className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-emerald-300 text-[12px] font-500"
119
225
  >
120
226
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
121
- {cp.agentName}
227
+ {cp.name}
228
+ <span className="text-emerald-300/70">
229
+ {cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
230
+ ? `· ${formatEndpointHost(cp.endpoint)}`
231
+ : ''}
232
+ {cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
233
+ </span>
122
234
  </span>
123
235
  ))}
124
236
  </div>
125
237
  )
126
238
  }
127
239
 
128
- const CONNECTOR_ICONS = [
129
- { name: 'Discord', icon: 'D' },
130
- { name: 'Slack', icon: 'S' },
131
- { name: 'Telegram', icon: 'T' },
132
- { name: 'WhatsApp', icon: 'W' },
133
- ]
134
-
135
240
  function getOpenClawErrorHint(message: string): string | null {
136
241
  const lower = message.toLowerCase()
137
- if (lower.includes('timeout') || lower.includes('timed out'))
242
+ if (lower.includes('timeout') || lower.includes('timed out')) {
138
243
  return 'Ensure the port is open and reachable from this machine.'
139
- if (lower.includes('401') || lower.includes('unauthorized'))
244
+ }
245
+ if (lower.includes('401') || lower.includes('unauthorized')) {
140
246
  return 'Check your gateway auth token.'
141
- if (lower.includes('405') || lower.includes('method not allowed'))
247
+ }
248
+ if (lower.includes('405') || lower.includes('method not allowed')) {
142
249
  return 'Enable chatCompletions in your OpenClaw config: openclaw config set gateway.http.endpoints.chatCompletions.enabled true'
143
- if (lower.includes('econnrefused') || lower.includes('connection refused') || lower.includes('connect econnrefused'))
250
+ }
251
+ if (lower.includes('econnrefused') || lower.includes('connection refused') || lower.includes('connect econnrefused')) {
144
252
  return 'Verify that the OpenClaw gateway is running on the target host.'
253
+ }
145
254
  return null
146
255
  }
147
256
 
257
+ function preferredConfiguredProvider(
258
+ template: StarterKitAgentTemplate,
259
+ configuredProviders: ConfiguredProvider[],
260
+ fallbackProviderConfigId?: string | null,
261
+ fallbackProvider?: SetupProvider | null,
262
+ ): ConfiguredProvider | null {
263
+ if (fallbackProviderConfigId) {
264
+ const exact = configuredProviders.find((candidate) => candidate.id === fallbackProviderConfigId)
265
+ if (exact) return exact
266
+ }
267
+
268
+ if (fallbackProvider) {
269
+ const exact = configuredProviders.find((candidate) => candidate.provider === fallbackProvider)
270
+ if (exact) return exact
271
+ }
272
+
273
+ for (const provider of template.recommendedProviders || []) {
274
+ const exact = configuredProviders.find((candidate) => candidate.provider === provider)
275
+ if (exact) return exact
276
+ }
277
+
278
+ return configuredProviders[0] || null
279
+ }
280
+
281
+ function buildStarterDrafts(args: {
282
+ starterKitId: string | null
283
+ intentText: string
284
+ configuredProviders: ConfiguredProvider[]
285
+ previousDrafts?: StarterDraftAgent[]
286
+ }): StarterDraftAgent[] {
287
+ const { starterKitId, intentText, configuredProviders, previousDrafts = [] } = args
288
+ const starterKit = STARTER_KITS.find((kit) => kit.id === starterKitId)
289
+ if (!starterKit) return []
290
+
291
+ const previousById = new Map(previousDrafts.map((draft) => [draft.id, draft]))
292
+
293
+ return starterKit.agents.map((template) => {
294
+ const id = `${starterKit.id}:${template.id}`
295
+ const previous = previousById.get(id)
296
+ const configuredProvider = preferredConfiguredProvider(
297
+ template,
298
+ configuredProviders,
299
+ previous?.providerConfigId,
300
+ previous?.provider,
301
+ )
302
+ const oldProvider = previous?.provider || null
303
+ const oldProviderDefault = oldProvider ? getDefaultModelForProvider(oldProvider) : ''
304
+ const nextProviderDefault = configuredProvider?.defaultModel || ''
305
+ const shouldRefreshModel =
306
+ !previous?.model
307
+ || (oldProvider !== configuredProvider?.provider && previous.model === oldProviderDefault)
308
+
309
+ return {
310
+ id,
311
+ templateId: template.id,
312
+ name: previous?.name || template.name,
313
+ description: previous?.description || template.description,
314
+ systemPrompt: previous?.systemPrompt || applyIntentContext(template.systemPrompt, intentText),
315
+ providerConfigId: configuredProvider?.id || null,
316
+ provider: configuredProvider?.provider || null,
317
+ model: shouldRefreshModel ? nextProviderDefault : previous.model,
318
+ credentialId: configuredProvider?.credentialId || null,
319
+ apiEndpoint: configuredProvider?.endpoint || null,
320
+ gatewayProfileId: configuredProvider?.gatewayProfileId || null,
321
+ tools: template.tools,
322
+ capabilities: previous?.capabilities || template.capabilities || [],
323
+ platformAssignScope: previous?.platformAssignScope || template.platformAssignScope || 'self',
324
+ enabled: previous?.enabled ?? true,
325
+ }
326
+ })
327
+ }
328
+
148
329
  export function SetupWizard({ onComplete }: SetupWizardProps) {
149
- const [step, setStep] = useState(0)
330
+ const [step, setStep] = useState<SetupStep>('path')
331
+ const [onboardingPath, setOnboardingPath] = useState<OnboardingPath | null>(null)
332
+ const [starterKitId, setStarterKitId] = useState<string | null>(null)
333
+ const [intentText, setIntentText] = useState('')
334
+
150
335
  const [provider, setProvider] = useState<SetupProvider | null>(null)
336
+ const [providerLabel, setProviderLabel] = useState('')
151
337
  const [endpoint, setEndpoint] = useState('')
152
338
  const [apiKey, setApiKey] = useState('')
153
339
  const [credentialId, setCredentialId] = useState<string | null>(null)
154
340
  const [checkState, setCheckState] = useState<CheckState>('idle')
155
341
  const [checkMessage, setCheckMessage] = useState('')
342
+ const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
343
+ const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
344
+ const [providerSuggestedModel, setProviderSuggestedModel] = useState('')
345
+ const [commandCopyState, setCommandCopyState] = useState<'idle' | 'copied' | 'failed'>('idle')
346
+
156
347
  const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
157
348
  const [doctorError, setDoctorError] = useState('')
158
349
  const [doctorReport, setDoctorReport] = useState<SetupDoctorResponse | null>(null)
350
+ const [configuredProviders, setConfiguredProviders] = useState<ConfiguredProvider[]>([])
351
+ const [draftAgents, setDraftAgents] = useState<StarterDraftAgent[]>([])
352
+ const [createdAgents, setCreatedAgents] = useState<CreatedAgentSummary[]>([])
159
353
  const [saving, setSaving] = useState(false)
160
354
  const [error, setError] = useState('')
161
355
 
162
- const [agentName, setAgentName] = useState('')
163
- const [agentDescription, setAgentDescription] = useState('')
164
- const [agentPrompt, setAgentPrompt] = useState('')
165
- const [agentModel, setAgentModel] = useState('')
166
-
167
- const [configuredProviders, setConfiguredProviders] = useState<ConfiguredProvider[]>([])
168
-
169
356
  const selectedProvider = useMemo(
170
- () => SETUP_PROVIDERS.find((p) => p.id === provider) || null,
357
+ () => SETUP_PROVIDERS.find((candidate) => candidate.id === provider) || null,
171
358
  [provider],
172
359
  )
173
- const totalSteps = 4
360
+ const selectedStarterKit = useMemo(
361
+ () => STARTER_KITS.find((candidate) => candidate.id === starterKitId) || null,
362
+ [starterKitId],
363
+ )
364
+ const totalSteps = STEP_ORDER.length
365
+ const singleUseProviderIds = new Set(
366
+ configuredProviders
367
+ .filter((cp) => !SETUP_PROVIDERS.find((candidate) => candidate.id === cp.provider)?.allowMultiple)
368
+ .map((cp) => cp.provider),
369
+ )
370
+
174
371
  const requiresKey = selectedProvider?.requiresKey || false
175
372
  const supportsEndpoint = selectedProvider?.supportsEndpoint || false
176
373
  const keyIsOptional = selectedProvider?.optionalKey || false
177
374
  const requiresVerifiedConnection = provider === 'openclaw'
178
- const configuredIds = new Set(configuredProviders.map((cp) => cp.provider))
375
+ const canContinueFromProviders = configuredProviders.length > 0 || (selectedStarterKit?.agents.length || 0) === 0
376
+ const openClawEndpointValue = provider === 'openclaw'
377
+ ? (endpoint.trim() || selectedProvider?.defaultEndpoint || 'http://localhost:18789/v1')
378
+ : null
379
+ const openClawEndpointHost = openClawEndpointValue ? formatEndpointHost(openClawEndpointValue) : null
380
+ const openClawDashboardUrl = provider === 'openclaw'
381
+ ? resolveOpenClawDashboardUrl(openClawEndpointValue)
382
+ : null
383
+ const openClawLocal = provider === 'openclaw' ? isLocalOpenClawEndpoint(openClawEndpointValue) : false
384
+ const openClawPort = provider === 'openclaw' ? resolveOpenClawPort(openClawEndpointValue) : 18789
385
+ const openClawLocalCommand = `npx openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
386
+ const openClawLocalCommandPnpm = `pnpm openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
387
+
388
+ const resetProviderForm = () => {
389
+ setProvider(null)
390
+ setProviderLabel('')
391
+ setEndpoint('')
392
+ setApiKey('')
393
+ setCredentialId(null)
394
+ setCheckState('idle')
395
+ setCheckMessage('')
396
+ setCheckErrorCode(null)
397
+ setOpenclawDeviceId(null)
398
+ setProviderSuggestedModel('')
399
+ setCommandCopyState('idle')
400
+ setError('')
401
+ }
179
402
 
180
403
  const skip = async () => {
181
404
  try {
@@ -186,24 +409,47 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
186
409
  onComplete()
187
410
  }
188
411
 
189
- const selectProvider = (next: SetupProvider) => {
190
- const defaults = DEFAULT_AGENTS[next]
191
- const meta = SETUP_PROVIDERS.find((p) => p.id === next)
412
+ const applyPathSelection = (nextPath: OnboardingPath) => {
413
+ setOnboardingPath(nextPath)
414
+ setStarterKitId((current) => current || defaultKitForPath(nextPath))
415
+ setError('')
416
+ }
192
417
 
193
- setProvider(next)
418
+ const continueFromPath = () => {
419
+ if (!onboardingPath) {
420
+ setError('Choose how you want to get started.')
421
+ return
422
+ }
423
+ if (!starterKitId) {
424
+ setError('Choose a starter kit or blank workspace.')
425
+ return
426
+ }
427
+
428
+ setDraftAgents(buildStarterDrafts({
429
+ starterKitId,
430
+ intentText,
431
+ configuredProviders,
432
+ }))
433
+ setError('')
434
+ setStep('providers')
435
+ }
436
+
437
+ const selectProvider = (nextProvider: SetupProvider) => {
438
+ const meta = SETUP_PROVIDERS.find((candidate) => candidate.id === nextProvider)
439
+ const nextCount = configuredProviders.filter((candidate) => candidate.provider === nextProvider).length + 1
440
+ setProvider(nextProvider)
441
+ setProviderLabel(meta?.allowMultiple ? `${meta.name} ${nextCount}` : (meta?.name || ''))
194
442
  setEndpoint(meta?.defaultEndpoint || '')
195
443
  setApiKey('')
196
444
  setCredentialId(null)
197
445
  setCheckState('idle')
198
446
  setCheckMessage('')
447
+ setCheckErrorCode(null)
448
+ setOpenclawDeviceId(null)
449
+ setProviderSuggestedModel(getDefaultModelForProvider(nextProvider))
450
+ setCommandCopyState('idle')
199
451
  setError('')
200
-
201
- setAgentName(defaults.name)
202
- setAgentDescription(defaults.description)
203
- setAgentPrompt(defaults.systemPrompt)
204
- setAgentModel(defaults.model)
205
-
206
- setStep(1)
452
+ setStep('connect')
207
453
  }
208
454
 
209
455
  const runConnectionCheck = async (): Promise<boolean> => {
@@ -216,31 +462,30 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
216
462
 
217
463
  setCheckState('checking')
218
464
  setCheckMessage('')
465
+ setCheckErrorCode(null)
219
466
  setError('')
220
467
  try {
221
468
  const result = await api<ProviderCheckResponse>('POST', '/setup/check-provider', {
222
469
  provider,
223
470
  apiKey: apiKey.trim() || undefined,
224
471
  endpoint: supportsEndpoint ? endpoint.trim() || undefined : undefined,
225
- model: agentModel.trim() || undefined,
226
472
  })
227
473
 
228
474
  if (result.normalizedEndpoint && supportsEndpoint) {
229
475
  setEndpoint(result.normalizedEndpoint)
230
476
  }
231
- if (result.recommendedModel && provider) {
232
- const currentModel = agentModel.trim()
233
- const defaultModel = DEFAULT_AGENTS[provider].model
234
- if (!currentModel || currentModel === defaultModel) {
235
- setAgentModel(result.recommendedModel)
236
- }
477
+ if (result.recommendedModel) {
478
+ setProviderSuggestedModel(result.recommendedModel)
237
479
  }
480
+ setCheckErrorCode(result.errorCode || null)
481
+ setOpenclawDeviceId(result.deviceId || null)
238
482
  setCheckState(result.ok ? 'ok' : 'error')
239
483
  setCheckMessage(result.message || (result.ok ? 'Connected successfully.' : 'Connection failed.'))
240
484
  return !!result.ok
241
485
  } catch (err: unknown) {
242
486
  setCheckState('error')
243
487
  setCheckMessage(err instanceof Error ? err.message : String(err))
488
+ setCheckErrorCode(null)
244
489
  return false
245
490
  }
246
491
  }
@@ -259,7 +504,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
259
504
  }
260
505
  }
261
506
 
262
- const saveProviderAndContinue = async () => {
507
+ const saveProvider = async () => {
263
508
  if (!provider || !selectedProvider) return
264
509
  if (requiresKey && !apiKey.trim()) {
265
510
  setError('This provider requires an API key.')
@@ -283,14 +528,33 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
283
528
  if (shouldSaveCredential && !nextCredentialId) {
284
529
  const cred = await api<Credential>('POST', '/credentials', {
285
530
  provider,
286
- name: `${selectedProvider.name} key`,
531
+ name: `${providerLabel.trim() || selectedProvider.name} key`,
287
532
  apiKey: apiKey.trim(),
288
533
  })
289
534
  nextCredentialId = cred.id
290
535
  }
291
536
 
537
+ const configuredProvider: ConfiguredProvider = {
538
+ id: crypto.randomUUID(),
539
+ provider,
540
+ name: providerLabel.trim() || selectedProvider.name,
541
+ credentialId: nextCredentialId || null,
542
+ endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
543
+ defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
544
+ gatewayProfileId: null,
545
+ }
546
+
547
+ const nextConfigured = [...configuredProviders, configuredProvider]
292
548
  setCredentialId(nextCredentialId || null)
293
- setStep(2)
549
+ setConfiguredProviders(nextConfigured)
550
+ setDraftAgents((current) => buildStarterDrafts({
551
+ starterKitId,
552
+ intentText,
553
+ configuredProviders: nextConfigured,
554
+ previousDrafts: current,
555
+ }))
556
+ resetProviderForm()
557
+ setStep('providers')
294
558
  } catch (err: unknown) {
295
559
  setError(err instanceof Error ? err.message : String(err))
296
560
  } finally {
@@ -298,87 +562,165 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
298
562
  }
299
563
  }
300
564
 
301
- const createAgentAndFinish = async (addAnother: boolean) => {
302
- if (!provider || !agentName.trim()) return
303
- if (requiresVerifiedConnection && checkState !== 'ok') {
304
- setError('OpenClaw connection is not verified. Go back and run the connection check.')
305
- setStep(1)
565
+ const goToAgentReview = () => {
566
+ setDraftAgents((current) => buildStarterDrafts({
567
+ starterKitId,
568
+ intentText,
569
+ configuredProviders,
570
+ previousDrafts: current,
571
+ }))
572
+ setError('')
573
+ setStep('agents')
574
+ }
575
+
576
+ const updateDraftAgent = (id: string, patch: Partial<StarterDraftAgent>) => {
577
+ setDraftAgents((current) => current.map((draft) => (
578
+ draft.id === id
579
+ ? { ...draft, ...patch }
580
+ : draft
581
+ )))
582
+ }
583
+
584
+ const updateDraftAgentProvider = (id: string, nextProviderConfigId: string) => {
585
+ const configuredProvider = configuredProviders.find((candidate) => candidate.id === nextProviderConfigId)
586
+ if (!configuredProvider) return
587
+
588
+ setDraftAgents((current) => current.map((draft) => {
589
+ if (draft.id !== id) return draft
590
+ const previousDefault = draft.provider ? getDefaultModelForProvider(draft.provider) : ''
591
+ const nextModel = !draft.model || draft.model === previousDefault
592
+ ? configuredProvider.defaultModel
593
+ : draft.model
594
+ return {
595
+ ...draft,
596
+ providerConfigId: configuredProvider.id,
597
+ provider: configuredProvider.provider,
598
+ credentialId: configuredProvider.credentialId,
599
+ apiEndpoint: configuredProvider.endpoint,
600
+ gatewayProfileId: configuredProvider.gatewayProfileId,
601
+ model: nextModel,
602
+ }
603
+ }))
604
+ }
605
+
606
+ const copyOpenClawLocalCommand = async () => {
607
+ try {
608
+ await navigator.clipboard.writeText(openClawLocalCommand)
609
+ setCommandCopyState('copied')
610
+ window.setTimeout(() => setCommandCopyState('idle'), 1200)
611
+ } catch {
612
+ setCommandCopyState('failed')
613
+ window.setTimeout(() => setCommandCopyState('idle'), 1800)
614
+ }
615
+ }
616
+
617
+ const createAgentsAndFinish = async () => {
618
+ const enabledDrafts = draftAgents.filter((draft) => draft.enabled)
619
+ if (enabledDrafts.some((draft) => !draft.provider)) {
620
+ setError('Every enabled agent needs a provider assignment before you continue.')
306
621
  return
307
622
  }
623
+
308
624
  setSaving(true)
309
625
  setError('')
310
626
  try {
311
- const payload: Record<string, unknown> = {
312
- name: agentName.trim(),
313
- description: agentDescription.trim(),
314
- systemPrompt: agentPrompt.trim(),
315
- provider: provider as ProviderType,
316
- model: agentModel.trim() || DEFAULT_AGENTS[provider].model,
317
- credentialId: credentialId || null,
318
- plugins: DEFAULT_AGENTS[provider].tools,
319
- }
320
-
321
- if (supportsEndpoint && endpoint.trim()) {
322
- payload.apiEndpoint = endpoint.trim()
627
+ const gatewayProfileIdsByProviderConfig = new Map<string, string>()
628
+ const openClawProviders = configuredProviders.filter((candidate) => candidate.provider === 'openclaw')
629
+ if (openClawProviders.length > 0) {
630
+ const existingGateways = await api<GatewayProfile[]>('GET', '/gateways')
631
+ let shouldCreateDefault = existingGateways.length === 0
632
+
633
+ for (const configuredProvider of openClawProviders) {
634
+ const normalizedEndpoint = (configuredProvider.endpoint || 'http://localhost:18789').trim()
635
+ const existing = existingGateways.find((gateway) => (
636
+ gateway.provider === 'openclaw'
637
+ && gateway.endpoint === normalizedEndpoint
638
+ && (gateway.credentialId || null) === (configuredProvider.credentialId || null)
639
+ ))
640
+ if (existing) {
641
+ gatewayProfileIdsByProviderConfig.set(configuredProvider.id, existing.id)
642
+ continue
643
+ }
644
+
645
+ const createdGateway = await api<GatewayProfile>('POST', '/gateways', {
646
+ name: configuredProvider.name,
647
+ endpoint: normalizedEndpoint,
648
+ credentialId: configuredProvider.credentialId || null,
649
+ tags: ['onboarding'],
650
+ notes: `Created during setup for ${configuredProvider.name}.`,
651
+ isDefault: shouldCreateDefault,
652
+ })
653
+ gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
654
+ existingGateways.push(createdGateway)
655
+ shouldCreateDefault = false
656
+ }
323
657
  }
324
658
 
325
- const isFirstProvider = configuredProviders.length === 0
326
- let agentId: string
327
-
328
- if (isFirstProvider) {
329
- const agents = await api<Record<string, { id: string }>>('GET', '/agents')
330
- const canReuseDefault =
331
- !!agents.default
332
- && Object.keys(agents).length === 1
333
- && agentName.trim().toLowerCase() === DEFAULT_AGENTS[provider].name.toLowerCase()
659
+ const existingAgents = await api<Record<string, { id: string }>>('GET', '/agents')
660
+ let canReuseDefault = !!existingAgents.default && Object.keys(existingAgents).length === 1
661
+ const created: CreatedAgentSummary[] = []
662
+
663
+ for (const draft of enabledDrafts) {
664
+ const payload: Record<string, unknown> = {
665
+ name: draft.name.trim(),
666
+ description: draft.description.trim(),
667
+ systemPrompt: draft.systemPrompt.trim(),
668
+ provider: draft.provider as ProviderType,
669
+ model: draft.model.trim() || getDefaultModelForProvider(draft.provider as SetupProvider),
670
+ credentialId: draft.credentialId || null,
671
+ plugins: draft.tools,
672
+ capabilities: draft.capabilities,
673
+ platformAssignScope: draft.platformAssignScope,
674
+ }
334
675
 
335
- agentId = canReuseDefault
336
- ? 'default'
337
- : (await api<{ id: string }>('POST', '/agents', payload)).id
676
+ if (draft.apiEndpoint) {
677
+ payload.apiEndpoint = draft.apiEndpoint
678
+ }
679
+ const gatewayProfileId = (draft.providerConfigId && gatewayProfileIdsByProviderConfig.get(draft.providerConfigId)) || draft.gatewayProfileId
680
+ if (gatewayProfileId) {
681
+ payload.gatewayProfileId = gatewayProfileId
682
+ }
338
683
 
684
+ let agentId: string
339
685
  if (canReuseDefault) {
340
686
  await api('PUT', '/agents/default', payload)
687
+ agentId = 'default'
688
+ canReuseDefault = false
689
+ } else {
690
+ agentId = (await api<{ id: string }>('POST', '/agents', payload)).id
341
691
  }
342
692
 
343
- // Set the default agent and open its thread
344
- const appState = useAppStore.getState()
345
- if (agentId) {
346
- await appState.updateSettings({ defaultAgentId: agentId })
347
- await appState.setCurrentAgent(agentId)
348
- }
349
- } else {
350
- // Additional providers just create the agent
351
- agentId = (await api<{ id: string }>('POST', '/agents', payload)).id
693
+ created.push({
694
+ id: agentId,
695
+ name: draft.name.trim(),
696
+ provider: draft.provider as SetupProvider,
697
+ providerName: configuredProviders.find((candidate) => candidate.id === draft.providerConfigId)?.name || draft.provider as SetupProvider,
698
+ })
352
699
  }
353
700
 
354
- const newConfigured: ConfiguredProvider = {
355
- provider,
356
- credentialId: credentialId || null,
357
- agentId,
358
- agentName: agentName.trim(),
701
+ if (created[0]) {
702
+ const appState = useAppStore.getState()
703
+ await appState.updateSettings({ defaultAgentId: created[0].id })
704
+ await appState.setCurrentAgent(created[0].id)
359
705
  }
360
- const nextConfigured = [...configuredProviders, newConfigured]
361
- setConfiguredProviders(nextConfigured)
362
706
 
363
- if (addAnother) {
364
- // Reset for next provider selection
365
- setProvider(null)
366
- setEndpoint('')
367
- setApiKey('')
368
- setCredentialId(null)
369
- setCheckState('idle')
370
- setCheckMessage('')
371
- setAgentName('')
372
- setAgentDescription('')
373
- setAgentPrompt('')
374
- setAgentModel('')
375
- setError('')
376
- setStep(0)
377
- } else {
378
- // Finish setup
379
- await api('PUT', '/settings', { setupCompleted: true })
380
- setStep(3)
381
- }
707
+ await api('PUT', '/settings', { setupCompleted: true })
708
+ setCreatedAgents(created)
709
+ setStep('done')
710
+ } catch (err: unknown) {
711
+ setError(err instanceof Error ? err.message : String(err))
712
+ } finally {
713
+ setSaving(false)
714
+ }
715
+ }
716
+
717
+ const finishWithoutAgents = async () => {
718
+ setSaving(true)
719
+ setError('')
720
+ try {
721
+ await api('PUT', '/settings', { setupCompleted: true })
722
+ setCreatedAgents([])
723
+ setStep('done')
382
724
  } catch (err: unknown) {
383
725
  setError(err instanceof Error ? err.message : String(err))
384
726
  } finally {
@@ -399,35 +741,171 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
399
741
  </div>
400
742
 
401
743
  <div
402
- className="relative max-w-[520px] w-full text-center"
744
+ className="relative max-w-[760px] w-full text-center"
403
745
  style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}
404
746
  >
405
747
  <SparkleIcon />
406
- <StepDots current={step} total={totalSteps} />
748
+ <StepDots current={stepIndex(step)} total={totalSteps} />
407
749
 
408
- {step === 0 && (
750
+ {step === 'path' && (
409
751
  <>
410
752
  <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
411
- {configuredProviders.length > 0 ? 'Add Another Provider' : '2-Minute Setup'}
753
+ Choose Your Setup Path
412
754
  </h1>
413
755
  <p className="text-[15px] text-text-2 mb-2">
414
- {configuredProviders.length > 0
415
- ? 'Pick another provider to set up, or finish below.'
416
- : 'No coding required. Pick a provider, paste a key if needed, and start chatting.'}
756
+ Start from your intent, start from your provider, or build it yourself.
417
757
  </p>
418
758
  <p className="text-[13px] text-text-3 mb-8">
419
- You can change providers, models, and agent settings anytime later.
759
+ You can still change providers, models, agents, and templates later.
420
760
  </p>
421
761
 
422
- <ConfiguredChips providers={configuredProviders} />
762
+ <div className="grid gap-3 md:grid-cols-3 text-left mb-6">
763
+ {ONBOARDING_PATHS.map((path) => {
764
+ const active = onboardingPath === path.id
765
+ return (
766
+ <button
767
+ key={path.id}
768
+ onClick={() => applyPathSelection(path.id)}
769
+ className={`rounded-[16px] border px-5 py-4 transition-all duration-200 cursor-pointer ${
770
+ active
771
+ ? 'border-accent-bright/40 bg-accent-bright/10'
772
+ : 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-surface-hover'
773
+ }`}
774
+ >
775
+ <div className="flex items-center gap-2 mb-2">
776
+ <span className="text-[15px] font-display font-700 text-text">{path.title}</span>
777
+ {path.badge && (
778
+ <span className="inline-flex items-center px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
779
+ {path.badge}
780
+ </span>
781
+ )}
782
+ </div>
783
+ <p className="text-[13px] text-text-2 leading-relaxed mb-2">{path.description}</p>
784
+ <p className="text-[12px] text-text-3 leading-relaxed">{path.detail}</p>
785
+ </button>
786
+ )
787
+ })}
788
+ </div>
423
789
 
424
- <div className="flex flex-col gap-3 max-h-[44vh] overflow-y-auto pr-1">
425
- {SETUP_PROVIDERS.map((p) => {
426
- const isConfigured = configuredIds.has(p.id)
790
+ {onboardingPath === 'intent' && (
791
+ <div className="mb-6 text-left rounded-[16px] border border-white/[0.08] bg-surface p-4">
792
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
793
+ What do you want SwarmClaw to help with?
794
+ </label>
795
+ <textarea
796
+ value={intentText}
797
+ onChange={(e) => setIntentText(e.target.value)}
798
+ rows={3}
799
+ placeholder="Examples: help me research AI products, build a SaaS app, manage personal projects, write better content..."
800
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
801
+ text-text text-[14px] outline-none transition-all duration-200 resize-none
802
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
803
+ />
804
+ <p className="mt-2 text-[12px] text-text-3">
805
+ This is used to tailor your starter agents. You can leave it blank and refine later.
806
+ </p>
807
+ </div>
808
+ )}
809
+
810
+ <div className="text-left mb-6">
811
+ <div className="flex items-center justify-between mb-3">
812
+ <div>
813
+ <div className="text-[13px] font-600 text-text">Starter Kits</div>
814
+ <div className="text-[12px] text-text-3">Choose a template or start blank. You can opt individual agents in or out on the next screen.</div>
815
+ </div>
816
+ </div>
817
+ <div className="grid gap-3 md:grid-cols-2">
818
+ {STARTER_KITS.map((kit) => {
819
+ const active = starterKitId === kit.id
820
+ return (
821
+ <button
822
+ key={kit.id}
823
+ onClick={() => setStarterKitId(kit.id)}
824
+ className={`rounded-[16px] border px-5 py-4 text-left transition-all duration-200 cursor-pointer ${
825
+ active
826
+ ? 'border-accent-bright/40 bg-accent-bright/10'
827
+ : 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-surface-hover'
828
+ }`}
829
+ >
830
+ <div className="flex items-center gap-2 mb-2">
831
+ <span className="text-[15px] font-display font-700 text-text">{kit.name}</span>
832
+ {kit.badge && (
833
+ <span className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.06] text-text-2 text-[10px] uppercase tracking-[0.08em] font-600">
834
+ {kit.badge}
835
+ </span>
836
+ )}
837
+ </div>
838
+ <p className="text-[13px] text-text-2 mb-2">{kit.description}</p>
839
+ <p className="text-[12px] text-text-3 leading-relaxed mb-3">{kit.detail}</p>
840
+ <div className="flex items-center gap-2 text-[11px] text-text-3">
841
+ <span className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.04] border border-white/[0.06]">
842
+ {formatAgentCount(kit.agents.length)}
843
+ </span>
844
+ {kit.recommendedFor?.includes(onboardingPath || 'quick') && (
845
+ <span className="inline-flex items-center px-2 py-0.5 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-300">
846
+ Fits this path
847
+ </span>
848
+ )}
849
+ </div>
850
+ </button>
851
+ )
852
+ })}
853
+ </div>
854
+ </div>
855
+
856
+ {error && <p className="mb-4 text-[13px] text-red-400">{error}</p>}
857
+
858
+ <div className="flex items-center justify-center gap-3">
859
+ <button
860
+ onClick={continueFromPath}
861
+ disabled={!onboardingPath || !starterKitId}
862
+ className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
863
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
864
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
865
+ >
866
+ Continue
867
+ </button>
868
+ </div>
869
+
870
+ <SkipLink onClick={skip} />
871
+ </>
872
+ )}
873
+
874
+ {step === 'providers' && (
875
+ <>
876
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
877
+ Connect Providers
878
+ </h1>
879
+ <p className="text-[15px] text-text-2 mb-2">
880
+ Add one or more providers, then map them onto your starter agents.
881
+ </p>
882
+ <p className="text-[13px] text-text-3 mb-8">
883
+ Providers are reusable. You will choose or change the provider and model for each starter agent on the next step.
884
+ </p>
885
+
886
+ {selectedStarterKit && (
887
+ <div className="mb-6 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
888
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Starter Kit</div>
889
+ <div className="text-[14px] text-text mb-1">{selectedStarterKit.name}</div>
890
+ <div className="text-[12px] text-text-3">{selectedStarterKit.detail}</div>
891
+ {!!intentText.trim() && (
892
+ <div className="mt-3 text-[12px] text-text-2">
893
+ Intent: <span className="text-text-3">{intentText.trim()}</span>
894
+ </div>
895
+ )}
896
+ </div>
897
+ )}
898
+
899
+ <ConfiguredProviderChips providers={configuredProviders} />
900
+
901
+ <div className="flex flex-col gap-3 max-h-[42vh] overflow-y-auto pr-1">
902
+ {SETUP_PROVIDERS.map((candidate) => {
903
+ const isConfigured = !candidate.allowMultiple && singleUseProviderIds.has(candidate.id)
904
+ const configuredCount = configuredProviders.filter((cp) => cp.provider === candidate.id).length
427
905
  return (
428
906
  <button
429
- key={p.id}
430
- onClick={() => !isConfigured && selectProvider(p.id)}
907
+ key={candidate.id}
908
+ onClick={() => !isConfigured && selectProvider(candidate.id)}
431
909
  disabled={isConfigured}
432
910
  className={`w-full px-5 py-4 rounded-[14px] border border-white/[0.08] bg-surface text-left
433
911
  transition-all duration-200 flex items-start gap-4
@@ -438,19 +916,26 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
438
916
  >
439
917
  <div className="w-10 h-10 rounded-[10px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0 mt-0.5">
440
918
  <span className="text-[16px] font-display font-700 text-accent-bright">
441
- {p.icon}
919
+ {candidate.icon}
442
920
  </span>
443
921
  </div>
444
922
  <div>
445
923
  <div className="text-[15px] font-display font-600 text-text mb-1">
446
- {p.name}
447
- {isConfigured
448
- ? <span className="ml-2 text-[10px] text-emerald-400 uppercase tracking-[0.08em]">Configured</span>
449
- : <ProviderBadge label={p.badge} />
450
- }
924
+ {candidate.name}
925
+ {isConfigured ? (
926
+ <span className="ml-2 text-[10px] text-emerald-400 uppercase tracking-[0.08em]">Ready</span>
927
+ ) : candidate.allowMultiple && configuredCount > 0 ? (
928
+ <span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-300 text-[10px] uppercase tracking-[0.08em] font-600">
929
+ {configuredCount} saved
930
+ </span>
931
+ ) : candidate.badge ? (
932
+ <span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
933
+ {candidate.badge}
934
+ </span>
935
+ ) : null}
451
936
  </div>
452
- <div className="text-[13px] text-text-3 leading-relaxed">{p.description}</div>
453
- {!p.requiresKey && !isConfigured && (
937
+ <div className="text-[13px] text-text-3 leading-relaxed">{candidate.description}</div>
938
+ {!candidate.requiresKey && !isConfigured && (
454
939
  <div className="mt-1.5 inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-400 text-[11px] font-500">
455
940
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
456
941
  No API key required
@@ -481,7 +966,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
481
966
  <div className={`text-[12px] font-600 ${doctorReport.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
482
967
  {doctorReport.summary}
483
968
  </div>
484
- {doctorReport.checks.filter((c) => c.status !== 'pass').slice(0, 3).map((check) => (
969
+ {doctorReport.checks.filter((check) => check.status !== 'pass').slice(0, 3).map((check) => (
485
970
  <div key={check.id} className="mt-1 text-[11px] text-text-3">
486
971
  - {check.label}: {check.detail}
487
972
  </div>
@@ -495,31 +980,64 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
495
980
  )}
496
981
  </div>
497
982
 
498
- <SkipLink
499
- onClick={configuredProviders.length > 0 ? async () => {
500
- await api('PUT', '/settings', { setupCompleted: true })
501
- setStep(3)
502
- } : skip}
503
- label={configuredProviders.length > 0 ? 'Finish Setup' : 'Skip setup for now'}
504
- />
983
+ {error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
984
+
985
+ <div className="mt-6 flex items-center justify-center gap-3">
986
+ <button
987
+ onClick={() => setStep('path')}
988
+ className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
989
+ font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
990
+ >
991
+ Back
992
+ </button>
993
+ <button
994
+ onClick={goToAgentReview}
995
+ disabled={!canContinueFromProviders}
996
+ className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
997
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
998
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
999
+ >
1000
+ {selectedStarterKit?.agents.length ? 'Review Starter Agents' : 'Continue'}
1001
+ </button>
1002
+ </div>
1003
+
1004
+ <SkipLink onClick={skip} />
505
1005
  </>
506
1006
  )}
507
1007
 
508
- {step === 1 && provider && selectedProvider && (
1008
+ {step === 'connect' && provider && selectedProvider && (
509
1009
  <>
510
1010
  <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
511
1011
  Connect {selectedProvider.name}
512
1012
  </h1>
513
1013
  <p className="text-[15px] text-text-2 mb-2">
514
- Add only what is needed for this provider, then check connection.
1014
+ Save this provider once, then reuse it across your starter agents.
515
1015
  </p>
516
1016
  <p className="text-[13px] text-text-3 mb-7">
517
1017
  {requiresVerifiedConnection
518
1018
  ? 'OpenClaw must pass connection check before you can continue.'
519
- : 'You can keep going even if the check fails and fix details later.'}
1019
+ : 'You can still continue even if the check fails and fix details later.'}
520
1020
  </p>
521
1021
 
522
1022
  <div className="flex flex-col gap-3 text-left mb-4">
1023
+ <div>
1024
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
1025
+ Connection name
1026
+ </label>
1027
+ <input
1028
+ type="text"
1029
+ value={providerLabel}
1030
+ onChange={(e) => setProviderLabel(e.target.value)}
1031
+ placeholder={selectedProvider.name}
1032
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
1033
+ text-text text-[14px] outline-none transition-all duration-200
1034
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1035
+ />
1036
+ <p className="mt-1.5 text-[11px] text-text-3">
1037
+ Helpful for multiple OpenClaw gateways or distinct provider profiles.
1038
+ </p>
1039
+ </div>
1040
+
523
1041
  {supportsEndpoint && (
524
1042
  <div>
525
1043
  <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
@@ -536,18 +1054,76 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
536
1054
  />
537
1055
  {provider === 'openclaw' && (
538
1056
  <div className="mt-2 space-y-0.5">
539
- <p className="text-[12px] text-text-3">Works with local (<code className="text-text-2">localhost:18789</code>) or remote OpenClaw instances.</p>
540
- <p className="text-[12px] text-text-3">For remote: use your server&apos;s IP/domain with port (e.g. <code className="text-text-2">http://your-server:60924/v1</code>).</p>
541
- <p className="text-[12px] text-text-3 mt-1">
542
- <a href="/docs/openclaw-setup" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">
543
- Setup guide &rarr;
544
- </a>
545
- </p>
1057
+ <p className="text-[12px] text-text-3">Works with local (<code className="text-text-2">http://localhost:18789/v1</code>) or remote OpenClaw instances.</p>
1058
+ <p className="text-[12px] text-text-3">Remote example: <code className="text-text-2">https://your-gateway.ts.net/v1</code>.</p>
546
1059
  </div>
547
1060
  )}
548
1061
  </div>
549
1062
  )}
550
1063
 
1064
+ {provider === 'openclaw' && (
1065
+ <div className="rounded-[14px] border border-white/[0.08] bg-surface p-4 space-y-4">
1066
+ <div className="grid gap-3 md:grid-cols-2">
1067
+ <div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
1068
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Remote gateway</div>
1069
+ <p className="text-[13px] text-text-2 leading-relaxed">
1070
+ Recommended when your OpenClaw node runs on another machine or VPS. Use a URL reachable from the machine running SwarmClaw.
1071
+ </p>
1072
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1073
+ Tailscale example: <code className="text-text-2">https://&lt;gateway-host&gt;.ts.net/v1</code>
1074
+ </p>
1075
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1076
+ If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
1077
+ </p>
1078
+ </div>
1079
+ <div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
1080
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Run locally</div>
1081
+ <p className="text-[13px] text-text-2 leading-relaxed">
1082
+ Use this when SwarmClaw and OpenClaw are on the same host. <code className="text-text-2">localhost</code> always refers to the SwarmClaw host.
1083
+ </p>
1084
+ <div className="mt-3 rounded-[10px] border border-white/[0.06] bg-surface px-3 py-2">
1085
+ <code className="block overflow-x-auto whitespace-nowrap text-[12px] text-text-2">
1086
+ {openClawLocalCommand}
1087
+ </code>
1088
+ </div>
1089
+ <div className="mt-2 flex items-center gap-2">
1090
+ <button
1091
+ type="button"
1092
+ onClick={copyOpenClawLocalCommand}
1093
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
1094
+ >
1095
+ {commandCopyState === 'copied'
1096
+ ? 'Copied'
1097
+ : commandCopyState === 'failed'
1098
+ ? 'Copy failed'
1099
+ : 'Copy command'}
1100
+ </button>
1101
+ <button
1102
+ type="button"
1103
+ onClick={() => { setEndpoint(selectedProvider.defaultEndpoint || 'http://localhost:18789/v1'); setCheckState('idle'); setCheckMessage(''); setCheckErrorCode(null) }}
1104
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
1105
+ >
1106
+ Use local default
1107
+ </button>
1108
+ </div>
1109
+ <p className="mt-2 text-[11px] text-text-3">
1110
+ In a source checkout, use <code className="text-text-2">{openClawLocalCommandPnpm}</code>.
1111
+ </p>
1112
+ </div>
1113
+ </div>
1114
+
1115
+ <div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
1116
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Connection mental model</div>
1117
+ <p className="text-[12px] text-text-3 leading-relaxed">
1118
+ SwarmClaw talks to this endpoint from its own host. If SwarmClaw is on a server, <code className="text-text-2">localhost</code> means that server, not your laptop.
1119
+ </p>
1120
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1121
+ Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
1122
+ </p>
1123
+ </div>
1124
+ </div>
1125
+ )}
1126
+
551
1127
  {(requiresKey || keyIsOptional) && (
552
1128
  <div>
553
1129
  <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
@@ -594,6 +1170,48 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
594
1170
  const hint = getOpenClawErrorHint(checkMessage)
595
1171
  return hint ? <p className="mt-1.5 text-[11px] text-text-3">{hint}</p> : null
596
1172
  })()}
1173
+ {providerSuggestedModel && (
1174
+ <p className="mt-1.5 text-[11px] text-text-3">Suggested model: {providerSuggestedModel}</p>
1175
+ )}
1176
+ {provider === 'openclaw' && checkState === 'ok' && openclawDeviceId && (
1177
+ <p className="mt-1.5 text-[11px] text-text-3">
1178
+ Device paired as <code className="text-text-2">{openclawDeviceId.slice(0, 12)}...</code>.
1179
+ </p>
1180
+ )}
1181
+ </div>
1182
+ )}
1183
+
1184
+ {provider === 'openclaw' && checkState === 'error' && checkErrorCode === 'PAIRING_REQUIRED' && (
1185
+ <div className="mb-4 rounded-[12px] border border-emerald-500/20 bg-emerald-500/5 px-4 py-3 text-left">
1186
+ <div className="text-[13px] font-600 text-emerald-300">Awaiting gateway approval</div>
1187
+ <p className="mt-1.5 text-[12px] text-text-3 leading-relaxed">
1188
+ This device is pending approval on that OpenClaw gateway. Approve it from Nodes, then run the connection check again.
1189
+ {openclawDeviceId ? (
1190
+ <> Device: <code className="text-text-2">{openclawDeviceId}</code>.</>
1191
+ ) : null}
1192
+ </p>
1193
+ {openClawDashboardUrl && (
1194
+ <a
1195
+ href={openClawDashboardUrl}
1196
+ target="_blank"
1197
+ rel="noopener noreferrer"
1198
+ className="mt-3 inline-flex items-center gap-1.5 rounded-[10px] border border-white/[0.08] bg-white/[0.03] px-3 py-2 text-[12px] text-text hover:bg-white/[0.06] transition-all duration-200"
1199
+ >
1200
+ Open gateway dashboard
1201
+ </a>
1202
+ )}
1203
+ </div>
1204
+ )}
1205
+
1206
+ {provider === 'openclaw' && checkState === 'error' && checkErrorCode === 'DEVICE_AUTH_INVALID' && (
1207
+ <div className="mb-4 rounded-[12px] border border-white/[0.08] bg-surface px-4 py-3 text-left">
1208
+ <div className="text-[13px] font-600 text-text">Device not paired</div>
1209
+ <p className="mt-1.5 text-[12px] text-text-3 leading-relaxed">
1210
+ The gateway does not recognize this device yet. Add or approve it from Nodes, then retry.
1211
+ {openclawDeviceId ? (
1212
+ <> Device: <code className="text-text-2">{openclawDeviceId}</code>.</>
1213
+ ) : null}
1214
+ </p>
597
1215
  </div>
598
1216
  )}
599
1217
 
@@ -601,7 +1219,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
601
1219
 
602
1220
  <div className="flex items-center justify-center gap-3">
603
1221
  <button
604
- onClick={() => { setStep(0); setError('') }}
1222
+ onClick={() => { resetProviderForm(); setStep('providers') }}
605
1223
  className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
606
1224
  font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
607
1225
  >
@@ -616,171 +1234,248 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
616
1234
  {checkState === 'checking' ? 'Checking...' : 'Check Connection'}
617
1235
  </button>
618
1236
  <button
619
- onClick={saveProviderAndContinue}
1237
+ onClick={saveProvider}
620
1238
  disabled={(requiresKey && !apiKey.trim()) || saving}
621
1239
  className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
622
1240
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
623
1241
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
624
1242
  >
625
- {saving
626
- ? 'Saving...'
627
- : requiresVerifiedConnection
628
- ? 'Verify & Continue'
629
- : 'Save & Continue'}
1243
+ {saving ? 'Saving...' : 'Save Provider'}
630
1244
  </button>
631
1245
  </div>
632
1246
 
633
- <SkipLink
634
- onClick={configuredProviders.length > 0 ? async () => {
635
- await api('PUT', '/settings', { setupCompleted: true })
636
- setStep(3)
637
- } : skip}
638
- label={configuredProviders.length > 0 ? 'Finish Setup' : 'Skip setup for now'}
639
- />
1247
+ <SkipLink onClick={skip} />
640
1248
  </>
641
1249
  )}
642
1250
 
643
- {step === 2 && provider && selectedProvider && (
1251
+ {step === 'agents' && (
644
1252
  <>
645
1253
  <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
646
- Create Your Agent
1254
+ Review Starter Agents
647
1255
  </h1>
648
- <p className="text-[15px] text-text-2 mb-7">
649
- We&apos;ll create a starter agent so you can begin immediately.
1256
+ <p className="text-[15px] text-text-2 mb-2">
1257
+ Choose which agents to start with, then adjust provider and model per agent.
1258
+ </p>
1259
+ <p className="text-[13px] text-text-3 mb-7">
1260
+ These are just starting points. You can edit them later from Agents.
650
1261
  </p>
651
1262
 
652
- <div className="mb-5 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
653
- <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Setup Summary</div>
654
- <div className="text-[14px] text-text mb-1">Provider: {selectedProvider.name}</div>
655
- {supportsEndpoint && endpoint.trim() && (
656
- <div className="text-[12px] font-mono text-text-3 break-all">Endpoint: {endpoint.trim()}</div>
657
- )}
658
- {checkState === 'ok' && (
659
- <div className="mt-2 text-[12px] text-emerald-300">{checkMessage}</div>
660
- )}
661
- {checkState === 'error' && (
662
- <div className="mt-2 text-[12px] text-amber-300">Connection was not verified. You can still continue.</div>
663
- )}
664
- </div>
665
-
666
- <details className="mb-6 text-left rounded-[14px] border border-white/[0.08] bg-surface px-4 py-3">
667
- <summary className="cursor-pointer text-[13px] text-text-2 font-600">
668
- Advanced agent settings (optional)
669
- </summary>
670
- <div className="mt-3 space-y-3">
671
- <div>
672
- <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Name</label>
673
- <input
674
- type="text"
675
- value={agentName}
676
- onChange={(e) => setAgentName(e.target.value)}
677
- className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
678
- text-text text-[14px] outline-none transition-all duration-200
679
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
680
- />
681
- </div>
682
- <div>
683
- <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Description</label>
684
- <input
685
- type="text"
686
- value={agentDescription}
687
- onChange={(e) => setAgentDescription(e.target.value)}
688
- className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
689
- text-text text-[14px] outline-none transition-all duration-200
690
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
691
- />
692
- </div>
693
- <div>
694
- <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">System Prompt</label>
695
- <textarea
696
- value={agentPrompt}
697
- onChange={(e) => setAgentPrompt(e.target.value)}
698
- rows={3}
699
- className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
700
- text-text text-[14px] outline-none transition-all duration-200 resize-none
701
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
702
- />
703
- </div>
704
- <div>
705
- <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Model</label>
706
- <input
707
- type="text"
708
- value={agentModel}
709
- onChange={(e) => setAgentModel(e.target.value)}
710
- className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
711
- text-text text-[14px] font-mono outline-none transition-all duration-200
712
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
713
- />
1263
+ {selectedStarterKit && (
1264
+ <div className="mb-5 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
1265
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Setup Summary</div>
1266
+ <div className="text-[14px] text-text mb-1">{selectedStarterKit.name}</div>
1267
+ <div className="text-[12px] text-text-3">{selectedStarterKit.detail}</div>
1268
+ {!!intentText.trim() && (
1269
+ <div className="mt-3 text-[12px] text-text-2">
1270
+ Intent: <span className="text-text-3">{intentText.trim()}</span>
1271
+ </div>
1272
+ )}
1273
+ <div className="mt-3 text-[12px] text-text-3">
1274
+ Providers ready: {configuredProviders.length || 'none'}
714
1275
  </div>
715
1276
  </div>
716
- </details>
1277
+ )}
1278
+
1279
+ {draftAgents.length === 0 ? (
1280
+ <div className="mb-6 p-6 rounded-[16px] border border-white/[0.08] bg-surface text-left">
1281
+ <div className="text-[16px] font-display font-700 text-text mb-2">Blank workspace selected</div>
1282
+ <p className="text-[13px] text-text-3 leading-relaxed">
1283
+ Finish setup now and create your first provider, agent, task, or project later from inside the app.
1284
+ </p>
1285
+ </div>
1286
+ ) : (
1287
+ <div className="space-y-4 max-h-[46vh] overflow-y-auto pr-1 text-left mb-6">
1288
+ {draftAgents.map((draft) => (
1289
+ <div key={draft.id} className="rounded-[16px] border border-white/[0.08] bg-surface p-4">
1290
+ <div className="flex items-center justify-between gap-4 mb-4">
1291
+ <div>
1292
+ <div className="text-[15px] font-display font-700 text-text">{draft.name}</div>
1293
+ <div className="text-[12px] text-text-3">{draft.description}</div>
1294
+ </div>
1295
+ <label className="inline-flex items-center gap-2 text-[12px] text-text-2">
1296
+ <input
1297
+ type="checkbox"
1298
+ checked={draft.enabled}
1299
+ onChange={(e) => updateDraftAgent(draft.id, { enabled: e.target.checked })}
1300
+ />
1301
+ Start with this agent
1302
+ </label>
1303
+ </div>
1304
+
1305
+ <div className="grid gap-3 md:grid-cols-2">
1306
+ <div>
1307
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Name</label>
1308
+ <input
1309
+ type="text"
1310
+ value={draft.name}
1311
+ onChange={(e) => updateDraftAgent(draft.id, { name: e.target.value })}
1312
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
1313
+ text-text text-[14px] outline-none transition-all duration-200
1314
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1315
+ />
1316
+ </div>
1317
+ <div>
1318
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Provider</label>
1319
+ <select
1320
+ value={draft.providerConfigId || ''}
1321
+ onChange={(e) => updateDraftAgentProvider(draft.id, e.target.value)}
1322
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
1323
+ text-text text-[14px] outline-none transition-all duration-200
1324
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1325
+ >
1326
+ <option value="">Choose provider</option>
1327
+ {configuredProviders.map((configuredProvider) => (
1328
+ <option key={configuredProvider.id} value={configuredProvider.id}>
1329
+ {configuredProvider.name}
1330
+ {configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
1331
+ ? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
1332
+ : ''}
1333
+ {configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
1334
+ </option>
1335
+ ))}
1336
+ </select>
1337
+ </div>
1338
+ <div className="md:col-span-2">
1339
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Description</label>
1340
+ <input
1341
+ type="text"
1342
+ value={draft.description}
1343
+ onChange={(e) => updateDraftAgent(draft.id, { description: e.target.value })}
1344
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
1345
+ text-text text-[14px] outline-none transition-all duration-200
1346
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1347
+ />
1348
+ </div>
1349
+ <div>
1350
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Model</label>
1351
+ <input
1352
+ type="text"
1353
+ value={draft.model}
1354
+ onChange={(e) => updateDraftAgent(draft.id, { model: e.target.value })}
1355
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
1356
+ text-text text-[14px] font-mono outline-none transition-all duration-200
1357
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1358
+ />
1359
+ </div>
1360
+ <div>
1361
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Mode</label>
1362
+ <select
1363
+ value={draft.platformAssignScope}
1364
+ onChange={(e) => updateDraftAgent(draft.id, { platformAssignScope: e.target.value as 'self' | 'all' })}
1365
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
1366
+ text-text text-[14px] outline-none transition-all duration-200
1367
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1368
+ >
1369
+ <option value="self">Focused agent</option>
1370
+ <option value="all">Delegating orchestrator</option>
1371
+ </select>
1372
+ </div>
1373
+ </div>
1374
+
1375
+ <details className="mt-4 rounded-[12px] border border-white/[0.08] bg-bg px-4 py-3">
1376
+ <summary className="cursor-pointer text-[13px] text-text-2 font-600">
1377
+ Prompt and tools
1378
+ </summary>
1379
+ <div className="mt-3 space-y-3">
1380
+ <div>
1381
+ <label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">System Prompt</label>
1382
+ <textarea
1383
+ value={draft.systemPrompt}
1384
+ onChange={(e) => updateDraftAgent(draft.id, { systemPrompt: e.target.value })}
1385
+ rows={5}
1386
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
1387
+ text-text text-[14px] outline-none transition-all duration-200 resize-none
1388
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
1389
+ />
1390
+ </div>
1391
+ <div>
1392
+ <div className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Tools</div>
1393
+ <div className="flex flex-wrap gap-2">
1394
+ {draft.tools.map((tool) => (
1395
+ <span
1396
+ key={tool}
1397
+ className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.04] border border-white/[0.06] text-[11px] text-text-2"
1398
+ >
1399
+ {tool}
1400
+ </span>
1401
+ ))}
1402
+ </div>
1403
+ </div>
1404
+ </div>
1405
+ </details>
1406
+ </div>
1407
+ ))}
1408
+ </div>
1409
+ )}
717
1410
 
718
1411
  {error && <p className="mb-4 text-[13px] text-red-400">{error}</p>}
719
1412
 
720
1413
  <div className="flex items-center justify-center gap-3">
721
1414
  <button
722
- onClick={() => { setStep(1); setError('') }}
1415
+ onClick={() => setStep('providers')}
723
1416
  className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
724
1417
  font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
725
1418
  >
726
1419
  Back
727
1420
  </button>
728
1421
  <button
729
- onClick={() => createAgentAndFinish(true)}
730
- disabled={!agentName.trim() || saving}
1422
+ onClick={() => setStep('providers')}
731
1423
  className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-white/[0.03] text-text text-[14px]
732
- font-display font-600 cursor-pointer hover:bg-white/[0.06] transition-all duration-200 disabled:opacity-40"
1424
+ font-display font-600 cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
733
1425
  >
734
- {saving ? 'Creating...' : 'Create & Add Another'}
1426
+ Add Another Provider
735
1427
  </button>
736
1428
  <button
737
- onClick={() => createAgentAndFinish(false)}
738
- disabled={!agentName.trim() || saving}
1429
+ onClick={draftAgents.length === 0 ? finishWithoutAgents : createAgentsAndFinish}
1430
+ disabled={saving}
739
1431
  className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
740
1432
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
741
1433
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
742
1434
  >
743
- {saving ? 'Creating...' : 'Create & Finish'}
1435
+ {saving
1436
+ ? 'Saving...'
1437
+ : draftAgents.length === 0
1438
+ ? 'Finish Setup'
1439
+ : `Create ${draftAgents.filter((draft) => draft.enabled).length} Agent${draftAgents.filter((draft) => draft.enabled).length === 1 ? '' : 's'}`}
744
1440
  </button>
745
1441
  </div>
746
1442
 
747
- <SkipLink
748
- onClick={configuredProviders.length > 0 ? async () => {
749
- await api('PUT', '/settings', { setupCompleted: true })
750
- setStep(3)
751
- } : skip}
752
- label={configuredProviders.length > 0 ? 'Finish Setup' : 'Skip setup for now'}
753
- />
1443
+ <SkipLink onClick={skip} />
754
1444
  </>
755
1445
  )}
756
1446
 
757
- {step === 3 && (
1447
+ {step === 'done' && (
758
1448
  <>
759
1449
  <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
760
1450
  You&apos;re All Set
761
1451
  </h1>
762
1452
  <p className="text-[15px] text-text-2 mb-7">
763
- {configuredProviders.length === 1
764
- ? 'Your agent is ready to chat.'
765
- : `${configuredProviders.length} agents created and ready to chat.`}
1453
+ {createdAgents.length === 0
1454
+ ? 'Your workspace is ready. Add providers and agents whenever you want.'
1455
+ : createdAgents.length === 1
1456
+ ? 'Your starter agent is ready to chat.'
1457
+ : `${createdAgents.length} starter agents are ready to go.`}
766
1458
  </p>
767
1459
 
768
- {configuredProviders.length > 0 && (
1460
+ {createdAgents.length > 0 && (
769
1461
  <div className="mb-6 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
770
1462
  <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">Agents Created</div>
771
1463
  <div className="space-y-2">
772
- {configuredProviders.map((cp) => {
773
- const meta = SETUP_PROVIDERS.find((p) => p.id === cp.provider)
1464
+ {createdAgents.map((agent) => {
1465
+ const meta = SETUP_PROVIDERS.find((candidate) => candidate.id === agent.provider)
774
1466
  return (
775
- <div key={cp.provider} className="flex items-center gap-3">
1467
+ <div key={agent.id} className="flex items-center gap-3">
776
1468
  <div className="w-8 h-8 rounded-[8px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
777
1469
  <span className="text-[13px] font-display font-700 text-accent-bright">
778
1470
  {meta?.icon || '?'}
779
1471
  </span>
780
1472
  </div>
781
1473
  <div>
782
- <div className="text-[14px] text-text font-500">{cp.agentName}</div>
783
- <div className="text-[12px] text-text-3">{meta?.name || cp.provider}</div>
1474
+ <div className="text-[14px] text-text font-500">{agent.name}</div>
1475
+ <div className="text-[12px] text-text-3">
1476
+ {agent.providerName}
1477
+ {agent.providerName !== (meta?.name || agent.provider) ? ` · ${meta?.name || agent.provider}` : ''}
1478
+ </div>
784
1479
  </div>
785
1480
  </div>
786
1481
  )
@@ -790,17 +1485,17 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
790
1485
  )}
791
1486
 
792
1487
  <div className="mb-8 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
793
- <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">Coming Soon — Connectors</div>
1488
+ <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">Next Up — Connectors</div>
794
1489
  <p className="text-[13px] text-text-2 mb-3">
795
- Bridge your agents to chat platforms. Set this up anytime from Connectors.
1490
+ Bridge your agents to chat platforms any time from Connectors.
796
1491
  </p>
797
1492
  <div className="flex gap-3">
798
- {CONNECTOR_ICONS.map((c) => (
799
- <div key={c.name} className="flex flex-col items-center gap-1.5">
1493
+ {CONNECTOR_ICONS.map((connector) => (
1494
+ <div key={connector.name} className="flex flex-col items-center gap-1.5">
800
1495
  <div className="w-10 h-10 rounded-[10px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center">
801
- <span className="text-[14px] font-display font-600 text-text-3">{c.icon}</span>
1496
+ <span className="text-[14px] font-display font-600 text-text-3">{connector.icon}</span>
802
1497
  </div>
803
- <span className="text-[10px] text-text-3">{c.name}</span>
1498
+ <span className="text-[10px] text-text-3">{connector.name}</span>
804
1499
  </div>
805
1500
  ))}
806
1501
  </div>