@swarmclawai/swarmclaw 0.3.0 → 0.4.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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
5
7
|
import { ClawHubBrowser } from './clawhub-browser'
|
|
8
|
+
import { toast } from 'sonner'
|
|
9
|
+
|
|
10
|
+
interface ClawHubSkill {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
author: string
|
|
15
|
+
tags: string[]
|
|
16
|
+
downloads: number
|
|
17
|
+
url: string
|
|
18
|
+
version: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SearchResponse {
|
|
22
|
+
skills: ClawHubSkill[]
|
|
23
|
+
total: number
|
|
24
|
+
page: number
|
|
25
|
+
}
|
|
6
26
|
|
|
7
27
|
export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
8
28
|
const skills = useAppStore((s) => s.skills)
|
|
@@ -11,6 +31,17 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
11
31
|
const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
|
|
12
32
|
const [clawHubOpen, setClawHubOpen] = useState(false)
|
|
13
33
|
|
|
34
|
+
// Embedded ClawHub state (full-width only)
|
|
35
|
+
const [tab, setTab] = useState<'skills' | 'clawhub'>('skills')
|
|
36
|
+
const [hubQuery, setHubQuery] = useState('')
|
|
37
|
+
const [hubSkills, setHubSkills] = useState<ClawHubSkill[]>([])
|
|
38
|
+
const [hubPage, setHubPage] = useState(1)
|
|
39
|
+
const [hubTotal, setHubTotal] = useState(0)
|
|
40
|
+
const [hubLoading, setHubLoading] = useState(false)
|
|
41
|
+
const [hubSearched, setHubSearched] = useState(false)
|
|
42
|
+
const [hubError, setHubError] = useState<string | null>(null)
|
|
43
|
+
const [installing, setInstalling] = useState<string | null>(null)
|
|
44
|
+
|
|
14
45
|
useEffect(() => {
|
|
15
46
|
loadSkills()
|
|
16
47
|
}, [])
|
|
@@ -22,49 +53,244 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
22
53
|
setSkillSheetOpen(true)
|
|
23
54
|
}
|
|
24
55
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
|
57
|
+
e.stopPropagation()
|
|
58
|
+
await api('DELETE', `/skills/${id}`)
|
|
59
|
+
loadSkills()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Embedded ClawHub search
|
|
63
|
+
const searchHub = useCallback(async (q: string, p: number, append = false) => {
|
|
64
|
+
setHubLoading(true)
|
|
65
|
+
setHubError(null)
|
|
66
|
+
try {
|
|
67
|
+
const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
|
|
68
|
+
if (append) {
|
|
69
|
+
setHubSkills(prev => [...prev, ...res.skills])
|
|
70
|
+
} else {
|
|
71
|
+
setHubSkills(res.skills)
|
|
72
|
+
}
|
|
73
|
+
setHubTotal(res.total)
|
|
74
|
+
setHubPage(res.page)
|
|
75
|
+
setHubSearched(true)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
setHubError(err instanceof Error ? err.message : 'Failed to search ClawHub')
|
|
78
|
+
} finally {
|
|
79
|
+
setHubLoading(false)
|
|
80
|
+
}
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!inSidebar && tab === 'clawhub' && !hubSearched) {
|
|
85
|
+
searchHub('', 1)
|
|
86
|
+
}
|
|
87
|
+
}, [tab, inSidebar, hubSearched, searchHub])
|
|
88
|
+
|
|
89
|
+
const handleHubSearch = () => {
|
|
90
|
+
setHubSkills([])
|
|
91
|
+
searchHub(hubQuery, 1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleInstall = async (skill: ClawHubSkill) => {
|
|
95
|
+
setInstalling(skill.id)
|
|
96
|
+
try {
|
|
97
|
+
await api('POST', '/clawhub/install', {
|
|
98
|
+
name: skill.name,
|
|
99
|
+
description: skill.description,
|
|
100
|
+
url: skill.url,
|
|
101
|
+
tags: skill.tags,
|
|
102
|
+
})
|
|
103
|
+
toast.success(`Installed "${skill.name}"`)
|
|
104
|
+
loadSkills()
|
|
105
|
+
} catch (err) {
|
|
106
|
+
toast.error(err instanceof Error ? err.message : 'Install failed')
|
|
107
|
+
} finally {
|
|
108
|
+
setInstalling(null)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tabClass = (t: string) =>
|
|
113
|
+
`py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
114
|
+
${tab === t
|
|
115
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
116
|
+
: 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
|
|
117
|
+
|
|
118
|
+
const renderClawHub = () => {
|
|
119
|
+
const hasMore = hubSkills.length < hubTotal
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="space-y-3">
|
|
123
|
+
<div className="flex gap-2">
|
|
124
|
+
<input
|
|
125
|
+
placeholder="Search skills..."
|
|
126
|
+
value={hubQuery}
|
|
127
|
+
onChange={(e) => setHubQuery(e.target.value)}
|
|
128
|
+
onKeyDown={(e) => e.key === 'Enter' && handleHubSearch()}
|
|
129
|
+
className="flex-1 px-3 py-2.5 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30"
|
|
130
|
+
style={{ fontFamily: 'inherit' }}
|
|
131
|
+
/>
|
|
38
132
|
<button
|
|
39
|
-
onClick={
|
|
40
|
-
|
|
133
|
+
onClick={handleHubSearch}
|
|
134
|
+
disabled={hubLoading}
|
|
135
|
+
className="px-3.5 py-2 rounded-[10px] text-[12px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20 hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-50"
|
|
41
136
|
style={{ fontFamily: 'inherit' }}
|
|
42
137
|
>
|
|
43
|
-
|
|
138
|
+
Search
|
|
44
139
|
</button>
|
|
45
140
|
</div>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
141
|
+
|
|
142
|
+
{hubError && (
|
|
143
|
+
<div className="text-center py-8">
|
|
144
|
+
<p className="text-[13px] text-red-400">{hubError}</p>
|
|
145
|
+
<button onClick={() => searchHub(hubQuery, 1)} className="mt-2 text-[12px] text-text-3/60 hover:text-text-3 cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>
|
|
146
|
+
Retry
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{!hubError && !hubLoading && hubSearched && hubSkills.length === 0 && (
|
|
152
|
+
<div className="text-center py-8">
|
|
153
|
+
<p className="text-[13px] text-text-3/60">No skills found</p>
|
|
154
|
+
{hubQuery && <p className="text-[11px] text-text-3/40 mt-1">Try a different search term</p>}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{hubSkills.length > 0 && (
|
|
159
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
160
|
+
{hubSkills.map((skill) => (
|
|
161
|
+
<div
|
|
162
|
+
key={skill.id}
|
|
163
|
+
className="p-4 rounded-[14px] border border-white/[0.06] bg-surface"
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-start justify-between gap-2">
|
|
166
|
+
<div className="min-w-0 flex-1">
|
|
167
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
168
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
|
|
169
|
+
<span className="text-[10px] font-mono text-text-3/40 shrink-0">v{skill.version}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2 mb-2">{skill.description}</p>
|
|
172
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
173
|
+
{skill.tags.slice(0, 4).map((tag) => (
|
|
174
|
+
<Badge key={tag} variant="secondary" className="text-[10px] px-1.5 py-0">{tag}</Badge>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
<div className="flex items-center gap-3 mt-2 text-[11px] text-text-3/50">
|
|
178
|
+
<span>{skill.author}</span>
|
|
179
|
+
<span>{skill.downloads.toLocaleString()} installs</span>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => handleInstall(skill)}
|
|
184
|
+
disabled={installing === skill.id}
|
|
185
|
+
className="shrink-0 py-2 px-3.5 rounded-[10px] text-[12px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20 hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-50"
|
|
186
|
+
style={{ fontFamily: 'inherit' }}
|
|
187
|
+
>
|
|
188
|
+
{installing === skill.id ? 'Installing...' : 'Install'}
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{hasMore && (
|
|
197
|
+
<div className="pt-2 pb-4 text-center">
|
|
49
198
|
<button
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
className="
|
|
199
|
+
onClick={() => searchHub(hubQuery, hubPage + 1, true)}
|
|
200
|
+
disabled={hubLoading}
|
|
201
|
+
className="text-[12px] text-text-3/60 hover:text-text-3 cursor-pointer bg-transparent border-none"
|
|
202
|
+
style={{ fontFamily: 'inherit' }}
|
|
53
203
|
>
|
|
54
|
-
|
|
55
|
-
<span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
|
|
56
|
-
<span className="text-[10px] font-mono text-text-3/50 shrink-0 ml-2">{skill.filename}</span>
|
|
57
|
-
</div>
|
|
58
|
-
{skill.description && (
|
|
59
|
-
<p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
|
|
60
|
-
)}
|
|
61
|
-
<div className="text-[11px] text-text-3/70 mt-1.5">
|
|
62
|
-
{skill.content.length} chars
|
|
63
|
-
</div>
|
|
204
|
+
{hubLoading ? 'Loading...' : 'Load More'}
|
|
64
205
|
</button>
|
|
65
|
-
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{hubLoading && hubSkills.length === 0 && (
|
|
210
|
+
<div className="flex items-center justify-center py-12">
|
|
211
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-text-3/20 border-t-text-3/60" />
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
220
|
+
{/* Sidebar: ClawHub button + Sheet */}
|
|
221
|
+
{inSidebar && (
|
|
222
|
+
<>
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => setClawHubOpen(true)}
|
|
225
|
+
className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent"
|
|
226
|
+
style={{ fontFamily: 'inherit' }}
|
|
227
|
+
>
|
|
228
|
+
Browse ClawHub Skills
|
|
229
|
+
</button>
|
|
230
|
+
<ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
|
|
231
|
+
</>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* Full-width: tabs */}
|
|
235
|
+
{!inSidebar && (
|
|
236
|
+
<div className="flex gap-1 mb-4">
|
|
237
|
+
<button onClick={() => setTab('skills')} className={tabClass('skills')} style={{ fontFamily: 'inherit' }}>
|
|
238
|
+
My Skills
|
|
239
|
+
</button>
|
|
240
|
+
<button onClick={() => setTab('clawhub')} className={tabClass('clawhub')} style={{ fontFamily: 'inherit' }}>
|
|
241
|
+
ClawHub
|
|
242
|
+
</button>
|
|
66
243
|
</div>
|
|
67
244
|
)}
|
|
245
|
+
|
|
246
|
+
{(!inSidebar && tab === 'clawhub') ? renderClawHub() : (
|
|
247
|
+
skillList.length === 0 ? (
|
|
248
|
+
<div className="text-center py-12">
|
|
249
|
+
<p className="text-[13px] text-text-3/60">No skills yet</p>
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => setSkillSheetOpen(true)}
|
|
252
|
+
className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
|
|
253
|
+
style={{ fontFamily: 'inherit' }}
|
|
254
|
+
>
|
|
255
|
+
+ Add Skill
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
) : (
|
|
259
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
260
|
+
{skillList.map((skill) => (
|
|
261
|
+
<button
|
|
262
|
+
key={skill.id}
|
|
263
|
+
onClick={() => handleEdit(skill.id)}
|
|
264
|
+
className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
265
|
+
>
|
|
266
|
+
<div className="flex items-center justify-between mb-1">
|
|
267
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
|
|
268
|
+
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
269
|
+
<span className="text-[10px] font-mono text-text-3/50">{skill.filename}</span>
|
|
270
|
+
{!inSidebar && (
|
|
271
|
+
<button
|
|
272
|
+
onClick={(e) => handleDelete(e, skill.id)}
|
|
273
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
274
|
+
title="Delete"
|
|
275
|
+
>
|
|
276
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
277
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
278
|
+
</svg>
|
|
279
|
+
</button>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
{skill.description && (
|
|
284
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
|
|
285
|
+
)}
|
|
286
|
+
<div className="text-[11px] text-text-3/70 mt-1.5">
|
|
287
|
+
{skill.content.length} chars
|
|
288
|
+
</div>
|
|
289
|
+
</button>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
)}
|
|
68
294
|
</div>
|
|
69
295
|
)
|
|
70
296
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useEffect, useState, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
-
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
7
6
|
import { api } from '@/lib/api-client'
|
|
8
7
|
|
|
9
8
|
export function SkillSheet() {
|
|
@@ -24,38 +23,8 @@ export function SkillSheet() {
|
|
|
24
23
|
const [importError, setImportError] = useState('')
|
|
25
24
|
const [importNotice, setImportNotice] = useState('')
|
|
26
25
|
|
|
27
|
-
// AI generation state
|
|
28
|
-
const [aiPrompt, setAiPrompt] = useState('')
|
|
29
|
-
const [generating, setGenerating] = useState(false)
|
|
30
|
-
const [generated, setGenerated] = useState(false)
|
|
31
|
-
const [genError, setGenError] = useState('')
|
|
32
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
33
|
-
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
34
|
-
|
|
35
26
|
const editing = editingId ? skills[editingId] : null
|
|
36
27
|
|
|
37
|
-
const handleGenerate = async () => {
|
|
38
|
-
if (!aiPrompt.trim()) return
|
|
39
|
-
setGenerating(true)
|
|
40
|
-
setGenError('')
|
|
41
|
-
try {
|
|
42
|
-
const result = await api<{ name?: string; description?: string; content?: string; error?: string }>('POST', '/generate', { type: 'skill', prompt: aiPrompt })
|
|
43
|
-
if (result.error) {
|
|
44
|
-
setGenError(result.error)
|
|
45
|
-
} else if (result.name || result.content) {
|
|
46
|
-
if (result.name) { setName(result.name); setFilename(`${result.name.toLowerCase().replace(/\s+/g, '-')}.md`) }
|
|
47
|
-
if (result.description) setDescription(result.description)
|
|
48
|
-
if (result.content) setContent(result.content)
|
|
49
|
-
setGenerated(true)
|
|
50
|
-
} else {
|
|
51
|
-
setGenError('AI returned empty response — try again')
|
|
52
|
-
}
|
|
53
|
-
} catch (err: unknown) {
|
|
54
|
-
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
55
|
-
}
|
|
56
|
-
setGenerating(false)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
28
|
const handleImportFromUrl = async () => {
|
|
60
29
|
if (!importUrl.trim()) return
|
|
61
30
|
setImportingUrl(true)
|
|
@@ -72,7 +41,6 @@ export function SkillSheet() {
|
|
|
72
41
|
} else {
|
|
73
42
|
setImportNotice('Skill imported from URL.')
|
|
74
43
|
}
|
|
75
|
-
setGenerated(false)
|
|
76
44
|
} catch (err: unknown) {
|
|
77
45
|
setImportError(err instanceof Error ? err.message : 'Failed to import skill URL')
|
|
78
46
|
} finally {
|
|
@@ -82,11 +50,6 @@ export function SkillSheet() {
|
|
|
82
50
|
|
|
83
51
|
useEffect(() => {
|
|
84
52
|
if (open) {
|
|
85
|
-
loadSettings()
|
|
86
|
-
setAiPrompt('')
|
|
87
|
-
setGenerating(false)
|
|
88
|
-
setGenerated(false)
|
|
89
|
-
setGenError('')
|
|
90
53
|
setImportUrl('')
|
|
91
54
|
setImportingUrl(false)
|
|
92
55
|
setImportError('')
|
|
@@ -159,14 +122,6 @@ export function SkillSheet() {
|
|
|
159
122
|
<p className="text-[14px] text-text-3">Upload or write a reusable instruction set for agents</p>
|
|
160
123
|
</div>
|
|
161
124
|
|
|
162
|
-
{/* AI Generation */}
|
|
163
|
-
{!editing && <AiGenBlock
|
|
164
|
-
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
165
|
-
generating={generating} generated={generated} genError={genError}
|
|
166
|
-
onGenerate={handleGenerate} appSettings={appSettings}
|
|
167
|
-
placeholder='Describe the skill, e.g. "A frontend design skill for building polished React components with Tailwind"'
|
|
168
|
-
/>}
|
|
169
|
-
|
|
170
125
|
{/* File upload */}
|
|
171
126
|
{!editing && (
|
|
172
127
|
<div className="mb-8">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useCallback, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { updateTask } from '@/lib/tasks'
|
|
6
7
|
import { TaskColumn } from './task-column'
|
|
7
8
|
import type { BoardTaskStatus } from '@/types'
|
|
@@ -19,12 +20,8 @@ export function TaskBoard() {
|
|
|
19
20
|
const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
|
|
20
21
|
const [filterAgentId, setFilterAgentId] = useState<string>('')
|
|
21
22
|
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
loadAgents()
|
|
25
|
-
const interval = setInterval(loadTasks, 5000)
|
|
26
|
-
return () => clearInterval(interval)
|
|
27
|
-
}, [])
|
|
23
|
+
useEffect(() => { loadTasks(); loadAgents() }, [])
|
|
24
|
+
useWs('tasks', loadTasks, 5000)
|
|
28
25
|
|
|
29
26
|
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
30
27
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
5
6
|
import { updateTask, archiveTask } from '@/lib/tasks'
|
|
6
7
|
import type { BoardTask, BoardTaskStatus } from '@/types'
|
|
7
8
|
|
|
@@ -40,7 +41,7 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
40
41
|
e.stopPropagation()
|
|
41
42
|
if (task.sessionId) {
|
|
42
43
|
setCurrentSession(task.sessionId)
|
|
43
|
-
setActiveView('
|
|
44
|
+
setActiveView('agents')
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -161,6 +162,47 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
161
162
|
<p className="mt-2 text-[11px] text-red-400/80 line-clamp-2">{task.error}</p>
|
|
162
163
|
)}
|
|
163
164
|
|
|
165
|
+
{/* Pending tool approval */}
|
|
166
|
+
{task.pendingApproval && (
|
|
167
|
+
<div className="mt-3 p-3 rounded-[10px] bg-amber-500/[0.08] border border-amber-500/20">
|
|
168
|
+
<div className="flex items-center gap-2 mb-2">
|
|
169
|
+
<svg className="w-3.5 h-3.5 text-amber-400" viewBox="0 0 16 16" fill="none">
|
|
170
|
+
<path d="M8 1l7 14H1L8 1z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
|
|
171
|
+
<path d="M8 6v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
172
|
+
</svg>
|
|
173
|
+
<span className="text-[11px] font-600 text-amber-400">Approval Required</span>
|
|
174
|
+
</div>
|
|
175
|
+
<p className="text-[12px] text-text-2 mb-1 font-600">{task.pendingApproval.toolName}</p>
|
|
176
|
+
<pre className="text-[10px] text-text-3 bg-black/20 rounded-[6px] px-2 py-1.5 mb-2 overflow-x-auto max-h-[80px] overflow-y-auto whitespace-pre-wrap break-all">
|
|
177
|
+
{JSON.stringify(task.pendingApproval.args, null, 2).slice(0, 500)}
|
|
178
|
+
</pre>
|
|
179
|
+
<div className="flex gap-2">
|
|
180
|
+
<button
|
|
181
|
+
onClick={async (e) => {
|
|
182
|
+
e.stopPropagation()
|
|
183
|
+
await api('POST', `/tasks/${task.id}/approve`, { approved: true })
|
|
184
|
+
await loadTasks()
|
|
185
|
+
}}
|
|
186
|
+
className="flex-1 px-3 py-1.5 rounded-[8px] text-[11px] font-600 bg-green-500/20 text-green-400 border-none cursor-pointer hover:bg-green-500/30 transition-colors"
|
|
187
|
+
style={{ fontFamily: 'inherit' }}
|
|
188
|
+
>
|
|
189
|
+
Approve
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
onClick={async (e) => {
|
|
193
|
+
e.stopPropagation()
|
|
194
|
+
await api('POST', `/tasks/${task.id}/approve`, { approved: false })
|
|
195
|
+
await loadTasks()
|
|
196
|
+
}}
|
|
197
|
+
className="flex-1 px-3 py-1.5 rounded-[8px] text-[11px] font-600 bg-red-500/20 text-red-400 border-none cursor-pointer hover:bg-red-500/30 transition-colors"
|
|
198
|
+
style={{ fontFamily: 'inherit' }}
|
|
199
|
+
>
|
|
200
|
+
Reject
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
164
206
|
{/* Inline comments — show latest 2 */}
|
|
165
207
|
{task.comments && task.comments.length > 0 && (
|
|
166
208
|
<div className="mt-3 pt-3 border-t border-white/[0.04] space-y-2">
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
import type { BoardTaskStatus } from '@/types'
|
|
7
8
|
|
|
@@ -23,11 +24,8 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
23
24
|
const [search, setSearch] = useState('')
|
|
24
25
|
const [clearing, setClearing] = useState(false)
|
|
25
26
|
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
const interval = setInterval(loadTasks, 5000)
|
|
29
|
-
return () => clearInterval(interval)
|
|
30
|
-
}, [])
|
|
27
|
+
useEffect(() => { loadTasks() }, [])
|
|
28
|
+
useWs('tasks', loadTasks, 5000)
|
|
31
29
|
|
|
32
30
|
const sorted = useMemo(() =>
|
|
33
31
|
Object.values(tasks).sort((a, b) => b.updatedAt - a.updatedAt),
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createTask, updateTask, archiveTask, unarchiveTask } from '@/lib/tasks'
|
|
6
|
-
import { api } from '@/lib/api-client'
|
|
7
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
-
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
9
7
|
import { DirBrowser } from '@/components/shared/dir-browser'
|
|
10
8
|
import type { BoardTask, TaskComment } from '@/types'
|
|
11
9
|
|
|
@@ -36,46 +34,12 @@ export function TaskSheet() {
|
|
|
36
34
|
const [cwd, setCwd] = useState('')
|
|
37
35
|
const [file, setFile] = useState<string | null>(null)
|
|
38
36
|
|
|
39
|
-
// AI generation state
|
|
40
|
-
const [aiPrompt, setAiPrompt] = useState('')
|
|
41
|
-
const [generating, setGenerating] = useState(false)
|
|
42
|
-
const [generated, setGenerated] = useState(false)
|
|
43
|
-
const [genError, setGenError] = useState('')
|
|
44
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
45
|
-
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
46
|
-
|
|
47
37
|
const editing = editingId ? tasks[editingId] : null
|
|
48
38
|
const orchestrators = Object.values(agents).filter((p) => p.isOrchestrator)
|
|
49
39
|
|
|
50
|
-
const handleGenerate = async () => {
|
|
51
|
-
if (!aiPrompt.trim()) return
|
|
52
|
-
setGenerating(true)
|
|
53
|
-
setGenError('')
|
|
54
|
-
try {
|
|
55
|
-
const result = await api<{ title?: string; description?: string; error?: string }>('POST', '/generate', { type: 'task', prompt: aiPrompt })
|
|
56
|
-
if (result.error) {
|
|
57
|
-
setGenError(result.error)
|
|
58
|
-
} else if (result.title || result.description) {
|
|
59
|
-
if (result.title) setTitle(result.title)
|
|
60
|
-
if (result.description) setDescription(result.description)
|
|
61
|
-
setGenerated(true)
|
|
62
|
-
} else {
|
|
63
|
-
setGenError('AI returned empty response — try again')
|
|
64
|
-
}
|
|
65
|
-
} catch (err: unknown) {
|
|
66
|
-
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
67
|
-
}
|
|
68
|
-
setGenerating(false)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
40
|
useEffect(() => {
|
|
72
41
|
if (open) {
|
|
73
42
|
loadAgents()
|
|
74
|
-
loadSettings()
|
|
75
|
-
setAiPrompt('')
|
|
76
|
-
setGenerating(false)
|
|
77
|
-
setGenerated(false)
|
|
78
|
-
setGenError('')
|
|
79
43
|
if (editing) {
|
|
80
44
|
setTitle(editing.title)
|
|
81
45
|
setDescription(editing.description)
|
|
@@ -185,14 +149,6 @@ export function TaskSheet() {
|
|
|
185
149
|
</p>
|
|
186
150
|
</div>
|
|
187
151
|
|
|
188
|
-
{/* AI Generation */}
|
|
189
|
-
{!editing && <AiGenBlock
|
|
190
|
-
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
191
|
-
generating={generating} generated={generated} genError={genError}
|
|
192
|
-
onGenerate={handleGenerate} appSettings={appSettings}
|
|
193
|
-
placeholder='Describe the task, e.g. "Audit all pages on example.com for SEO issues and broken links"'
|
|
194
|
-
/>}
|
|
195
|
-
|
|
196
152
|
<div className="mb-8">
|
|
197
153
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
|
|
198
154
|
<input
|
|
@@ -52,9 +52,9 @@ export function UsageList() {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="flex-1 overflow-y-auto px-
|
|
55
|
+
<div className="flex-1 overflow-y-auto px-5 pb-8">
|
|
56
56
|
{/* Summary */}
|
|
57
|
-
<div className="grid grid-cols-2 gap-
|
|
57
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 mt-1">
|
|
58
58
|
<div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
|
|
59
59
|
<div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Cost</div>
|
|
60
60
|
<div className="text-[18px] font-700 text-text tracking-tight">{formatCost(data.totalCost)}</div>
|
|
@@ -63,15 +63,23 @@ export function UsageList() {
|
|
|
63
63
|
<div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Tokens</div>
|
|
64
64
|
<div className="text-[18px] font-700 text-text tracking-tight">{formatTokens(data.totalTokens)}</div>
|
|
65
65
|
</div>
|
|
66
|
+
<div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
|
|
67
|
+
<div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Requests</div>
|
|
68
|
+
<div className="text-[18px] font-700 text-text tracking-tight">{providers.reduce((sum, [, s]) => sum + s.requests, 0)}</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
|
|
71
|
+
<div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Providers</div>
|
|
72
|
+
<div className="text-[18px] font-700 text-text tracking-tight">{providers.length}</div>
|
|
73
|
+
</div>
|
|
66
74
|
</div>
|
|
67
75
|
|
|
68
76
|
{/* Provider breakdown */}
|
|
69
77
|
<div className="mb-2">
|
|
70
|
-
<h3 className="text-[11px] font-600 text-text-3 uppercase tracking-wider
|
|
78
|
+
<h3 className="text-[11px] font-600 text-text-3 uppercase tracking-wider mb-2">By Provider</h3>
|
|
71
79
|
{providers.length === 0 ? (
|
|
72
80
|
<div className="text-center py-6 text-[12px] text-text-3/60">No usage data yet</div>
|
|
73
81
|
) : (
|
|
74
|
-
<div className="
|
|
82
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
75
83
|
{providers.map(([provider, stats]) => {
|
|
76
84
|
const pct = data.totalCost > 0 ? (stats.cost / data.totalCost) * 100 : 0
|
|
77
85
|
return (
|