@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,478 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { useChatStore } from '@/stores/use-chat-store'
6
+ import { createSession, createCredential } from '@/lib/sessions'
7
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
8
+ import { DirBrowser } from '@/components/shared/dir-browser'
9
+ import { TOOL_LABELS, TOOL_DESCRIPTIONS } from '@/components/chat/tool-call-bubble'
10
+ import type { ProviderType, SessionTool } from '@/types'
11
+
12
+ export function NewSessionSheet() {
13
+ const open = useAppStore((s) => s.newSessionOpen)
14
+ const setOpen = useAppStore((s) => s.setNewSessionOpen)
15
+
16
+ const [name, setName] = useState('')
17
+ const [selectedDir, setSelectedDir] = useState<string | null>(null)
18
+ const [selectedFile, setSelectedFile] = useState<string | null>(null)
19
+ const [provider, setProvider] = useState<ProviderType>('claude-cli')
20
+ const [model, setModel] = useState('')
21
+ const [credentialId, setCredentialId] = useState<string | null>(null)
22
+ const [endpoint, setEndpoint] = useState('http://localhost:11434')
23
+ const [addingKey, setAddingKey] = useState(false)
24
+ const [newKeyName, setNewKeyName] = useState('')
25
+ const [newKeyValue, setNewKeyValue] = useState('')
26
+ const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
27
+ const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null)
28
+ const [selectedTools, setSelectedTools] = useState<SessionTool[]>([])
29
+
30
+ const providers = useAppStore((s) => s.providers)
31
+ const loadProviders = useAppStore((s) => s.loadProviders)
32
+ const credentials = useAppStore((s) => s.credentials)
33
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
34
+ const agents = useAppStore((s) => s.agents)
35
+ const loadAgents = useAppStore((s) => s.loadAgents)
36
+ const currentUser = useAppStore((s) => s.currentUser)
37
+ const updateSessionInStore = useAppStore((s) => s.updateSessionInStore)
38
+ const setCurrentSession = useAppStore((s) => s.setCurrentSession)
39
+ const setMessages = useChatStore((s) => s.setMessages)
40
+
41
+ const currentProvider = providers.find((p) => p.id === provider)
42
+ const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
43
+
44
+ useEffect(() => {
45
+ if (open) {
46
+ loadProviders()
47
+ loadCredentials()
48
+ loadAgents()
49
+ setName('')
50
+ setSelectedDir(null)
51
+ setSelectedFile(null)
52
+ setProvider('claude-cli')
53
+ setModel('')
54
+ setCredentialId(null)
55
+ setEndpoint('http://localhost:11434')
56
+ setAddingKey(false)
57
+ setNewKeyName('')
58
+ setNewKeyValue('')
59
+ setOllamaMode('local')
60
+ // Auto-select last used agent, or default agent if no history
61
+ const agentsList = Object.values(agents)
62
+ const lastAgentId = typeof window !== 'undefined' ? localStorage.getItem('swarmclaw-last-agent') : null
63
+ const lastAgent = lastAgentId ? agentsList.find((a: any) => a.id === lastAgentId) : null
64
+ const defaultAgent = lastAgent || agentsList.find((a: any) => a.id === 'default') || agentsList[0]
65
+ if (defaultAgent) {
66
+ setSelectedAgentId((defaultAgent as any).id)
67
+ setProvider((defaultAgent as any).provider || 'claude-cli')
68
+ setModel((defaultAgent as any).model || '')
69
+ setCredentialId((defaultAgent as any).credentialId || null)
70
+ if ((defaultAgent as any).apiEndpoint) setEndpoint((defaultAgent as any).apiEndpoint)
71
+ } else {
72
+ setSelectedAgentId(null)
73
+ }
74
+ setSelectedTools([])
75
+ }
76
+ }, [open])
77
+
78
+ // Derive model, endpoint, and credential from provider + ollamaMode (consolidated)
79
+ useEffect(() => {
80
+ // Set model from provider defaults
81
+ if (currentProvider?.models.length) {
82
+ setModel(currentProvider.models[0])
83
+ }
84
+
85
+ // Reset ollama mode for non-ollama providers
86
+ if (provider !== 'ollama') {
87
+ setOllamaMode('local')
88
+ }
89
+
90
+ // Derive endpoint
91
+ if (provider === 'ollama') {
92
+ setEndpoint(ollamaMode === 'local' ? 'http://localhost:11434' : '')
93
+ } else if (currentProvider?.defaultEndpoint) {
94
+ setEndpoint(currentProvider.defaultEndpoint)
95
+ }
96
+
97
+ // Derive credential
98
+ const needsKey = currentProvider?.requiresApiKey || (provider === 'ollama' && ollamaMode === 'cloud')
99
+ if (needsKey && providerCredentials.length > 0) {
100
+ setCredentialId(providerCredentials[0].id)
101
+ } else {
102
+ setCredentialId(null)
103
+ }
104
+ }, [provider, providers, ollamaMode, providerCredentials.length])
105
+
106
+ const handleAddKey = async () => {
107
+ if (!newKeyValue.trim()) return
108
+ const cred = await createCredential(provider, newKeyName || `${provider} key`, newKeyValue)
109
+ await loadCredentials()
110
+ setCredentialId(cred.id)
111
+ setAddingKey(false)
112
+ setNewKeyName('')
113
+ setNewKeyValue('')
114
+ }
115
+
116
+ const onClose = () => setOpen(false)
117
+
118
+ const handleSelectAgent = (agentId: string | null) => {
119
+ setSelectedAgentId(agentId)
120
+ if (agentId && agents[agentId]) {
121
+ const p = agents[agentId]
122
+ setProvider(p.provider)
123
+ setModel(p.model)
124
+ setCredentialId(p.credentialId || null)
125
+ if (p.apiEndpoint) setEndpoint(p.apiEndpoint)
126
+ if (!name) setName(p.name)
127
+ }
128
+ }
129
+
130
+ const handleCreate = async () => {
131
+ const sessionName = name.trim() || 'New Session'
132
+ const cwd = selectedDir || ''
133
+ const resolvedCredentialId = currentProvider?.requiresApiKey
134
+ ? credentialId
135
+ : (currentProvider?.optionalApiKey && ollamaMode === 'cloud') ? credentialId : null
136
+ const agent = selectedAgentId ? agents[selectedAgentId] : null
137
+ const agentTools = agent?.tools || (selectedTools.length ? selectedTools : undefined)
138
+ const s = await createSession(
139
+ sessionName, cwd || (agent ? '~' : ''), currentUser!,
140
+ agent?.provider || provider,
141
+ agent?.model || model || undefined,
142
+ agent?.credentialId || resolvedCredentialId,
143
+ selectedAgentId ? (agent?.apiEndpoint || null) : (currentProvider?.requiresEndpoint ? endpoint : null),
144
+ selectedAgentId ? 'human' : undefined,
145
+ selectedAgentId,
146
+ agentTools || undefined,
147
+ selectedFile,
148
+ )
149
+ // Remember agent selection for next time
150
+ if (selectedAgentId) {
151
+ localStorage.setItem('swarmclaw-last-agent', selectedAgentId)
152
+ } else {
153
+ localStorage.removeItem('swarmclaw-last-agent')
154
+ }
155
+ updateSessionInStore(s)
156
+ setCurrentSession(s.id)
157
+ setMessages([])
158
+ onClose()
159
+ }
160
+
161
+ const canCreate = () => {
162
+ if (!selectedAgentId) {
163
+ if (currentProvider?.requiresApiKey && !credentialId) return false
164
+ if (provider === 'ollama' && ollamaMode === 'cloud' && !credentialId) return false
165
+ if (provider === 'claude-cli' && !selectedDir) return false
166
+ }
167
+ return true
168
+ }
169
+
170
+ 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"
171
+
172
+ return (
173
+ <BottomSheet open={open} onClose={onClose} wide>
174
+ {/* Header */}
175
+ <div className="mb-10">
176
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Session</h2>
177
+ <p className="text-[14px] text-text-3">Configure your AI session</p>
178
+ </div>
179
+
180
+ {/* Name */}
181
+ <div className="mb-8">
182
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
183
+ Session Name
184
+ </label>
185
+ <input
186
+ type="text"
187
+ value={name}
188
+ onChange={(e) => setName(e.target.value)}
189
+ placeholder="e.g. Fix login bug"
190
+ className={inputClass}
191
+ style={{ fontFamily: 'inherit' }}
192
+ />
193
+ </div>
194
+
195
+ {/* Agent (optional) */}
196
+ {Object.keys(agents).length > 0 && (
197
+ <div className="mb-8">
198
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
199
+ Agent <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
200
+ </label>
201
+ <select
202
+ value={selectedAgentId || ''}
203
+ onChange={(e) => handleSelectAgent(e.target.value || null)}
204
+ className={`${inputClass} appearance-none cursor-pointer`}
205
+ style={{ fontFamily: 'inherit' }}
206
+ >
207
+ <option value="">None — manual configuration</option>
208
+ {Object.values(agents).map((p) => (
209
+ <option key={p.id} value={p.id}>{p.name}{p.isOrchestrator ? ' (Orchestrator)' : ''}</option>
210
+ ))}
211
+ </select>
212
+ </div>
213
+ )}
214
+
215
+ {/* Provider/Model/Key/Endpoint — only show when no agent selected */}
216
+ {!selectedAgentId && (
217
+ <>
218
+ {/* Provider */}
219
+ <div className="mb-8">
220
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
221
+ Provider
222
+ </label>
223
+ <div className="grid grid-cols-3 gap-3">
224
+ {providers.map((p) => (
225
+ <button
226
+ key={p.id}
227
+ onClick={() => setProvider(p.id)}
228
+ className={`py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
229
+ active:scale-[0.97] text-[14px] font-600 border
230
+ ${provider === p.id
231
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
232
+ : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2 hover:border-white/[0.08]'}`}
233
+ style={{ fontFamily: 'inherit' }}
234
+ >
235
+ {p.name}
236
+ </button>
237
+ ))}
238
+ </div>
239
+ </div>
240
+
241
+ {/* Ollama Mode Toggle */}
242
+ {provider === 'ollama' && (
243
+ <div className="mb-8">
244
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
245
+ Mode
246
+ </label>
247
+ <div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
248
+ {(['local', 'cloud'] as const).map((mode) => (
249
+ <button
250
+ key={mode}
251
+ onClick={() => setOllamaMode(mode)}
252
+ className={`flex-1 py-3 rounded-[12px] text-center cursor-pointer transition-all duration-200
253
+ text-[14px] font-600 capitalize
254
+ ${ollamaMode === mode
255
+ ? 'bg-accent-soft text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
256
+ : 'bg-transparent text-text-3 hover:text-text-2'}`}
257
+ style={{ fontFamily: 'inherit' }}
258
+ >
259
+ {mode}
260
+ </button>
261
+ ))}
262
+ </div>
263
+ </div>
264
+ )}
265
+
266
+ {/* Model */}
267
+ {currentProvider && currentProvider.models.length > 0 && (
268
+ <div className="mb-8">
269
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
270
+ Model
271
+ </label>
272
+ <select
273
+ value={model}
274
+ onChange={(e) => setModel(e.target.value)}
275
+ className={`${inputClass} appearance-none cursor-pointer`}
276
+ style={{ fontFamily: 'inherit' }}
277
+ >
278
+ {currentProvider.models.map((m) => (
279
+ <option key={m} value={m}>{m}</option>
280
+ ))}
281
+ </select>
282
+ </div>
283
+ )}
284
+
285
+ {/* API Key */}
286
+ {(currentProvider?.requiresApiKey || (currentProvider?.optionalApiKey && ollamaMode === 'cloud')) && (
287
+ <div className="mb-8">
288
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
289
+ API Key
290
+ </label>
291
+ {providerCredentials.length > 0 && !addingKey ? (
292
+ <select
293
+ value={credentialId || ''}
294
+ onChange={(e) => {
295
+ if (e.target.value === '__add__') {
296
+ setAddingKey(true)
297
+ } else {
298
+ setCredentialId(e.target.value)
299
+ }
300
+ }}
301
+ className={`${inputClass} appearance-none cursor-pointer`}
302
+ style={{ fontFamily: 'inherit' }}
303
+ >
304
+ {providerCredentials.map((c) => (
305
+ <option key={c.id} value={c.id}>{c.name}</option>
306
+ ))}
307
+ <option value="__add__">+ Add new key...</option>
308
+ </select>
309
+ ) : (
310
+ <div className="space-y-3 p-5 rounded-[16px] bg-surface-2 border border-white/[0.06]">
311
+ <input
312
+ type="text"
313
+ value={newKeyName}
314
+ onChange={(e) => setNewKeyName(e.target.value)}
315
+ placeholder="Key name (optional)"
316
+ className={inputClass}
317
+ style={{ fontFamily: 'inherit' }}
318
+ />
319
+ <input
320
+ type="password"
321
+ value={newKeyValue}
322
+ onChange={(e) => setNewKeyValue(e.target.value)}
323
+ placeholder="sk-..."
324
+ className={inputClass}
325
+ style={{ fontFamily: 'inherit' }}
326
+ />
327
+ <div className="flex gap-3 pt-2">
328
+ {providerCredentials.length > 0 && (
329
+ <button
330
+ onClick={() => setAddingKey(false)}
331
+ className="flex-1 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"
332
+ style={{ fontFamily: 'inherit' }}
333
+ >
334
+ Cancel
335
+ </button>
336
+ )}
337
+ <button
338
+ onClick={handleAddKey}
339
+ disabled={!newKeyValue.trim()}
340
+ className="flex-1 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
341
+ style={{ fontFamily: 'inherit' }}
342
+ >
343
+ Save Key
344
+ </button>
345
+ </div>
346
+ </div>
347
+ )}
348
+ </div>
349
+ )}
350
+
351
+ {/* Endpoint — show for providers that require it (Ollama local, OpenClaw) */}
352
+ {currentProvider?.requiresEndpoint && (provider === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
353
+ <div className="mb-8">
354
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
355
+ {provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}
356
+ </label>
357
+ <input
358
+ type="text"
359
+ value={endpoint}
360
+ onChange={(e) => setEndpoint(e.target.value)}
361
+ placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'}
362
+ className={`${inputClass} font-mono text-[14px]`}
363
+ />
364
+ {provider === 'openclaw' && (
365
+ <p className="text-[11px] text-text-3/60 mt-2">
366
+ The /v1 endpoint of your remote OpenClaw instance
367
+ </p>
368
+ )}
369
+ </div>
370
+ )}
371
+ {/* Tools */}
372
+ {provider !== 'claude-cli' && (
373
+ <div className="mb-8">
374
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
375
+ Tools <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
376
+ </label>
377
+ <p className="text-[12px] text-text-3/60 mb-3">Allow this model to execute commands and access files in the session directory.</p>
378
+ <div className="flex flex-wrap gap-2.5">
379
+ {([
380
+ { id: 'shell' as SessionTool, label: 'Shell' },
381
+ { id: 'files' as SessionTool, label: 'Files' },
382
+ { id: 'edit_file' as SessionTool, label: 'Edit File' },
383
+ { id: 'web_search' as SessionTool, label: 'Web Search' },
384
+ { id: 'web_fetch' as SessionTool, label: 'Web Fetch' },
385
+ { id: 'claude_code' as SessionTool, label: 'Claude Code' },
386
+ { id: 'codex_cli' as SessionTool, label: 'Codex CLI' },
387
+ { id: 'opencode_cli' as SessionTool, label: 'OpenCode CLI' },
388
+ ]).map(({ id, label }) => {
389
+ const active = selectedTools.includes(id)
390
+ return (
391
+ <button
392
+ key={id}
393
+ onClick={() => {
394
+ setSelectedTools((prev) =>
395
+ active ? prev.filter((t) => t !== id) : [...prev, id],
396
+ )
397
+ }}
398
+ className={`px-4 py-2.5 rounded-[12px] text-[13px] font-600 border cursor-pointer transition-all duration-200 active:scale-[0.97]
399
+ ${active
400
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
401
+ : 'bg-surface border-white/[0.06] text-text-3 hover:bg-surface-2 hover:border-white/[0.08]'}`}
402
+ style={{ fontFamily: 'inherit' }}
403
+ >
404
+ {label}
405
+ </button>
406
+ )
407
+ })}
408
+ </div>
409
+ </div>
410
+ )}
411
+ </>
412
+ )}
413
+
414
+ {/* Summary when agent selected */}
415
+ {selectedAgentId && agents[selectedAgentId] && (
416
+ <div className="mb-8 px-4 py-3 rounded-[14px] bg-surface border border-white/[0.06]">
417
+ <span className="text-[13px] text-text-3">
418
+ Using <span className="text-text-2 font-600">{agents[selectedAgentId].provider}</span>
419
+ {' / '}
420
+ <span className="text-text-2 font-600">{agents[selectedAgentId].model}</span>
421
+ {agents[selectedAgentId].tools?.length ? (
422
+ <> + {agents[selectedAgentId].tools!.map((tool, i) => (
423
+ <span key={tool}>
424
+ {i > 0 && ', '}
425
+ <span className="text-sky-400/70 font-600 cursor-help" title={TOOL_DESCRIPTIONS[tool] || tool}>
426
+ {TOOL_LABELS[tool] || tool.replace(/_/g, ' ')}
427
+ </span>
428
+ </span>
429
+ ))}</>
430
+ ) : null}
431
+ </span>
432
+ </div>
433
+ )}
434
+
435
+ {/* Project */}
436
+ <div className="mb-10">
437
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
438
+ Directory {provider !== 'claude-cli' && <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>}
439
+ </label>
440
+ <DirBrowser
441
+ value={selectedDir}
442
+ file={selectedFile}
443
+ onChange={(dir, file) => {
444
+ setSelectedDir(dir)
445
+ setSelectedFile(file ?? null)
446
+ if (!name) {
447
+ const dirName = dir.split('/').pop() || ''
448
+ setName(dirName)
449
+ }
450
+ }}
451
+ onClear={() => { setSelectedDir(null); setSelectedFile(null) }}
452
+ />
453
+ </div>
454
+
455
+ {/* Actions */}
456
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
457
+ <button
458
+ onClick={onClose}
459
+ className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer
460
+ hover:bg-surface-2 transition-all duration-200"
461
+ style={{ fontFamily: 'inherit' }}
462
+ >
463
+ Cancel
464
+ </button>
465
+ <button
466
+ onClick={handleCreate}
467
+ disabled={!canCreate()}
468
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer
469
+ active:scale-[0.97] disabled:opacity-30 transition-all duration-200
470
+ shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
471
+ style={{ fontFamily: 'inherit' }}
472
+ >
473
+ Create Session
474
+ </button>
475
+ </div>
476
+ </BottomSheet>
477
+ )
478
+ }
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+
3
+ import type { Session } from '@/types'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { useChatStore } from '@/stores/use-chat-store'
7
+ import { ConnectorPlatformBadge, getSessionConnector } from '@/components/shared/connector-platform-icon'
8
+
9
+ function timeAgo(ts: number): string {
10
+ if (!ts) return ''
11
+ const s = Math.floor((Date.now() - ts) / 1000)
12
+ if (s < 60) return 'now'
13
+ if (s < 3600) return Math.floor(s / 60) + 'm'
14
+ if (s < 86400) return Math.floor(s / 3600) + 'h'
15
+ return Math.floor(s / 86400) + 'd'
16
+ }
17
+
18
+ function shortPath(p: string): string {
19
+ return (p || '').replace(/^\/Users\/\w+/, '~')
20
+ }
21
+
22
+ const PROVIDER_LABELS: Record<string, string> = {
23
+ 'claude-cli': '',
24
+ openai: 'GPT',
25
+ ollama: 'OLL',
26
+ anthropic: 'ANT',
27
+ }
28
+
29
+ interface Props {
30
+ session: Session
31
+ active?: boolean
32
+ onClick: () => void
33
+ }
34
+
35
+ export function SessionCard({ session, active, onClick }: Props) {
36
+ const removeSession = useAppStore((s) => s.removeSession)
37
+ const appSettings = useAppStore((s) => s.appSettings)
38
+ const agents = useAppStore((s) => s.agents)
39
+ const connectors = useAppStore((s) => s.connectors)
40
+ const streamingSessionId = useChatStore((s) => s.streamingSessionId)
41
+ const isTyping = streamingSessionId === session.id
42
+
43
+ const handleDelete = async (e: React.MouseEvent) => {
44
+ e.stopPropagation()
45
+ await api('DELETE', `/sessions/${session.id}`)
46
+ removeSession(session.id)
47
+ }
48
+
49
+ const last = session.messages?.length
50
+ ? session.messages[session.messages.length - 1]
51
+ : null
52
+ const preview = last
53
+ ? (last.role === 'user' ? 'You: ' : '') + last.text.slice(0, 70)
54
+ : 'No messages'
55
+ const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
56
+ const agent = session.agentId ? agents[session.agentId] : null
57
+ const connector = getSessionConnector(session, connectors)
58
+ const loopIsOngoing = appSettings.loopMode === 'ongoing'
59
+ const intervalRaw = session.heartbeatIntervalSec ?? agent?.heartbeatIntervalSec ?? appSettings.heartbeatIntervalSec ?? 120
60
+ const intervalNum = typeof intervalRaw === 'number' ? intervalRaw : Number.parseInt(String(intervalRaw), 10)
61
+ const intervalEnabled = Number.isFinite(intervalNum) ? intervalNum > 0 : true
62
+ const heartbeatEnabled =
63
+ loopIsOngoing
64
+ && (session.tools?.length ?? 0) > 0
65
+ && intervalEnabled
66
+ && (session.heartbeatEnabled === true || (session.heartbeatEnabled !== false && agent?.heartbeatEnabled !== false))
67
+
68
+ return (
69
+ <div
70
+ onClick={onClick}
71
+ className={`group/card relative py-3.5 px-4 cursor-pointer rounded-[14px]
72
+ transition-all duration-200 active:scale-[0.98]
73
+ ${active
74
+ ? 'bg-accent-soft border border-accent-bright/10'
75
+ : 'bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03]'}`}
76
+ >
77
+ {active && (
78
+ <div className="absolute left-0 top-3.5 bottom-3.5 w-[2.5px] rounded-full bg-accent-bright" />
79
+ )}
80
+ <div className="flex items-center gap-2.5">
81
+ {session.active && (
82
+ <span className="inline-block w-[6px] h-[6px] rounded-full bg-success shrink-0"
83
+ style={{ animation: 'pulse 2s ease-in-out infinite' }} />
84
+ )}
85
+ {heartbeatEnabled && (
86
+ <span
87
+ className="inline-flex items-center justify-center w-[10px] h-[10px] rounded-full bg-emerald-400/15 border border-emerald-400/30 shrink-0"
88
+ title="Heartbeat enabled"
89
+ >
90
+ <span className="w-[4px] h-[4px] rounded-full bg-emerald-400" />
91
+ </span>
92
+ )}
93
+ {connector && (
94
+ <ConnectorPlatformBadge
95
+ platform={connector.platform}
96
+ size={16}
97
+ iconSize={9}
98
+ roundedClassName="rounded-[5px]"
99
+ title={`${connector.name} (${connector.platform})`}
100
+ />
101
+ )}
102
+ <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{session.name}</span>
103
+ {session.sessionType === 'orchestrated' && (
104
+ <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px]">
105
+ AI
106
+ </span>
107
+ )}
108
+ {providerLabel && (
109
+ <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-text-3/70 bg-white/[0.03] px-2 py-0.5 rounded-[6px]">
110
+ {providerLabel}
111
+ </span>
112
+ )}
113
+ <span className="text-[11px] text-text-3/70 shrink-0 tabular-nums font-mono">
114
+ {timeAgo(session.lastActiveAt)}
115
+ </span>
116
+ <button
117
+ onClick={handleDelete}
118
+ className="shrink-0 opacity-0 group-hover/card:opacity-100 transition-opacity duration-150
119
+ text-text-3 hover:text-red-400 p-0.5 -mr-1 cursor-pointer bg-transparent border-none"
120
+ title="Delete session"
121
+ >
122
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
123
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
124
+ </svg>
125
+ </button>
126
+ </div>
127
+ <div className="text-[12px] text-text-3/70 font-mono mt-1.5 truncate">
128
+ {shortPath(session.cwd)}
129
+ </div>
130
+ {isTyping ? (
131
+ <div className="text-[13px] text-accent-bright/70 truncate mt-1 leading-relaxed flex items-center gap-1.5">
132
+ <span className="flex gap-0.5">
133
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
134
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
135
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
136
+ </span>
137
+ Typing...
138
+ </div>
139
+ ) : (
140
+ <div className="text-[13px] text-text-2/50 truncate mt-1 leading-relaxed">{preview}</div>
141
+ )}
142
+ </div>
143
+ )
144
+ }