@swarmclawai/swarmclaw 1.7.0 → 1.7.2

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 (42) hide show
  1. package/README.md +25 -9
  2. package/bin/swarmclaw.js +87 -0
  3. package/electron-dist/main.js +218 -0
  4. package/package.json +2 -2
  5. package/scripts/run-next-build.mjs +1 -1
  6. package/src/app/api/setup/check-provider/route.ts +5 -62
  7. package/src/app/api/setup/doctor/route.ts +19 -9
  8. package/src/app/home/page.tsx +19 -10
  9. package/src/cli/index.js +8 -2
  10. package/src/cli/index.ts +12 -3
  11. package/src/components/agents/inspector-panel.tsx +25 -3
  12. package/src/components/auth/setup-wizard/index.tsx +6 -2
  13. package/src/components/auth/setup-wizard/step-next.tsx +46 -39
  14. package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
  15. package/src/components/auth/setup-wizard/types.ts +5 -2
  16. package/src/components/auth/setup-wizard/utils.test.ts +0 -19
  17. package/src/components/auth/setup-wizard/utils.ts +0 -69
  18. package/src/components/chat/chat-card.tsx +5 -0
  19. package/src/components/home/home-launchpad.tsx +123 -71
  20. package/src/components/layout/update-banner.tsx +43 -9
  21. package/src/lib/home-launchpad.test.ts +1 -31
  22. package/src/lib/home-launchpad.ts +0 -58
  23. package/src/lib/provider-sets.test.ts +19 -0
  24. package/src/lib/provider-sets.ts +8 -3
  25. package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
  26. package/src/lib/providers/cli-provider-metadata.ts +208 -0
  27. package/src/lib/providers/cli-utils.test.ts +65 -1
  28. package/src/lib/providers/cli-utils.ts +26 -44
  29. package/src/lib/providers/codex-cli.ts +71 -75
  30. package/src/lib/providers/generic-cli.ts +2 -31
  31. package/src/lib/providers/index.ts +14 -44
  32. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
  33. package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
  34. package/src/lib/server/cli-provider-readiness.test.ts +45 -0
  35. package/src/lib/server/cli-provider-readiness.ts +84 -0
  36. package/src/lib/server/provider-health.test.ts +6 -0
  37. package/src/lib/server/provider-health.ts +2 -2
  38. package/src/lib/setup-defaults.test.ts +8 -0
  39. package/src/lib/setup-defaults.ts +38 -178
  40. package/src/stores/slices/session-slice.test.ts +40 -2
  41. package/src/stores/slices/session-slice.ts +41 -1
  42. package/tsconfig.json +1 -0
package/src/cli/index.ts CHANGED
@@ -45,9 +45,18 @@ function resolveDefaultAccessKey(cwd: string = process.cwd()): string {
45
45
  ).trim()
46
46
  if (envKey) return envKey
47
47
 
48
- const keyFile = path.join(cwd, 'platform-api-key.txt')
49
- if (!fs.existsSync(keyFile)) return ''
50
- return fs.readFileSync(keyFile, 'utf8').trim()
48
+ const keyLocations = [
49
+ path.join(cwd, 'platform-api-key.txt'),
50
+ ]
51
+ const serviceHome = (process.env.SWARMCLAW_HOME || '').trim()
52
+ if (serviceHome) keyLocations.push(path.join(serviceHome, 'platform-api-key.txt'))
53
+
54
+ for (const keyFile of keyLocations) {
55
+ if (!fs.existsSync(keyFile)) continue
56
+ const value = fs.readFileSync(keyFile, 'utf8').trim()
57
+ if (value) return value
58
+ }
59
+ return ''
51
60
  }
52
61
 
53
62
  const DEFAULT_ACCESS_KEY = resolveDefaultAccessKey()
@@ -4,9 +4,9 @@ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults
4
4
  import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
5
5
  import type { Agent, MemoryEntry, Session } from '@/types'
6
6
  import { useAppStore } from '@/stores/use-app-store'
7
+ import { selectActiveSessionId } from '@/stores/slices/session-slice'
7
8
  import { useChatStore } from '@/stores/use-chat-store'
8
9
  import { api } from '@/lib/app/api-client'
9
- import { sortSessionsNewestFirst } from '@/lib/chat/new-session'
10
10
  import { AgentAvatar } from './agent-avatar'
11
11
  import { AgentFilesEditor } from './agent-files-editor'
12
12
  import { OpenClawSkillsPanel } from './openclaw-skills-panel'
@@ -901,6 +901,7 @@ function QuickActionsSection({ agent, session }: { agent: Agent; session: Sessio
901
901
 
902
902
  function SessionsSection({ agent }: { agent: Agent }) {
903
903
  const sessions = useAppStore((s) => s.sessions)
904
+ const activeSessionId = useAppStore(selectActiveSessionId)
904
905
  const connectors = useAppStore((s) => s.connectors)
905
906
  const agents = useAppStore((s) => s.agents)
906
907
  const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
@@ -908,7 +909,19 @@ function SessionsSection({ agent }: { agent: Agent }) {
908
909
  const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
909
910
 
910
911
  const agentSessions = useMemo(() => {
911
- return sortSessionsNewestFirst(Object.values(sessions).filter((s) => s.agentId === agent.id))
912
+ const getLastMessageTime = (session: Session): number => {
913
+ const summaryTime = session.lastMessageSummary?.time
914
+ if (typeof summaryTime === 'number' && Number.isFinite(summaryTime)) return summaryTime
915
+ if (Array.isArray(session.messages) && session.messages.length > 0) {
916
+ const last = session.messages[session.messages.length - 1]
917
+ if (typeof last?.time === 'number' && Number.isFinite(last.time)) return last.time
918
+ }
919
+ return session.lastActiveAt || session.createdAt || 0
920
+ }
921
+
922
+ return Object.values(sessions)
923
+ .filter((s) => s.agentId === agent.id)
924
+ .sort((left, right) => getLastMessageTime(right) - getLastMessageTime(left))
912
925
  }, [sessions, agent.id])
913
926
 
914
927
  if (agentSessions.length === 0) return null
@@ -918,6 +931,7 @@ function SessionsSection({ agent }: { agent: Agent }) {
918
931
  <SectionLabel>Sessions ({agentSessions.length})</SectionLabel>
919
932
  <div className="flex flex-col gap-1.5">
920
933
  {agentSessions.map((s) => {
934
+ const isSelected = s.id === activeSessionId
921
935
  const connector = getSessionConnector(s, connectors)
922
936
  const delegatedByAgentId = (s as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
923
937
  const delegatedBy = delegatedByAgentId ? agents[delegatedByAgentId] : null
@@ -934,7 +948,10 @@ function SessionsSection({ agent }: { agent: Agent }) {
934
948
  }
935
949
  }).catch(() => {})
936
950
  }}
937
- className="flex items-center gap-2 w-full py-1.5 px-2 rounded-[8px] bg-transparent border-none cursor-pointer hover:bg-white/[0.04] transition-colors text-left"
951
+ className={`flex items-center gap-2 w-full py-1.5 px-2 rounded-[8px] border-none cursor-pointer transition-colors text-left
952
+ ${isSelected
953
+ ? 'bg-accent-soft/70 ring-1 ring-accent-bright/25'
954
+ : 'bg-transparent hover:bg-white/[0.04]'}`}
938
955
  >
939
956
  {connector ? (
940
957
  <ConnectorPlatformIcon platform={connector.platform} size={14} />
@@ -944,6 +961,11 @@ function SessionsSection({ agent }: { agent: Agent }) {
944
961
  </svg>
945
962
  )}
946
963
  <span className="text-[12px] text-text-2 truncate flex-1">{s.name}</span>
964
+ {isSelected && (
965
+ <span className="text-[9px] font-700 uppercase tracking-[0.08em] text-accent-bright bg-accent-bright/15 px-1.5 py-0.5 rounded-[6px] shrink-0">
966
+ Selected
967
+ </span>
968
+ )}
947
969
  {delegatedBy && (
948
970
  <span className="text-[9px] text-amber-300/60 font-600 shrink-0">from {delegatedBy.name}</span>
949
971
  )}
@@ -11,7 +11,7 @@ import {
11
11
  type OnboardingPath,
12
12
  type SetupProvider,
13
13
  } from '@/lib/setup-defaults'
14
- import { resolveLaunchPathHref } from '@/lib/home-launchpad'
14
+ import { DEFAULT_BUILDER_ROUTE } from '@/lib/home-launchpad'
15
15
  import type {
16
16
  SetupStep,
17
17
  SetupWizardProps,
@@ -482,7 +482,11 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
482
482
  <StepNext
483
483
  createdAgents={createdAgents}
484
484
  onContinueToDashboard={() => finishSetup('/home')}
485
- onLaunchPathAction={(id, action) => finishSetup(resolveLaunchPathHref(id, action, createdAgents[0]?.id))}
485
+ onOpenFirstAgent={() => finishSetup(createdAgents[0]?.id ? `/agents/${encodeURIComponent(createdAgents[0].id)}` : '/agents')}
486
+ onOpenProtocols={() => finishSetup('/protocols')}
487
+ onOpenBuilder={() => finishSetup(DEFAULT_BUILDER_ROUTE)}
488
+ onOpenConnectors={() => finishSetup('/connectors')}
489
+ onOpenUsage={() => finishSetup('/usage')}
486
490
  />
487
491
  )}
488
492
 
@@ -1,48 +1,19 @@
1
1
  'use client'
2
2
 
3
- import { getLaunchPathCards, type LaunchPathAction, type LaunchPathCardCopy, type LaunchPathId } from '@/lib/home-launchpad'
3
+ import { LaunchActionCard } from '@/components/shared/launch-action-card'
4
4
  import type { StepNextProps } from './types'
5
5
  import { StepShell } from './shared'
6
6
 
7
- function PathCard({
8
- card,
9
- onAction,
10
- }: {
11
- card: LaunchPathCardCopy
12
- onAction: (id: LaunchPathId, action: LaunchPathAction) => void
13
- }) {
14
- return (
15
- <div className="flex min-h-[220px] flex-col rounded-[18px] border border-white/[0.07] bg-white/[0.03] p-5 text-left">
16
- <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{card.kicker}</div>
17
- <div className="mt-3 text-[18px] font-display font-700 tracking-normal text-text">{card.title}</div>
18
- <p className="mt-2 flex-1 text-[13px] leading-relaxed text-text-3/72">{card.description}</p>
19
- <div className="mt-5 flex flex-wrap gap-2">
20
- <button
21
- type="button"
22
- onClick={() => onAction(card.id, 'primary')}
23
- className="rounded-[10px] bg-accent-bright px-3.5 py-2 text-[12px] font-display font-700 text-black transition-opacity hover:opacity-90"
24
- >
25
- {card.primaryLabel}
26
- </button>
27
- <button
28
- type="button"
29
- onClick={() => onAction(card.id, 'secondary')}
30
- className="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3.5 py-2 text-[12px] font-display font-700 text-text-2 transition-colors hover:bg-white/[0.08]"
31
- >
32
- {card.secondaryLabel}
33
- </button>
34
- </div>
35
- </div>
36
- )
37
- }
38
-
39
7
  export function StepNext({
40
8
  createdAgents,
41
9
  onContinueToDashboard,
42
- onLaunchPathAction,
10
+ onOpenFirstAgent,
11
+ onOpenProtocols,
12
+ onOpenBuilder,
13
+ onOpenConnectors,
14
+ onOpenUsage,
43
15
  }: StepNextProps) {
44
16
  const firstAgent = createdAgents[0] || null
45
- const launchPathCards = getLaunchPathCards({ firstAgentName: firstAgent?.name })
46
17
 
47
18
  return (
48
19
  <StepShell wide>
@@ -58,10 +29,46 @@ export function StepNext({
58
29
  : 'You finished setup without starter agents, so the launch options below focus on wiring up the rest of the workspace.'}
59
30
  </p>
60
31
 
61
- <div className="grid gap-3 xl:grid-cols-3 mb-8">
62
- {launchPathCards.map((card) => (
63
- <PathCard key={card.id} card={card} onAction={onLaunchPathAction} />
64
- ))}
32
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 mb-8">
33
+ <LaunchActionCard
34
+ title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
35
+ description={firstAgent
36
+ ? `Jump straight into ${firstAgent.name} and start working from the workspace you just created.`
37
+ : 'Open the agents workspace so you can create or tune the first agent manually.'}
38
+ actionLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
39
+ onClick={onOpenFirstAgent}
40
+ tone="primary"
41
+ />
42
+ <LaunchActionCard
43
+ title="Start Structured Session"
44
+ description="Open bounded collaboration runs for reviews, planning rounds, decision-making, or focused multi-agent work."
45
+ actionLabel="Open Protocols"
46
+ onClick={onOpenProtocols}
47
+ />
48
+ <LaunchActionCard
49
+ title="Open Workflow Builder"
50
+ description="Jump into the visual protocol builder if you want a reusable orchestration graph instead of a one-off run."
51
+ actionLabel="Open Builder"
52
+ onClick={onOpenBuilder}
53
+ />
54
+ <LaunchActionCard
55
+ title="Connect a Platform"
56
+ description="Bridge agents into Discord, Slack, Telegram, WhatsApp, or other runtime connectors."
57
+ actionLabel="Open Connectors"
58
+ onClick={onOpenConnectors}
59
+ />
60
+ <LaunchActionCard
61
+ title="Review Usage"
62
+ description="Inspect cost, provider health, and agent activity so the workspace stays observable from day one."
63
+ actionLabel="Open Usage"
64
+ onClick={onOpenUsage}
65
+ />
66
+ <LaunchActionCard
67
+ title="Go to Dashboard"
68
+ description="Land on the main home view. Fresh workspaces open in guided launch mode before switching to the normal ops dashboard."
69
+ actionLabel="Open Home"
70
+ onClick={onContinueToDashboard}
71
+ />
65
72
  </div>
66
73
 
67
74
  <button
@@ -3,89 +3,10 @@
3
3
  import { useState } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { errorMessage } from '@/lib/shared-utils'
6
+ import { SETUP_PROVIDERS } from '@/lib/setup-defaults'
6
7
  import type { StepProvidersProps, SetupDoctorResponse } from './types'
7
- import { getSetupProviderGroups } from './utils'
8
8
  import { StepShell, ConfiguredProviderChips } from './shared'
9
9
 
10
- function ReadinessPanel({
11
- state,
12
- report,
13
- error,
14
- onRun,
15
- }: {
16
- state: 'idle' | 'checking' | 'done' | 'error'
17
- report: SetupDoctorResponse | null
18
- error: string
19
- onRun: () => void
20
- }) {
21
- const problemChecks = report?.checks.filter((check) => check.status !== 'pass') || []
22
- const statusLabel = state === 'done'
23
- ? report?.ok
24
- ? 'Ready'
25
- : `${problemChecks.length} item${problemChecks.length === 1 ? '' : 's'} to review`
26
- : state === 'error'
27
- ? 'Check failed'
28
- : 'Optional check'
29
-
30
- return (
31
- <div className="mb-6 rounded-[16px] border border-white/[0.08] bg-white/[0.025] p-4 text-left">
32
- <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
33
- <div>
34
- <div className="text-[12px] font-display font-700 text-text">Setup readiness</div>
35
- <p className="mt-1 text-[12px] leading-relaxed text-text-3/70">
36
- Check Node, npm, local storage, secrets, and starter workspace state before connecting a provider.
37
- </p>
38
- </div>
39
- <button
40
- type="button"
41
- onClick={onRun}
42
- disabled={state === 'checking'}
43
- className="shrink-0 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-display font-700 text-text-2 transition-colors hover:bg-white/[0.08] disabled:opacity-40"
44
- >
45
- {state === 'checking' ? 'Checking...' : 'Run Check'}
46
- </button>
47
- </div>
48
-
49
- <div className="mt-3 flex flex-wrap items-center gap-2">
50
- <span className={`rounded-full px-2.5 py-1 text-[11px] font-700 ${
51
- state === 'done' && report?.ok
52
- ? 'bg-emerald-500/10 text-emerald-300'
53
- : state === 'done' || state === 'error'
54
- ? 'bg-amber-500/10 text-amber-300'
55
- : 'bg-white/[0.05] text-text-3'
56
- }`}>
57
- {statusLabel}
58
- </span>
59
- {report && (
60
- <span className="text-[11px] text-text-3/65">
61
- {report.summary}
62
- </span>
63
- )}
64
- {state === 'error' && error && (
65
- <span className="text-[11px] text-rose-300">{error}</span>
66
- )}
67
- </div>
68
-
69
- {problemChecks.length > 0 && (
70
- <div className="mt-3 grid gap-2 sm:grid-cols-2">
71
- {problemChecks.slice(0, 4).map((check) => (
72
- <div key={check.id} className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-2">
73
- <div className="text-[11px] font-700 text-text-2">{check.label}</div>
74
- <div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{check.detail}</div>
75
- </div>
76
- ))}
77
- </div>
78
- )}
79
-
80
- {!!report?.actions?.length && (
81
- <div className="mt-3 text-[11px] leading-relaxed text-text-3/70">
82
- Next: {report.actions.slice(0, 2).join(' ')}
83
- </div>
84
- )}
85
- </div>
86
- )
87
- }
88
-
89
10
  export function StepProviders({
90
11
  configuredProviders,
91
12
  configuredProviderIds,
@@ -97,10 +18,10 @@ export function StepProviders({
97
18
  onContinue,
98
19
  onSkip,
99
20
  }: StepProvidersProps) {
21
+ const [providerSearch, setProviderSearch] = useState('')
100
22
  const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
101
23
  const [doctorError, setDoctorError] = useState('')
102
24
  const [doctorReport, setDoctorReport] = useState<SetupDoctorResponse | null>(null)
103
- const providerGroups = getSetupProviderGroups()
104
25
 
105
26
  const runSetupDoctor = async () => {
106
27
  setDoctorState('checking')
@@ -116,6 +37,23 @@ export function StepProviders({
116
37
  }
117
38
  }
118
39
 
40
+ const normalizedSearch = providerSearch.trim().toLowerCase()
41
+ const visibleProviders = SETUP_PROVIDERS.filter((candidate) => {
42
+ if (!normalizedSearch) return true
43
+ return [
44
+ candidate.name,
45
+ candidate.description,
46
+ candidate.badge || '',
47
+ candidate.id,
48
+ ].some((part) => part.toLowerCase().includes(normalizedSearch))
49
+ })
50
+ const providerGroups = [
51
+ { id: 'cli', label: 'CLI Agents', items: visibleProviders.filter((candidate) => candidate.category === 'cli') },
52
+ { id: 'gateway', label: 'Gateways and Local Runtimes', items: visibleProviders.filter((candidate) => candidate.category === 'gateway' || candidate.category === 'local') },
53
+ { id: 'api', label: 'API Providers', items: visibleProviders.filter((candidate) => !candidate.category || candidate.category === 'api') },
54
+ { id: 'custom', label: 'Custom', items: visibleProviders.filter((candidate) => candidate.category === 'custom') },
55
+ ].filter((group) => group.items.length > 0)
56
+
119
57
  return (
120
58
  <StepShell>
121
59
  <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
@@ -130,70 +68,105 @@ export function StepProviders({
130
68
 
131
69
  <ConfiguredProviderChips providers={configuredProviders} onRemove={onRemoveProvider} />
132
70
 
133
- <ReadinessPanel
134
- state={doctorState}
135
- report={doctorReport}
136
- error={doctorError}
137
- onRun={runSetupDoctor}
71
+ <input
72
+ type="search"
73
+ value={providerSearch}
74
+ onChange={(e) => setProviderSearch(e.target.value)}
75
+ placeholder="Search providers, CLIs, or runtimes..."
76
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface text-text text-[13px]
77
+ outline-none transition-all duration-200 placeholder:text-text-3/50 focus:border-accent-bright/30 mb-4"
138
78
  />
139
79
 
140
- <div className="max-h-[46vh] overflow-y-auto pr-1 text-left">
141
- <div className="flex flex-col gap-5">
142
- {providerGroups.map((group) => (
143
- <section key={group.id}>
144
- <div className="mb-2 px-1">
145
- <div className="text-[12px] font-display font-700 text-text">{group.title}</div>
146
- <p className="mt-0.5 text-[11px] leading-relaxed text-text-3/65">{group.description}</p>
147
- </div>
148
- <div className="flex flex-col gap-3">
149
- {group.providers.map((candidate) => {
150
- const isConfigured = configuredProviderIds.has(candidate.id)
151
- return (
152
- <button
153
- key={candidate.id}
154
- onClick={() => onSelectProvider(candidate.id)}
155
- className={`w-full px-5 py-4 rounded-[14px] border bg-surface text-left
156
- transition-all duration-200 flex items-start gap-4 cursor-pointer
157
- ${isConfigured
158
- ? 'border-emerald-500/25 hover:border-emerald-500/40 hover:bg-surface-hover'
159
- : 'border-white/[0.08] hover:border-accent-bright/30 hover:bg-surface-hover'
160
- }`}
161
- >
162
- <div className={`w-10 h-10 rounded-[10px] border flex items-center justify-center shrink-0 mt-0.5 ${
163
- isConfigured ? 'bg-emerald-500/10 border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'
164
- }`}>
165
- <span className={`text-[16px] font-display font-700 ${isConfigured ? 'text-emerald-400' : 'text-accent-bright'}`}>
166
- {candidate.icon}
80
+ <div className="flex flex-col gap-3 max-h-[42vh] overflow-y-auto pr-1">
81
+ {providerGroups.map((group) => (
82
+ <div key={group.id} className="space-y-2">
83
+ <div className="px-1 text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/70">
84
+ {group.label}
85
+ </div>
86
+ {group.items.map((candidate) => {
87
+ const isConfigured = configuredProviderIds.has(candidate.id)
88
+ return (
89
+ <button
90
+ key={candidate.id}
91
+ onClick={() => onSelectProvider(candidate.id)}
92
+ className={`w-full px-5 py-4 rounded-[14px] border bg-surface text-left
93
+ transition-all duration-200 flex items-start gap-4 cursor-pointer
94
+ ${isConfigured
95
+ ? 'border-emerald-500/25 hover:border-emerald-500/40 hover:bg-surface-hover'
96
+ : 'border-white/[0.08] hover:border-accent-bright/30 hover:bg-surface-hover'
97
+ }`}
98
+ >
99
+ <div className={`w-10 h-10 rounded-[10px] border flex items-center justify-center shrink-0 mt-0.5 ${
100
+ isConfigured ? 'bg-emerald-500/10 border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'
101
+ }`}>
102
+ <span className={`text-[16px] font-display font-700 ${isConfigured ? 'text-emerald-400' : 'text-accent-bright'}`}>
103
+ {candidate.icon}
104
+ </span>
105
+ </div>
106
+ <div className="flex-1">
107
+ <div className="text-[15px] font-display font-600 text-text mb-1">
108
+ {candidate.name}
109
+ {isConfigured ? (
110
+ <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">
111
+ Connected · Edit
167
112
  </span>
113
+ ) : candidate.badge ? (
114
+ <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">
115
+ {candidate.badge}
116
+ </span>
117
+ ) : null}
118
+ </div>
119
+ <div className="text-[13px] text-text-3 leading-relaxed">{candidate.description}</div>
120
+ {!candidate.requiresKey && !isConfigured && (
121
+ <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">
122
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
123
+ No API key required
168
124
  </div>
169
- <div className="flex-1">
170
- <div className="text-[15px] font-display font-600 text-text mb-1">
171
- {candidate.name}
172
- {isConfigured ? (
173
- <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">
174
- Connected · Edit
175
- </span>
176
- ) : candidate.badge ? (
177
- <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">
178
- {candidate.badge}
179
- </span>
180
- ) : null}
181
- </div>
182
- <div className="text-[13px] text-text-3 leading-relaxed">{candidate.description}</div>
183
- {!candidate.requiresKey && !isConfigured && (
184
- <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">
185
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
186
- No API key required
187
- </div>
188
- )}
189
- </div>
190
- </button>
191
- )
192
- })}
125
+ )}
126
+ </div>
127
+ </button>
128
+ )
129
+ })}
130
+ </div>
131
+ ))}
132
+ {providerGroups.length === 0 && (
133
+ <div className="px-5 py-6 rounded-[14px] border border-white/[0.08] bg-surface text-center text-[13px] text-text-3">
134
+ No providers match that search.
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ <div className="mt-4 text-left">
140
+ <button
141
+ onClick={runSetupDoctor}
142
+ disabled={doctorState === 'checking'}
143
+ className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-white/[0.02] text-[13px] text-text-2
144
+ cursor-pointer hover:bg-white/[0.05] transition-all duration-200 disabled:opacity-40"
145
+ >
146
+ {doctorState === 'checking' ? 'Running System Check...' : 'Run System Check'}
147
+ </button>
148
+
149
+ {doctorState === 'error' && doctorError && (
150
+ <p className="mt-2 text-[12px] text-red-300">{doctorError}</p>
151
+ )}
152
+
153
+ {doctorReport && doctorState === 'done' && (
154
+ <div className="mt-3 p-3 rounded-[12px] border border-white/[0.08] bg-surface">
155
+ <div className={`text-[12px] font-600 ${doctorReport.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
156
+ {doctorReport.summary}
157
+ </div>
158
+ {doctorReport.checks.filter((check) => check.status !== 'pass').slice(0, 3).map((check) => (
159
+ <div key={check.id} className="mt-1 text-[11px] text-text-3">
160
+ - {check.label}: {check.detail}
193
161
  </div>
194
- </section>
195
- ))}
196
- </div>
162
+ ))}
163
+ {!!doctorReport.actions?.length && (
164
+ <div className="mt-2 text-[11px] text-text-3/80">
165
+ Next: {doctorReport.actions.slice(0, 2).join(' ')}
166
+ </div>
167
+ )}
168
+ </div>
169
+ )}
197
170
  </div>
198
171
 
199
172
  {error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
@@ -1,6 +1,5 @@
1
1
  import type { GatewayProfile, ProviderId } from '@/types'
2
2
  import type { SetupProvider } from '@/lib/setup-defaults'
3
- import type { LaunchPathAction, LaunchPathId } from '@/lib/home-launchpad'
4
3
 
5
4
  export type SetupStep = 'profile' | 'path' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
6
5
  export type CheckState = 'idle' | 'checking' | 'ok' | 'error'
@@ -163,7 +162,11 @@ export interface StepAgentsProps {
163
162
  export interface StepNextProps {
164
163
  createdAgents: CreatedAgentSummary[]
165
164
  onContinueToDashboard: () => void
166
- onLaunchPathAction: (id: LaunchPathId, action: LaunchPathAction) => void
165
+ onOpenFirstAgent: () => void
166
+ onOpenProtocols: () => void
167
+ onOpenBuilder: () => void
168
+ onOpenConnectors: () => void
169
+ onOpenUsage: () => void
167
170
  }
168
171
 
169
172
  export interface StepDoneProps {
@@ -8,7 +8,6 @@ import {
8
8
  isLocalOpenClawEndpoint,
9
9
  resolveOpenClawDashboardUrl,
10
10
  getOpenClawErrorHint,
11
- getSetupProviderGroups,
12
11
  requiresSetupProviderVerification,
13
12
  withHttpScheme,
14
13
  buildStarterDrafts,
@@ -77,24 +76,6 @@ test('getStarterKitsForPath: manual keeps the full catalog', () => {
77
76
  assert.equal(ids.has('openclaw_fleet'), true)
78
77
  })
79
78
 
80
- test('getSetupProviderGroups puts fast local and no-key providers first', () => {
81
- const groups = getSetupProviderGroups()
82
- assert.deepEqual(groups.map((group) => group.id), ['fast-local', 'recommended-api', 'advanced-catalog'])
83
- assert.equal(groups[0]?.providers[0]?.id, 'claude-cli')
84
- assert.equal(groups[0]?.providers.some((provider) => provider.id === 'openclaw'), true)
85
- assert.equal(groups[0]?.providers.some((provider) => provider.id === 'ollama'), true)
86
- })
87
-
88
- test('getSetupProviderGroups separates recommended APIs from advanced catalogs', () => {
89
- const groups = getSetupProviderGroups()
90
- const recommendedIds = new Set(groups.find((group) => group.id === 'recommended-api')?.providers.map((provider) => provider.id))
91
- const advancedIds = new Set(groups.find((group) => group.id === 'advanced-catalog')?.providers.map((provider) => provider.id))
92
- assert.equal(recommendedIds.has('openai'), true)
93
- assert.equal(recommendedIds.has('openrouter'), true)
94
- assert.equal(advancedIds.has('custom'), true)
95
- assert.equal(advancedIds.has('deepseek'), true)
96
- })
97
-
98
79
  // ---------------------------------------------------------------------------
99
80
  // formatEndpointHost
100
81
  // ---------------------------------------------------------------------------
@@ -1,47 +1,14 @@
1
1
  import {
2
- SETUP_PROVIDERS,
3
2
  STARTER_KITS,
4
3
  getDefaultModelForProvider,
5
4
  type OnboardingPath,
6
5
  type StarterKit,
7
- type SetupProviderOption,
8
6
  type SetupProvider,
9
7
  type StarterKitAgentTemplate,
10
8
  } from '@/lib/setup-defaults'
11
9
  import type { ConfiguredProvider, SetupStep, StarterDraftAgent } from './types'
12
10
  import { STEP_ORDER } from './types'
13
11
 
14
- export type SetupProviderGroupId = 'fast-local' | 'recommended-api' | 'advanced-catalog'
15
-
16
- export interface SetupProviderGroup {
17
- id: SetupProviderGroupId
18
- title: string
19
- description: string
20
- providers: SetupProviderOption[]
21
- }
22
-
23
- const FAST_LOCAL_PROVIDER_IDS = new Set<SetupProvider>([
24
- 'claude-cli',
25
- 'codex-cli',
26
- 'opencode-cli',
27
- 'gemini-cli',
28
- 'copilot-cli',
29
- 'droid-cli',
30
- 'cursor-cli',
31
- 'qwen-code-cli',
32
- 'goose',
33
- 'ollama',
34
- 'openclaw',
35
- 'hermes',
36
- ])
37
-
38
- const RECOMMENDED_API_PROVIDER_IDS = new Set<SetupProvider>([
39
- 'openai',
40
- 'openrouter',
41
- 'anthropic',
42
- 'google',
43
- ])
44
-
45
12
  export function stepIndex(step: SetupStep): number {
46
13
  if (step === 'connect') return STEP_ORDER.indexOf('providers')
47
14
  return STEP_ORDER.indexOf(step)
@@ -71,42 +38,6 @@ export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
71
38
  return STARTER_KITS
72
39
  }
73
40
 
74
- export function getSetupProviderGroups(providers: SetupProviderOption[] = SETUP_PROVIDERS): SetupProviderGroup[] {
75
- const groups: SetupProviderGroup[] = [
76
- {
77
- id: 'fast-local',
78
- title: 'Fast local and no-key starts',
79
- description: 'Use an installed CLI, a local runtime, or an existing OpenClaw/Hermes gateway.',
80
- providers: [],
81
- },
82
- {
83
- id: 'recommended-api',
84
- title: 'Recommended API providers',
85
- description: 'Good first choices for cloud-backed chats, agents, and workflow runs.',
86
- providers: [],
87
- },
88
- {
89
- id: 'advanced-catalog',
90
- title: 'Advanced catalog and custom endpoints',
91
- description: 'Specialized model catalogs, OpenAI-compatible servers, and provider-specific setups.',
92
- providers: [],
93
- },
94
- ]
95
- const byId = new Map(groups.map((group) => [group.id, group]))
96
-
97
- for (const provider of providers) {
98
- if (FAST_LOCAL_PROVIDER_IDS.has(provider.id)) {
99
- byId.get('fast-local')!.providers.push(provider)
100
- } else if (RECOMMENDED_API_PROVIDER_IDS.has(provider.id)) {
101
- byId.get('recommended-api')!.providers.push(provider)
102
- } else {
103
- byId.get('advanced-catalog')!.providers.push(provider)
104
- }
105
- }
106
-
107
- return groups.filter((group) => group.providers.length > 0)
108
- }
109
-
110
41
  export function applyIntentContext(prompt: string, intentText: string): string {
111
42
  const trimmed = intentText.trim()
112
43
  if (!trimmed) return prompt