@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,166 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+
6
+ function webhookUrl(id: string): string {
7
+ if (typeof window === 'undefined') return `/api/webhooks/${id}`
8
+ return `${window.location.origin}/api/webhooks/${id}`
9
+ }
10
+
11
+ function formatEvents(events: string[] | undefined): string {
12
+ const list = Array.isArray(events) ? events.filter(Boolean) : []
13
+ if (list.length === 0) return 'all events'
14
+ if (list.length <= 2) return list.join(', ')
15
+ return `${list.slice(0, 2).join(', ')}, +${list.length - 2}`
16
+ }
17
+
18
+ export function WebhookList({ inSidebar }: { inSidebar?: boolean }) {
19
+ const webhooks = useAppStore((s) => s.webhooks)
20
+ const loadWebhooks = useAppStore((s) => s.loadWebhooks)
21
+ const setWebhookSheetOpen = useAppStore((s) => s.setWebhookSheetOpen)
22
+ const setEditingWebhookId = useAppStore((s) => s.setEditingWebhookId)
23
+ const agents = useAppStore((s) => s.agents)
24
+ const loadAgents = useAppStore((s) => s.loadAgents)
25
+ const [copied, setCopied] = useState<string | null>(null)
26
+
27
+ useEffect(() => {
28
+ loadWebhooks()
29
+ loadAgents()
30
+ }, [loadWebhooks, loadAgents])
31
+
32
+ const list = useMemo(
33
+ () => Object.values(webhooks).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)),
34
+ [webhooks]
35
+ )
36
+
37
+ const copyText = async (key: string, value: string) => {
38
+ try {
39
+ await navigator.clipboard.writeText(value)
40
+ setCopied(key)
41
+ setTimeout(() => setCopied((prev) => (prev === key ? null : prev)), 1400)
42
+ } catch {
43
+ // ignore clipboard failures (e.g. unsupported environment)
44
+ }
45
+ }
46
+
47
+ if (!list.length) {
48
+ return (
49
+ <div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
50
+ <div className="w-12 h-12 rounded-[14px] bg-white/[0.03] border border-white/[0.06] flex items-center justify-center mb-4">
51
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3">
52
+ <path d="M22 12h-4l-3 7L9 5l-3 7H2" />
53
+ </svg>
54
+ </div>
55
+ <p className="text-[13px] text-text-3 mb-1 font-600">No webhooks yet</p>
56
+ <p className="text-[12px] text-text-3/60">Create inbound endpoints to trigger agent runs</p>
57
+ <button
58
+ onClick={() => {
59
+ setEditingWebhookId(null)
60
+ setWebhookSheetOpen(true)
61
+ }}
62
+ className="mt-3 text-[13px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
63
+ >
64
+ + Add Webhook
65
+ </button>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'pb-10' : 'pb-20'}`}>
72
+ {list.map((hook) => {
73
+ const agentName = hook.agentId ? agents[hook.agentId]?.name : null
74
+ const endpoint = webhookUrl(hook.id)
75
+ const copiedEndpoint = copied === `endpoint:${hook.id}`
76
+ const copiedSecret = copied === `secret:${hook.id}`
77
+ const hasSecret = typeof hook.secret === 'string' && hook.secret.trim().length > 0
78
+
79
+ return (
80
+ <div
81
+ key={hook.id}
82
+ className="w-full flex items-center gap-2.5 px-5 py-3 hover:bg-white/[0.02] transition-colors group"
83
+ >
84
+ <button
85
+ onClick={() => {
86
+ setEditingWebhookId(hook.id)
87
+ setWebhookSheetOpen(true)
88
+ }}
89
+ className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer bg-transparent border-none text-left p-0"
90
+ >
91
+ <div className={`shrink-0 w-9 h-9 rounded-[10px] border flex items-center justify-center ${
92
+ hook.isEnabled
93
+ ? 'bg-emerald-500/12 border-emerald-500/20 text-emerald-300'
94
+ : 'bg-white/[0.03] border-white/[0.08] text-text-3'
95
+ }`}>
96
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
97
+ <path d="M22 12h-4l-3 7L9 5l-3 7H2" />
98
+ </svg>
99
+ </div>
100
+
101
+ <div className="flex-1 min-w-0">
102
+ <div className="flex items-center gap-2">
103
+ <span className="text-[13px] font-600 text-text truncate">{hook.name || 'Unnamed Webhook'}</span>
104
+ <span className={`shrink-0 w-2 h-2 rounded-full ${hook.isEnabled ? 'bg-emerald-400' : 'bg-white/20'}`} />
105
+ </div>
106
+ <div className="text-[11px] text-text-3 truncate">
107
+ {hook.source || 'custom'} · {formatEvents(hook.events)}{agentName ? ` · ${agentName}` : ''}
108
+ </div>
109
+ </div>
110
+ </button>
111
+
112
+ <button
113
+ onClick={(e) => {
114
+ e.stopPropagation()
115
+ copyText(`endpoint:${hook.id}`, endpoint)
116
+ }}
117
+ title={copiedEndpoint ? 'Copied endpoint' : 'Copy endpoint URL'}
118
+ className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
119
+ copiedEndpoint
120
+ ? 'opacity-100 bg-emerald-500/15 text-emerald-300'
121
+ : 'opacity-0 group-hover:opacity-100 focus:opacity-100 bg-accent-soft/40 text-accent-bright hover:bg-accent-soft'
122
+ }`}
123
+ >
124
+ {copiedEndpoint ? (
125
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
126
+ <polyline points="20 6 9 17 4 12" />
127
+ </svg>
128
+ ) : (
129
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
130
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
131
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
132
+ </svg>
133
+ )}
134
+ </button>
135
+
136
+ {hasSecret && (
137
+ <button
138
+ onClick={(e) => {
139
+ e.stopPropagation()
140
+ copyText(`secret:${hook.id}`, hook.secret!.trim())
141
+ }}
142
+ title={copiedSecret ? 'Copied secret' : 'Copy secret'}
143
+ className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
144
+ copiedSecret
145
+ ? 'opacity-100 bg-emerald-500/15 text-emerald-300'
146
+ : 'opacity-0 group-hover:opacity-100 focus:opacity-100 bg-white/[0.04] text-text-2 hover:bg-white/[0.08]'
147
+ }`}
148
+ >
149
+ {copiedSecret ? (
150
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
151
+ <polyline points="20 6 9 17 4 12" />
152
+ </svg>
153
+ ) : (
154
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
155
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
156
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
157
+ </svg>
158
+ )}
159
+ </button>
160
+ )}
161
+ </div>
162
+ )
163
+ })}
164
+ </div>
165
+ )
166
+ }
@@ -0,0 +1,402 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { api } from '@/lib/api-client'
7
+ import type { Webhook, WebhookLogEntry } from '@/types'
8
+
9
+ type WebhookApiResponse = Webhook | { error: string }
10
+ type DeleteWebhookResponse = { ok: boolean } | { error: string }
11
+
12
+ const inputClass = 'w-full px-4 py-3 rounded-[14px] bg-bg border border-white/[0.06] text-text text-[14px] outline-none focus:border-accent-bright/40 transition-colors placeholder:text-text-3/70'
13
+
14
+ function webhookUrl(id: string): string {
15
+ if (typeof window === 'undefined') return `/api/webhooks/${id}`
16
+ return `${window.location.origin}/api/webhooks/${id}`
17
+ }
18
+
19
+ function parseEvents(input: string): string[] {
20
+ const values = input
21
+ .split(/[\n,]+/)
22
+ .map((v) => v.trim())
23
+ .filter(Boolean)
24
+ return Array.from(new Set(values))
25
+ }
26
+
27
+ function makeSecret(length = 28): string {
28
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
29
+ const arr = new Uint8Array(length)
30
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
31
+ crypto.getRandomValues(arr)
32
+ } else {
33
+ for (let i = 0; i < length; i++) arr[i] = Math.floor(Math.random() * 256)
34
+ }
35
+ let out = ''
36
+ for (let i = 0; i < length; i++) out += chars[arr[i] % chars.length]
37
+ return out
38
+ }
39
+
40
+ export function WebhookSheet() {
41
+ const open = useAppStore((s) => s.webhookSheetOpen)
42
+ const setOpen = useAppStore((s) => s.setWebhookSheetOpen)
43
+ const editingId = useAppStore((s) => s.editingWebhookId)
44
+ const setEditingId = useAppStore((s) => s.setEditingWebhookId)
45
+ const webhooks = useAppStore((s) => s.webhooks)
46
+ const loadWebhooks = useAppStore((s) => s.loadWebhooks)
47
+ const agents = useAppStore((s) => s.agents)
48
+ const loadAgents = useAppStore((s) => s.loadAgents)
49
+
50
+ const [name, setName] = useState('')
51
+ const [source, setSource] = useState('custom')
52
+ const [eventsText, setEventsText] = useState('')
53
+ const [agentId, setAgentId] = useState('')
54
+ const [secret, setSecret] = useState('')
55
+ const [isEnabled, setIsEnabled] = useState(true)
56
+ const [saving, setSaving] = useState(false)
57
+ const [copied, setCopied] = useState<'endpoint' | 'secret' | null>(null)
58
+ const [error, setError] = useState<string | null>(null)
59
+ const [tab, setTab] = useState<'config' | 'history'>('config')
60
+ const [history, setHistory] = useState<WebhookLogEntry[]>([])
61
+ const [historyLoading, setHistoryLoading] = useState(false)
62
+
63
+ const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
64
+ const endpoint = editing ? webhookUrl(editing.id) : ''
65
+ const orchestrators = useMemo(
66
+ () => Object.values(agents).filter((a) => a.isOrchestrator),
67
+ [agents]
68
+ )
69
+
70
+ useEffect(() => {
71
+ if (open) {
72
+ loadWebhooks()
73
+ loadAgents()
74
+ setCopied(null)
75
+ setError(null)
76
+ setTab('config')
77
+ setHistory([])
78
+ }
79
+ }, [open, loadWebhooks, loadAgents])
80
+
81
+ useEffect(() => {
82
+ if (tab === 'history' && editing) {
83
+ setHistoryLoading(true)
84
+ api<WebhookLogEntry[]>('GET', `/webhooks/${editing.id}/history`)
85
+ .then((res) => setHistory(Array.isArray(res) ? res : []))
86
+ .catch(() => setHistory([]))
87
+ .finally(() => setHistoryLoading(false))
88
+ }
89
+ }, [tab, editing])
90
+
91
+ useEffect(() => {
92
+ if (editing) {
93
+ setName(editing.name || '')
94
+ setSource(editing.source || 'custom')
95
+ setEventsText((editing.events || []).join(', '))
96
+ setAgentId(editing.agentId || '')
97
+ setSecret(editing.secret || '')
98
+ setIsEnabled(editing.isEnabled !== false)
99
+ } else {
100
+ setName('')
101
+ setSource('custom')
102
+ setEventsText('')
103
+ setAgentId('')
104
+ setSecret(makeSecret())
105
+ setIsEnabled(true)
106
+ }
107
+ }, [editing, open])
108
+
109
+ const handleClose = () => {
110
+ setOpen(false)
111
+ setEditingId(null)
112
+ }
113
+
114
+ const copyText = async (type: 'endpoint' | 'secret', value: string) => {
115
+ if (!value) return
116
+ try {
117
+ await navigator.clipboard.writeText(value)
118
+ setCopied(type)
119
+ setTimeout(() => setCopied((prev) => (prev === type ? null : prev)), 1500)
120
+ } catch {
121
+ // ignore clipboard errors
122
+ }
123
+ }
124
+
125
+ const handleSave = async () => {
126
+ if (!agentId) {
127
+ setError('An orchestrator agent is required.')
128
+ return
129
+ }
130
+
131
+ const payload = {
132
+ name: name.trim() || 'Unnamed Webhook',
133
+ source: source.trim() || 'custom',
134
+ events: parseEvents(eventsText),
135
+ agentId: agentId || null,
136
+ secret: secret.trim(),
137
+ isEnabled,
138
+ }
139
+
140
+ setSaving(true)
141
+ setError(null)
142
+ try {
143
+ if (editing) {
144
+ const updated = await api<WebhookApiResponse>('PUT', `/webhooks/${editing.id}`, payload)
145
+ if ('error' in updated && updated.error) throw new Error(updated.error)
146
+ } else {
147
+ const created = await api<WebhookApiResponse>('POST', '/webhooks', payload)
148
+ if ('error' in created && created.error) throw new Error(created.error)
149
+ }
150
+ await loadWebhooks()
151
+ handleClose()
152
+ } catch (err: unknown) {
153
+ setError(err instanceof Error ? err.message : 'Failed to save webhook')
154
+ } finally {
155
+ setSaving(false)
156
+ }
157
+ }
158
+
159
+ const handleDelete = async () => {
160
+ if (!editing || !confirm('Delete this webhook?')) return
161
+ try {
162
+ const res = await api<DeleteWebhookResponse>('DELETE', `/webhooks/${editing.id}`)
163
+ if ('error' in res && res.error) throw new Error(res.error)
164
+ await loadWebhooks()
165
+ handleClose()
166
+ } catch (err: unknown) {
167
+ setError(err instanceof Error ? err.message : 'Failed to delete webhook')
168
+ }
169
+ }
170
+
171
+ return (
172
+ <BottomSheet open={open} onClose={handleClose} wide>
173
+ <div className="space-y-6">
174
+ <div>
175
+ <h2 className="font-display text-[24px] font-700 tracking-[-0.02em] mb-1">
176
+ {editing ? 'Edit Webhook' : 'New Webhook'}
177
+ </h2>
178
+ <p className="text-[13px] text-text-3">Create an inbound endpoint that triggers an orchestrator</p>
179
+ </div>
180
+
181
+ {editing && (
182
+ <div className="flex gap-1 p-1 rounded-[12px] bg-bg border border-white/[0.06]">
183
+ {(['config', 'history'] as const).map((t) => (
184
+ <button
185
+ key={t}
186
+ onClick={() => setTab(t)}
187
+ className={`flex-1 py-2 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none capitalize ${
188
+ tab === t ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
189
+ }`}
190
+ style={{ fontFamily: 'inherit' }}
191
+ >
192
+ {t}
193
+ </button>
194
+ ))}
195
+ </div>
196
+ )}
197
+
198
+ {tab === 'history' && editing ? (
199
+ <div>
200
+ {historyLoading ? (
201
+ <div className="text-center py-8 text-[13px] text-text-3">Loading history...</div>
202
+ ) : history.length === 0 ? (
203
+ <div className="text-center py-8 text-[13px] text-text-3/60">No webhook invocations yet</div>
204
+ ) : (
205
+ <div className="space-y-2 max-h-[400px] overflow-y-auto">
206
+ {history.map((entry) => (
207
+ <div key={entry.id} className="p-3 rounded-[10px] border border-white/[0.06] bg-white/[0.02]">
208
+ <div className="flex items-center gap-2 mb-1">
209
+ <span className={`text-[10px] font-700 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px] ${
210
+ entry.status === 'success' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'
211
+ }`}>
212
+ {entry.status}
213
+ </span>
214
+ <span className="text-[11px] text-text-3/60 font-mono">{entry.event}</span>
215
+ <span className="text-[10px] text-text-3/40 ml-auto">
216
+ {new Date(entry.timestamp).toLocaleString()}
217
+ </span>
218
+ </div>
219
+ {entry.error && (
220
+ <div className="text-[11px] text-red-300/80 mt-1">{entry.error}</div>
221
+ )}
222
+ {entry.sessionId && (
223
+ <div className="text-[10px] text-text-3/50 mt-1 font-mono">Session: {entry.sessionId}</div>
224
+ )}
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+ ) : null}
231
+
232
+ {tab === 'config' && error && (
233
+ <div className="px-3.5 py-2.5 rounded-[12px] bg-red-500/10 border border-red-500/20 text-[12px] text-red-300">
234
+ {error}
235
+ </div>
236
+ )}
237
+
238
+ {tab === 'config' && editing && (
239
+ <div className="p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
240
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Endpoint URL</label>
241
+ <div className="flex gap-2">
242
+ <input
243
+ readOnly
244
+ value={endpoint}
245
+ className={`${inputClass} font-mono text-[12px]`}
246
+ />
247
+ <button
248
+ onClick={() => copyText('endpoint', endpoint)}
249
+ className="px-3.5 py-2 rounded-[10px] border border-accent-bright/20 bg-accent-soft/40 text-accent-bright text-[12px] font-600 cursor-pointer hover:bg-accent-soft transition-colors"
250
+ style={{ fontFamily: 'inherit' }}
251
+ >
252
+ {copied === 'endpoint' ? 'Copied' : 'Copy'}
253
+ </button>
254
+ </div>
255
+ <p className="mt-2 text-[11px] text-text-3/70">
256
+ POST JSON payloads to this URL. Include <code className="font-mono">x-webhook-secret</code> if a secret is set.
257
+ </p>
258
+ </div>
259
+ )}
260
+
261
+ {tab === 'config' && <>
262
+ <div>
263
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Name</label>
264
+ <input
265
+ type="text"
266
+ value={name}
267
+ onChange={(e) => setName(e.target.value)}
268
+ placeholder="e.g. GitHub Push"
269
+ className={inputClass}
270
+ style={{ fontFamily: 'inherit' }}
271
+ />
272
+ </div>
273
+
274
+ <div>
275
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Source</label>
276
+ <input
277
+ type="text"
278
+ value={source}
279
+ onChange={(e) => setSource(e.target.value)}
280
+ placeholder="custom, github, slack..."
281
+ className={inputClass}
282
+ style={{ fontFamily: 'inherit' }}
283
+ />
284
+ </div>
285
+
286
+ <div>
287
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Orchestrator</label>
288
+ <select
289
+ value={agentId}
290
+ onChange={(e) => setAgentId(e.target.value)}
291
+ className={`${inputClass} appearance-none cursor-pointer`}
292
+ style={{ fontFamily: 'inherit' }}
293
+ >
294
+ <option value="">Select orchestrator...</option>
295
+ {orchestrators.map((agent) => (
296
+ <option key={agent.id} value={agent.id}>{agent.name}</option>
297
+ ))}
298
+ </select>
299
+ </div>
300
+
301
+ <div>
302
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
303
+ Events <span className="normal-case tracking-normal font-normal text-text-3/70">(optional)</span>
304
+ </label>
305
+ <textarea
306
+ value={eventsText}
307
+ onChange={(e) => setEventsText(e.target.value)}
308
+ placeholder="push, release or *"
309
+ rows={3}
310
+ className={`${inputClass} resize-y min-h-[86px] font-mono text-[12px]`}
311
+ style={{ fontFamily: 'inherit' }}
312
+ />
313
+ <p className="mt-1.5 text-[11px] text-text-3/70">Leave blank for all events. Use commas or new lines. Use <code>*</code> to match all.</p>
314
+ </div>
315
+
316
+ <div>
317
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
318
+ Secret <span className="normal-case tracking-normal font-normal text-text-3/70">(optional but recommended)</span>
319
+ </label>
320
+ <div className="flex gap-2">
321
+ <input
322
+ type="text"
323
+ value={secret}
324
+ onChange={(e) => setSecret(e.target.value)}
325
+ placeholder="x-webhook-secret value"
326
+ className={`${inputClass} font-mono text-[12px]`}
327
+ style={{ fontFamily: 'inherit' }}
328
+ />
329
+ <button
330
+ onClick={() => copyText('secret', secret)}
331
+ disabled={!secret.trim()}
332
+ className="px-3.5 py-2 rounded-[10px] border border-white/[0.1] bg-white/[0.04] text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.08] transition-colors disabled:opacity-40"
333
+ style={{ fontFamily: 'inherit' }}
334
+ >
335
+ {copied === 'secret' ? 'Copied' : 'Copy'}
336
+ </button>
337
+ <button
338
+ onClick={() => setSecret(makeSecret())}
339
+ className="px-3.5 py-2 rounded-[10px] border border-accent-bright/20 bg-accent-soft/40 text-accent-bright text-[12px] font-600 cursor-pointer hover:bg-accent-soft transition-colors"
340
+ style={{ fontFamily: 'inherit' }}
341
+ >
342
+ Regenerate
343
+ </button>
344
+ </div>
345
+ </div>
346
+
347
+ <div>
348
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Status</label>
349
+ <div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
350
+ <button
351
+ onClick={() => setIsEnabled(true)}
352
+ className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
353
+ isEnabled ? 'bg-emerald-500/15 text-emerald-300' : 'bg-transparent text-text-3 hover:text-text-2'
354
+ }`}
355
+ style={{ fontFamily: 'inherit' }}
356
+ >
357
+ Enabled
358
+ </button>
359
+ <button
360
+ onClick={() => setIsEnabled(false)}
361
+ className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
362
+ !isEnabled ? 'bg-white/[0.08] text-text-2' : 'bg-transparent text-text-3 hover:text-text-2'
363
+ }`}
364
+ style={{ fontFamily: 'inherit' }}
365
+ >
366
+ Disabled
367
+ </button>
368
+ </div>
369
+ </div>
370
+
371
+ <div className="flex gap-3 pt-2">
372
+ {editing && (
373
+ <button
374
+ onClick={handleDelete}
375
+ className="px-5 py-3 rounded-[14px] border border-danger/30 bg-transparent text-danger text-[14px] font-600 cursor-pointer hover:bg-danger/10 transition-colors"
376
+ style={{ fontFamily: 'inherit' }}
377
+ >
378
+ Delete
379
+ </button>
380
+ )}
381
+ <div className="flex-1" />
382
+ <button
383
+ onClick={handleClose}
384
+ className="px-5 py-3 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 transition-colors"
385
+ style={{ fontFamily: 'inherit' }}
386
+ >
387
+ Cancel
388
+ </button>
389
+ <button
390
+ onClick={handleSave}
391
+ disabled={saving}
392
+ className="px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
393
+ style={{ fontFamily: 'inherit' }}
394
+ >
395
+ {saving ? 'Saving...' : editing ? 'Update' : 'Create'}
396
+ </button>
397
+ </div>
398
+ </>}
399
+ </div>
400
+ </BottomSheet>
401
+ )
402
+ }
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef } from 'react'
4
+
5
+ export function useAutoResize(maxHeight = 120) {
6
+ const ref = useRef<HTMLTextAreaElement>(null)
7
+
8
+ const resize = useCallback(() => {
9
+ const el = ref.current
10
+ if (!el) return
11
+ el.style.height = 'auto'
12
+ el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'
13
+ }, [maxHeight])
14
+
15
+ useEffect(() => {
16
+ resize()
17
+ }, [resize])
18
+
19
+ return { ref, resize }
20
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useSyncExternalStore } from 'react'
4
+
5
+ export function useMediaQuery(query: string): boolean {
6
+ const subscribe = useCallback(
7
+ (callback: () => void) => {
8
+ const mql = window.matchMedia(query)
9
+ mql.addEventListener('change', callback)
10
+ return () => mql.removeEventListener('change', callback)
11
+ },
12
+ [query],
13
+ )
14
+
15
+ const getSnapshot = () => window.matchMedia(query).matches
16
+
17
+ // Return false during SSR — matches initial client render before hydration
18
+ const getServerSnapshot = () => false
19
+
20
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
21
+ }