@swarmclawai/swarmclaw 1.4.9 → 1.5.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.
@@ -0,0 +1,159 @@
1
+ 'use client'
2
+
3
+ import { ONBOARDING_PATHS } from '@/lib/setup-defaults'
4
+ import type { StepPathProps } from './types'
5
+ import { StepShell, SkipLink } from './shared'
6
+ import { formatAgentCount, getStarterKitsForPath } from './utils'
7
+
8
+ export function StepPath({
9
+ onboardingPath,
10
+ starterKitId,
11
+ intentText,
12
+ onPathChange,
13
+ onStarterKitChange,
14
+ onIntentTextChange,
15
+ onContinue,
16
+ onBack,
17
+ onSkip,
18
+ }: StepPathProps) {
19
+ const visibleStarterKits = getStarterKitsForPath(onboardingPath)
20
+
21
+ return (
22
+ <StepShell wide>
23
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
24
+ Choose Your Start
25
+ </h1>
26
+ <p className="text-[15px] text-text-2 mb-2">
27
+ Pick the setup path that matches how much guidance you want.
28
+ </p>
29
+ <p className="text-[13px] text-text-3 mb-7">
30
+ You can still edit providers, prompts, tools, and agent details before finishing setup.
31
+ </p>
32
+
33
+ <div className="grid gap-3 md:grid-cols-3 text-left mb-6">
34
+ {ONBOARDING_PATHS.map((path) => {
35
+ const active = path.id === onboardingPath
36
+ return (
37
+ <button
38
+ key={path.id}
39
+ type="button"
40
+ onClick={() => onPathChange(path.id)}
41
+ className={`rounded-[18px] border px-5 py-4 text-left transition-all duration-200 cursor-pointer ${
42
+ active
43
+ ? 'border-accent-bright/35 bg-accent-soft shadow-[0_0_24px_rgba(99,102,241,0.12)]'
44
+ : 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-white/[0.04]'
45
+ }`}
46
+ >
47
+ <div className="flex items-start justify-between gap-3">
48
+ <div className="text-[15px] font-display font-700 text-text">{path.title}</div>
49
+ {path.badge ? (
50
+ <span className={`rounded-full px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] ${
51
+ active ? 'bg-accent-bright text-black' : 'bg-white/[0.05] text-text-3/80'
52
+ }`}>
53
+ {path.badge}
54
+ </span>
55
+ ) : null}
56
+ </div>
57
+ <p className="mt-2 text-[13px] leading-relaxed text-text-2">{path.description}</p>
58
+ <p className="mt-3 text-[12px] leading-relaxed text-text-3/72">{path.detail}</p>
59
+ </button>
60
+ )
61
+ })}
62
+ </div>
63
+
64
+ {onboardingPath === 'intent' && (
65
+ <div className="mb-6 rounded-[18px] border border-white/[0.08] bg-surface px-5 py-4 text-left">
66
+ <label className="block text-[12px] font-700 uppercase tracking-[0.12em] text-text-3/60 mb-2">
67
+ What Are You Setting Up SwarmClaw To Do?
68
+ </label>
69
+ <textarea
70
+ value={intentText}
71
+ onChange={(event) => onIntentTextChange(event.target.value)}
72
+ rows={3}
73
+ placeholder="e.g. Help me run product research every week, summarize findings, and turn them into follow-up tasks."
74
+ className="w-full rounded-[14px] border border-white/[0.08] bg-bg px-4 py-3 text-[14px] text-text outline-none transition-all duration-200 resize-none placeholder:text-text-3/45 focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
75
+ />
76
+ <p className="mt-2 text-[12px] leading-relaxed text-text-3/72">
77
+ This is used only to seed the starter prompts. It does not auto-classify your workflow.
78
+ </p>
79
+ </div>
80
+ )}
81
+
82
+ <div className="rounded-[20px] border border-white/[0.08] bg-surface p-5 text-left">
83
+ <div className="flex flex-wrap items-start justify-between gap-3">
84
+ <div>
85
+ <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Starting Shape</div>
86
+ <div className="mt-1 text-[13px] text-text-3/72">
87
+ Start from a broad team shape instead of a niche preset. You can still edit every agent before setup finishes.
88
+ </div>
89
+ </div>
90
+ <div className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/70">
91
+ {visibleStarterKits.length} options
92
+ </div>
93
+ </div>
94
+
95
+ <div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
96
+ {visibleStarterKits.map((kit) => {
97
+ const active = starterKitId === kit.id
98
+ return (
99
+ <button
100
+ key={kit.id}
101
+ type="button"
102
+ onClick={() => onStarterKitChange(kit.id)}
103
+ className={`rounded-[18px] border px-4 py-4 text-left transition-all duration-200 cursor-pointer ${
104
+ active
105
+ ? 'border-accent-bright/35 bg-accent-soft shadow-[0_0_24px_rgba(99,102,241,0.12)]'
106
+ : 'border-white/[0.08] bg-white/[0.02] hover:border-accent-bright/20 hover:bg-white/[0.04]'
107
+ }`}
108
+ >
109
+ <div className="flex items-start justify-between gap-3">
110
+ <div className="text-[15px] font-display font-700 text-text">{kit.name}</div>
111
+ <span className={`rounded-full px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] ${
112
+ active ? 'bg-accent-bright text-black' : 'bg-white/[0.05] text-text-3/80'
113
+ }`}>
114
+ {kit.badge || formatAgentCount(kit.agents.length)}
115
+ </span>
116
+ </div>
117
+ <p className="mt-2 text-[13px] leading-relaxed text-text-2">{kit.description}</p>
118
+ <p className="mt-3 text-[12px] leading-relaxed text-text-3/72">{kit.detail}</p>
119
+ {kit.agents.length > 0 ? (
120
+ <div className="mt-4 flex flex-wrap gap-2">
121
+ {kit.agents.map((agent) => (
122
+ <span
123
+ key={agent.id}
124
+ className="rounded-full border border-white/[0.08] bg-white/[0.03] px-2.5 py-1 text-[11px] text-text-2"
125
+ >
126
+ {agent.name}
127
+ </span>
128
+ ))}
129
+ </div>
130
+ ) : (
131
+ <div className="mt-4 rounded-[12px] border border-dashed border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[11px] text-text-3/70">
132
+ Finish setup without starter agents.
133
+ </div>
134
+ )}
135
+ </button>
136
+ )
137
+ })}
138
+ </div>
139
+ </div>
140
+
141
+ <div className="mt-6 flex items-center justify-center gap-3">
142
+ <button
143
+ onClick={onBack}
144
+ className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
145
+ >
146
+ Back
147
+ </button>
148
+ <button
149
+ onClick={onContinue}
150
+ className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600 cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200 shadow-[0_6px_28px_rgba(99,102,241,0.3)]"
151
+ >
152
+ Continue to Providers
153
+ </button>
154
+ </div>
155
+
156
+ <SkipLink onClick={onSkip} />
157
+ </StepShell>
158
+ )
159
+ }
@@ -2,6 +2,7 @@ import { STEP_ORDER } from './types'
2
2
 
3
3
  const STEP_LABELS: Record<string, string> = {
4
4
  profile: 'You',
5
+ path: 'Start',
5
6
  providers: 'Providers',
6
7
  agents: 'Agents',
7
8
  }
@@ -12,6 +12,7 @@ export function StepProviders({
12
12
  configuredProviderIds,
13
13
  error,
14
14
  canContinue,
15
+ onBack,
15
16
  onSelectProvider,
16
17
  onRemoveProvider,
17
18
  onContinue,
@@ -132,6 +133,14 @@ export function StepProviders({
132
133
  {error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
133
134
 
134
135
  <div className="mt-6 flex items-center justify-center gap-3">
136
+ <button
137
+ type="button"
138
+ onClick={onBack}
139
+ className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
140
+ font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
141
+ >
142
+ Back
143
+ </button>
135
144
  <button
136
145
  onClick={onSkip}
137
146
  className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
@@ -2,10 +2,10 @@ import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
3
  import { STEP_ORDER } from './types'
4
4
 
5
- test('STEP_ORDER is [profile, providers, agents]', () => {
6
- assert.deepEqual(STEP_ORDER, ['profile', 'providers', 'agents'])
5
+ test('STEP_ORDER includes the new onboarding path step', () => {
6
+ assert.deepEqual(STEP_ORDER, ['profile', 'path', 'providers', 'agents'])
7
7
  })
8
8
 
9
- test('STEP_ORDER has exactly 3 steps', () => {
10
- assert.equal(STEP_ORDER.length, 3)
9
+ test('STEP_ORDER has exactly 4 steps', () => {
10
+ assert.equal(STEP_ORDER.length, 4)
11
11
  })
@@ -1,7 +1,7 @@
1
1
  import type { GatewayProfile, ProviderId } from '@/types'
2
2
  import type { SetupProvider } from '@/lib/setup-defaults'
3
3
 
4
- export type SetupStep = 'profile' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
4
+ export type SetupStep = 'profile' | 'path' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
5
5
  export type CheckState = 'idle' | 'checking' | 'ok' | 'error'
6
6
 
7
7
  export interface ProviderCheckResponse {
@@ -29,7 +29,7 @@ export interface SetupDoctorResponse {
29
29
  }
30
30
 
31
31
  export interface SetupWizardProps {
32
- onComplete: () => void
32
+ onComplete: (destination?: string) => void
33
33
  }
34
34
 
35
35
  export interface ConfiguredProvider {
@@ -83,7 +83,7 @@ export interface CreatedAgentSummary {
83
83
  providerName: string
84
84
  }
85
85
 
86
- export const STEP_ORDER: SetupStep[] = ['profile', 'providers', 'agents']
86
+ export const STEP_ORDER: SetupStep[] = ['profile', 'path', 'providers', 'agents']
87
87
 
88
88
  export const CONNECTOR_ICONS = [
89
89
  { name: 'Discord', icon: 'D' },
@@ -114,12 +114,25 @@ export interface StepProvidersProps {
114
114
  configuredProviderIds: Set<SetupProvider>
115
115
  error: string
116
116
  canContinue: boolean
117
+ onBack: () => void
117
118
  onSelectProvider: (provider: SetupProvider) => void
118
119
  onRemoveProvider: (id: string) => void
119
120
  onContinue: () => void
120
121
  onSkip: () => void
121
122
  }
122
123
 
124
+ export interface StepPathProps {
125
+ onboardingPath: import('@/lib/setup-defaults').OnboardingPath
126
+ starterKitId: string | null
127
+ intentText: string
128
+ onPathChange: (path: import('@/lib/setup-defaults').OnboardingPath) => void
129
+ onStarterKitChange: (starterKitId: string) => void
130
+ onIntentTextChange: (value: string) => void
131
+ onContinue: () => void
132
+ onBack: () => void
133
+ onSkip: () => void
134
+ }
135
+
123
136
  export interface StepConnectProps {
124
137
  provider: SetupProvider
125
138
  selectedProvider: import('@/lib/setup-defaults').SetupProviderOption
@@ -147,9 +160,13 @@ export interface StepAgentsProps {
147
160
  }
148
161
 
149
162
  export interface StepNextProps {
150
- onAddProvider: () => void
151
- onAddAgent: () => void
163
+ createdAgents: CreatedAgentSummary[]
152
164
  onContinueToDashboard: () => void
165
+ onOpenFirstAgent: () => void
166
+ onOpenProtocols: () => void
167
+ onOpenBuilder: () => void
168
+ onOpenConnectors: () => void
169
+ onOpenUsage: () => void
153
170
  }
154
171
 
155
172
  export interface StepDoneProps {
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
3
  import {
4
4
  stepIndex,
5
+ defaultKitForPath,
5
6
  formatEndpointHost,
7
+ getStarterKitsForPath,
6
8
  isLocalOpenClawEndpoint,
7
9
  resolveOpenClawDashboardUrl,
8
10
  getOpenClawErrorHint,
@@ -21,16 +23,50 @@ test('stepIndex: profile → 0', () => {
21
23
  assert.equal(stepIndex('profile'), 0)
22
24
  })
23
25
 
24
- test('stepIndex: providers → 1', () => {
25
- assert.equal(stepIndex('providers'), 1)
26
+ test('stepIndex: path → 1', () => {
27
+ assert.equal(stepIndex('path'), 1)
26
28
  })
27
29
 
28
- test('stepIndex: connect maps to providers index (1)', () => {
29
- assert.equal(stepIndex('connect'), 1)
30
+ test('stepIndex: providers 2', () => {
31
+ assert.equal(stepIndex('providers'), 2)
30
32
  })
31
33
 
32
- test('stepIndex: agents 2', () => {
33
- assert.equal(stepIndex('agents'), 2)
34
+ test('stepIndex: connect maps to providers index (2)', () => {
35
+ assert.equal(stepIndex('connect'), 2)
36
+ })
37
+
38
+ test('stepIndex: agents → 3', () => {
39
+ assert.equal(stepIndex('agents'), 3)
40
+ })
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // onboarding path defaults
44
+ // ---------------------------------------------------------------------------
45
+
46
+ test('defaultKitForPath returns personal assistant for quick and intent', () => {
47
+ assert.equal(defaultKitForPath('quick'), 'personal_assistant')
48
+ assert.equal(defaultKitForPath('intent'), 'personal_assistant')
49
+ })
50
+
51
+ test('defaultKitForPath returns blank workspace for manual', () => {
52
+ assert.equal(defaultKitForPath('manual'), 'blank_workspace')
53
+ })
54
+
55
+ test('getStarterKitsForPath: quick exposes a reduced starter set', () => {
56
+ const ids = getStarterKitsForPath('quick').map((kit) => kit.id)
57
+ assert.deepEqual(ids, ['personal_assistant', 'research_copilot', 'builder_studio'])
58
+ })
59
+
60
+ test('getStarterKitsForPath: intent stays focused on broad starter shapes', () => {
61
+ const ids = getStarterKitsForPath('intent').map((kit) => kit.id)
62
+ assert.deepEqual(ids, ['personal_assistant', 'research_copilot', 'builder_studio', 'operator_swarm'])
63
+ })
64
+
65
+ test('getStarterKitsForPath: manual keeps the full catalog', () => {
66
+ const ids = new Set(getStarterKitsForPath('manual').map((kit) => kit.id))
67
+ assert.equal(ids.has('blank_workspace'), true)
68
+ assert.equal(ids.has('content_studio'), true)
69
+ assert.equal(ids.has('openclaw_fleet'), true)
34
70
  })
35
71
 
36
72
  // ---------------------------------------------------------------------------
@@ -261,6 +297,37 @@ test('buildStarterDrafts carries custom runtime provider ids alongside custom se
261
297
  }
262
298
  })
263
299
 
300
+ test('buildStarterDrafts injects current intent into starter prompts', () => {
301
+ const cp = makeConfiguredProvider({
302
+ setupProvider: 'openai',
303
+ provider: 'openai',
304
+ defaultModel: 'gpt-4o',
305
+ })
306
+ const drafts = buildStarterDrafts({
307
+ starterKitId: 'personal_assistant',
308
+ intentText: 'Help me run weekly product research and turn it into follow-up tasks.',
309
+ configuredProviders: [cp],
310
+ })
311
+
312
+ assert.match(drafts[0]?.systemPrompt || '', /Current user intent:/)
313
+ assert.match(drafts[0]?.systemPrompt || '', /weekly product research/i)
314
+ })
315
+
316
+ test('buildStarterDrafts creates the delegate team starter pair', () => {
317
+ const cp = makeConfiguredProvider({
318
+ setupProvider: 'openai',
319
+ provider: 'openai',
320
+ defaultModel: 'gpt-4o',
321
+ })
322
+ const drafts = buildStarterDrafts({
323
+ starterKitId: 'operator_swarm',
324
+ intentText: '',
325
+ configuredProviders: [cp],
326
+ })
327
+
328
+ assert.deepEqual(drafts.map((draft) => draft.name), ['Operator', 'Maker'])
329
+ })
330
+
264
331
  test('requiresSetupProviderVerification skips custom providers', () => {
265
332
  assert.equal(requiresSetupProviderVerification('custom'), false)
266
333
  assert.equal(requiresSetupProviderVerification('openclaw'), false)
@@ -2,6 +2,7 @@ import {
2
2
  STARTER_KITS,
3
3
  getDefaultModelForProvider,
4
4
  type OnboardingPath,
5
+ type StarterKit,
5
6
  type SetupProvider,
6
7
  type StarterKitAgentTemplate,
7
8
  } from '@/lib/setup-defaults'
@@ -18,6 +19,18 @@ export function defaultKitForPath(path: OnboardingPath): string {
18
19
  return 'personal_assistant'
19
20
  }
20
21
 
22
+ export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
23
+ if (path === 'quick') {
24
+ const quickIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot'])
25
+ return STARTER_KITS.filter((kit) => quickIds.has(kit.id))
26
+ }
27
+ if (path === 'intent') {
28
+ const intentIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot', 'operator_swarm'])
29
+ return STARTER_KITS.filter((kit) => intentIds.has(kit.id))
30
+ }
31
+ return STARTER_KITS
32
+ }
33
+
21
34
  export function applyIntentContext(prompt: string, intentText: string): string {
22
35
  const trimmed = intentText.trim()
23
36
  if (!trimmed) return prompt
@@ -0,0 +1,135 @@
1
+ 'use client'
2
+
3
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
4
+ import { LaunchActionCard } from '@/components/shared/launch-action-card'
5
+ import type { Agent } from '@/types'
6
+
7
+ function SnapshotItem({ label, value, hint }: { label: string; value: string; hint: string }) {
8
+ return (
9
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-4 py-3">
10
+ <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{label}</div>
11
+ <div className="mt-2 text-[24px] font-display font-700 tracking-[-0.03em] text-text">{value}</div>
12
+ <div className="mt-1 text-[12px] leading-relaxed text-text-3/68">{hint}</div>
13
+ </div>
14
+ )
15
+ }
16
+
17
+ type Props = {
18
+ firstAgent: Agent | null
19
+ agentCount: number
20
+ sessionCount: number
21
+ taskCount: number
22
+ scheduleCount: number
23
+ connectorCount: number
24
+ todayCost: number
25
+ onOpenFirstAgent: () => void
26
+ onOpenProtocols: () => void
27
+ onOpenBuilder: () => void
28
+ onOpenConnectors: () => void
29
+ onOpenUsage: () => void
30
+ }
31
+
32
+ export function HomeLaunchpad({
33
+ firstAgent,
34
+ agentCount,
35
+ sessionCount,
36
+ taskCount,
37
+ scheduleCount,
38
+ connectorCount,
39
+ todayCost,
40
+ onOpenFirstAgent,
41
+ onOpenProtocols,
42
+ onOpenBuilder,
43
+ onOpenConnectors,
44
+ onOpenUsage,
45
+ }: Props) {
46
+ return (
47
+ <div className="max-w-[980px] mx-auto px-6 py-10">
48
+ <div className="rounded-[24px] border border-white/[0.06] bg-gradient-to-br from-white/[0.05] via-white/[0.02] to-transparent p-6">
49
+ <div className="inline-flex rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.16em] text-text-3/70">
50
+ Launchpad
51
+ </div>
52
+ <div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
53
+ <div className="max-w-[620px]">
54
+ <h1 className="font-display text-[34px] font-700 tracking-[-0.03em] text-text">
55
+ Start with the result you want, not the control plane.
56
+ </h1>
57
+ <p className="mt-3 text-[15px] leading-relaxed text-text-3/72">
58
+ SwarmClaw already has the building blocks. Use this workspace to start a live agent chat, launch a bounded session, wire a connector, or move straight into reusable workflows.
59
+ </p>
60
+ </div>
61
+ <div className="rounded-[18px] border border-white/[0.06] bg-white/[0.03] p-4 min-w-[240px]">
62
+ <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Workspace Anchor</div>
63
+ <div className="mt-3 flex items-center gap-3">
64
+ {firstAgent ? (
65
+ <>
66
+ <AgentAvatar
67
+ seed={firstAgent.avatarSeed}
68
+ avatarUrl={firstAgent.avatarUrl}
69
+ name={firstAgent.name}
70
+ size={44}
71
+ />
72
+ <div>
73
+ <div className="text-[14px] font-display font-700 text-text">{firstAgent.name}</div>
74
+ <div className="text-[12px] text-text-3/70">
75
+ {firstAgent.model ? firstAgent.model.split('/').pop()?.split(':')[0] : firstAgent.provider}
76
+ </div>
77
+ </div>
78
+ </>
79
+ ) : (
80
+ <div className="text-[13px] leading-relaxed text-text-3/72">
81
+ No agents yet. Start by creating one or use the workflow tools first.
82
+ </div>
83
+ )}
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div className="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
90
+ <LaunchActionCard
91
+ title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
92
+ description={firstAgent
93
+ ? `Jump into ${firstAgent.name} and start using the workspace immediately.`
94
+ : 'Open the agents workspace to create or tune the first specialist agent.'}
95
+ actionLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
96
+ onClick={onOpenFirstAgent}
97
+ tone="primary"
98
+ />
99
+ <LaunchActionCard
100
+ title="Start Structured Session"
101
+ description="Open bounded collaboration runs for planning, review, decision-making, or focused multi-agent work."
102
+ actionLabel="Open Protocols"
103
+ onClick={onOpenProtocols}
104
+ />
105
+ <LaunchActionCard
106
+ title="Open Workflow Builder"
107
+ description="Move straight into reusable orchestration graphs if you want a durable workflow instead of a one-off run."
108
+ actionLabel="Open Builder"
109
+ onClick={onOpenBuilder}
110
+ />
111
+ <LaunchActionCard
112
+ title="Connect a Platform"
113
+ description="Bridge agents into chat surfaces like Discord, Slack, Telegram, and WhatsApp."
114
+ actionLabel="Open Connectors"
115
+ onClick={onOpenConnectors}
116
+ />
117
+ <LaunchActionCard
118
+ title="Review Usage"
119
+ description="Check cost, provider health, and activity so the workspace stays observable from the start."
120
+ actionLabel="Open Usage"
121
+ onClick={onOpenUsage}
122
+ />
123
+ </div>
124
+
125
+ <div className="mt-8 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
126
+ <SnapshotItem label="Agents" value={String(agentCount)} hint="Configured specialists available in this workspace." />
127
+ <SnapshotItem label="Chats" value={String(sessionCount)} hint="Durable conversations already created." />
128
+ <SnapshotItem label="Tasks" value={String(taskCount)} hint="Queued or archived work items in the board." />
129
+ <SnapshotItem label="Schedules" value={String(scheduleCount)} hint="Recurring or delayed automations ready to run." />
130
+ <SnapshotItem label="Connectors" value={String(connectorCount)} hint="Platform bridges currently configured." />
131
+ <SnapshotItem label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated usage cost for today across providers." />
132
+ </div>
133
+ </div>
134
+ )
135
+ }
@@ -1,31 +1,38 @@
1
1
  import { useProtocolTemplatesQuery } from '@/features/protocols/queries'
2
2
  import { useRouter } from 'next/navigation'
3
- import { cn } from '@/lib/utils'
4
3
  import type { ProtocolTemplate } from '@/types'
5
4
 
6
- export function TemplateGallery() {
7
- const { data: templates } = useProtocolTemplatesQuery()
5
+ type Props = {
6
+ templates?: ProtocolTemplate[]
7
+ }
8
+
9
+ export function TemplateGallery({ templates: providedTemplates }: Props) {
10
+ const { data: queriedTemplates } = useProtocolTemplatesQuery({ enabled: !providedTemplates })
8
11
  const router = useRouter()
12
+ const templates = providedTemplates ?? queriedTemplates ?? []
9
13
 
10
- const builtInTemplates = templates?.filter((t) => t.builtIn) || []
11
- const customTemplates = templates?.filter((t) => !t.builtIn) || []
14
+ const builtInTemplates = templates.filter((t) => t.builtIn)
15
+ const customTemplates = templates.filter((t) => !t.builtIn)
12
16
 
13
17
  const renderCard = (template: ProtocolTemplate) => (
14
18
  <button
15
19
  key={template.id}
16
20
  onClick={() => router.push(`/protocols/builder/${template.id}`)}
17
- className={cn(
18
- 'rounded-lg border bg-card p-4 text-left transition-shadow hover:shadow-md',
19
- )}
21
+ className="rounded-[16px] border border-white/[0.06] bg-white/[0.03] p-4 text-left transition-all hover:border-accent-bright/20 hover:bg-white/[0.05] cursor-pointer"
20
22
  >
21
- <div className="text-sm font-semibold">{template.name}</div>
22
- <div className="mt-1 text-xs text-muted-foreground line-clamp-2">
23
+ <div className="flex items-start justify-between gap-3">
24
+ <div className="text-[14px] font-display font-700 text-text">{template.name}</div>
25
+ <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] text-text-3/70">
26
+ {template.builtIn ? 'Built-in' : 'Custom'}
27
+ </span>
28
+ </div>
29
+ <div className="mt-2 text-[12px] leading-relaxed text-text-3/72 line-clamp-3">
23
30
  {template.description}
24
31
  </div>
25
32
  {template.tags && template.tags.length > 0 && (
26
- <div className="mt-2 flex gap-1">
33
+ <div className="mt-3 flex flex-wrap gap-1.5">
27
34
  {template.tags.slice(0, 2).map((tag) => (
28
- <span key={tag} className="rounded bg-muted px-1.5 py-0.5 text-[10px]">
35
+ <span key={tag} className="rounded-full border border-white/[0.08] bg-white/[0.03] px-2 py-1 text-[10px] text-text-2">
29
36
  {tag}
30
37
  </span>
31
38
  ))}
@@ -38,14 +45,14 @@ export function TemplateGallery() {
38
45
  <div className="space-y-4">
39
46
  {builtInTemplates.length > 0 && (
40
47
  <div>
41
- <h4 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Built-in</h4>
42
- <div className="grid grid-cols-2 gap-3">{builtInTemplates.map(renderCard)}</div>
48
+ <h4 className="mb-2 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Built-in</h4>
49
+ <div className="grid gap-3 md:grid-cols-2">{builtInTemplates.map(renderCard)}</div>
43
50
  </div>
44
51
  )}
45
52
  {customTemplates.length > 0 && (
46
53
  <div>
47
- <h4 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Custom</h4>
48
- <div className="grid grid-cols-2 gap-3">{customTemplates.map(renderCard)}</div>
54
+ <h4 className="mb-2 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Custom</h4>
55
+ <div className="grid gap-3 md:grid-cols-2">{customTemplates.map(renderCard)}</div>
49
56
  </div>
50
57
  )}
51
58
  </div>
@@ -0,0 +1,27 @@
1
+ type LaunchActionCardProps = {
2
+ title: string
3
+ description: string
4
+ actionLabel: string
5
+ onClick: () => void
6
+ tone?: 'primary' | 'default'
7
+ }
8
+
9
+ export function LaunchActionCard({ title, description, actionLabel, onClick, tone = 'default' }: LaunchActionCardProps) {
10
+ return (
11
+ <div className="rounded-[18px] border border-white/[0.06] bg-white/[0.03] p-4">
12
+ <div className="text-[15px] font-display font-700 text-text">{title}</div>
13
+ <p className="mt-2 text-[13px] leading-relaxed text-text-3/72">{description}</p>
14
+ <button
15
+ type="button"
16
+ onClick={onClick}
17
+ className={`mt-4 rounded-[12px] px-4 py-2.5 text-[13px] font-display font-700 transition-all cursor-pointer ${
18
+ tone === 'primary'
19
+ ? 'bg-accent-bright text-black hover:opacity-90'
20
+ : 'border border-white/[0.08] bg-white/[0.04] text-text-2 hover:bg-white/[0.08]'
21
+ }`}
22
+ >
23
+ {actionLabel}
24
+ </button>
25
+ </div>
26
+ )
27
+ }