@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,415 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createTask, updateTask, archiveTask, unarchiveTask } from '@/lib/tasks'
6
+ import { api } from '@/lib/api-client'
7
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
8
+ import { AiGenBlock } from '@/components/shared/ai-gen-block'
9
+ import { DirBrowser } from '@/components/shared/dir-browser'
10
+ import type { BoardTask, TaskComment } from '@/types'
11
+
12
+ function fmtTime(ts: number) {
13
+ const d = new Date(ts)
14
+ const now = new Date()
15
+ const isToday = d.toDateString() === now.toDateString()
16
+ if (isToday) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
17
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
18
+ }
19
+
20
+ export function TaskSheet() {
21
+ const open = useAppStore((s) => s.taskSheetOpen)
22
+ const setOpen = useAppStore((s) => s.setTaskSheetOpen)
23
+ const editingId = useAppStore((s) => s.editingTaskId)
24
+ const setEditingId = useAppStore((s) => s.setEditingTaskId)
25
+ const tasks = useAppStore((s) => s.tasks)
26
+ const loadTasks = useAppStore((s) => s.loadTasks)
27
+ const agents = useAppStore((s) => s.agents)
28
+ const loadAgents = useAppStore((s) => s.loadAgents)
29
+
30
+ const [title, setTitle] = useState('')
31
+ const [description, setDescription] = useState('')
32
+ const [agentId, setAgentId] = useState('')
33
+ const [commentText, setCommentText] = useState('')
34
+ const [images, setImages] = useState<string[]>([])
35
+ const [uploading, setUploading] = useState(false)
36
+ const [cwd, setCwd] = useState('')
37
+ const [file, setFile] = useState<string | null>(null)
38
+
39
+ // AI generation state
40
+ const [aiPrompt, setAiPrompt] = useState('')
41
+ const [generating, setGenerating] = useState(false)
42
+ const [generated, setGenerated] = useState(false)
43
+ const [genError, setGenError] = useState('')
44
+ const appSettings = useAppStore((s) => s.appSettings)
45
+ const loadSettings = useAppStore((s) => s.loadSettings)
46
+
47
+ const editing = editingId ? tasks[editingId] : null
48
+ const orchestrators = Object.values(agents).filter((p) => p.isOrchestrator)
49
+
50
+ const handleGenerate = async () => {
51
+ if (!aiPrompt.trim()) return
52
+ setGenerating(true)
53
+ setGenError('')
54
+ try {
55
+ const result = await api<{ title?: string; description?: string; error?: string }>('POST', '/generate', { type: 'task', prompt: aiPrompt })
56
+ if (result.error) {
57
+ setGenError(result.error)
58
+ } else if (result.title || result.description) {
59
+ if (result.title) setTitle(result.title)
60
+ if (result.description) setDescription(result.description)
61
+ setGenerated(true)
62
+ } else {
63
+ setGenError('AI returned empty response — try again')
64
+ }
65
+ } catch (err: unknown) {
66
+ setGenError(err instanceof Error ? err.message : 'Generation failed')
67
+ }
68
+ setGenerating(false)
69
+ }
70
+
71
+ useEffect(() => {
72
+ if (open) {
73
+ loadAgents()
74
+ loadSettings()
75
+ setAiPrompt('')
76
+ setGenerating(false)
77
+ setGenerated(false)
78
+ setGenError('')
79
+ if (editing) {
80
+ setTitle(editing.title)
81
+ setDescription(editing.description)
82
+ setAgentId(editing.agentId)
83
+ setImages(editing.images || [])
84
+ setCwd(editing.cwd || '')
85
+ setFile(editing.file || null)
86
+ } else {
87
+ setTitle('')
88
+ setDescription('')
89
+ setAgentId(orchestrators[0]?.id || '')
90
+ setImages([])
91
+ setCwd('')
92
+ setFile(null)
93
+ }
94
+ }
95
+ }, [open, editingId])
96
+
97
+ // Update default agent when orchestrators load (only if no agent selected yet)
98
+ useEffect(() => {
99
+ if (open && !editing && !agentId && orchestrators.length) {
100
+ setAgentId(orchestrators[0].id)
101
+ }
102
+ }, [open, editing, agentId, orchestrators.length, agents])
103
+
104
+ const onClose = () => {
105
+ setOpen(false)
106
+ setEditingId(null)
107
+ }
108
+
109
+ const handleSave = async () => {
110
+ const payload: Partial<BoardTask> & { title: string; description: string; agentId: string } = { title: title.trim() || 'Untitled Task', description, agentId, images, cwd: cwd || undefined, file: file || undefined }
111
+ if (editing) {
112
+ await updateTask(editing.id, payload)
113
+ } else {
114
+ await createTask(payload)
115
+ }
116
+ await loadTasks()
117
+ onClose()
118
+ }
119
+
120
+ const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
121
+ const file = e.target.files?.[0]
122
+ if (!file) return
123
+ setUploading(true)
124
+ try {
125
+ const res = await fetch('/api/upload', {
126
+ method: 'POST',
127
+ headers: { 'x-filename': file.name },
128
+ body: await file.arrayBuffer(),
129
+ })
130
+ const data = await res.json()
131
+ if (data.url) setImages((prev) => [...prev, data.url])
132
+ } catch { /* ignore */ }
133
+ setUploading(false)
134
+ e.target.value = ''
135
+ }
136
+
137
+ const handleArchive = async () => {
138
+ if (editing) {
139
+ await archiveTask(editing.id)
140
+ await loadTasks()
141
+ onClose()
142
+ }
143
+ }
144
+
145
+ const handleUnarchive = async () => {
146
+ if (editing) {
147
+ await unarchiveTask(editing.id)
148
+ await loadTasks()
149
+ onClose()
150
+ }
151
+ }
152
+
153
+ const handleQueue = async () => {
154
+ if (editing && editing.status === 'backlog') {
155
+ await updateTask(editing.id, { status: 'queued' })
156
+ await loadTasks()
157
+ onClose()
158
+ }
159
+ }
160
+
161
+ const handleAddComment = async () => {
162
+ if (!editing || !commentText.trim()) return
163
+ const c: TaskComment = {
164
+ id: crypto.randomUUID().slice(0, 8),
165
+ author: 'You',
166
+ text: commentText.trim(),
167
+ createdAt: Date.now(),
168
+ }
169
+ // Use atomic append to avoid race conditions with queue-added comments
170
+ await updateTask(editing.id, { appendComment: c } as Partial<BoardTask> & { appendComment: TaskComment })
171
+ await loadTasks()
172
+ setCommentText('')
173
+ }
174
+
175
+ 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"
176
+
177
+ return (
178
+ <BottomSheet open={open} onClose={onClose}>
179
+ <div className="mb-10">
180
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
181
+ {editing ? 'Edit Task' : 'New Task'}
182
+ </h2>
183
+ <p className="text-[14px] text-text-3">
184
+ {editing ? `Status: ${editing.status}` : 'Create a task and assign an orchestrator'}
185
+ </p>
186
+ </div>
187
+
188
+ {/* AI Generation */}
189
+ {!editing && <AiGenBlock
190
+ aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
191
+ generating={generating} generated={generated} genError={genError}
192
+ onGenerate={handleGenerate} appSettings={appSettings}
193
+ placeholder='Describe the task, e.g. "Audit all pages on example.com for SEO issues and broken links"'
194
+ />}
195
+
196
+ <div className="mb-8">
197
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
198
+ <input
199
+ type="text"
200
+ value={title}
201
+ onChange={(e) => setTitle(e.target.value)}
202
+ placeholder="e.g. Run full site audit"
203
+ className={inputClass}
204
+ style={{ fontFamily: 'inherit' }}
205
+ />
206
+ </div>
207
+
208
+ <div className="mb-8">
209
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
210
+ <textarea
211
+ value={description}
212
+ onChange={(e) => setDescription(e.target.value)}
213
+ placeholder="Detailed task instructions for the orchestrator..."
214
+ rows={4}
215
+ className={`${inputClass} resize-y min-h-[100px]`}
216
+ style={{ fontFamily: 'inherit' }}
217
+ />
218
+ </div>
219
+
220
+ {/* Images */}
221
+ <div className="mb-8">
222
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
223
+ Images <span className="normal-case tracking-normal font-normal text-text-3">(optional — reference designs, mockups, etc.)</span>
224
+ </label>
225
+ {images.length > 0 && (
226
+ <div className="flex gap-2 flex-wrap mb-3">
227
+ {images.map((url, i) => (
228
+ <div key={i} className="relative group">
229
+ <img src={url} alt="" className="w-20 h-20 rounded-[10px] object-cover border border-white/[0.08]" />
230
+ <button
231
+ onClick={() => setImages((prev) => prev.filter((_, idx) => idx !== i))}
232
+ className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center text-[11px] font-700 cursor-pointer
233
+ opacity-0 group-hover:opacity-100 transition-opacity border-none"
234
+ >
235
+ x
236
+ </button>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ )}
241
+ <label className={`inline-flex items-center gap-2 px-4 py-2.5 rounded-[12px] border border-white/[0.06] bg-surface text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-colors ${uploading ? 'opacity-50 pointer-events-none' : ''}`}>
242
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
243
+ <rect x="3" y="3" width="18" height="18" rx="2" />
244
+ <circle cx="8.5" cy="8.5" r="1.5" />
245
+ <polyline points="21 15 16 10 5 21" />
246
+ </svg>
247
+ {uploading ? 'Uploading...' : 'Add Image'}
248
+ <input type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
249
+ </label>
250
+ </div>
251
+
252
+ <div className="mb-8">
253
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Orchestrator</label>
254
+ {orchestrators.length > 0 ? (
255
+ <div className="flex flex-wrap gap-2">
256
+ {orchestrators.map((p) => (
257
+ <button
258
+ key={p.id}
259
+ onClick={() => setAgentId(p.id)}
260
+ className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border
261
+ ${agentId === p.id
262
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
263
+ : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
264
+ style={{ fontFamily: 'inherit' }}
265
+ >
266
+ {p.name}
267
+ </button>
268
+ ))}
269
+ </div>
270
+ ) : (
271
+ <p className="text-[13px] text-text-3">No orchestrator agents configured. Create one in Agents first.</p>
272
+ )}
273
+ </div>
274
+
275
+ {/* Directory (optional) */}
276
+ <div className="mb-8">
277
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
278
+ Directory <span className="normal-case tracking-normal font-normal text-text-3">(optional — project to work in)</span>
279
+ </label>
280
+ <DirBrowser
281
+ value={cwd || null}
282
+ file={file}
283
+ onChange={(dir, f) => {
284
+ setCwd(dir)
285
+ setFile(f ?? null)
286
+ if (!title) {
287
+ const dirName = dir.split('/').pop() || ''
288
+ setTitle(dirName)
289
+ }
290
+ }}
291
+ onClear={() => { setCwd(''); setFile(null) }}
292
+ />
293
+ </div>
294
+
295
+ {editing?.result && (
296
+ <div className="mb-8">
297
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Result</label>
298
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 whitespace-pre-wrap max-h-[200px] overflow-y-auto">
299
+ {editing.result}
300
+ </div>
301
+ </div>
302
+ )}
303
+
304
+ {editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
305
+ <div className="mb-8">
306
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">CLI Sessions</label>
307
+ <div className="flex flex-wrap gap-2">
308
+ {editing.claudeResumeId && (
309
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
310
+ <span className="text-[11px] font-600 text-amber-400">Claude</span>
311
+ <code className="text-[11px] text-text-3 font-mono">{editing.claudeResumeId}</code>
312
+ </div>
313
+ )}
314
+ {editing.codexResumeId && (
315
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
316
+ <span className="text-[11px] font-600 text-emerald-400">Codex</span>
317
+ <code className="text-[11px] text-text-3 font-mono">{editing.codexResumeId}</code>
318
+ </div>
319
+ )}
320
+ {editing.opencodeResumeId && (
321
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
322
+ <span className="text-[11px] font-600 text-sky-400">OpenCode</span>
323
+ <code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
324
+ </div>
325
+ )}
326
+ {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId) && editing.cliResumeId && (
327
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
328
+ <span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
329
+ <code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
330
+ </div>
331
+ )}
332
+ </div>
333
+ </div>
334
+ )}
335
+
336
+ {editing?.error && (
337
+ <div className="mb-8">
338
+ <label className="block font-display text-[12px] font-600 text-red-400 uppercase tracking-[0.08em] mb-3">Error</label>
339
+ <div className="p-4 rounded-[14px] border border-red-500/10 bg-red-500/[0.03] text-[13px] text-red-400/80 whitespace-pre-wrap">
340
+ {editing.error}
341
+ </div>
342
+ </div>
343
+ )}
344
+
345
+ {/* Comments */}
346
+ {editing && (
347
+ <div className="mb-8">
348
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
349
+ Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}
350
+ </label>
351
+
352
+ {editing.comments && editing.comments.length > 0 && (
353
+ <div className="space-y-3 mb-4 max-h-[300px] overflow-y-auto">
354
+ {editing.comments.map((c) => (
355
+ <div key={c.id} className="p-3.5 rounded-[12px] border border-white/[0.06] bg-surface">
356
+ <div className="flex items-center gap-2 mb-1.5">
357
+ <span className={`text-[12px] font-600 ${c.agentId ? 'text-accent-bright' : 'text-text-2'}`}>
358
+ {c.author}
359
+ </span>
360
+ <span className="text-[10px] text-text-3/50 font-mono">{fmtTime(c.createdAt)}</span>
361
+ </div>
362
+ <p className="text-[13px] text-text-2 leading-[1.5] whitespace-pre-wrap">{c.text}</p>
363
+ </div>
364
+ ))}
365
+ </div>
366
+ )}
367
+
368
+ <div className="flex gap-2">
369
+ <input
370
+ type="text"
371
+ value={commentText}
372
+ onChange={(e) => setCommentText(e.target.value)}
373
+ placeholder="Add a comment..."
374
+ className={`${inputClass} flex-1`}
375
+ style={{ fontFamily: 'inherit' }}
376
+ onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment() } }}
377
+ />
378
+ <button
379
+ onClick={handleAddComment}
380
+ disabled={!commentText.trim()}
381
+ className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600 cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
382
+ style={{ fontFamily: 'inherit' }}
383
+ >
384
+ Post
385
+ </button>
386
+ </div>
387
+ </div>
388
+ )}
389
+
390
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
391
+ {editing && editing.status !== 'archived' && (
392
+ <button onClick={handleArchive} className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[15px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all" style={{ fontFamily: 'inherit' }}>
393
+ Archive
394
+ </button>
395
+ )}
396
+ {editing && editing.status === 'archived' && (
397
+ <button onClick={handleUnarchive} className="py-3.5 px-6 rounded-[14px] border border-accent-bright/20 bg-transparent text-accent-bright text-[15px] font-600 cursor-pointer hover:bg-accent-bright/10 transition-all" style={{ fontFamily: 'inherit' }}>
398
+ Unarchive
399
+ </button>
400
+ )}
401
+ {editing && editing.status === 'backlog' && (
402
+ <button onClick={handleQueue} className="py-3.5 px-6 rounded-[14px] border border-amber-500/20 bg-transparent text-amber-400 text-[15px] font-600 cursor-pointer hover:bg-amber-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
403
+ Queue
404
+ </button>
405
+ )}
406
+ <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' }}>
407
+ Cancel
408
+ </button>
409
+ <button onClick={handleSave} disabled={!title.trim() || !agentId} 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' }}>
410
+ {editing ? 'Save' : 'Create'}
411
+ </button>
412
+ </div>
413
+ </BottomSheet>
414
+ )
415
+ }
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Avatar as AvatarPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
13
+ size?: "default" | "sm" | "lg"
14
+ }) {
15
+ return (
16
+ <AvatarPrimitive.Root
17
+ data-slot="avatar"
18
+ data-size={size}
19
+ className={cn(
20
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AvatarImage({
29
+ className,
30
+ ...props
31
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
32
+ return (
33
+ <AvatarPrimitive.Image
34
+ data-slot="avatar-image"
35
+ className={cn("aspect-square size-full", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function AvatarFallback({
42
+ className,
43
+ ...props
44
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
45
+ return (
46
+ <AvatarPrimitive.Fallback
47
+ data-slot="avatar-fallback"
48
+ className={cn(
49
+ "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
50
+ className
51
+ )}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
58
+ return (
59
+ <span
60
+ data-slot="avatar-badge"
61
+ className={cn(
62
+ "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none",
63
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
64
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
65
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
66
+ className
67
+ )}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="avatar-group"
77
+ className={cn(
78
+ "*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function AvatarGroupCount({
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<"div">) {
90
+ return (
91
+ <div
92
+ data-slot="avatar-group-count"
93
+ className={cn(
94
+ "bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
95
+ className
96
+ )}
97
+ {...props}
98
+ />
99
+ )
100
+ }
101
+
102
+ export {
103
+ Avatar,
104
+ AvatarImage,
105
+ AvatarFallback,
106
+ AvatarBadge,
107
+ AvatarGroup,
108
+ AvatarGroupCount,
109
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15
+ destructive:
16
+ "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
17
+ outline:
18
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ },
26
+ }
27
+ )
28
+
29
+ function Badge({
30
+ className,
31
+ variant = "default",
32
+ asChild = false,
33
+ ...props
34
+ }: React.ComponentProps<"span"> &
35
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
36
+ const Comp = asChild ? Slot.Root : "span"
37
+
38
+ return (
39
+ <Comp
40
+ data-slot="badge"
41
+ data-variant={variant}
42
+ className={cn(badgeVariants({ variant }), className)}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ export { Badge, badgeVariants }
@@ -0,0 +1,64 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
30
+ "icon-sm": "size-8",
31
+ "icon-lg": "size-10",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ size: "default",
37
+ },
38
+ }
39
+ )
40
+
41
+ function Button({
42
+ className,
43
+ variant = "default",
44
+ size = "default",
45
+ asChild = false,
46
+ ...props
47
+ }: React.ComponentProps<"button"> &
48
+ VariantProps<typeof buttonVariants> & {
49
+ asChild?: boolean
50
+ }) {
51
+ const Comp = asChild ? Slot.Root : "button"
52
+
53
+ return (
54
+ <Comp
55
+ data-slot="button"
56
+ data-variant={variant}
57
+ data-size={size}
58
+ className={cn(buttonVariants({ variant, size, className }))}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ export { Button, buttonVariants }