@swarmclawai/swarmclaw 1.4.0 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +13 -71
  2. package/next.config.ts +9 -4
  3. package/package.json +10 -8
  4. package/scripts/build-bootstrap-env.mjs +24 -0
  5. package/scripts/run-next-build.mjs +120 -0
  6. package/scripts/run-next-typegen.mjs +61 -0
  7. package/src/app/api/approvals/route.test.ts +29 -3
  8. package/src/app/api/approvals/route.ts +13 -7
  9. package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
  10. package/src/app/api/chats/[id]/chat/route.ts +24 -8
  11. package/src/app/api/chats/chat-route.test.ts +68 -0
  12. package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
  13. package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
  14. package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
  15. package/src/app/api/logs/route.test.ts +61 -0
  16. package/src/app/api/logs/route.ts +35 -0
  17. package/src/app/api/swarmdock/route.ts +25 -0
  18. package/src/app/api/swarmfeed/posts/route.ts +44 -6
  19. package/src/app/api/swarmfeed/route.ts +4 -0
  20. package/src/app/api/tts/route.test.ts +82 -0
  21. package/src/app/api/tts/route.ts +13 -6
  22. package/src/app/api/tts/stream/route.ts +12 -5
  23. package/src/app/error.tsx +32 -0
  24. package/src/app/global-error.tsx +33 -0
  25. package/src/app/marketplace/page.tsx +7 -0
  26. package/src/cli/index.js +10 -0
  27. package/src/cli/spec.js +1 -0
  28. package/src/components/agents/agent-sheet.tsx +10 -0
  29. package/src/components/layout/error-boundary.tsx +12 -30
  30. package/src/components/layout/error-fallback.tsx +61 -0
  31. package/src/components/layout/sidebar-rail.tsx +5 -0
  32. package/src/features/swarmdock/agent-marketplace-settings.tsx +303 -0
  33. package/src/features/swarmdock/marketplace-page.tsx +189 -0
  34. package/src/features/swarmfeed/feed-page.tsx +3 -33
  35. package/src/features/swarmfeed/queries.ts +3 -3
  36. package/src/lib/app/navigation.ts +1 -0
  37. package/src/lib/app/report-client-error.ts +52 -0
  38. package/src/lib/app/view-constants.ts +9 -1
  39. package/src/lib/providers/anthropic.ts +9 -1
  40. package/src/lib/providers/ollama.ts +34 -14
  41. package/src/lib/providers/openai.ts +9 -1
  42. package/src/lib/providers/openclaw.ts +3 -3
  43. package/src/lib/server/agents/agent-service.ts +18 -0
  44. package/src/lib/server/agents/agent-swarm-registration.ts +35 -0
  45. package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
  46. package/src/lib/server/connectors/swarmdock.ts +29 -7
  47. package/src/lib/server/messages/message-repository.ts +31 -0
  48. package/src/lib/server/provider-health.ts +19 -3
  49. package/src/lib/server/safe-parse-body.test.ts +32 -0
  50. package/src/lib/server/safe-parse-body.ts +20 -3
  51. package/src/lib/server/session-tools/index.ts +4 -0
  52. package/src/lib/server/session-tools/swarmdock.ts +104 -0
  53. package/src/lib/server/session-tools/swarmfeed.ts +150 -0
  54. package/src/lib/server/storage-normalization.ts +10 -0
  55. package/src/lib/server/storage.ts +13 -4
  56. package/src/lib/swarmfeed-client.ts +1 -1
  57. package/src/lib/tool-definitions.ts +2 -0
  58. package/src/types/agent.ts +23 -0
  59. package/src/types/session.ts +1 -1
  60. package/tsconfig.json +1 -2
  61. package/src/.env.local +0 -4
package/src/cli/spec.js CHANGED
@@ -215,6 +215,7 @@ const COMMAND_GROUPS = {
215
215
  commands: {
216
216
  list: { description: 'Fetch logs (supports --query lines=200,level=INFO)', method: 'GET', path: '/logs' },
217
217
  clear: { description: 'Clear log file', method: 'DELETE', path: '/logs' },
218
+ report: { description: 'Write a client/browser error entry to the application log', method: 'POST', path: '/logs' },
218
219
  },
219
220
  },
220
221
 
@@ -29,6 +29,7 @@ import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
29
29
  import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
30
30
  import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
31
31
  import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
32
+ import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
32
33
 
33
34
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
34
35
  const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
@@ -1983,6 +1984,15 @@ export function AgentSheet() {
1983
1984
  </SectionCard>
1984
1985
  )}
1985
1986
 
1987
+ {editing && (
1988
+ <SectionCard
1989
+ title="Marketplace"
1990
+ description="SwarmDock integration — list this agent on the AI marketplace to accept tasks and earn USDC."
1991
+ >
1992
+ <AgentMarketplaceSettings agent={editing} />
1993
+ </SectionCard>
1994
+ )}
1995
+
1986
1996
  {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
1987
1997
  <AdvancedSettingsSection
1988
1998
  open={showAdvancedSettings}
@@ -3,6 +3,9 @@
3
3
  import { Component } from 'react'
4
4
  import type { ReactNode, ErrorInfo } from 'react'
5
5
 
6
+ import { reportClientError } from '@/lib/app/report-client-error'
7
+ import { ErrorFallback } from '@/components/layout/error-fallback'
8
+
6
9
  export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
7
10
  constructor(props: { children: ReactNode }) {
8
11
  super(props)
@@ -15,41 +18,20 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError
15
18
  }
16
19
 
17
20
  componentDidCatch(error: Error, info: ErrorInfo) {
18
- console.error('ErrorBoundary caught:', error, info)
21
+ reportClientError({
22
+ source: 'error-boundary',
23
+ error,
24
+ componentStack: info.componentStack,
25
+ })
19
26
  }
20
27
 
21
28
  render() {
22
29
  if (this.state.hasError) {
23
30
  return (
24
- <div className="flex-1 flex flex-col items-center justify-center px-8 bg-bg">
25
- <div className="text-center max-w-[400px]">
26
- <div className="w-14 h-14 rounded-[16px] bg-red-500/10 border border-red-500/20 flex items-center justify-center mx-auto mb-5">
27
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-red-400">
28
- <circle cx="12" cy="12" r="10" />
29
- <line x1="12" y1="8" x2="12" y2="12" />
30
- <line x1="12" y1="16" x2="12.01" y2="16" />
31
- </svg>
32
- </div>
33
- <h2 className="font-display text-[22px] font-700 text-text mb-2 tracking-[-0.02em]">
34
- Something went wrong
35
- </h2>
36
- <p className="text-[14px] text-text-3 mb-6">
37
- An unexpected error occurred. Try reloading the page.
38
- </p>
39
- <button
40
- onClick={() => window.location.reload()}
41
- className="inline-flex items-center gap-2 px-6 py-3 rounded-[12px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer
42
- hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
43
- style={{ fontFamily: 'inherit' }}
44
- >
45
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
46
- <polyline points="23 4 23 10 17 10" />
47
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
48
- </svg>
49
- Reload
50
- </button>
51
- </div>
52
- </div>
31
+ <ErrorFallback
32
+ message="An unexpected dashboard error occurred. Reload the page to recover."
33
+ onPrimaryAction={() => window.location.reload()}
34
+ />
53
35
  )
54
36
  }
55
37
 
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ type ErrorFallbackProps = {
4
+ title?: string
5
+ message?: string
6
+ primaryLabel?: string
7
+ onPrimaryAction?: () => void
8
+ secondaryLabel?: string
9
+ onSecondaryAction?: () => void
10
+ }
11
+
12
+ export function ErrorFallback({
13
+ title = 'Something went wrong',
14
+ message = 'An unexpected error occurred. Try again or reload the page.',
15
+ primaryLabel = 'Reload',
16
+ onPrimaryAction,
17
+ secondaryLabel,
18
+ onSecondaryAction,
19
+ }: ErrorFallbackProps) {
20
+ return (
21
+ <div className="flex min-h-[50vh] flex-1 flex-col items-center justify-center bg-bg px-8">
22
+ <div className="max-w-[420px] text-center">
23
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-[16px] border border-red-500/20 bg-red-500/10">
24
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-red-400">
25
+ <circle cx="12" cy="12" r="10" />
26
+ <line x1="12" y1="8" x2="12" y2="12" />
27
+ <line x1="12" y1="16" x2="12.01" y2="16" />
28
+ </svg>
29
+ </div>
30
+ <h2 className="mb-2 font-display text-[22px] font-700 tracking-[-0.02em] text-text">
31
+ {title}
32
+ </h2>
33
+ <p className="mb-6 text-[14px] text-text-3">
34
+ {message}
35
+ </p>
36
+ <div className="flex flex-wrap items-center justify-center gap-3">
37
+ <button
38
+ onClick={onPrimaryAction}
39
+ className="inline-flex cursor-pointer items-center gap-2 rounded-[12px] border-none bg-accent-bright px-6 py-3 text-[14px] font-600 text-white shadow-[0_4px_16px_rgba(99,102,241,0.2)] transition-all hover:brightness-110 active:scale-[0.97]"
40
+ style={{ fontFamily: 'inherit' }}
41
+ >
42
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
43
+ <polyline points="23 4 23 10 17 10" />
44
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
45
+ </svg>
46
+ {primaryLabel}
47
+ </button>
48
+ {secondaryLabel && onSecondaryAction ? (
49
+ <button
50
+ onClick={onSecondaryAction}
51
+ className="inline-flex cursor-pointer items-center rounded-[12px] border border-border bg-transparent px-5 py-3 text-[14px] font-600 text-text transition-colors hover:bg-panel/60"
52
+ style={{ fontFamily: 'inherit' }}
53
+ >
54
+ {secondaryLabel}
55
+ </button>
56
+ ) : null}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ )
61
+ }
@@ -267,6 +267,11 @@ export function SidebarRail({
267
267
  <path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1" />
268
268
  </svg>
269
269
  </NavItem>
270
+ <NavItem view="marketplace" label="Marketplace" expanded={railExpanded} isActive={isNavActive('marketplace')} onClick={() => handleNavClick('marketplace')}>
271
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
272
+ <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
273
+ </svg>
274
+ </NavItem>
270
275
  </div>
271
276
 
272
277
  <div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
@@ -0,0 +1,303 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useMemo } from 'react'
4
+ import { updateAgent } from '@/lib/agents'
5
+ import { toast } from 'sonner'
6
+ import { HintTip } from '@/components/shared/hint-tip'
7
+ import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
8
+ import { useAppStore } from '@/stores/use-app-store'
9
+ import type { Agent, SwarmDockMarketplaceConfig } from '@/types'
10
+
11
+ const DEFAULT_MARKETPLACE: SwarmDockMarketplaceConfig = {
12
+ enabled: false,
13
+ autoDiscover: false,
14
+ maxBudgetUsdc: '5000000',
15
+ autoBid: false,
16
+ autoBidMaxPrice: '1000000',
17
+ taskNotifications: true,
18
+ preferredCategories: [],
19
+ }
20
+
21
+ export function AgentMarketplaceSettings({ agent, onUpdate }: {
22
+ agent: Agent
23
+ onUpdate?: (agent: Agent) => void
24
+ }) {
25
+ const [enabled, setEnabled] = useState(agent.swarmdockEnabled || false)
26
+ const [description, setDescription] = useState(agent.swarmdockDescription || '')
27
+ const [skills, setSkills] = useState<string[]>(agent.swarmdockSkills || [])
28
+ const [walletId, setWalletId] = useState<string | null>(agent.swarmdockWalletId || null)
29
+ const [marketplace, setMarketplace] = useState<SwarmDockMarketplaceConfig>(agent.swarmdockMarketplace || DEFAULT_MARKETPLACE)
30
+ const [showAdvanced, setShowAdvanced] = useState(false)
31
+ const [skillInput, setSkillInput] = useState('')
32
+ const [saving, setSaving] = useState(false)
33
+
34
+ const wallets = useAppStore((s) => s.wallets)
35
+ const loadWallets = useAppStore((s) => s.loadWallets)
36
+
37
+ useEffect(() => {
38
+ loadWallets()
39
+ }, [loadWallets])
40
+
41
+ const agentWallets = useMemo(() =>
42
+ Object.values(wallets).filter((w) => w.agentId === agent.id),
43
+ [wallets, agent.id])
44
+
45
+ const handleSave = useCallback(async () => {
46
+ setSaving(true)
47
+ try {
48
+ const updated = await updateAgent(agent.id, {
49
+ swarmdockEnabled: enabled,
50
+ swarmdockDescription: description.trim() || null,
51
+ swarmdockSkills: skills,
52
+ swarmdockWalletId: walletId,
53
+ swarmdockListedAt: enabled && !agent.swarmdockListedAt ? Date.now() : agent.swarmdockListedAt,
54
+ swarmdockMarketplace: marketplace.enabled ? marketplace : null,
55
+ })
56
+ toast.success('Marketplace settings saved')
57
+ onUpdate?.(updated)
58
+ } catch {
59
+ toast.error('Failed to save marketplace settings')
60
+ } finally {
61
+ setSaving(false)
62
+ }
63
+ }, [agent.id, agent.swarmdockListedAt, enabled, description, skills, walletId, marketplace, onUpdate])
64
+
65
+ const addSkill = useCallback(() => {
66
+ const trimmed = skillInput.trim().toLowerCase().replace(/\s+/g, '-')
67
+ if (trimmed && !skills.includes(trimmed)) {
68
+ setSkills((prev) => [...prev, trimmed])
69
+ }
70
+ setSkillInput('')
71
+ }, [skillInput, skills])
72
+
73
+ const removeSkill = useCallback((skill: string) => {
74
+ setSkills((prev) => prev.filter((s) => s !== skill))
75
+ }, [])
76
+
77
+ const truncateAddr = (addr: string) =>
78
+ addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr
79
+
80
+ return (
81
+ <div className="space-y-5">
82
+ {/* Enable/Disable toggle */}
83
+ <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
84
+ <div className="min-w-0">
85
+ <div className="flex items-center gap-2">
86
+ <p className="text-[14px] font-600 text-text">SwarmDock</p>
87
+ <HintTip text="Enable this agent to list on the SwarmDock AI marketplace" />
88
+ </div>
89
+ <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
90
+ List this agent on the marketplace to accept tasks and earn USDC.
91
+ </p>
92
+ </div>
93
+ <button
94
+ type="button"
95
+ onClick={() => setEnabled((c) => !c)}
96
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
97
+ aria-pressed={enabled}
98
+ >
99
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${enabled ? 'translate-x-5' : 'translate-x-0'}`} />
100
+ </button>
101
+ </div>
102
+
103
+ {enabled && (
104
+ <>
105
+ {/* Description */}
106
+ <div>
107
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
108
+ Marketplace Description <HintTip text="A short description shown on the agent's marketplace profile" />
109
+ </label>
110
+ <textarea
111
+ value={description}
112
+ onChange={(e) => setDescription(e.target.value)}
113
+ placeholder="Describe what this agent specializes in..."
114
+ className="w-full min-h-[80px] px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow resize-y"
115
+ style={{ fontFamily: 'inherit' }}
116
+ maxLength={500}
117
+ />
118
+ </div>
119
+
120
+ {/* Skills */}
121
+ <div>
122
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
123
+ Skills <HintTip text="Skill tags for task matching on the marketplace" />
124
+ </label>
125
+ {skills.length > 0 && (
126
+ <div className="flex flex-wrap gap-2 mb-2">
127
+ {skills.map((skill) => (
128
+ <button
129
+ key={skill}
130
+ onClick={() => removeSkill(skill)}
131
+ className="px-3 py-1.5 rounded-[10px] border border-accent-bright/40 bg-accent-bright/10 text-accent-bright text-[12px] font-500 transition-all cursor-pointer bg-transparent hover:bg-red-500/10 hover:border-red-500/40 hover:text-red-400"
132
+ >
133
+ {skill} &times;
134
+ </button>
135
+ ))}
136
+ </div>
137
+ )}
138
+ <div className="flex gap-2">
139
+ <input
140
+ value={skillInput}
141
+ onChange={(e) => setSkillInput(e.target.value)}
142
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addSkill() } }}
143
+ placeholder="e.g. data-analysis, web-design"
144
+ className="flex-1 px-4 py-2.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
145
+ style={{ fontFamily: 'inherit' }}
146
+ />
147
+ <button
148
+ type="button"
149
+ onClick={addSkill}
150
+ disabled={!skillInput.trim()}
151
+ className="px-4 py-2.5 rounded-[12px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 transition-all hover:bg-white/[0.08] disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
152
+ >
153
+ Add
154
+ </button>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Wallet picker */}
159
+ <div>
160
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
161
+ Payout Wallet <HintTip text="Base L2 wallet for receiving USDC payments" />
162
+ </label>
163
+ {agentWallets.length > 0 ? (
164
+ <select
165
+ value={walletId || ''}
166
+ onChange={(e) => setWalletId(e.target.value || null)}
167
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none cursor-pointer"
168
+ style={{ fontFamily: 'inherit' }}
169
+ >
170
+ <option value="">No wallet selected</option>
171
+ {agentWallets.map((w) => (
172
+ <option key={w.id} value={w.id}>
173
+ {w.label ? `${w.label} (${truncateAddr(w.walletAddress)})` : truncateAddr(w.walletAddress)}
174
+ </option>
175
+ ))}
176
+ </select>
177
+ ) : (
178
+ <p className="text-[13px] text-text-3/75">
179
+ No wallets linked to this agent. Add a wallet in the Wallets section first.
180
+ </p>
181
+ )}
182
+ </div>
183
+
184
+ {/* Advanced: Marketplace config */}
185
+ <AdvancedSettingsSection
186
+ open={showAdvanced}
187
+ onToggle={() => setShowAdvanced((c) => !c)}
188
+ summary={marketplace.enabled ? 'Active' : undefined}
189
+ badges={marketplace.enabled ? [
190
+ ...(marketplace.autoDiscover ? ['auto-discover'] : []),
191
+ ...(marketplace.autoBid ? ['auto-bid'] : []),
192
+ ] : []}
193
+ >
194
+ <div className="space-y-4">
195
+ <div className="flex items-center justify-between gap-4">
196
+ <div className="min-w-0">
197
+ <div className="flex items-center gap-2">
198
+ <p className="text-[13px] font-600 text-text">Marketplace Automation</p>
199
+ <HintTip text="Enable automated task discovery and bidding on the marketplace" />
200
+ </div>
201
+ </div>
202
+ <button
203
+ type="button"
204
+ onClick={() => setMarketplace((m) => ({ ...m, enabled: !m.enabled }))}
205
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${marketplace.enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
206
+ aria-pressed={marketplace.enabled}
207
+ >
208
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${marketplace.enabled ? 'translate-x-5' : 'translate-x-0'}`} />
209
+ </button>
210
+ </div>
211
+
212
+ {marketplace.enabled && (
213
+ <>
214
+ <label className="flex items-center gap-3 cursor-pointer">
215
+ <div
216
+ onClick={() => setMarketplace((m) => ({ ...m, autoDiscover: !m.autoDiscover }))}
217
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoDiscover ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
218
+ >
219
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoDiscover ? 'left-[22px]' : 'left-0.5'}`} />
220
+ </div>
221
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
222
+ Auto-discover <HintTip text="Automatically scan for matching tasks" />
223
+ </span>
224
+ </label>
225
+
226
+ <label className="flex items-center gap-3 cursor-pointer">
227
+ <div
228
+ onClick={() => setMarketplace((m) => ({ ...m, autoBid: !m.autoBid }))}
229
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoBid ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
230
+ >
231
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoBid ? 'left-[22px]' : 'left-0.5'}`} />
232
+ </div>
233
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
234
+ Auto-bid <HintTip text="Automatically bid on matching tasks" />
235
+ </span>
236
+ </label>
237
+
238
+ <label className="flex items-center gap-3 cursor-pointer">
239
+ <div
240
+ onClick={() => setMarketplace((m) => ({ ...m, taskNotifications: !m.taskNotifications }))}
241
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.taskNotifications ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
242
+ >
243
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.taskNotifications ? 'left-[22px]' : 'left-0.5'}`} />
244
+ </div>
245
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
246
+ Task notifications <HintTip text="Show notifications for new matching tasks" />
247
+ </span>
248
+ </label>
249
+
250
+ <div>
251
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
252
+ Max budget (USDC) <HintTip text="Maximum budget cap per task. 1000000 = $1.00" />
253
+ </label>
254
+ <input
255
+ value={marketplace.maxBudgetUsdc}
256
+ onChange={(e) => setMarketplace((m) => ({ ...m, maxBudgetUsdc: e.target.value.replace(/[^0-9]/g, '') }))}
257
+ placeholder="5000000"
258
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
259
+ style={{ fontFamily: 'inherit' }}
260
+ />
261
+ <p className="mt-1 text-[11px] text-text-3/60">
262
+ = ${(parseInt(marketplace.maxBudgetUsdc || '0', 10) / 1_000_000).toFixed(2)} USDC
263
+ </p>
264
+ </div>
265
+
266
+ {marketplace.autoBid && (
267
+ <div>
268
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
269
+ Auto-bid max price (USDC) <HintTip text="Maximum amount to auto-bid per task. 1000000 = $1.00" />
270
+ </label>
271
+ <input
272
+ value={marketplace.autoBidMaxPrice}
273
+ onChange={(e) => setMarketplace((m) => ({ ...m, autoBidMaxPrice: e.target.value.replace(/[^0-9]/g, '') }))}
274
+ placeholder="1000000"
275
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
276
+ style={{ fontFamily: 'inherit' }}
277
+ />
278
+ <p className="mt-1 text-[11px] text-text-3/60">
279
+ = ${(parseInt(marketplace.autoBidMaxPrice || '0', 10) / 1_000_000).toFixed(2)} USDC
280
+ </p>
281
+ </div>
282
+ )}
283
+ </>
284
+ )}
285
+ </div>
286
+ </AdvancedSettingsSection>
287
+ </>
288
+ )}
289
+
290
+ {/* Save button */}
291
+ <div className="flex justify-end pt-2">
292
+ <button
293
+ onClick={handleSave}
294
+ disabled={saving}
295
+ className="px-6 py-2.5 rounded-[12px] bg-accent-bright text-white text-[14px] font-600 transition-all
296
+ hover:bg-accent-bright/90 disabled:opacity-40 disabled:cursor-not-allowed border-none cursor-pointer"
297
+ >
298
+ {saving ? 'Saving...' : 'Save Marketplace Settings'}
299
+ </button>
300
+ </div>
301
+ </div>
302
+ )
303
+ }
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import { MainContent } from '@/components/layout/main-content'
5
+ import { PageLoader } from '@/components/ui/page-loader'
6
+
7
+ interface MarketplaceTask {
8
+ id: string
9
+ title: string
10
+ description: string
11
+ status: string
12
+ budgetMin: string
13
+ budgetMax: string
14
+ skillRequirements: string[]
15
+ bidCount: number
16
+ createdAt: string
17
+ }
18
+
19
+ interface MarketplaceAgent {
20
+ id: string
21
+ displayName: string
22
+ description: string | null
23
+ framework: string | null
24
+ trustLevel: number
25
+ status: string
26
+ createdAt: string
27
+ }
28
+
29
+ type Tab = 'tasks' | 'agents'
30
+
31
+ // Proxy through SwarmClaw API to avoid CORS
32
+ const API_PREFIX = '/api/swarmdock'
33
+
34
+ function formatUsdc(microUnits: string): string {
35
+ const dollars = Number(microUnits) / 1_000_000
36
+ return `$${dollars.toFixed(2)}`
37
+ }
38
+
39
+ function timeAgo(dateStr: string): string {
40
+ const diff = Date.now() - new Date(dateStr).getTime()
41
+ const mins = Math.floor(diff / 60000)
42
+ if (mins < 60) return `${mins}m ago`
43
+ const hrs = Math.floor(mins / 60)
44
+ if (hrs < 24) return `${hrs}h ago`
45
+ const days = Math.floor(hrs / 24)
46
+ return `${days}d ago`
47
+ }
48
+
49
+ export function MarketplacePage() {
50
+ const [tab, setTab] = useState<Tab>('tasks')
51
+ const [tasks, setTasks] = useState<MarketplaceTask[]>([])
52
+ const [agents, setAgents] = useState<MarketplaceAgent[]>([])
53
+ const [loading, setLoading] = useState(true)
54
+ const [error, setError] = useState<string | null>(null)
55
+
56
+ const loadData = useCallback(async (activeTab: Tab) => {
57
+ setLoading(true)
58
+ setError(null)
59
+ try {
60
+ const res = await fetch(`${API_PREFIX}?type=${activeTab}&limit=50`)
61
+ if (!res.ok) throw new Error(`API error ${res.status}`)
62
+ const data = await res.json()
63
+ if (activeTab === 'tasks') {
64
+ setTasks(data.tasks || data)
65
+ } else {
66
+ setAgents(data.agents || data)
67
+ }
68
+ } catch (err: unknown) {
69
+ setError(err instanceof Error ? err.message : 'Failed to load')
70
+ } finally {
71
+ setLoading(false)
72
+ }
73
+ }, [])
74
+
75
+ useEffect(() => {
76
+ void loadData(tab)
77
+ }, [tab, loadData])
78
+
79
+ return (
80
+ <MainContent>
81
+ <div className="flex-1 overflow-y-auto overscroll-contain">
82
+ <div className="mx-auto max-w-4xl px-4 sm:px-6 py-8">
83
+ <div className="mb-6">
84
+ <h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">Marketplace</h1>
85
+ <p className="mt-1 text-[13px] text-text-3/75">Browse the SwarmDock agent marketplace</p>
86
+ </div>
87
+
88
+ {/* Tab bar */}
89
+ <div className="flex gap-1 mb-6 rounded-[14px] border border-white/[0.06] bg-surface/50 p-1">
90
+ {(['tasks', 'agents'] as Tab[]).map((t) => (
91
+ <button
92
+ key={t}
93
+ onClick={() => setTab(t)}
94
+ className={`flex-1 px-4 py-2.5 rounded-[10px] text-[13px] font-600 transition-all border-none cursor-pointer
95
+ ${tab === t
96
+ ? 'bg-accent-bright/15 text-accent-bright'
97
+ : 'bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04]'
98
+ }`}
99
+ >
100
+ {t === 'tasks' ? 'Tasks' : 'Agents'}
101
+ </button>
102
+ ))}
103
+ </div>
104
+
105
+ {/* Content */}
106
+ {loading ? (
107
+ <PageLoader />
108
+ ) : error ? (
109
+ <div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
110
+ <p className="text-[14px] text-text-3/75 mb-3">{error}</p>
111
+ <button
112
+ onClick={() => loadData(tab)}
113
+ className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 cursor-pointer hover:bg-white/[0.08] transition-all"
114
+ >
115
+ Retry
116
+ </button>
117
+ </div>
118
+ ) : tab === 'tasks' ? (
119
+ <div className="space-y-3">
120
+ {tasks.length === 0 ? (
121
+ <div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
122
+ <p className="text-[14px] font-600 text-text mb-1">No tasks yet</p>
123
+ <p className="text-[13px] text-text-3/75">Tasks will appear here when posted on SwarmDock.</p>
124
+ </div>
125
+ ) : (
126
+ tasks.map((task) => (
127
+ <div key={task.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
128
+ <div className="flex items-start justify-between gap-3">
129
+ <div className="min-w-0 flex-1">
130
+ <div className="flex items-center gap-2 mb-1">
131
+ <h3 className="text-[14px] font-600 text-text truncate">{task.title}</h3>
132
+ <span className={`px-2 py-0.5 rounded-full text-[11px] font-600 shrink-0
133
+ ${task.status === 'open' ? 'bg-green-500/15 text-green-400' :
134
+ task.status === 'bidding' ? 'bg-amber-500/15 text-amber-400' :
135
+ task.status === 'completed' ? 'bg-white/[0.08] text-text-3' :
136
+ 'bg-accent-bright/15 text-accent-bright'}`}>
137
+ {task.status}
138
+ </span>
139
+ </div>
140
+ <p className="text-[12px] text-text-3/75 line-clamp-2 mb-2">{task.description}</p>
141
+ <div className="flex items-center gap-3 text-[11px] text-text-3/60">
142
+ <span>{formatUsdc(task.budgetMin)}–{formatUsdc(task.budgetMax)}</span>
143
+ <span>{task.bidCount} bid{task.bidCount !== 1 ? 's' : ''}</span>
144
+ <span>{task.skillRequirements.join(', ')}</span>
145
+ <span>{timeAgo(task.createdAt)}</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ ))
151
+ )}
152
+ </div>
153
+ ) : (
154
+ <div className="space-y-3">
155
+ {agents.length === 0 ? (
156
+ <div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
157
+ <p className="text-[14px] font-600 text-text mb-1">No agents registered</p>
158
+ <p className="text-[13px] text-text-3/75">Agents will appear here when registered on SwarmDock.</p>
159
+ </div>
160
+ ) : (
161
+ agents.map((agent) => (
162
+ <div key={agent.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
163
+ <div className="flex items-start justify-between gap-3">
164
+ <div className="min-w-0 flex-1">
165
+ <div className="flex items-center gap-2 mb-1">
166
+ <h3 className="text-[14px] font-600 text-text">{agent.displayName}</h3>
167
+ {agent.framework && (
168
+ <span className="px-2 py-0.5 rounded-full text-[11px] font-500 bg-white/[0.06] text-text-3">
169
+ {agent.framework}
170
+ </span>
171
+ )}
172
+ <span className="px-2 py-0.5 rounded-full text-[11px] font-600 bg-accent-bright/15 text-accent-bright">
173
+ L{agent.trustLevel}
174
+ </span>
175
+ </div>
176
+ <p className="text-[12px] text-text-3/75">{agent.description || 'No description'}</p>
177
+ <p className="text-[11px] text-text-3/50 mt-1">{timeAgo(agent.createdAt)}</p>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ ))
182
+ )}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ </MainContent>
188
+ )
189
+ }