@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
|
@@ -143,6 +143,11 @@ export function ChatCard({ session, active, onClick }: Props) {
|
|
|
143
143
|
/>
|
|
144
144
|
)}
|
|
145
145
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{displayName}</span>
|
|
146
|
+
{active && (
|
|
147
|
+
<span className="shrink-0 text-[9px] font-700 uppercase tracking-[0.08em] text-accent-bright bg-accent-bright/15 px-1.5 py-0.5 rounded-[6px]">
|
|
148
|
+
Selected
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
146
151
|
{providerLabel && (
|
|
147
152
|
<span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-text-3/70 bg-white/[0.03] px-2 py-0.5 rounded-[6px]">
|
|
148
153
|
{providerLabel}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
4
|
-
import {
|
|
5
|
-
getLaunchPathCards,
|
|
6
|
-
type LaunchPathAction,
|
|
7
|
-
type LaunchPathCardCopy,
|
|
8
|
-
type LaunchPathId,
|
|
9
|
-
} from '@/lib/home-launchpad'
|
|
4
|
+
import { LaunchActionCard } from '@/components/shared/launch-action-card'
|
|
10
5
|
import type { Agent } from '@/types'
|
|
11
6
|
|
|
12
7
|
function SnapshotItem({ label, value, hint }: { label: string; value: string; hint: string }) {
|
|
@@ -20,58 +15,47 @@ function SnapshotItem({ label, value, hint }: { label: string; value: string; hi
|
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
function PathCard({
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
kicker,
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
primaryLabel,
|
|
22
|
+
secondaryLabel,
|
|
23
|
+
onPrimary,
|
|
24
|
+
onSecondary,
|
|
25
25
|
}: {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
kicker: string
|
|
27
|
+
title: string
|
|
28
|
+
description: string
|
|
29
|
+
primaryLabel: string
|
|
30
|
+
secondaryLabel: string
|
|
31
|
+
onPrimary: () => void
|
|
32
|
+
onSecondary: () => void
|
|
28
33
|
}) {
|
|
29
34
|
return (
|
|
30
35
|
<div className="flex min-h-[220px] flex-col rounded-[18px] border border-white/[0.07] bg-white/[0.03] p-5">
|
|
31
|
-
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{
|
|
32
|
-
<div className="mt-3 text-[18px] font-display font-700 tracking-normal text-text">{
|
|
33
|
-
<p className="mt-2 flex-1 text-[13px] leading-relaxed text-text-3/72">{
|
|
36
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{kicker}</div>
|
|
37
|
+
<div className="mt-3 text-[18px] font-display font-700 tracking-normal text-text">{title}</div>
|
|
38
|
+
<p className="mt-2 flex-1 text-[13px] leading-relaxed text-text-3/72">{description}</p>
|
|
34
39
|
<div className="mt-5 flex flex-wrap gap-2">
|
|
35
40
|
<button
|
|
36
41
|
type="button"
|
|
37
|
-
onClick={
|
|
42
|
+
onClick={onPrimary}
|
|
38
43
|
className="rounded-[10px] bg-accent-bright px-3.5 py-2 text-[12px] font-display font-700 text-black transition-opacity hover:opacity-90"
|
|
39
44
|
>
|
|
40
|
-
{
|
|
45
|
+
{primaryLabel}
|
|
41
46
|
</button>
|
|
42
47
|
<button
|
|
43
48
|
type="button"
|
|
44
|
-
onClick={
|
|
49
|
+
onClick={onSecondary}
|
|
45
50
|
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]"
|
|
46
51
|
>
|
|
47
|
-
{
|
|
52
|
+
{secondaryLabel}
|
|
48
53
|
</button>
|
|
49
54
|
</div>
|
|
50
55
|
</div>
|
|
51
56
|
)
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
function SecondaryAction({
|
|
55
|
-
label,
|
|
56
|
-
description,
|
|
57
|
-
onClick,
|
|
58
|
-
}: {
|
|
59
|
-
label: string
|
|
60
|
-
description: string
|
|
61
|
-
onClick: () => void
|
|
62
|
-
}) {
|
|
63
|
-
return (
|
|
64
|
-
<button
|
|
65
|
-
type="button"
|
|
66
|
-
onClick={onClick}
|
|
67
|
-
className="rounded-[12px] border border-white/[0.07] bg-white/[0.025] px-3 py-2 text-left transition-colors hover:bg-white/[0.05]"
|
|
68
|
-
>
|
|
69
|
-
<div className="text-[12px] font-display font-700 text-text-2">{label}</div>
|
|
70
|
-
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/65">{description}</div>
|
|
71
|
-
</button>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
59
|
type Props = {
|
|
76
60
|
firstAgent: Agent | null
|
|
77
61
|
agentCount: number
|
|
@@ -80,8 +64,15 @@ type Props = {
|
|
|
80
64
|
scheduleCount: number
|
|
81
65
|
connectorCount: number
|
|
82
66
|
todayCost: number
|
|
83
|
-
|
|
67
|
+
onOpenFirstAgent: () => void
|
|
68
|
+
onOpenProtocols: () => void
|
|
69
|
+
onOpenBuilder: () => void
|
|
70
|
+
onOpenConnectors: () => void
|
|
84
71
|
onOpenUsage: () => void
|
|
72
|
+
onRunEvalSuite: () => void
|
|
73
|
+
onReviewApprovals: () => void
|
|
74
|
+
onInspectFailedRuns: () => void
|
|
75
|
+
onStartReleaseQaMission: () => void
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
export function HomeLaunchpad({
|
|
@@ -92,11 +83,16 @@ export function HomeLaunchpad({
|
|
|
92
83
|
scheduleCount,
|
|
93
84
|
connectorCount,
|
|
94
85
|
todayCost,
|
|
95
|
-
|
|
86
|
+
onOpenFirstAgent,
|
|
87
|
+
onOpenProtocols,
|
|
88
|
+
onOpenBuilder,
|
|
89
|
+
onOpenConnectors,
|
|
96
90
|
onOpenUsage,
|
|
91
|
+
onRunEvalSuite,
|
|
92
|
+
onReviewApprovals,
|
|
93
|
+
onInspectFailedRuns,
|
|
94
|
+
onStartReleaseQaMission,
|
|
97
95
|
}: Props) {
|
|
98
|
-
const launchPathCards = getLaunchPathCards({ firstAgentName: firstAgent?.name })
|
|
99
|
-
|
|
100
96
|
return (
|
|
101
97
|
<div className="max-w-[980px] mx-auto px-6 py-10">
|
|
102
98
|
<div className="rounded-[20px] border border-white/[0.06] bg-white/[0.025] p-6">
|
|
@@ -141,37 +137,93 @@ export function HomeLaunchpad({
|
|
|
141
137
|
</div>
|
|
142
138
|
|
|
143
139
|
<div className="mt-6 grid gap-3 lg:grid-cols-3">
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
<PathCard
|
|
141
|
+
kicker="Self-hosted assistant"
|
|
142
|
+
title={firstAgent ? `Work with ${firstAgent.name}` : 'Create the first agent'}
|
|
143
|
+
description="Open a live agent chat, then add memory, local tools, provider routing, or connector access as the work demands."
|
|
144
|
+
primaryLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
|
|
145
|
+
secondaryLabel="Connect Platform"
|
|
146
|
+
onPrimary={onOpenFirstAgent}
|
|
147
|
+
onSecondary={onOpenConnectors}
|
|
148
|
+
/>
|
|
149
|
+
<PathCard
|
|
150
|
+
kicker="Visual workflow"
|
|
151
|
+
title="Shape a reusable run"
|
|
152
|
+
description="Use protocol templates and the builder to turn review, research, planning, or release checks into durable workflows."
|
|
153
|
+
primaryLabel="Open Builder"
|
|
154
|
+
secondaryLabel="Use Templates"
|
|
155
|
+
onPrimary={onOpenBuilder}
|
|
156
|
+
onSecondary={onOpenProtocols}
|
|
157
|
+
/>
|
|
158
|
+
<PathCard
|
|
159
|
+
kicker="Autonomous mission"
|
|
160
|
+
title="Run with budgets"
|
|
161
|
+
description="Start a mission template for release QA, research, support triage, cost audit, or failed-run review with reports and caps."
|
|
162
|
+
primaryLabel="Open Missions"
|
|
163
|
+
secondaryLabel="Quality Center"
|
|
164
|
+
onPrimary={onStartReleaseQaMission}
|
|
165
|
+
onSecondary={onRunEvalSuite}
|
|
166
|
+
/>
|
|
147
167
|
</div>
|
|
148
168
|
|
|
149
|
-
<div className="mt-6
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
<div className="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
170
|
+
<LaunchActionCard
|
|
171
|
+
title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
|
|
172
|
+
description={firstAgent
|
|
173
|
+
? `Jump into ${firstAgent.name} and start using the workspace immediately.`
|
|
174
|
+
: 'Open the agents workspace to create or tune the first specialist agent.'}
|
|
175
|
+
actionLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
|
|
176
|
+
onClick={onOpenFirstAgent}
|
|
177
|
+
tone="primary"
|
|
178
|
+
/>
|
|
179
|
+
<LaunchActionCard
|
|
180
|
+
title="Start Structured Session"
|
|
181
|
+
description="Open bounded collaboration runs for planning, review, decision-making, or focused multi-agent work."
|
|
182
|
+
actionLabel="Open Protocols"
|
|
183
|
+
onClick={onOpenProtocols}
|
|
184
|
+
/>
|
|
185
|
+
<LaunchActionCard
|
|
186
|
+
title="Open Workflow Builder"
|
|
187
|
+
description="Move straight into reusable orchestration graphs if you want a durable workflow instead of a one-off run."
|
|
188
|
+
actionLabel="Open Builder"
|
|
189
|
+
onClick={onOpenBuilder}
|
|
190
|
+
/>
|
|
191
|
+
<LaunchActionCard
|
|
192
|
+
title="Connect a Platform"
|
|
193
|
+
description="Bridge agents into chat surfaces like Discord, Slack, Telegram, and WhatsApp."
|
|
194
|
+
actionLabel="Open Connectors"
|
|
195
|
+
onClick={onOpenConnectors}
|
|
196
|
+
/>
|
|
197
|
+
<LaunchActionCard
|
|
198
|
+
title="Review Usage"
|
|
199
|
+
description="Check cost, provider health, and activity so the workspace stays observable from the start."
|
|
200
|
+
actionLabel="Open Usage"
|
|
201
|
+
onClick={onOpenUsage}
|
|
202
|
+
/>
|
|
203
|
+
<LaunchActionCard
|
|
204
|
+
title="Run Eval Suite"
|
|
205
|
+
description="Open the Quality Center and run scenario or suite checks against an agent before shipping."
|
|
206
|
+
actionLabel="Open Eval Lab"
|
|
207
|
+
onClick={onRunEvalSuite}
|
|
208
|
+
/>
|
|
209
|
+
<LaunchActionCard
|
|
210
|
+
title="Review Approvals"
|
|
211
|
+
description="Clear pending human-loop, tool, connector, skill, agent, and budget requests from one desk."
|
|
212
|
+
actionLabel="Open Approvals"
|
|
213
|
+
onClick={onReviewApprovals}
|
|
214
|
+
/>
|
|
215
|
+
<LaunchActionCard
|
|
216
|
+
title="Inspect Failed Runs"
|
|
217
|
+
description="Filter recent run failures and open replay evidence without leaving the operator workflow."
|
|
218
|
+
actionLabel="Open Run Review"
|
|
219
|
+
onClick={onInspectFailedRuns}
|
|
220
|
+
/>
|
|
221
|
+
<LaunchActionCard
|
|
222
|
+
title="Start Release QA Mission"
|
|
223
|
+
description="Use a budgeted mission template to collect release readiness evidence and quality notes."
|
|
224
|
+
actionLabel="Open Missions"
|
|
225
|
+
onClick={onStartReleaseQaMission}
|
|
226
|
+
/>
|
|
175
227
|
</div>
|
|
176
228
|
|
|
177
229
|
<div className="mt-8 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
@@ -1,22 +1,49 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
|
+
import { safeStorageGet, safeStorageSet } from '@/lib/app/safe-storage'
|
|
4
5
|
|
|
5
6
|
const CHECK_INTERVAL = 5 * 60_000 // 5 minutes
|
|
7
|
+
const DISMISSED_UPDATE_KEY = 'sc_update_banner_dismissed_target'
|
|
6
8
|
|
|
7
9
|
type VersionInfo = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
source: 'git' | 'package'
|
|
11
|
+
version: string
|
|
12
|
+
localSha: string | null
|
|
13
|
+
localTag: string | null
|
|
14
|
+
remoteSha: string | null
|
|
15
|
+
remoteTag: string | null
|
|
16
|
+
channel: 'stable' | 'main'
|
|
10
17
|
updateAvailable: boolean
|
|
11
18
|
behindBy: number
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
type UpdateResponse = {
|
|
22
|
+
success: boolean
|
|
23
|
+
newSha?: string
|
|
24
|
+
targetTag?: string | null
|
|
25
|
+
channel?: 'stable' | 'main'
|
|
26
|
+
needsRestart?: boolean
|
|
27
|
+
error?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
type UpdateState = 'idle' | 'updating' | 'done' | 'error'
|
|
15
31
|
|
|
32
|
+
function updateTargetKey(version: VersionInfo): string {
|
|
33
|
+
return version.remoteTag || version.remoteSha || `${version.channel}:${version.behindBy}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function updateTargetLabel(version: VersionInfo): string {
|
|
37
|
+
if (version.remoteTag) return version.remoteTag
|
|
38
|
+
if (version.remoteSha) return `${version.channel === 'stable' ? 'stable release' : 'main'} ${version.remoteSha}`
|
|
39
|
+
return 'latest release'
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
export function UpdateBanner() {
|
|
17
43
|
const [version, setVersion] = useState<VersionInfo | null>(null)
|
|
18
44
|
const [updateState, setUpdateState] = useState<UpdateState>('idle')
|
|
19
|
-
const [dismissed, setDismissed] = useState<string | null>(
|
|
45
|
+
const [dismissed, setDismissed] = useState<string | null>(() => safeStorageGet(DISMISSED_UPDATE_KEY))
|
|
46
|
+
const [appliedTarget, setAppliedTarget] = useState<string | null>(null)
|
|
20
47
|
const [errorMsg, setErrorMsg] = useState('')
|
|
21
48
|
|
|
22
49
|
useEffect(() => {
|
|
@@ -36,8 +63,9 @@ export function UpdateBanner() {
|
|
|
36
63
|
setErrorMsg('')
|
|
37
64
|
try {
|
|
38
65
|
const res = await fetch('/api/version/update', { method: 'POST' })
|
|
39
|
-
const data = await res.json()
|
|
66
|
+
const data = await res.json() as UpdateResponse
|
|
40
67
|
if (data.success) {
|
|
68
|
+
setAppliedTarget(data.targetTag || data.newSha || (version ? updateTargetLabel(version) : null))
|
|
41
69
|
setUpdateState('done')
|
|
42
70
|
} else {
|
|
43
71
|
setUpdateState('error')
|
|
@@ -50,12 +78,17 @@ export function UpdateBanner() {
|
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
const handleDismiss = () => {
|
|
53
|
-
if (version)
|
|
81
|
+
if (!version) return
|
|
82
|
+
const target = updateTargetKey(version)
|
|
83
|
+
setDismissed(target)
|
|
84
|
+
safeStorageSet(DISMISSED_UPDATE_KEY, target)
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
// Don't show if no update, or user dismissed this specific remote SHA
|
|
57
88
|
if (!version?.updateAvailable) return null
|
|
58
|
-
if (dismissed === version
|
|
89
|
+
if (dismissed === updateTargetKey(version) && updateState === 'idle') return null
|
|
90
|
+
|
|
91
|
+
const targetLabel = updateTargetLabel(version)
|
|
59
92
|
|
|
60
93
|
return (
|
|
61
94
|
<div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] flex items-center gap-2 shrink-0 bg-accent-bright/[0.04]">
|
|
@@ -65,7 +98,8 @@ export function UpdateBanner() {
|
|
|
65
98
|
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
66
99
|
</svg>
|
|
67
100
|
<span className="text-text-3 flex-1 min-w-0 truncate">
|
|
68
|
-
<span className="text-accent-bright font-600">{
|
|
101
|
+
<span className="text-accent-bright font-600">{targetLabel}</span> available
|
|
102
|
+
{version.behindBy > 0 ? ` - ${version.behindBy} commit${version.behindBy === 1 ? '' : 's'} ahead` : ''}
|
|
69
103
|
</span>
|
|
70
104
|
<button
|
|
71
105
|
onClick={handleUpdate}
|
|
@@ -90,7 +124,7 @@ export function UpdateBanner() {
|
|
|
90
124
|
<>
|
|
91
125
|
<span className="w-3 h-3 border-[1.5px] border-accent-bright/30 border-t-accent-bright rounded-full shrink-0"
|
|
92
126
|
style={{ animation: 'spin 0.8s linear infinite' }} />
|
|
93
|
-
<span className="text-text-3">Updating...</span>
|
|
127
|
+
<span className="text-text-3">Updating to {targetLabel}...</span>
|
|
94
128
|
</>
|
|
95
129
|
)}
|
|
96
130
|
|
|
@@ -99,7 +133,7 @@ export function UpdateBanner() {
|
|
|
99
133
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-success shrink-0">
|
|
100
134
|
<polyline points="20 6 9 17 4 12" />
|
|
101
135
|
</svg>
|
|
102
|
-
<span className="text-text-3 flex-1">Updated
|
|
136
|
+
<span className="text-text-3 flex-1">Updated{appliedTarget ? ` to ${appliedTarget}` : ''}. Restart SwarmClaw to apply.</span>
|
|
103
137
|
</>
|
|
104
138
|
)}
|
|
105
139
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_BUILDER_ROUTE,
|
|
5
|
-
deriveHomeMode,
|
|
6
|
-
getLaunchPathCards,
|
|
7
|
-
isSparseWorkspace,
|
|
8
|
-
resolveLaunchPathHref,
|
|
9
|
-
} from './home-launchpad'
|
|
3
|
+
import { deriveHomeMode, isSparseWorkspace } from './home-launchpad'
|
|
10
4
|
|
|
11
5
|
test('isSparseWorkspace detects a fresh workspace', () => {
|
|
12
6
|
assert.equal(isSparseWorkspace({
|
|
@@ -53,27 +47,3 @@ test('deriveHomeMode falls back to ops for active workspaces', () => {
|
|
|
53
47
|
todayCost: 0,
|
|
54
48
|
}), 'ops')
|
|
55
49
|
})
|
|
56
|
-
|
|
57
|
-
test('getLaunchPathCards returns the three first-run paths in order', () => {
|
|
58
|
-
const cards = getLaunchPathCards({ firstAgentName: 'Ada' })
|
|
59
|
-
assert.deepEqual(cards.map((card) => card.id), ['assistant', 'workflow', 'mission'])
|
|
60
|
-
assert.equal(cards[0]?.title, 'Work with Ada')
|
|
61
|
-
assert.equal(cards[0]?.primaryLabel, 'Open Chat')
|
|
62
|
-
assert.equal(cards[1]?.primaryLabel, 'Open Builder')
|
|
63
|
-
assert.equal(cards[2]?.secondaryLabel, 'Quality Center')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('getLaunchPathCards falls back to agent creation copy when no agent exists', () => {
|
|
67
|
-
const [assistant] = getLaunchPathCards()
|
|
68
|
-
assert.equal(assistant?.title, 'Create the first agent')
|
|
69
|
-
assert.equal(assistant?.primaryLabel, 'Open Agents')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('resolveLaunchPathHref builds primary and secondary destinations', () => {
|
|
73
|
-
assert.equal(resolveLaunchPathHref('assistant', 'primary', 'agent one'), '/agents/agent%20one')
|
|
74
|
-
assert.equal(resolveLaunchPathHref('assistant', 'secondary', 'agent one'), '/connectors')
|
|
75
|
-
assert.equal(resolveLaunchPathHref('workflow', 'primary'), DEFAULT_BUILDER_ROUTE)
|
|
76
|
-
assert.equal(resolveLaunchPathHref('workflow', 'secondary'), '/protocols')
|
|
77
|
-
assert.equal(resolveLaunchPathHref('mission', 'primary'), '/missions')
|
|
78
|
-
assert.equal(resolveLaunchPathHref('mission', 'secondary'), '/quality')
|
|
79
|
-
})
|
|
@@ -2,17 +2,6 @@ export const HOME_LAUNCHPAD_AFTER_SETUP_KEY = 'sc_launchpad_after_setup_v1'
|
|
|
2
2
|
export const DEFAULT_BUILDER_ROUTE = '/protocols/builder/facilitated_discussion'
|
|
3
3
|
|
|
4
4
|
export type HomeMode = 'launchpad' | 'ops'
|
|
5
|
-
export type LaunchPathId = 'assistant' | 'workflow' | 'mission'
|
|
6
|
-
export type LaunchPathAction = 'primary' | 'secondary'
|
|
7
|
-
|
|
8
|
-
export interface LaunchPathCardCopy {
|
|
9
|
-
id: LaunchPathId
|
|
10
|
-
kicker: string
|
|
11
|
-
title: string
|
|
12
|
-
description: string
|
|
13
|
-
primaryLabel: string
|
|
14
|
-
secondaryLabel: string
|
|
15
|
-
}
|
|
16
5
|
|
|
17
6
|
export interface HomeModeInput {
|
|
18
7
|
hasLaunchpadFlag: boolean
|
|
@@ -39,50 +28,3 @@ export function deriveHomeMode(input: HomeModeInput): HomeMode {
|
|
|
39
28
|
if (input.hasLaunchpadFlag) return 'launchpad'
|
|
40
29
|
return isSparseWorkspace(input) ? 'launchpad' : 'ops'
|
|
41
30
|
}
|
|
42
|
-
|
|
43
|
-
export function getLaunchPathCards(input: { firstAgentName?: string | null } = {}): LaunchPathCardCopy[] {
|
|
44
|
-
const firstAgentName = input.firstAgentName?.trim() || null
|
|
45
|
-
return [
|
|
46
|
-
{
|
|
47
|
-
id: 'assistant',
|
|
48
|
-
kicker: 'Assistant',
|
|
49
|
-
title: firstAgentName ? `Work with ${firstAgentName}` : 'Create the first agent',
|
|
50
|
-
description: 'Open a live agent chat, then add memory, local tools, provider routing, or connector access as the work demands.',
|
|
51
|
-
primaryLabel: firstAgentName ? 'Open Chat' : 'Open Agents',
|
|
52
|
-
secondaryLabel: 'Connect Platform',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: 'workflow',
|
|
56
|
-
kicker: 'Workflow',
|
|
57
|
-
title: 'Shape a reusable run',
|
|
58
|
-
description: 'Use protocol templates and the builder to turn review, research, planning, or release checks into durable workflows.',
|
|
59
|
-
primaryLabel: 'Open Builder',
|
|
60
|
-
secondaryLabel: 'Use Templates',
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: 'mission',
|
|
64
|
-
kicker: 'Mission',
|
|
65
|
-
title: 'Run with budgets',
|
|
66
|
-
description: 'Start a mission template for release QA, research, support triage, cost audit, or failed-run review with reports and caps.',
|
|
67
|
-
primaryLabel: 'Open Missions',
|
|
68
|
-
secondaryLabel: 'Quality Center',
|
|
69
|
-
},
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function resolveLaunchPathHref(
|
|
74
|
-
id: LaunchPathId,
|
|
75
|
-
action: LaunchPathAction,
|
|
76
|
-
firstAgentId?: string | null,
|
|
77
|
-
): string {
|
|
78
|
-
if (id === 'assistant') {
|
|
79
|
-
if (action === 'primary') {
|
|
80
|
-
return firstAgentId ? `/agents/${encodeURIComponent(firstAgentId)}` : '/agents'
|
|
81
|
-
}
|
|
82
|
-
return '/connectors'
|
|
83
|
-
}
|
|
84
|
-
if (id === 'workflow') {
|
|
85
|
-
return action === 'primary' ? DEFAULT_BUILDER_ROUTE : '/protocols'
|
|
86
|
-
}
|
|
87
|
-
return action === 'primary' ? '/missions' : '/quality'
|
|
88
|
-
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { GENERIC_CLI_PROVIDER_METADATA } from './providers/cli-provider-metadata'
|
|
5
|
+
import {
|
|
6
|
+
NATIVE_CAPABILITY_PROVIDER_IDS,
|
|
7
|
+
NON_LANGGRAPH_PROVIDER_IDS,
|
|
8
|
+
WORKER_ONLY_PROVIDER_IDS,
|
|
9
|
+
} from './provider-sets'
|
|
10
|
+
|
|
11
|
+
describe('provider sets', () => {
|
|
12
|
+
it('routes generic CLI providers through direct provider runtime', () => {
|
|
13
|
+
for (const provider of GENERIC_CLI_PROVIDER_METADATA) {
|
|
14
|
+
assert.equal(NON_LANGGRAPH_PROVIDER_IDS.has(provider.id), true, `${provider.id} should bypass LangGraph`)
|
|
15
|
+
assert.equal(NATIVE_CAPABILITY_PROVIDER_IDS.has(provider.id), true, `${provider.id} should be native-capability`)
|
|
16
|
+
assert.equal(WORKER_ONLY_PROVIDER_IDS.has(provider.id), true, `${provider.id} should be worker-only`)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
package/src/lib/provider-sets.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { CLI_PROVIDER_METADATA } from '@/lib/providers/cli-provider-metadata'
|
|
2
|
+
|
|
3
|
+
const CLI_PROVIDER_IDS = CLI_PROVIDER_METADATA.map((provider) => provider.id)
|
|
4
|
+
const DIRECT_CLI_PROVIDER_IDS = CLI_PROVIDER_IDS.filter((providerId) => providerId !== 'goose')
|
|
5
|
+
|
|
1
6
|
/** CLI providers that use their own tool execution outside the shared tool-runtime path. */
|
|
2
|
-
export const NON_LANGGRAPH_PROVIDER_IDS = new Set([
|
|
7
|
+
export const NON_LANGGRAPH_PROVIDER_IDS = new Set([...DIRECT_CLI_PROVIDER_IDS, 'opencode-web'])
|
|
3
8
|
|
|
4
9
|
/** Providers that manage their own runtime/tool loop even when reached over an API endpoint. */
|
|
5
10
|
export const RUNTIME_MANAGED_PROVIDER_IDS = new Set(['hermes', 'goose'])
|
|
6
11
|
|
|
7
12
|
/** Providers with native tool/capability support (CLI providers + OpenClaw + Hermes). */
|
|
8
|
-
export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set([
|
|
13
|
+
export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set([...CLI_PROVIDER_IDS, 'openclaw', 'hermes'])
|
|
9
14
|
|
|
10
15
|
/** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
|
|
11
|
-
export const WORKER_ONLY_PROVIDER_IDS = new Set([
|
|
16
|
+
export const WORKER_ONLY_PROVIDER_IDS = new Set([...CLI_PROVIDER_IDS, 'openclaw', 'hermes'])
|
|
12
17
|
|
|
13
18
|
/** CLI providers that support MCP server and skill injection at runtime (via provider-specific config mechanisms). */
|
|
14
19
|
export const MCP_INJECTION_PROVIDER_IDS = new Set(['copilot-cli', 'codex-cli'])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BESPOKE_CLI_PROVIDER_METADATA,
|
|
6
|
+
CLI_PROVIDER_METADATA,
|
|
7
|
+
CLI_PROVIDER_METADATA_BY_ID,
|
|
8
|
+
GENERIC_CLI_PROVIDER_METADATA,
|
|
9
|
+
isCliProviderId,
|
|
10
|
+
} from './cli-provider-metadata'
|
|
11
|
+
import { GENERIC_CLI_BINARIES } from './generic-cli'
|
|
12
|
+
import { CLI_PROVIDER_CAPABILITIES } from './cli-utils'
|
|
13
|
+
|
|
14
|
+
describe('CLI provider metadata', () => {
|
|
15
|
+
it('has one unique entry per CLI provider', () => {
|
|
16
|
+
const ids = CLI_PROVIDER_METADATA.map((provider) => provider.id)
|
|
17
|
+
assert.equal(new Set(ids).size, ids.length)
|
|
18
|
+
assert.ok(BESPOKE_CLI_PROVIDER_METADATA.length > 0)
|
|
19
|
+
assert.equal(GENERIC_CLI_PROVIDER_METADATA.length, 31)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('drives binary and capability maps for every provider', () => {
|
|
23
|
+
for (const provider of CLI_PROVIDER_METADATA) {
|
|
24
|
+
assert.equal(CLI_PROVIDER_METADATA_BY_ID[provider.id]?.displayName, provider.displayName)
|
|
25
|
+
assert.equal(isCliProviderId(provider.id), true)
|
|
26
|
+
assert.equal(CLI_PROVIDER_CAPABILITIES[provider.id], provider.capability)
|
|
27
|
+
assert.ok(provider.binaryName.length > 0)
|
|
28
|
+
assert.ok(provider.defaultModel.length > 0)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('keeps generic CLI binary mappings sourced from metadata', () => {
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
Object.fromEntries(GENERIC_CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider.binaryName])),
|
|
35
|
+
GENERIC_CLI_BINARIES,
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
})
|