@swarmclawai/swarmclaw 0.2.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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,225 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { api } from '@/lib/api-client'
9
+ import { toast } from 'sonner'
10
+
11
+ interface ClawHubSkill {
12
+ id: string
13
+ name: string
14
+ description: string
15
+ author: string
16
+ tags: string[]
17
+ downloads: number
18
+ url: string
19
+ version: string
20
+ }
21
+
22
+ interface SearchResponse {
23
+ skills: ClawHubSkill[]
24
+ total: number
25
+ page: number
26
+ }
27
+
28
+ interface ClawHubBrowserProps {
29
+ open: boolean
30
+ onOpenChange: (open: boolean) => void
31
+ onInstalled?: () => void
32
+ }
33
+
34
+ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrowserProps) {
35
+ const [query, setQuery] = useState('')
36
+ const [skills, setSkills] = useState<ClawHubSkill[]>([])
37
+ const [page, setPage] = useState(1)
38
+ const [total, setTotal] = useState(0)
39
+ const [loading, setLoading] = useState(false)
40
+ const [error, setError] = useState<string | null>(null)
41
+ const [installing, setInstalling] = useState<string | null>(null)
42
+ const [searched, setSearched] = useState(false)
43
+
44
+ const search = useCallback(async (q: string, p: number, append = false) => {
45
+ setLoading(true)
46
+ setError(null)
47
+ try {
48
+ const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
49
+ if (append) {
50
+ setSkills(prev => [...prev, ...res.skills])
51
+ } else {
52
+ setSkills(res.skills)
53
+ }
54
+ setTotal(res.total)
55
+ setPage(res.page)
56
+ setSearched(true)
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : 'Failed to search ClawHub')
59
+ } finally {
60
+ setLoading(false)
61
+ }
62
+ }, [])
63
+
64
+ useEffect(() => {
65
+ if (open) {
66
+ setQuery('')
67
+ setSkills([])
68
+ setPage(1)
69
+ setTotal(0)
70
+ setError(null)
71
+ setSearched(false)
72
+ search('', 1)
73
+ }
74
+ }, [open, search])
75
+
76
+ const handleSearch = () => {
77
+ setSkills([])
78
+ search(query, 1)
79
+ }
80
+
81
+ const handleKeyDown = (e: React.KeyboardEvent) => {
82
+ if (e.key === 'Enter') handleSearch()
83
+ }
84
+
85
+ const handleLoadMore = () => {
86
+ search(query, page + 1, true)
87
+ }
88
+
89
+ const handleInstall = async (skill: ClawHubSkill) => {
90
+ setInstalling(skill.id)
91
+ try {
92
+ await api('POST', '/clawhub/install', {
93
+ name: skill.name,
94
+ description: skill.description,
95
+ url: skill.url,
96
+ tags: skill.tags,
97
+ })
98
+ toast.success(`Installed "${skill.name}"`)
99
+ onInstalled?.()
100
+ } catch (err) {
101
+ toast.error(err instanceof Error ? err.message : 'Install failed')
102
+ } finally {
103
+ setInstalling(null)
104
+ }
105
+ }
106
+
107
+ const hasMore = skills.length < total
108
+
109
+ return (
110
+ <Sheet open={open} onOpenChange={onOpenChange}>
111
+ <SheetContent side="right" className="w-full sm:max-w-lg flex flex-col">
112
+ <SheetHeader>
113
+ <SheetTitle className="font-display text-[16px] font-600 text-text">
114
+ ClawHub
115
+ </SheetTitle>
116
+ <p className="text-[12px] text-text-3/60">Browse and install community skills</p>
117
+ </SheetHeader>
118
+
119
+ <div className="flex gap-2 px-4">
120
+ <Input
121
+ placeholder="Search skills..."
122
+ value={query}
123
+ onChange={(e) => setQuery(e.target.value)}
124
+ onKeyDown={handleKeyDown}
125
+ className="flex-1 text-[13px]"
126
+ />
127
+ <Button size="sm" onClick={handleSearch} disabled={loading}>
128
+ Search
129
+ </Button>
130
+ </div>
131
+
132
+ <div className="flex-1 overflow-y-auto px-4 pb-4">
133
+ {error && (
134
+ <div className="text-center py-12">
135
+ <p className="text-[13px] text-red-400">{error}</p>
136
+ <Button size="sm" variant="ghost" className="mt-2" onClick={() => search(query, 1)}>
137
+ Retry
138
+ </Button>
139
+ </div>
140
+ )}
141
+
142
+ {!error && !loading && searched && skills.length === 0 && (
143
+ <div className="text-center py-12">
144
+ <p className="text-[13px] text-text-3/60">No skills found</p>
145
+ {query && (
146
+ <p className="text-[11px] text-text-3/40 mt-1">Try a different search term</p>
147
+ )}
148
+ </div>
149
+ )}
150
+
151
+ {skills.length > 0 && (
152
+ <div className="space-y-2">
153
+ {skills.map((skill) => (
154
+ <div
155
+ key={skill.id}
156
+ className="p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all"
157
+ >
158
+ <div className="flex items-start justify-between gap-2">
159
+ <div className="min-w-0 flex-1">
160
+ <div className="flex items-center gap-2 mb-0.5">
161
+ <span className="font-display text-[14px] font-600 text-text truncate">
162
+ {skill.name}
163
+ </span>
164
+ <span className="text-[10px] font-mono text-text-3/40 shrink-0">
165
+ v{skill.version}
166
+ </span>
167
+ </div>
168
+ <p className="text-[12px] text-text-3/60 line-clamp-2 mb-2">
169
+ {skill.description}
170
+ </p>
171
+ <div className="flex items-center gap-2 flex-wrap">
172
+ {skill.tags.slice(0, 4).map((tag) => (
173
+ <Badge
174
+ key={tag}
175
+ variant="secondary"
176
+ className="text-[10px] px-1.5 py-0"
177
+ >
178
+ {tag}
179
+ </Badge>
180
+ ))}
181
+ </div>
182
+ <div className="flex items-center gap-3 mt-2 text-[11px] text-text-3/50">
183
+ <span>{skill.author}</span>
184
+ <span>{skill.downloads.toLocaleString()} installs</span>
185
+ </div>
186
+ </div>
187
+ <Button
188
+ size="sm"
189
+ variant="outline"
190
+ className="shrink-0 text-[12px]"
191
+ disabled={installing === skill.id}
192
+ onClick={() => handleInstall(skill)}
193
+ >
194
+ {installing === skill.id ? 'Installing...' : 'Install'}
195
+ </Button>
196
+ </div>
197
+ </div>
198
+ ))}
199
+
200
+ {hasMore && (
201
+ <div className="pt-2 pb-4 text-center">
202
+ <Button
203
+ size="sm"
204
+ variant="ghost"
205
+ onClick={handleLoadMore}
206
+ disabled={loading}
207
+ className="text-[12px] text-text-3/60"
208
+ >
209
+ {loading ? 'Loading...' : 'Load More'}
210
+ </Button>
211
+ </div>
212
+ )}
213
+ </div>
214
+ )}
215
+
216
+ {loading && skills.length === 0 && (
217
+ <div className="flex items-center justify-center py-12">
218
+ <div className="h-5 w-5 animate-spin rounded-full border-2 border-text-3/20 border-t-text-3/60" />
219
+ </div>
220
+ )}
221
+ </div>
222
+ </SheetContent>
223
+ </Sheet>
224
+ )
225
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { ClawHubBrowser } from './clawhub-browser'
6
+
7
+ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
8
+ const skills = useAppStore((s) => s.skills)
9
+ const loadSkills = useAppStore((s) => s.loadSkills)
10
+ const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
11
+ const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
12
+ const [clawHubOpen, setClawHubOpen] = useState(false)
13
+
14
+ useEffect(() => {
15
+ loadSkills()
16
+ }, [])
17
+
18
+ const skillList = Object.values(skills)
19
+
20
+ const handleEdit = (id: string) => {
21
+ setEditingSkillId(id)
22
+ setSkillSheetOpen(true)
23
+ }
24
+
25
+ return (
26
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
27
+ <button
28
+ onClick={() => setClawHubOpen(true)}
29
+ 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"
30
+ style={{ fontFamily: 'inherit' }}
31
+ >
32
+ Browse ClawHub Skills
33
+ </button>
34
+ <ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
35
+ {skillList.length === 0 ? (
36
+ <div className="text-center py-12">
37
+ <p className="text-[13px] text-text-3/60">No skills yet</p>
38
+ <button
39
+ onClick={() => setSkillSheetOpen(true)}
40
+ 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"
41
+ style={{ fontFamily: 'inherit' }}
42
+ >
43
+ + Add Skill
44
+ </button>
45
+ </div>
46
+ ) : (
47
+ <div className="space-y-2">
48
+ {skillList.map((skill) => (
49
+ <button
50
+ key={skill.id}
51
+ onClick={() => handleEdit(skill.id)}
52
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
53
+ >
54
+ <div className="flex items-center justify-between mb-1">
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>
64
+ </button>
65
+ ))}
66
+ </div>
67
+ )}
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,254 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { AiGenBlock } from '@/components/shared/ai-gen-block'
7
+ import { api } from '@/lib/api-client'
8
+
9
+ export function SkillSheet() {
10
+ const open = useAppStore((s) => s.skillSheetOpen)
11
+ const setOpen = useAppStore((s) => s.setSkillSheetOpen)
12
+ const editingId = useAppStore((s) => s.editingSkillId)
13
+ const setEditingId = useAppStore((s) => s.setEditingSkillId)
14
+ const skills = useAppStore((s) => s.skills)
15
+ const loadSkills = useAppStore((s) => s.loadSkills)
16
+ const fileRef = useRef<HTMLInputElement>(null)
17
+
18
+ const [name, setName] = useState('')
19
+ const [filename, setFilename] = useState('')
20
+ const [description, setDescription] = useState('')
21
+ const [content, setContent] = useState('')
22
+ const [importUrl, setImportUrl] = useState('')
23
+ const [importingUrl, setImportingUrl] = useState(false)
24
+ const [importError, setImportError] = useState('')
25
+ const [importNotice, setImportNotice] = useState('')
26
+
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
+ const editing = editingId ? skills[editingId] : null
36
+
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
+ const handleImportFromUrl = async () => {
60
+ if (!importUrl.trim()) return
61
+ setImportingUrl(true)
62
+ setImportError('')
63
+ setImportNotice('')
64
+ try {
65
+ const result = await api<{ name: string; filename: string; description?: string; content: string; sourceFormat?: 'openclaw' | 'plain' }>('POST', '/skills/import', { url: importUrl.trim() })
66
+ setName(result.name || '')
67
+ setFilename(result.filename || '')
68
+ setDescription(result.description || '')
69
+ setContent(result.content || '')
70
+ if (result.sourceFormat === 'openclaw') {
71
+ setImportNotice('Imported OpenClaw SKILL.md format and stripped frontmatter automatically.')
72
+ } else {
73
+ setImportNotice('Skill imported from URL.')
74
+ }
75
+ setGenerated(false)
76
+ } catch (err: unknown) {
77
+ setImportError(err instanceof Error ? err.message : 'Failed to import skill URL')
78
+ } finally {
79
+ setImportingUrl(false)
80
+ }
81
+ }
82
+
83
+ useEffect(() => {
84
+ if (open) {
85
+ loadSettings()
86
+ setAiPrompt('')
87
+ setGenerating(false)
88
+ setGenerated(false)
89
+ setGenError('')
90
+ setImportUrl('')
91
+ setImportingUrl(false)
92
+ setImportError('')
93
+ setImportNotice('')
94
+ if (editing) {
95
+ setName(editing.name)
96
+ setFilename(editing.filename)
97
+ setDescription(editing.description || '')
98
+ setContent(editing.content)
99
+ } else {
100
+ setName('')
101
+ setFilename('')
102
+ setDescription('')
103
+ setContent('')
104
+ }
105
+ }
106
+ }, [open, editingId])
107
+
108
+ const onClose = () => {
109
+ setOpen(false)
110
+ setEditingId(null)
111
+ }
112
+
113
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
114
+ const file = e.target.files?.[0]
115
+ if (!file) return
116
+ const reader = new FileReader()
117
+ reader.onload = (ev) => {
118
+ const text = ev.target?.result as string
119
+ setContent(text)
120
+ if (!name) setName(file.name.replace(/\.\w+$/, '').replace(/[-_]/g, ' '))
121
+ if (!filename) setFilename(file.name)
122
+ }
123
+ reader.readAsText(file)
124
+ e.target.value = ''
125
+ }
126
+
127
+ const handleSave = async () => {
128
+ const data = {
129
+ name: name.trim() || 'Unnamed Skill',
130
+ filename: filename.trim() || `${name.trim().toLowerCase().replace(/\s+/g, '-')}.md`,
131
+ description,
132
+ content,
133
+ }
134
+ if (editing) {
135
+ await api('PUT', `/skills/${editing.id}`, data)
136
+ } else {
137
+ await api('POST', '/skills', data)
138
+ }
139
+ await loadSkills()
140
+ onClose()
141
+ }
142
+
143
+ const handleDelete = async () => {
144
+ if (editing) {
145
+ await api('DELETE', `/skills/${editing.id}`)
146
+ await loadSkills()
147
+ onClose()
148
+ }
149
+ }
150
+
151
+ const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
152
+
153
+ return (
154
+ <BottomSheet open={open} onClose={onClose} wide>
155
+ <div className="mb-10">
156
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
157
+ {editing ? 'Edit Skill' : 'New Skill'}
158
+ </h2>
159
+ <p className="text-[14px] text-text-3">Upload or write a reusable instruction set for agents</p>
160
+ </div>
161
+
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
+ {/* File upload */}
171
+ {!editing && (
172
+ <div className="mb-8">
173
+ <label
174
+ onClick={() => fileRef.current?.click()}
175
+ className="flex items-center justify-center gap-2.5 w-full py-4 rounded-[14px] border border-dashed border-white/[0.1] bg-transparent text-text-3 text-[14px] font-600 cursor-pointer hover:border-accent-bright/30 hover:text-accent-bright hover:bg-accent-soft transition-all duration-200"
176
+ >
177
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
178
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
179
+ <polyline points="17 8 12 3 7 8" />
180
+ <line x1="12" y1="3" x2="12" y2="15" />
181
+ </svg>
182
+ Upload .md file
183
+ </label>
184
+ <input ref={fileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload} className="hidden" />
185
+ </div>
186
+ )}
187
+
188
+ {!editing && (
189
+ <div className="mb-8 p-4 rounded-[14px] border border-white/[0.08] bg-surface">
190
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Import from URL</label>
191
+ <div className="flex gap-2">
192
+ <input
193
+ type="url"
194
+ value={importUrl}
195
+ onChange={(e) => setImportUrl(e.target.value)}
196
+ placeholder="https://.../SKILL.md"
197
+ className={`${inputClass} flex-1`}
198
+ style={{ fontFamily: 'inherit' }}
199
+ />
200
+ <button
201
+ onClick={handleImportFromUrl}
202
+ disabled={importingUrl || !importUrl.trim()}
203
+ className="px-4 py-3 rounded-[12px] border-none bg-[#6366F1] text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
204
+ style={{ fontFamily: 'inherit' }}
205
+ >
206
+ {importingUrl ? 'Importing...' : 'Import'}
207
+ </button>
208
+ </div>
209
+ {importError && <p className="mt-2 text-[12px] text-red-400/80">{importError}</p>}
210
+ {importNotice && <p className="mt-2 text-[12px] text-emerald-400/80">{importNotice}</p>}
211
+ </div>
212
+ )}
213
+
214
+ <div className="mb-8">
215
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
216
+ <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Frontend Design" className={inputClass} style={{ fontFamily: 'inherit' }} />
217
+ </div>
218
+
219
+ <div className="mb-8">
220
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
221
+ Description <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
222
+ </label>
223
+ <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Short summary of what this skill does" className={inputClass} style={{ fontFamily: 'inherit' }} />
224
+ </div>
225
+
226
+ <div className="mb-8">
227
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Content</label>
228
+ <textarea
229
+ value={content}
230
+ onChange={(e) => setContent(e.target.value)}
231
+ placeholder="# Skill Instructions&#10;&#10;Write your skill content in markdown..."
232
+ rows={10}
233
+ className={`${inputClass} resize-y min-h-[200px] font-mono text-[13px]`}
234
+ style={{ fontFamily: 'inherit' }}
235
+ />
236
+ <p className="text-[11px] text-text-3/70 mt-2">{content.length} characters</p>
237
+ </div>
238
+
239
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
240
+ {editing && (
241
+ <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
242
+ Delete
243
+ </button>
244
+ )}
245
+ <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
246
+ Cancel
247
+ </button>
248
+ <button onClick={handleSave} disabled={!name.trim() || !content.trim()} className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" style={{ fontFamily: 'inherit' }}>
249
+ {editing ? 'Save' : 'Create'}
250
+ </button>
251
+ </div>
252
+ </BottomSheet>
253
+ )
254
+ }
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useCallback, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { updateTask } from '@/lib/tasks'
6
+ import { TaskColumn } from './task-column'
7
+ import type { BoardTaskStatus } from '@/types'
8
+
9
+ const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
10
+
11
+ export function TaskBoard() {
12
+ const tasks = useAppStore((s) => s.tasks)
13
+ const loadTasks = useAppStore((s) => s.loadTasks)
14
+ const loadAgents = useAppStore((s) => s.loadAgents)
15
+ const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
16
+ const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
17
+ const agents = useAppStore((s) => s.agents)
18
+ const showArchived = useAppStore((s) => s.showArchivedTasks)
19
+ const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
20
+ const [filterAgentId, setFilterAgentId] = useState<string>('')
21
+
22
+ useEffect(() => {
23
+ loadTasks()
24
+ loadAgents()
25
+ const interval = setInterval(loadTasks, 5000)
26
+ return () => clearInterval(interval)
27
+ }, [])
28
+
29
+ const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
30
+
31
+ const tasksByStatus = (status: BoardTaskStatus) =>
32
+ Object.values(tasks)
33
+ .filter((t) => t.status === status && (!filterAgentId || t.agentId === filterAgentId))
34
+ .sort((a, b) => b.updatedAt - a.updatedAt)
35
+
36
+ const handleDrop = useCallback(async (taskId: string, newStatus: BoardTaskStatus) => {
37
+ const task = tasks[taskId]
38
+ if (!task || task.status === newStatus) return
39
+ await updateTask(taskId, { status: newStatus })
40
+ await loadTasks()
41
+ }, [tasks, loadTasks])
42
+
43
+ const archivedCount = Object.values(tasks).filter((t) => t.status === 'archived').length
44
+
45
+ return (
46
+ <div className="flex-1 flex flex-col h-full overflow-hidden">
47
+ <div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
48
+ <div>
49
+ <h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Task Board</h1>
50
+ <p className="text-[13px] text-text-3 mt-1">Create tasks and assign orchestrators to run them sequentially</p>
51
+ </div>
52
+ <div className="flex items-center gap-3">
53
+ <select
54
+ value={filterAgentId}
55
+ onChange={(e) => setFilterAgentId(e.target.value)}
56
+ className="px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
57
+ bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03] appearance-none"
58
+ style={{ fontFamily: 'inherit', minWidth: 130 }}
59
+ >
60
+ <option value="">All Agents</option>
61
+ {Object.values(agents).map((a) => (
62
+ <option key={a.id} value={a.id}>{a.name}</option>
63
+ ))}
64
+ </select>
65
+ <button
66
+ onClick={() => setShowArchived(!showArchived)}
67
+ className={`px-4 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
68
+ ${showArchived
69
+ ? 'bg-white/[0.06] border-white/[0.1] text-text-2'
70
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
71
+ style={{ fontFamily: 'inherit' }}
72
+ >
73
+ {showArchived ? 'Hide' : 'Show'} Archived{!showArchived && archivedCount > 0 ? '' : ''}
74
+ </button>
75
+ <button
76
+ onClick={() => {
77
+ setEditingTaskId(null)
78
+ setTaskSheetOpen(true)
79
+ }}
80
+ className="px-5 py-2.5 rounded-[12px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer
81
+ hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_2px_12px_rgba(99,102,241,0.2)]"
82
+ style={{ fontFamily: 'inherit' }}
83
+ >
84
+ + New Task
85
+ </button>
86
+ </div>
87
+ </div>
88
+
89
+ <div className="flex-1 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden">
90
+ {columns.map((status) => (
91
+ <TaskColumn key={status} status={status} tasks={tasksByStatus(status)} onDrop={handleDrop} />
92
+ ))}
93
+ </div>
94
+ </div>
95
+ )
96
+ }