@swarmclawai/swarmclaw 1.4.8 → 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.
- package/README.md +11 -1
- package/package.json +1 -1
- package/scripts/run-next-build.mjs +44 -0
- package/src/app/home/page.tsx +62 -0
- package/src/app/protocols/page.tsx +31 -0
- package/src/app/setup/page.tsx +4 -2
- package/src/components/auth/setup-wizard/index.tsx +66 -74
- package/src/components/auth/setup-wizard/step-next.tsx +60 -48
- package/src/components/auth/setup-wizard/step-path.tsx +159 -0
- package/src/components/auth/setup-wizard/step-progress.tsx +1 -0
- package/src/components/auth/setup-wizard/step-providers.tsx +9 -0
- package/src/components/auth/setup-wizard/types.test.ts +4 -4
- package/src/components/auth/setup-wizard/types.ts +22 -5
- package/src/components/auth/setup-wizard/utils.test.ts +73 -6
- package/src/components/auth/setup-wizard/utils.ts +13 -0
- package/src/components/home/home-launchpad.tsx +135 -0
- package/src/components/protocols/builder/template-gallery.tsx +23 -16
- package/src/components/shared/launch-action-card.tsx +27 -0
- package/src/features/swarmfeed/feed-page.tsx +20 -27
- package/src/features/swarmfeed/post-thread-sheet.tsx +83 -68
- package/src/lib/home-launchpad.test.ts +49 -0
- package/src/lib/home-launchpad.ts +30 -0
- package/src/lib/setup-defaults.ts +13 -6
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
|
11
|
-
const customTemplates = templates
|
|
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=
|
|
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="
|
|
22
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
42
|
-
<div className="grid grid-cols-2
|
|
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-
|
|
48
|
-
<div className="grid grid-cols-2
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useDeferredValue,
|
|
3
|
+
import { useDeferredValue, useState } from 'react'
|
|
4
4
|
import { Bell, Hash, Search, Sparkles, TrendingUp, Users } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
@@ -77,30 +77,23 @@ export function FeedPage() {
|
|
|
77
77
|
const [profileAgentId, setProfileAgentId] = useState<string | null>(null)
|
|
78
78
|
|
|
79
79
|
const deferredSearchQuery = useDeferredValue(searchQuery.trim())
|
|
80
|
-
const
|
|
80
|
+
const resolvedSelectedAgentId = selectedAgentId && feedAgents.some((agent) => agent.id === selectedAgentId)
|
|
81
|
+
? selectedAgentId
|
|
82
|
+
: (feedAgents[0]?.id || '')
|
|
83
|
+
const selectedAgent = resolvedSelectedAgentId ? agents[resolvedSelectedAgentId] : null
|
|
81
84
|
const isSearching = deferredSearchQuery.length >= 2
|
|
82
85
|
const currentFeedType = isFeedTab(activeTab) ? activeTab : 'for_you'
|
|
83
86
|
const requiresActor = activeTab === 'following' || activeTab === 'bookmarks' || activeTab === 'notifications'
|
|
84
87
|
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (feedAgents.length === 0) {
|
|
87
|
-
setSelectedAgentId('')
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (!selectedAgentId || !feedAgents.some((agent) => agent.id === selectedAgentId)) {
|
|
91
|
-
setSelectedAgentId(feedAgents[0].id)
|
|
92
|
-
}
|
|
93
|
-
}, [feedAgents, selectedAgentId])
|
|
94
|
-
|
|
95
88
|
const channelsQuery = useSwarmFeedChannelsQuery()
|
|
96
89
|
const feedQuery = useSwarmFeedFeedQuery({
|
|
97
90
|
type: currentFeedType,
|
|
98
|
-
agentId: activeTab === 'following' ?
|
|
99
|
-
enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!
|
|
91
|
+
agentId: activeTab === 'following' ? resolvedSelectedAgentId : undefined,
|
|
92
|
+
enabled: !isSearching && isFeedTab(activeTab) && (activeTab !== 'following' || !!resolvedSelectedAgentId),
|
|
100
93
|
})
|
|
101
|
-
const bookmarksQuery = useSwarmFeedBookmarksQuery(
|
|
102
|
-
const notificationsQuery = useSwarmFeedNotificationsQuery(
|
|
103
|
-
const suggestedQuery = useSwarmFeedSuggestedQuery(
|
|
94
|
+
const bookmarksQuery = useSwarmFeedBookmarksQuery(resolvedSelectedAgentId, !isSearching && activeTab === 'bookmarks')
|
|
95
|
+
const notificationsQuery = useSwarmFeedNotificationsQuery(resolvedSelectedAgentId, !isSearching && activeTab === 'notifications')
|
|
96
|
+
const suggestedQuery = useSwarmFeedSuggestedQuery(resolvedSelectedAgentId || undefined, true)
|
|
104
97
|
const searchResultsQuery = useSwarmFeedSearchQuery({
|
|
105
98
|
query: deferredSearchQuery,
|
|
106
99
|
type: searchType,
|
|
@@ -114,13 +107,13 @@ export function FeedPage() {
|
|
|
114
107
|
)
|
|
115
108
|
|
|
116
109
|
async function handlePostAction(action: PostCardAction, post: SwarmFeedPost) {
|
|
117
|
-
if (!
|
|
110
|
+
if (!resolvedSelectedAgentId) {
|
|
118
111
|
throw new Error('Select an acting agent before interacting with SwarmFeed.')
|
|
119
112
|
}
|
|
120
113
|
try {
|
|
121
114
|
await actionMutation.mutateAsync({
|
|
122
115
|
action,
|
|
123
|
-
agentId:
|
|
116
|
+
agentId: resolvedSelectedAgentId,
|
|
124
117
|
postId: post.id,
|
|
125
118
|
})
|
|
126
119
|
} catch (err: unknown) {
|
|
@@ -131,14 +124,14 @@ export function FeedPage() {
|
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
async function handleFollow(targetAgentId: string) {
|
|
134
|
-
if (!
|
|
127
|
+
if (!resolvedSelectedAgentId) {
|
|
135
128
|
toast.error('Select an acting agent before following other agents.')
|
|
136
129
|
return
|
|
137
130
|
}
|
|
138
131
|
try {
|
|
139
132
|
await actionMutation.mutateAsync({
|
|
140
133
|
action: 'follow',
|
|
141
|
-
agentId:
|
|
134
|
+
agentId: resolvedSelectedAgentId,
|
|
142
135
|
targetAgentId,
|
|
143
136
|
})
|
|
144
137
|
toast.success('Agent followed')
|
|
@@ -164,7 +157,7 @@ export function FeedPage() {
|
|
|
164
157
|
key={post.id}
|
|
165
158
|
post={post}
|
|
166
159
|
channelLabel={post.channelId ? channelLabels[post.channelId] : null}
|
|
167
|
-
canInteract={!!
|
|
160
|
+
canInteract={!!resolvedSelectedAgentId}
|
|
168
161
|
onAction={handlePostAction}
|
|
169
162
|
onProfileOpen={setProfileAgentId}
|
|
170
163
|
onThreadOpen={(postId, mode = 'reply') => setThreadState({ postId, mode })}
|
|
@@ -274,7 +267,7 @@ export function FeedPage() {
|
|
|
274
267
|
)
|
|
275
268
|
}
|
|
276
269
|
|
|
277
|
-
if (requiresActor && !
|
|
270
|
+
if (requiresActor && !resolvedSelectedAgentId) {
|
|
278
271
|
return (
|
|
279
272
|
<EmptyState
|
|
280
273
|
title="Choose an acting agent"
|
|
@@ -365,7 +358,7 @@ export function FeedPage() {
|
|
|
365
358
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.7fr)_360px]">
|
|
366
359
|
<aside className="order-1 space-y-5 lg:order-2">
|
|
367
360
|
<ComposePost
|
|
368
|
-
selectedAgentId={
|
|
361
|
+
selectedAgentId={resolvedSelectedAgentId}
|
|
369
362
|
onSelectAgent={setSelectedAgentId}
|
|
370
363
|
/>
|
|
371
364
|
|
|
@@ -425,7 +418,7 @@ export function FeedPage() {
|
|
|
425
418
|
<SuggestedAgentRow
|
|
426
419
|
key={agent.id}
|
|
427
420
|
agent={agent}
|
|
428
|
-
canFollow={!!
|
|
421
|
+
canFollow={!!resolvedSelectedAgentId}
|
|
429
422
|
busy={actionMutation.isPending}
|
|
430
423
|
onFollow={handleFollow}
|
|
431
424
|
onOpenProfile={setProfileAgentId}
|
|
@@ -487,7 +480,7 @@ export function FeedPage() {
|
|
|
487
480
|
<PostThreadSheet
|
|
488
481
|
open={!!threadState}
|
|
489
482
|
postId={threadState?.postId || null}
|
|
490
|
-
actingAgentId={
|
|
483
|
+
actingAgentId={resolvedSelectedAgentId || undefined}
|
|
491
484
|
channelLabels={channelLabels}
|
|
492
485
|
initialMode={threadState?.mode || 'reply'}
|
|
493
486
|
onClose={() => setThreadState(null)}
|
|
@@ -497,7 +490,7 @@ export function FeedPage() {
|
|
|
497
490
|
<SwarmFeedProfileSheet
|
|
498
491
|
open={!!profileAgentId}
|
|
499
492
|
agentId={profileAgentId}
|
|
500
|
-
viewerAgentId={
|
|
493
|
+
viewerAgentId={resolvedSelectedAgentId || undefined}
|
|
501
494
|
channelLabels={channelLabels}
|
|
502
495
|
onClose={() => setProfileAgentId(null)}
|
|
503
496
|
onOpenThread={(postId, mode = 'reply') => setThreadState({ postId, mode })}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from 'react'
|
|
4
4
|
import { toast } from 'sonner'
|
|
5
5
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
6
6
|
import { PostCard } from './post-card'
|
|
@@ -26,40 +26,6 @@ export function PostThreadSheet({
|
|
|
26
26
|
onProfileOpen,
|
|
27
27
|
}: Props) {
|
|
28
28
|
const threadQuery = useSwarmFeedThreadQuery(postId || '', open && !!postId)
|
|
29
|
-
const postMutation = useSwarmFeedPostMutation()
|
|
30
|
-
const actionMutation = useSwarmFeedActionMutation()
|
|
31
|
-
const [mode, setMode] = useState<'reply' | 'quote'>(initialMode)
|
|
32
|
-
const [content, setContent] = useState('')
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!open) return
|
|
36
|
-
setMode(initialMode)
|
|
37
|
-
setContent('')
|
|
38
|
-
}, [initialMode, open, postId])
|
|
39
|
-
|
|
40
|
-
async function submit() {
|
|
41
|
-
if (!actingAgentId || !postId || !content.trim()) return
|
|
42
|
-
try {
|
|
43
|
-
if (mode === 'reply') {
|
|
44
|
-
await postMutation.mutateAsync({
|
|
45
|
-
agentId: actingAgentId,
|
|
46
|
-
input: { content: content.trim(), parentId: postId },
|
|
47
|
-
})
|
|
48
|
-
} else {
|
|
49
|
-
await actionMutation.mutateAsync({
|
|
50
|
-
action: 'quote_repost',
|
|
51
|
-
agentId: actingAgentId,
|
|
52
|
-
postId,
|
|
53
|
-
content: content.trim(),
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
toast.success(mode === 'reply' ? 'Reply posted' : 'Quote repost published')
|
|
57
|
-
setContent('')
|
|
58
|
-
} catch (err: unknown) {
|
|
59
|
-
toast.error(err instanceof Error ? err.message : 'Failed to publish response')
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
const post = threadQuery.data?.post
|
|
64
30
|
const replies = threadQuery.data?.replies || []
|
|
65
31
|
|
|
@@ -113,43 +79,92 @@ export function PostThreadSheet({
|
|
|
113
79
|
</div>
|
|
114
80
|
|
|
115
81
|
<div className="border-t border-white/[0.06] pt-4">
|
|
116
|
-
<
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
onClick={() => setMode(option)}
|
|
122
|
-
className={`cursor-pointer rounded-[999px] border px-3 py-1.5 text-[12px] font-700 uppercase tracking-[0.08em] transition-all ${
|
|
123
|
-
mode === option
|
|
124
|
-
? 'border-accent-bright/50 bg-accent-bright/10 text-accent-bright'
|
|
125
|
-
: 'border-white/[0.08] bg-transparent text-text-3 hover:text-text'
|
|
126
|
-
}`}
|
|
127
|
-
>
|
|
128
|
-
{option === 'reply' ? 'Reply' : 'Quote'}
|
|
129
|
-
</button>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
<textarea
|
|
133
|
-
value={content}
|
|
134
|
-
onChange={(event) => setContent(event.target.value)}
|
|
135
|
-
placeholder={mode === 'reply' ? 'Write a concise reply…' : 'Add your commentary before reposting…'}
|
|
136
|
-
className="min-h-[110px] w-full resize-y rounded-[14px] border border-white/[0.08] bg-surface/70 px-4 py-3 text-[14px] text-text outline-none focus-glow"
|
|
137
|
-
maxLength={2000}
|
|
82
|
+
<ThreadComposer
|
|
83
|
+
key={`${postId || 'none'}:${initialMode}:${open ? 'open' : 'closed'}`}
|
|
84
|
+
actingAgentId={actingAgentId}
|
|
85
|
+
postId={postId}
|
|
86
|
+
initialMode={initialMode}
|
|
138
87
|
/>
|
|
139
|
-
<div className="mt-3 flex items-center justify-between">
|
|
140
|
-
<span className="text-[11px] text-text-3/55">{content.length}/2000</span>
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={() => { void submit() }}
|
|
144
|
-
disabled={!actingAgentId || !content.trim() || postMutation.isPending || actionMutation.isPending}
|
|
145
|
-
className="cursor-pointer rounded-[12px] bg-accent-bright px-4 py-2.5 text-[13px] font-700 text-white transition-all hover:bg-accent-bright/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
146
|
-
>
|
|
147
|
-
{mode === 'reply' ? 'Reply' : 'Quote repost'}
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
88
|
</div>
|
|
151
89
|
</div>
|
|
152
90
|
</SheetContent>
|
|
153
91
|
</Sheet>
|
|
154
92
|
)
|
|
155
93
|
}
|
|
94
|
+
|
|
95
|
+
function ThreadComposer({
|
|
96
|
+
actingAgentId,
|
|
97
|
+
postId,
|
|
98
|
+
initialMode,
|
|
99
|
+
}: {
|
|
100
|
+
actingAgentId?: string
|
|
101
|
+
postId: string | null
|
|
102
|
+
initialMode: 'reply' | 'quote'
|
|
103
|
+
}) {
|
|
104
|
+
const postMutation = useSwarmFeedPostMutation()
|
|
105
|
+
const actionMutation = useSwarmFeedActionMutation()
|
|
106
|
+
const [mode, setMode] = useState<'reply' | 'quote'>(initialMode)
|
|
107
|
+
const [content, setContent] = useState('')
|
|
108
|
+
|
|
109
|
+
async function submit() {
|
|
110
|
+
if (!actingAgentId || !postId || !content.trim()) return
|
|
111
|
+
try {
|
|
112
|
+
if (mode === 'reply') {
|
|
113
|
+
await postMutation.mutateAsync({
|
|
114
|
+
agentId: actingAgentId,
|
|
115
|
+
input: { content: content.trim(), parentId: postId },
|
|
116
|
+
})
|
|
117
|
+
} else {
|
|
118
|
+
await actionMutation.mutateAsync({
|
|
119
|
+
action: 'quote_repost',
|
|
120
|
+
agentId: actingAgentId,
|
|
121
|
+
postId,
|
|
122
|
+
content: content.trim(),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
toast.success(mode === 'reply' ? 'Reply posted' : 'Quote repost published')
|
|
126
|
+
setContent('')
|
|
127
|
+
} catch (err: unknown) {
|
|
128
|
+
toast.error(err instanceof Error ? err.message : 'Failed to publish response')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<div className="mb-3 flex gap-2">
|
|
135
|
+
{(['reply', 'quote'] as const).map((option) => (
|
|
136
|
+
<button
|
|
137
|
+
key={option}
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={() => setMode(option)}
|
|
140
|
+
className={`cursor-pointer rounded-[999px] border px-3 py-1.5 text-[12px] font-700 uppercase tracking-[0.08em] transition-all ${
|
|
141
|
+
mode === option
|
|
142
|
+
? 'border-accent-bright/50 bg-accent-bright/10 text-accent-bright'
|
|
143
|
+
: 'border-white/[0.08] bg-transparent text-text-3 hover:text-text'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
{option === 'reply' ? 'Reply' : 'Quote'}
|
|
147
|
+
</button>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
<textarea
|
|
151
|
+
value={content}
|
|
152
|
+
onChange={(event) => setContent(event.target.value)}
|
|
153
|
+
placeholder={mode === 'reply' ? 'Write a concise reply…' : 'Add your commentary before reposting…'}
|
|
154
|
+
className="min-h-[110px] w-full resize-y rounded-[14px] border border-white/[0.08] bg-surface/70 px-4 py-3 text-[14px] text-text outline-none focus-glow"
|
|
155
|
+
maxLength={2000}
|
|
156
|
+
/>
|
|
157
|
+
<div className="mt-3 flex items-center justify-between">
|
|
158
|
+
<span className="text-[11px] text-text-3/55">{content.length}/2000</span>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={() => { void submit() }}
|
|
162
|
+
disabled={!actingAgentId || !content.trim() || postMutation.isPending || actionMutation.isPending}
|
|
163
|
+
className="cursor-pointer rounded-[12px] bg-accent-bright px-4 py-2.5 text-[13px] font-700 text-white transition-all hover:bg-accent-bright/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
164
|
+
>
|
|
165
|
+
{mode === 'reply' ? 'Reply' : 'Quote repost'}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { deriveHomeMode, isSparseWorkspace } from './home-launchpad'
|
|
4
|
+
|
|
5
|
+
test('isSparseWorkspace detects a fresh workspace', () => {
|
|
6
|
+
assert.equal(isSparseWorkspace({
|
|
7
|
+
agentCount: 1,
|
|
8
|
+
sessionCount: 0,
|
|
9
|
+
taskCount: 0,
|
|
10
|
+
scheduleCount: 0,
|
|
11
|
+
connectorCount: 0,
|
|
12
|
+
todayCost: 0,
|
|
13
|
+
}), true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('isSparseWorkspace returns false once work exists', () => {
|
|
17
|
+
assert.equal(isSparseWorkspace({
|
|
18
|
+
agentCount: 2,
|
|
19
|
+
sessionCount: 1,
|
|
20
|
+
taskCount: 0,
|
|
21
|
+
scheduleCount: 0,
|
|
22
|
+
connectorCount: 0,
|
|
23
|
+
todayCost: 0,
|
|
24
|
+
}), false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('deriveHomeMode prioritizes the post-setup launchpad flag', () => {
|
|
28
|
+
assert.equal(deriveHomeMode({
|
|
29
|
+
hasLaunchpadFlag: true,
|
|
30
|
+
agentCount: 5,
|
|
31
|
+
sessionCount: 8,
|
|
32
|
+
taskCount: 3,
|
|
33
|
+
scheduleCount: 2,
|
|
34
|
+
connectorCount: 1,
|
|
35
|
+
todayCost: 12.4,
|
|
36
|
+
}), 'launchpad')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('deriveHomeMode falls back to ops for active workspaces', () => {
|
|
40
|
+
assert.equal(deriveHomeMode({
|
|
41
|
+
hasLaunchpadFlag: false,
|
|
42
|
+
agentCount: 3,
|
|
43
|
+
sessionCount: 1,
|
|
44
|
+
taskCount: 0,
|
|
45
|
+
scheduleCount: 0,
|
|
46
|
+
connectorCount: 0,
|
|
47
|
+
todayCost: 0,
|
|
48
|
+
}), 'ops')
|
|
49
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const HOME_LAUNCHPAD_AFTER_SETUP_KEY = 'sc_launchpad_after_setup_v1'
|
|
2
|
+
export const DEFAULT_BUILDER_ROUTE = '/protocols/builder/facilitated_discussion'
|
|
3
|
+
|
|
4
|
+
export type HomeMode = 'launchpad' | 'ops'
|
|
5
|
+
|
|
6
|
+
export interface HomeModeInput {
|
|
7
|
+
hasLaunchpadFlag: boolean
|
|
8
|
+
agentCount: number
|
|
9
|
+
sessionCount: number
|
|
10
|
+
taskCount: number
|
|
11
|
+
scheduleCount: number
|
|
12
|
+
connectorCount: number
|
|
13
|
+
todayCost: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isSparseWorkspace(input: Omit<HomeModeInput, 'hasLaunchpadFlag'>): boolean {
|
|
17
|
+
return (
|
|
18
|
+
input.agentCount <= 2
|
|
19
|
+
&& input.sessionCount === 0
|
|
20
|
+
&& input.taskCount === 0
|
|
21
|
+
&& input.scheduleCount === 0
|
|
22
|
+
&& input.connectorCount === 0
|
|
23
|
+
&& input.todayCost === 0
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function deriveHomeMode(input: HomeModeInput): HomeMode {
|
|
28
|
+
if (input.hasLaunchpadFlag) return 'launchpad'
|
|
29
|
+
return isSparseWorkspace(input) ? 'launchpad' : 'ops'
|
|
30
|
+
}
|
|
@@ -359,6 +359,13 @@ export const ONBOARDING_PATHS: OnboardingPathOption[] = [
|
|
|
359
359
|
detail: 'Best when you already know which provider you want and want to get moving quickly.',
|
|
360
360
|
badge: 'Fastest',
|
|
361
361
|
},
|
|
362
|
+
{
|
|
363
|
+
id: 'intent',
|
|
364
|
+
title: 'Goal-Driven Setup',
|
|
365
|
+
description: 'Choose a starter team around what you want to accomplish.',
|
|
366
|
+
detail: 'Best when you know the outcome you want but want SwarmClaw to start from a stronger template.',
|
|
367
|
+
badge: 'Guided',
|
|
368
|
+
},
|
|
362
369
|
{
|
|
363
370
|
id: 'manual',
|
|
364
371
|
title: 'Custom Setup',
|
|
@@ -462,7 +469,7 @@ export const STARTER_KITS: StarterKit[] = [
|
|
|
462
469
|
name: 'Research Copilot',
|
|
463
470
|
description: 'A focused setup for investigation and synthesis.',
|
|
464
471
|
detail: 'Useful for market scans, comparisons, technical investigation, and source-backed summaries.',
|
|
465
|
-
recommendedFor: ['intent', 'manual'],
|
|
472
|
+
recommendedFor: ['quick', 'intent', 'manual'],
|
|
466
473
|
agents: [
|
|
467
474
|
{
|
|
468
475
|
id: 'researcher',
|
|
@@ -479,7 +486,7 @@ export const STARTER_KITS: StarterKit[] = [
|
|
|
479
486
|
name: 'Builder Studio',
|
|
480
487
|
description: 'Start with a builder and a reviewer.',
|
|
481
488
|
detail: 'Good for coding, prototyping, product work, and technical iteration.',
|
|
482
|
-
recommendedFor: ['intent', 'manual'],
|
|
489
|
+
recommendedFor: ['quick', 'intent', 'manual'],
|
|
483
490
|
agents: [
|
|
484
491
|
{
|
|
485
492
|
id: 'builder',
|
|
@@ -528,10 +535,10 @@ export const STARTER_KITS: StarterKit[] = [
|
|
|
528
535
|
},
|
|
529
536
|
{
|
|
530
537
|
id: 'operator_swarm',
|
|
531
|
-
name: '
|
|
532
|
-
description: 'A
|
|
533
|
-
detail: '
|
|
534
|
-
recommendedFor: ['manual'],
|
|
538
|
+
name: 'Delegate Team',
|
|
539
|
+
description: 'A coordinator plus an execution agent for multi-agent work.',
|
|
540
|
+
detail: 'Use this when you want one agent to plan and delegate while another handles focused execution.',
|
|
541
|
+
recommendedFor: ['intent', 'manual'],
|
|
535
542
|
agents: [
|
|
536
543
|
{
|
|
537
544
|
id: 'operator',
|