@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.
- package/README.md +25 -9
- package/bin/swarmclaw.js +87 -0
- package/electron-dist/main.js +218 -0
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +1 -1
- package/src/app/api/setup/check-provider/route.ts +5 -62
- package/src/app/api/setup/doctor/route.ts +19 -9
- package/src/app/home/page.tsx +19 -10
- package/src/cli/index.js +8 -2
- package/src/cli/index.ts +12 -3
- package/src/components/agents/inspector-panel.tsx +25 -3
- package/src/components/auth/setup-wizard/index.tsx +6 -2
- package/src/components/auth/setup-wizard/step-next.tsx +46 -39
- package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
- package/src/components/auth/setup-wizard/types.ts +5 -2
- package/src/components/auth/setup-wizard/utils.test.ts +0 -19
- package/src/components/auth/setup-wizard/utils.ts +0 -69
- package/src/components/chat/chat-card.tsx +5 -0
- package/src/components/home/home-launchpad.tsx +123 -71
- package/src/components/layout/update-banner.tsx +43 -9
- package/src/lib/home-launchpad.test.ts +1 -31
- package/src/lib/home-launchpad.ts +0 -58
- package/src/lib/provider-sets.test.ts +19 -0
- package/src/lib/provider-sets.ts +8 -3
- package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
- package/src/lib/providers/cli-provider-metadata.ts +208 -0
- package/src/lib/providers/cli-utils.test.ts +65 -1
- package/src/lib/providers/cli-utils.ts +26 -44
- package/src/lib/providers/codex-cli.ts +71 -75
- package/src/lib/providers/generic-cli.ts +2 -31
- package/src/lib/providers/index.ts +14 -44
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
- package/src/lib/server/cli-provider-readiness.test.ts +45 -0
- package/src/lib/server/cli-provider-readiness.ts +84 -0
- package/src/lib/server/provider-health.test.ts +6 -0
- package/src/lib/server/provider-health.ts +2 -2
- package/src/lib/setup-defaults.test.ts +8 -0
- package/src/lib/setup-defaults.ts +38 -178
- package/src/stores/slices/session-slice.test.ts +40 -2
- package/src/stores/slices/session-slice.ts +41 -1
- 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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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=
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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-[
|
|
141
|
-
|
|
142
|
-
{
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|