@swarmclawai/swarmclaw 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -399,15 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
- ### v1.6.1 Highlights
402
+ ### v1.7.0 Highlights
403
403
 
404
- Follow-up release for v1.6 with workflow starts, safer metadata handling, A2A discovery polish, and [#61](https://github.com/swarmclawai/swarmclaw/pull/61) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
404
+ Extended CLI provider roster, first-run polish, and a batch of v1.6 follow-ups including [#61](https://github.com/swarmclawai/swarmclaw/pull/61) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
405
405
 
406
+ **Coding-agent reach**
407
+
408
+ - **31 new CLI providers.** Aider, Amp, Augment, AdaL, IBM Bob, Cline, CodeBuddy, Command Code, Continue, Cortex Code, Crush, Deep Agents, Firebender, iFlow, Junie, Kilo Code, Kimi, Kode, MCPJam, Mistral Vibe, Mux, Neovate, OpenHands, Pochi, Qoder, Replit Agent, Roo Code, TRAE CN, Warp Agent, Windsurf, and Zencoder are now first-class provider IDs in `ProviderType`, matching the SwarmSkills roster.
409
+ - **Generic CLI streamer.** New `streamGenericCliChat` (`src/lib/providers/generic-cli.ts`) spawns the configured binary with the prompt as final argv and emits stdout lines as SSE deltas. Used by the new providers when no bespoke parser is available; existing bespoke parsers (Claude, Codex, Cursor, Gemini, Copilot, Droid, Qwen, OpenCode, Goose) are untouched.
410
+ - **Capability metadata.** `CLI_PROVIDER_CAPABILITIES` (`src/lib/providers/cli-utils.ts`) carries a one-line description for each new provider so the UI and `isCliProvider()` recognize them.
411
+
412
+ **First-run and operator polish**
413
+
414
+ - **First useful action after install.** Setup completion and the home launchpad share the same assistant, workflow, and mission paths.
415
+ - **Provider setup is easier to scan.** The setup wizard groups providers into fast local/no-key starts, recommended API providers, and advanced catalog/custom options, with the readiness check visible before provider details.
406
416
  - **Mission and protocol templates for real work.** New starter paths cover codebase review sprints, research bureau scans, content studio cycles, release readiness panels, synthesis panels, and builder review loops.
407
- - **Home launchpad paths.** First-run users can choose a self-hosted assistant, visual workflow, or autonomous mission path, with quality actions still one click away.
408
- - **A2A discovery is easier to integrate.** The canonical `/.well-known/agent-card.json` endpoint now works alongside the legacy API route and hides disabled or trashed agents from public discovery.
417
+ - **A2A discovery is easier to integrate.** The canonical `/.well-known/agent-card.json` endpoint works alongside the legacy API route and hides disabled or trashed agents from public discovery.
409
418
  - **Internal metadata stripping is safer.** Side-channel JSON is removed with balanced-object parsing and zod validation so nested payloads are scrubbed without deleting ordinary user JSON.
410
- - **Browser smoke gate restored.** `npm run test:e2e` now runs a Playwright smoke against health, A2A discovery, `/home`, and `/quality`, either against a live URL or a temporary local dev server.
419
+ - **Browser smoke gate restored.** `npm run test:e2e` runs a Playwright smoke against health, A2A discovery, `/home` launch paths, and `/quality`, either against a live URL or a temporary local dev server.
411
420
  - **OpenCode CLI hang fixed.** OpenCode CLI delegation no longer keeps an inherited stdin pipe open, preventing hangs in non-interactive runs.
412
421
 
413
422
  ### v1.6.0 Highlights
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
89
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/providers/opencode-cli.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -14,7 +14,13 @@ import { useNavigate } from '@/lib/app/navigation'
14
14
  import { safeStorageGet, safeStorageRemove } from '@/lib/app/safe-storage'
15
15
  import { isLocalhostBrowser, isVisibleSessionForViewer } from '@/lib/observability/local-observability'
16
16
  import { getSessionLastMessage } from '@/lib/chat/session-summary'
17
- import { DEFAULT_BUILDER_ROUTE, deriveHomeMode, HOME_LAUNCHPAD_AFTER_SETUP_KEY } from '@/lib/home-launchpad'
17
+ import {
18
+ deriveHomeMode,
19
+ HOME_LAUNCHPAD_AFTER_SETUP_KEY,
20
+ resolveLaunchPathHref,
21
+ type LaunchPathAction,
22
+ type LaunchPathId,
23
+ } from '@/lib/home-launchpad'
18
24
  import { getNotificationActivityAt, getNotificationOccurrenceCount } from '@/lib/notifications/notification-utils'
19
25
  import { timeAgo, timeUntil } from '@/lib/time-format'
20
26
  import type { Agent, Session, BoardTask, AppNotification, ActivityEntry } from '@/types'
@@ -254,16 +260,8 @@ export default function HomePage() {
254
260
  todayCost,
255
261
  })
256
262
 
257
- const openFirstAgent = () => {
258
- if (firstAgent) {
259
- navigateTo('agents', firstAgent.id)
260
- return
261
- }
262
- navigateTo('agents')
263
- }
264
-
265
- const openBuilder = () => {
266
- router.push(DEFAULT_BUILDER_ROUTE)
263
+ const handleLaunchPathAction = (id: LaunchPathId, action: LaunchPathAction) => {
264
+ router.push(resolveLaunchPathHref(id, action, firstAgent?.id))
267
265
  }
268
266
 
269
267
  if (homeMode === 'launchpad') {
@@ -278,15 +276,8 @@ export default function HomePage() {
278
276
  scheduleCount={scheduleCount}
279
277
  connectorCount={connectorCount}
280
278
  todayCost={todayCost}
281
- onOpenFirstAgent={openFirstAgent}
282
- onOpenProtocols={() => navigateTo('protocols')}
283
- onOpenBuilder={openBuilder}
284
- onOpenConnectors={() => navigateTo('connectors')}
279
+ onLaunchPathAction={handleLaunchPathAction}
285
280
  onOpenUsage={() => navigateTo('usage')}
286
- onRunEvalSuite={() => navigateTo('quality')}
287
- onReviewApprovals={() => navigateTo('quality')}
288
- onInspectFailedRuns={() => navigateTo('quality')}
289
- onStartReleaseQaMission={() => navigateTo('missions')}
290
281
  />
291
282
  </div>
292
283
  </MainContent>
@@ -11,7 +11,7 @@ import {
11
11
  type OnboardingPath,
12
12
  type SetupProvider,
13
13
  } from '@/lib/setup-defaults'
14
- import { DEFAULT_BUILDER_ROUTE } from '@/lib/home-launchpad'
14
+ import { resolveLaunchPathHref } from '@/lib/home-launchpad'
15
15
  import type {
16
16
  SetupStep,
17
17
  SetupWizardProps,
@@ -482,11 +482,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
482
482
  <StepNext
483
483
  createdAgents={createdAgents}
484
484
  onContinueToDashboard={() => finishSetup('/home')}
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')}
485
+ onLaunchPathAction={(id, action) => finishSetup(resolveLaunchPathHref(id, action, createdAgents[0]?.id))}
490
486
  />
491
487
  )}
492
488
 
@@ -1,19 +1,48 @@
1
1
  'use client'
2
2
 
3
- import { LaunchActionCard } from '@/components/shared/launch-action-card'
3
+ import { getLaunchPathCards, type LaunchPathAction, type LaunchPathCardCopy, type LaunchPathId } from '@/lib/home-launchpad'
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
+
7
39
  export function StepNext({
8
40
  createdAgents,
9
41
  onContinueToDashboard,
10
- onOpenFirstAgent,
11
- onOpenProtocols,
12
- onOpenBuilder,
13
- onOpenConnectors,
14
- onOpenUsage,
42
+ onLaunchPathAction,
15
43
  }: StepNextProps) {
16
44
  const firstAgent = createdAgents[0] || null
45
+ const launchPathCards = getLaunchPathCards({ firstAgentName: firstAgent?.name })
17
46
 
18
47
  return (
19
48
  <StepShell wide>
@@ -29,46 +58,10 @@ export function StepNext({
29
58
  : 'You finished setup without starter agents, so the launch options below focus on wiring up the rest of the workspace.'}
30
59
  </p>
31
60
 
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
- />
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
+ ))}
72
65
  </div>
73
66
 
74
67
  <button
@@ -3,10 +3,89 @@
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'
7
6
  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
+
10
89
  export function StepProviders({
11
90
  configuredProviders,
12
91
  configuredProviderIds,
@@ -21,6 +100,7 @@ export function StepProviders({
21
100
  const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
22
101
  const [doctorError, setDoctorError] = useState('')
23
102
  const [doctorReport, setDoctorReport] = useState<SetupDoctorResponse | null>(null)
103
+ const providerGroups = getSetupProviderGroups()
24
104
 
25
105
  const runSetupDoctor = async () => {
26
106
  setDoctorState('checking')
@@ -50,84 +130,70 @@ export function StepProviders({
50
130
 
51
131
  <ConfiguredProviderChips providers={configuredProviders} onRemove={onRemoveProvider} />
52
132
 
53
- <div className="flex flex-col gap-3 max-h-[42vh] overflow-y-auto pr-1">
54
- {SETUP_PROVIDERS.map((candidate) => {
55
- const isConfigured = configuredProviderIds.has(candidate.id)
56
- return (
57
- <button
58
- key={candidate.id}
59
- onClick={() => onSelectProvider(candidate.id)}
60
- className={`w-full px-5 py-4 rounded-[14px] border bg-surface text-left
61
- transition-all duration-200 flex items-start gap-4 cursor-pointer
62
- ${isConfigured
63
- ? 'border-emerald-500/25 hover:border-emerald-500/40 hover:bg-surface-hover'
64
- : 'border-white/[0.08] hover:border-accent-bright/30 hover:bg-surface-hover'
65
- }`}
66
- >
67
- <div className={`w-10 h-10 rounded-[10px] border flex items-center justify-center shrink-0 mt-0.5 ${
68
- isConfigured ? 'bg-emerald-500/10 border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'
69
- }`}>
70
- <span className={`text-[16px] font-display font-700 ${isConfigured ? 'text-emerald-400' : 'text-accent-bright'}`}>
71
- {candidate.icon}
72
- </span>
73
- </div>
74
- <div className="flex-1">
75
- <div className="text-[15px] font-display font-600 text-text mb-1">
76
- {candidate.name}
77
- {isConfigured ? (
78
- <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">
79
- Connected · Edit
80
- </span>
81
- ) : candidate.badge ? (
82
- <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">
83
- {candidate.badge}
84
- </span>
85
- ) : null}
86
- </div>
87
- <div className="text-[13px] text-text-3 leading-relaxed">{candidate.description}</div>
88
- {!candidate.requiresKey && !isConfigured && (
89
- <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">
90
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
91
- No API key required
92
- </div>
93
- )}
94
- </div>
95
- </button>
96
- )
97
- })}
98
- </div>
99
-
100
- <div className="mt-4 text-left">
101
- <button
102
- onClick={runSetupDoctor}
103
- disabled={doctorState === 'checking'}
104
- className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-white/[0.02] text-[13px] text-text-2
105
- cursor-pointer hover:bg-white/[0.05] transition-all duration-200 disabled:opacity-40"
106
- >
107
- {doctorState === 'checking' ? 'Running System Check...' : 'Run System Check'}
108
- </button>
133
+ <ReadinessPanel
134
+ state={doctorState}
135
+ report={doctorReport}
136
+ error={doctorError}
137
+ onRun={runSetupDoctor}
138
+ />
109
139
 
110
- {doctorState === 'error' && doctorError && (
111
- <p className="mt-2 text-[12px] text-red-300">{doctorError}</p>
112
- )}
113
-
114
- {doctorReport && doctorState === 'done' && (
115
- <div className="mt-3 p-3 rounded-[12px] border border-white/[0.08] bg-surface">
116
- <div className={`text-[12px] font-600 ${doctorReport.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
117
- {doctorReport.summary}
118
- </div>
119
- {doctorReport.checks.filter((check) => check.status !== 'pass').slice(0, 3).map((check) => (
120
- <div key={check.id} className="mt-1 text-[11px] text-text-3">
121
- - {check.label}: {check.detail}
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>
122
147
  </div>
123
- ))}
124
- {!!doctorReport.actions?.length && (
125
- <div className="mt-2 text-[11px] text-text-3/80">
126
- Next: {doctorReport.actions.slice(0, 2).join(' ')}
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}
167
+ </span>
168
+ </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
+ })}
127
193
  </div>
128
- )}
129
- </div>
130
- )}
194
+ </section>
195
+ ))}
196
+ </div>
131
197
  </div>
132
198
 
133
199
  {error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
@@ -1,5 +1,6 @@
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'
3
4
 
4
5
  export type SetupStep = 'profile' | 'path' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
5
6
  export type CheckState = 'idle' | 'checking' | 'ok' | 'error'
@@ -162,11 +163,7 @@ export interface StepAgentsProps {
162
163
  export interface StepNextProps {
163
164
  createdAgents: CreatedAgentSummary[]
164
165
  onContinueToDashboard: () => void
165
- onOpenFirstAgent: () => void
166
- onOpenProtocols: () => void
167
- onOpenBuilder: () => void
168
- onOpenConnectors: () => void
169
- onOpenUsage: () => void
166
+ onLaunchPathAction: (id: LaunchPathId, action: LaunchPathAction) => void
170
167
  }
171
168
 
172
169
  export interface StepDoneProps {
@@ -8,6 +8,7 @@ import {
8
8
  isLocalOpenClawEndpoint,
9
9
  resolveOpenClawDashboardUrl,
10
10
  getOpenClawErrorHint,
11
+ getSetupProviderGroups,
11
12
  requiresSetupProviderVerification,
12
13
  withHttpScheme,
13
14
  buildStarterDrafts,
@@ -76,6 +77,24 @@ test('getStarterKitsForPath: manual keeps the full catalog', () => {
76
77
  assert.equal(ids.has('openclaw_fleet'), true)
77
78
  })
78
79
 
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
+
79
98
  // ---------------------------------------------------------------------------
80
99
  // formatEndpointHost
81
100
  // ---------------------------------------------------------------------------
@@ -1,14 +1,47 @@
1
1
  import {
2
+ SETUP_PROVIDERS,
2
3
  STARTER_KITS,
3
4
  getDefaultModelForProvider,
4
5
  type OnboardingPath,
5
6
  type StarterKit,
7
+ type SetupProviderOption,
6
8
  type SetupProvider,
7
9
  type StarterKitAgentTemplate,
8
10
  } from '@/lib/setup-defaults'
9
11
  import type { ConfiguredProvider, SetupStep, StarterDraftAgent } from './types'
10
12
  import { STEP_ORDER } from './types'
11
13
 
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
+
12
45
  export function stepIndex(step: SetupStep): number {
13
46
  if (step === 'connect') return STEP_ORDER.indexOf('providers')
14
47
  return STEP_ORDER.indexOf(step)
@@ -38,6 +71,42 @@ export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
38
71
  return STARTER_KITS
39
72
  }
40
73
 
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
+
41
110
  export function applyIntentContext(prompt: string, intentText: string): string {
42
111
  const trimmed = intentText.trim()
43
112
  if (!trimmed) return prompt