@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,56 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+
6
+ interface DaemonStatus {
7
+ running: boolean
8
+ schedulerActive: boolean
9
+ queueLength: number
10
+ lastProcessed: number | null
11
+ nextScheduled: number | null
12
+ }
13
+
14
+ export function DaemonIndicator() {
15
+ const [status, setStatus] = useState<DaemonStatus | null>(null)
16
+
17
+ const fetchStatus = async () => {
18
+ try {
19
+ const data = await api<DaemonStatus>('GET', '/daemon')
20
+ setStatus(data)
21
+ } catch { /* ignore */ }
22
+ }
23
+
24
+ useEffect(() => {
25
+ fetchStatus()
26
+ const interval = setInterval(fetchStatus, 30_000)
27
+ return () => clearInterval(interval)
28
+ }, [])
29
+
30
+ const toggle = async () => {
31
+ try {
32
+ await api('POST', '/daemon', { action: status?.running ? 'stop' : 'start' })
33
+ fetchStatus()
34
+ } catch { /* ignore */ }
35
+ }
36
+
37
+ if (!status) return null
38
+
39
+ return (
40
+ <button
41
+ onClick={toggle}
42
+ className="flex items-center gap-2 px-3 py-2 rounded-[10px] bg-surface border border-white/[0.06] hover:bg-surface-2 transition-colors cursor-pointer w-full"
43
+ title={status.running ? 'Daemon running — click to pause' : 'Daemon paused — click to start'}
44
+ >
45
+ <span className={`w-2 h-2 rounded-full shrink-0 ${status.running ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]' : 'bg-text-3/30'}`} />
46
+ <span className="text-[12px] font-600 text-text-2 flex-1 text-left">
47
+ Daemon
48
+ </span>
49
+ {status.queueLength > 0 && (
50
+ <span className="text-[10px] font-mono text-amber-400/70">
51
+ {status.queueLength} queued
52
+ </span>
53
+ )}
54
+ </button>
55
+ )
56
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import { useAppStore } from '@/stores/use-app-store'
4
+ import { IconButton } from '@/components/shared/icon-button'
5
+
6
+ export function MobileHeader() {
7
+ const toggleSidebar = useAppStore((s) => s.toggleSidebar)
8
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
9
+ const sessions = useAppStore((s) => s.sessions)
10
+ const session = currentSessionId ? sessions[currentSessionId] : null
11
+
12
+ return (
13
+ <header className="flex items-center gap-3 px-4 py-2.5 border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0 min-h-[48px]"
14
+ style={{ paddingTop: 'max(10px, env(safe-area-inset-top))' }}>
15
+ <IconButton onClick={toggleSidebar} aria-label="Toggle sidebar">
16
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
17
+ <line x1="3" y1="7" x2="21" y2="7" />
18
+ <line x1="3" y1="12" x2="15" y2="12" />
19
+ <line x1="3" y1="17" x2="18" y2="17" />
20
+ </svg>
21
+ </IconButton>
22
+ <h1 className="font-display text-[14px] font-600 tracking-[-0.02em] flex-1 truncate">
23
+ {session ? (
24
+ <span className="block truncate">{session.name}</span>
25
+ ) : (
26
+ <span className="font-700">SwarmClaw</span>
27
+ )}
28
+ </h1>
29
+ </header>
30
+ )
31
+ }
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { useAppStore } from '@/stores/use-app-store'
4
+
5
+ export function NetworkBanner() {
6
+ const info = useAppStore((s) => s.networkInfo)
7
+ if (!info) return null
8
+
9
+ return (
10
+ <div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] text-text-3 flex items-center gap-2 shrink-0">
11
+ <span className="w-[5px] h-[5px] rounded-full bg-success shrink-0" />
12
+ <code className="font-mono text-[10px] text-text-3/70 select-all">
13
+ {info.ip}:{info.port}
14
+ </code>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+
5
+ const CHECK_INTERVAL = 5 * 60_000 // 5 minutes
6
+
7
+ type VersionInfo = {
8
+ localSha: string
9
+ remoteSha: string
10
+ updateAvailable: boolean
11
+ behindBy: number
12
+ }
13
+
14
+ type UpdateState = 'idle' | 'updating' | 'done' | 'error'
15
+
16
+ export function UpdateBanner() {
17
+ const [version, setVersion] = useState<VersionInfo | null>(null)
18
+ const [updateState, setUpdateState] = useState<UpdateState>('idle')
19
+ const [dismissed, setDismissed] = useState<string | null>(null)
20
+ const [errorMsg, setErrorMsg] = useState('')
21
+
22
+ const checkVersion = useCallback(async () => {
23
+ try {
24
+ const res = await fetch('/api/version')
25
+ if (!res.ok) return
26
+ const data: VersionInfo = await res.json()
27
+ setVersion(data)
28
+ } catch {
29
+ // silently fail — no network or server issue
30
+ }
31
+ }, [])
32
+
33
+ useEffect(() => {
34
+ checkVersion()
35
+ const id = setInterval(checkVersion, CHECK_INTERVAL)
36
+ return () => clearInterval(id)
37
+ }, [checkVersion])
38
+
39
+ const handleUpdate = async () => {
40
+ setUpdateState('updating')
41
+ setErrorMsg('')
42
+ try {
43
+ const res = await fetch('/api/version/update', { method: 'POST' })
44
+ const data = await res.json()
45
+ if (data.success) {
46
+ setUpdateState('done')
47
+ } else {
48
+ setUpdateState('error')
49
+ setErrorMsg(data.error || 'Update failed')
50
+ }
51
+ } catch {
52
+ setUpdateState('error')
53
+ setErrorMsg('Network error')
54
+ }
55
+ }
56
+
57
+ const handleDismiss = () => {
58
+ if (version) setDismissed(version.remoteSha)
59
+ }
60
+
61
+ // Don't show if no update, or user dismissed this specific remote SHA
62
+ if (!version?.updateAvailable) return null
63
+ if (dismissed === version.remoteSha && updateState === 'idle') return null
64
+
65
+ return (
66
+ <div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] flex items-center gap-2 shrink-0 bg-[#6366F1]/[0.04]">
67
+ {updateState === 'idle' && (
68
+ <>
69
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
70
+ <path d="M12 19V5M5 12l7-7 7 7" />
71
+ </svg>
72
+ <span className="text-text-3 flex-1 min-w-0 truncate">
73
+ <span className="text-accent-bright font-600">{version.behindBy}</span> update{version.behindBy !== 1 ? 's' : ''} available
74
+ </span>
75
+ <button
76
+ onClick={handleUpdate}
77
+ className="text-[10px] font-600 text-accent-bright hover:text-white bg-[#6366F1]/20 hover:bg-[#6366F1]/30 px-2 py-0.5 rounded-[6px] border-none cursor-pointer transition-all shrink-0"
78
+ style={{ fontFamily: 'inherit' }}
79
+ >
80
+ Update
81
+ </button>
82
+ <button
83
+ onClick={handleDismiss}
84
+ className="text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer p-0 shrink-0 transition-colors"
85
+ >
86
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
87
+ <line x1="18" y1="6" x2="6" y2="18" />
88
+ <line x1="6" y1="6" x2="18" y2="18" />
89
+ </svg>
90
+ </button>
91
+ </>
92
+ )}
93
+
94
+ {updateState === 'updating' && (
95
+ <>
96
+ <span className="w-3 h-3 border-[1.5px] border-accent-bright/30 border-t-accent-bright rounded-full shrink-0"
97
+ style={{ animation: 'spin 0.8s linear infinite' }} />
98
+ <span className="text-text-3">Updating...</span>
99
+ </>
100
+ )}
101
+
102
+ {updateState === 'done' && (
103
+ <>
104
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-success shrink-0">
105
+ <polyline points="20 6 9 17 4 12" />
106
+ </svg>
107
+ <span className="text-text-3 flex-1">Updated! Restart to apply.</span>
108
+ </>
109
+ )}
110
+
111
+ {updateState === 'error' && (
112
+ <>
113
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-red-400 shrink-0">
114
+ <circle cx="12" cy="12" r="10" />
115
+ <line x1="15" y1="9" x2="9" y2="15" />
116
+ <line x1="9" y1="9" x2="15" y2="15" />
117
+ </svg>
118
+ <span className="text-red-400/80 flex-1 truncate">{errorMsg}</span>
119
+ <button
120
+ onClick={() => setUpdateState('idle')}
121
+ className="text-[10px] font-600 text-text-3 hover:text-text bg-white/[0.04] px-2 py-0.5 rounded-[6px] border-none cursor-pointer transition-all shrink-0"
122
+ style={{ fontFamily: 'inherit' }}
123
+ >
124
+ Retry
125
+ </button>
126
+ </>
127
+ )}
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,358 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef, useCallback } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
7
+
8
+ interface LogEntry {
9
+ time: string
10
+ level: string
11
+ tag: string
12
+ message: string
13
+ data?: string
14
+ }
15
+
16
+ const LEVEL_COLORS: Record<string, string> = {
17
+ ERROR: 'text-red-400',
18
+ WARN: 'text-amber-400',
19
+ INFO: 'text-blue-400',
20
+ DEBUG: 'text-text-3',
21
+ }
22
+
23
+ const LEVEL_BG: Record<string, string> = {
24
+ ERROR: 'bg-red-500/10',
25
+ WARN: 'bg-amber-500/10',
26
+ INFO: 'bg-blue-500/10',
27
+ DEBUG: 'bg-white/[0.02]',
28
+ }
29
+
30
+ export function LogList() {
31
+ const [entries, setEntries] = useState<LogEntry[]>([])
32
+ const [total, setTotal] = useState(0)
33
+ const [loading, setLoading] = useState(true)
34
+ const [search, setSearch] = useState('')
35
+ const [levelFilter, setLevelFilter] = useState<string[]>([])
36
+ const [autoRefresh, setAutoRefresh] = useState(true)
37
+ const [selected, setSelected] = useState<LogEntry | null>(null)
38
+ const [creatingTask, setCreatingTask] = useState(false)
39
+ const [taskAgentId, setTaskAgentId] = useState('')
40
+ const [savedFilters, setSavedFilters] = useState<Array<{ name: string; levels: string[]; search: string }>>(() => {
41
+ if (typeof window === 'undefined') return []
42
+ try { return JSON.parse(localStorage.getItem('sc_log_filters') || '[]') } catch { return [] }
43
+ })
44
+ const scrollRef = useRef<HTMLDivElement>(null)
45
+
46
+ const agents = useAppStore((s) => s.agents)
47
+ const loadAgents = useAppStore((s) => s.loadAgents)
48
+ const loadTasks = useAppStore((s) => s.loadTasks)
49
+
50
+ const fetchLogs = useCallback(async () => {
51
+ try {
52
+ const params = new URLSearchParams({ lines: '300' })
53
+ if (levelFilter.length) params.set('level', levelFilter.join(','))
54
+ if (search) params.set('search', search)
55
+ const res = await api('GET', `/logs?${params}`) as { entries: LogEntry[]; total: number }
56
+ setEntries(res.entries || [])
57
+ setTotal(res.total || 0)
58
+ } catch (err) {
59
+ console.error('Failed to fetch logs:', err)
60
+ } finally {
61
+ setLoading(false)
62
+ }
63
+ }, [levelFilter, search])
64
+
65
+ useEffect(() => {
66
+ fetchLogs()
67
+ loadAgents()
68
+ }, [fetchLogs])
69
+
70
+ // Auto-refresh every 3s
71
+ useEffect(() => {
72
+ if (!autoRefresh) return
73
+ const id = setInterval(fetchLogs, 3000)
74
+ return () => clearInterval(id)
75
+ }, [autoRefresh, fetchLogs])
76
+
77
+ const clearLogs = async () => {
78
+ try {
79
+ await api('DELETE', '/logs')
80
+ setEntries([])
81
+ setTotal(0)
82
+ } catch (err) {
83
+ console.error('Failed to clear logs:', err)
84
+ }
85
+ }
86
+
87
+ const toggleLevel = (level: string) => {
88
+ setLevelFilter((prev) =>
89
+ prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level]
90
+ )
91
+ }
92
+
93
+ const formatTime = (iso: string) => {
94
+ if (!iso) return ''
95
+ try {
96
+ const d = new Date(iso)
97
+ return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
98
+ } catch {
99
+ return iso.slice(11, 19)
100
+ }
101
+ }
102
+
103
+ const formatFullTime = (iso: string) => {
104
+ if (!iso) return ''
105
+ try {
106
+ const d = new Date(iso)
107
+ return d.toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'medium' })
108
+ } catch {
109
+ return iso
110
+ }
111
+ }
112
+
113
+ const handleCreateTask = async () => {
114
+ if (!selected) return
115
+ setCreatingTask(true)
116
+ try {
117
+ const title = `[${selected.level}] ${selected.tag}: ${selected.message}`.slice(0, 120)
118
+ const description = [
119
+ `**Log Level:** ${selected.level}`,
120
+ `**Source:** ${selected.tag}`,
121
+ `**Time:** ${selected.time}`,
122
+ `**Message:** ${selected.message}`,
123
+ selected.data ? `\n**Data:**\n\`\`\`\n${selected.data}\n\`\`\`` : '',
124
+ ].filter(Boolean).join('\n')
125
+
126
+ await api('POST', '/tasks', {
127
+ title,
128
+ description,
129
+ status: 'backlog',
130
+ agentId: taskAgentId || undefined,
131
+ })
132
+ await loadTasks()
133
+ setSelected(null)
134
+ } catch (err) {
135
+ console.error('Failed to create task:', err)
136
+ } finally {
137
+ setCreatingTask(false)
138
+ }
139
+ }
140
+
141
+ if (loading) {
142
+ return (
143
+ <div className="flex-1 flex items-center justify-center text-text-3 text-[13px]">
144
+ Loading logs...
145
+ </div>
146
+ )
147
+ }
148
+
149
+ const agentList = Object.values(agents)
150
+
151
+ return (
152
+ <div className="flex-1 flex flex-col overflow-hidden">
153
+ {/* Controls */}
154
+ <div className="px-4 py-2 space-y-2 shrink-0">
155
+ {/* Search */}
156
+ <input
157
+ value={search}
158
+ onChange={(e) => setSearch(e.target.value)}
159
+ placeholder="Search logs..."
160
+ className="w-full px-3 py-2 rounded-[8px] bg-white/[0.04] border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent/30"
161
+ />
162
+ {/* Saved filters */}
163
+ {savedFilters.length > 0 && (
164
+ <div className="flex items-center gap-1.5 flex-wrap">
165
+ {savedFilters.map((f, i) => (
166
+ <button
167
+ key={i}
168
+ onClick={() => { setLevelFilter(f.levels); setSearch(f.search) }}
169
+ className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-[#6366F1]/15"
170
+ >
171
+ {f.name}
172
+ <span
173
+ onClick={(e) => {
174
+ e.stopPropagation()
175
+ const next = savedFilters.filter((_, j) => j !== i)
176
+ localStorage.setItem('sc_log_filters', JSON.stringify(next))
177
+ setSavedFilters(next)
178
+ }}
179
+ className="text-accent-bright/50 hover:text-red-400 ml-0.5"
180
+ >
181
+ x
182
+ </span>
183
+ </button>
184
+ ))}
185
+ </div>
186
+ )}
187
+ {/* Level filters + controls */}
188
+ <div className="flex items-center gap-1.5 flex-wrap">
189
+ {['ERROR', 'WARN', 'INFO', 'DEBUG'].map((level) => (
190
+ <button
191
+ key={level}
192
+ onClick={() => toggleLevel(level)}
193
+ className={`px-2 py-1 rounded-[6px] text-[10px] font-700 uppercase tracking-wider cursor-pointer transition-all border-none ${
194
+ levelFilter.length === 0 || levelFilter.includes(level)
195
+ ? `${LEVEL_BG[level]} ${LEVEL_COLORS[level]}`
196
+ : 'bg-white/[0.02] text-text-3/70'
197
+ }`}
198
+ >
199
+ {level}
200
+ </button>
201
+ ))}
202
+ <div className="flex-1" />
203
+ <button
204
+ onClick={() => setAutoRefresh(!autoRefresh)}
205
+ className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
206
+ autoRefresh ? 'bg-green-500/10 text-green-400' : 'bg-white/[0.04] text-text-3'
207
+ }`}
208
+ title={autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
209
+ >
210
+ {autoRefresh ? 'LIVE' : 'PAUSED'}
211
+ </button>
212
+ <button
213
+ onClick={clearLogs}
214
+ className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-red-400 hover:bg-red-500/10"
215
+ title="Clear all logs"
216
+ >
217
+ CLEAR
218
+ </button>
219
+ <button
220
+ onClick={() => {
221
+ const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' })
222
+ const url = URL.createObjectURL(blob)
223
+ const a = document.createElement('a')
224
+ a.href = url
225
+ a.download = `swarmclaw-logs-${new Date().toISOString().slice(0, 10)}.json`
226
+ a.click()
227
+ URL.revokeObjectURL(url)
228
+ }}
229
+ className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-accent-bright hover:bg-accent-soft"
230
+ title="Export logs as JSON"
231
+ >
232
+ EXPORT
233
+ </button>
234
+ <button
235
+ onClick={() => {
236
+ const name = prompt('Filter name:')
237
+ if (!name?.trim()) return
238
+ const filter = { name: name.trim(), levels: levelFilter, search }
239
+ const existing = JSON.parse(localStorage.getItem('sc_log_filters') || '[]')
240
+ existing.push(filter)
241
+ localStorage.setItem('sc_log_filters', JSON.stringify(existing))
242
+ setSavedFilters(existing)
243
+ }}
244
+ className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-accent-bright hover:bg-accent-soft"
245
+ title="Save current filter"
246
+ >
247
+ SAVE
248
+ </button>
249
+ </div>
250
+ </div>
251
+
252
+ {/* Total count */}
253
+ <div className="px-4 py-1 text-[10px] text-text-3/60">
254
+ {entries.length} of {total} entries
255
+ </div>
256
+
257
+ {/* Log entries */}
258
+ <div ref={scrollRef} className="flex-1 overflow-y-auto px-2 pb-20">
259
+ {entries.length === 0 ? (
260
+ <div className="flex items-center justify-center h-32 text-text-3 text-[12px]">
261
+ No log entries
262
+ </div>
263
+ ) : (
264
+ entries.map((entry, i) => (
265
+ <button
266
+ key={i}
267
+ onClick={() => { setSelected(entry); setTaskAgentId('') }}
268
+ className={`w-full text-left px-2 py-1.5 rounded-[6px] hover:bg-white/[0.03] transition-colors cursor-pointer bg-transparent border-none block
269
+ ${entry.level === 'ERROR' ? 'hover:bg-red-500/[0.04]' : ''}`}
270
+ >
271
+ <div className="flex items-start gap-2">
272
+ <span className="text-[10px] text-text-3/50 font-mono shrink-0 mt-[1px] w-[58px]">
273
+ {formatTime(entry.time)}
274
+ </span>
275
+ <span className={`text-[9px] font-700 uppercase tracking-wider shrink-0 mt-[2px] w-[36px] ${LEVEL_COLORS[entry.level] || 'text-text-3'}`}>
276
+ {entry.level}
277
+ </span>
278
+ <span className="text-[10px] font-600 text-accent/60 shrink-0 mt-[1px]">
279
+ {entry.tag}
280
+ </span>
281
+ <span className="text-[11px] text-text-2 truncate flex-1">
282
+ {entry.message}
283
+ </span>
284
+ {/* Arrow indicator */}
285
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50 shrink-0 mt-[2px]">
286
+ <polyline points="9 18 15 12 9 6" />
287
+ </svg>
288
+ </div>
289
+ </button>
290
+ ))
291
+ )}
292
+ </div>
293
+
294
+ {/* Log Detail Sheet */}
295
+ <BottomSheet open={!!selected} onClose={() => setSelected(null)}>
296
+ {selected && (
297
+ <>
298
+ <div className="mb-8">
299
+ <div className="flex items-center gap-3 mb-3">
300
+ <span className={`text-[11px] font-700 uppercase tracking-wider px-2.5 py-1 rounded-[6px] ${LEVEL_BG[selected.level]} ${LEVEL_COLORS[selected.level]}`}>
301
+ {selected.level}
302
+ </span>
303
+ <span className="text-[12px] font-600 text-accent/80 font-mono">{selected.tag}</span>
304
+ </div>
305
+ <h2 className="font-display text-[22px] font-700 tracking-[-0.02em] mb-2 leading-snug">
306
+ {selected.message}
307
+ </h2>
308
+ <p className="text-[12px] text-text-3/60 font-mono">{formatFullTime(selected.time)}</p>
309
+ </div>
310
+
311
+ {/* Data payload */}
312
+ {selected.data && (
313
+ <div className="mb-8">
314
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Details</label>
315
+ <pre className="text-[11px] text-text-3/80 font-mono whitespace-pre-wrap break-all bg-white/[0.02] rounded-[12px] p-4 max-h-[300px] overflow-auto border border-white/[0.04]">
316
+ {selected.data}
317
+ </pre>
318
+ </div>
319
+ )}
320
+
321
+ {/* Create as Task */}
322
+ <div className="pt-4 border-t border-white/[0.04]">
323
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
324
+ Create as Task
325
+ </label>
326
+ <p className="text-[12px] text-text-3/60 mb-3">
327
+ Turn this log entry into a task and optionally assign it to an agent to investigate.
328
+ </p>
329
+ <div className="flex gap-2">
330
+ <select
331
+ value={taskAgentId}
332
+ onChange={(e) => setTaskAgentId(e.target.value)}
333
+ className="flex-1 px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none appearance-none cursor-pointer"
334
+ style={{ fontFamily: 'inherit' }}
335
+ >
336
+ <option value="">Unassigned</option>
337
+ {agentList.map((a) => (
338
+ <option key={a.id} value={a.id}>{a.name}</option>
339
+ ))}
340
+ </select>
341
+ <button
342
+ onClick={handleCreateTask}
343
+ disabled={creatingTask}
344
+ className="px-5 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600
345
+ cursor-pointer active:scale-[0.97] disabled:opacity-40 transition-all
346
+ shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110 shrink-0"
347
+ style={{ fontFamily: 'inherit' }}
348
+ >
349
+ {creatingTask ? 'Creating...' : 'Create Task'}
350
+ </button>
351
+ </div>
352
+ </div>
353
+ </>
354
+ )}
355
+ </BottomSheet>
356
+ </div>
357
+ )
358
+ }