@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,183 @@
1
+ 'use client'
2
+
3
+ import {
4
+ DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
5
+ DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
6
+ DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
7
+ DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
8
+ DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
9
+ DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
10
+ DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
11
+ DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
12
+ } from '@/lib/runtime-loop'
13
+ import type { LoopMode } from '@/types'
14
+ import type { SettingsSectionProps } from './types'
15
+
16
+ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
17
+ const loopMode: LoopMode = appSettings.loopMode === 'ongoing' ? 'ongoing' : 'bounded'
18
+
19
+ return (
20
+ <div className="mb-10">
21
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
22
+ Runtime &amp; Loop Controls
23
+ </h3>
24
+ <p className="text-[12px] text-text-3 mb-5">
25
+ Choose bounded or ongoing agent loops and set safety guards for task execution.
26
+ </p>
27
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
28
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Loop Mode</label>
29
+ <div className="grid grid-cols-2 gap-2 mb-5">
30
+ {([
31
+ { id: 'bounded' as const, name: 'Bounded' },
32
+ { id: 'ongoing' as const, name: 'Ongoing' },
33
+ ]).map((mode) => (
34
+ <button
35
+ key={mode.id}
36
+ onClick={() => patchSettings({ loopMode: mode.id })}
37
+ className={`py-3 px-3 rounded-[12px] text-center cursor-pointer transition-all text-[13px] font-600 border
38
+ ${loopMode === mode.id
39
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
40
+ : 'bg-bg border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
41
+ style={{ fontFamily: 'inherit' }}
42
+ >
43
+ {mode.name}
44
+ </button>
45
+ ))}
46
+ </div>
47
+
48
+ {loopMode === 'bounded' ? (
49
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
50
+ <div>
51
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Agent Steps</label>
52
+ <input
53
+ type="number"
54
+ min={1}
55
+ max={200}
56
+ value={appSettings.agentLoopRecursionLimit ?? DEFAULT_AGENT_LOOP_RECURSION_LIMIT}
57
+ onChange={(e) => {
58
+ const n = Number.parseInt(e.target.value, 10)
59
+ patchSettings({ agentLoopRecursionLimit: Number.isFinite(n) ? n : DEFAULT_AGENT_LOOP_RECURSION_LIMIT })
60
+ }}
61
+ className={inputClass}
62
+ style={{ fontFamily: 'inherit' }}
63
+ />
64
+ </div>
65
+ <div>
66
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Orchestrator Steps</label>
67
+ <input
68
+ type="number"
69
+ min={1}
70
+ max={300}
71
+ value={appSettings.orchestratorLoopRecursionLimit ?? DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT}
72
+ onChange={(e) => {
73
+ const n = Number.parseInt(e.target.value, 10)
74
+ patchSettings({ orchestratorLoopRecursionLimit: Number.isFinite(n) ? n : DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT })
75
+ }}
76
+ className={inputClass}
77
+ style={{ fontFamily: 'inherit' }}
78
+ />
79
+ </div>
80
+ <div>
81
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Legacy Turns</label>
82
+ <input
83
+ type="number"
84
+ min={1}
85
+ max={300}
86
+ value={appSettings.legacyOrchestratorMaxTurns ?? DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS}
87
+ onChange={(e) => {
88
+ const n = Number.parseInt(e.target.value, 10)
89
+ patchSettings({ legacyOrchestratorMaxTurns: Number.isFinite(n) ? n : DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS })
90
+ }}
91
+ className={inputClass}
92
+ style={{ fontFamily: 'inherit' }}
93
+ />
94
+ </div>
95
+ </div>
96
+ ) : (
97
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
98
+ <div>
99
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Max Steps (Safety Cap)</label>
100
+ <input
101
+ type="number"
102
+ min={10}
103
+ max={5000}
104
+ value={appSettings.ongoingLoopMaxIterations ?? DEFAULT_ONGOING_LOOP_MAX_ITERATIONS}
105
+ onChange={(e) => {
106
+ const n = Number.parseInt(e.target.value, 10)
107
+ patchSettings({ ongoingLoopMaxIterations: Number.isFinite(n) ? n : DEFAULT_ONGOING_LOOP_MAX_ITERATIONS })
108
+ }}
109
+ className={inputClass}
110
+ style={{ fontFamily: 'inherit' }}
111
+ />
112
+ </div>
113
+ <div>
114
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Max Runtime (Minutes)</label>
115
+ <input
116
+ type="number"
117
+ min={0}
118
+ max={1440}
119
+ value={appSettings.ongoingLoopMaxRuntimeMinutes ?? DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES}
120
+ onChange={(e) => {
121
+ const n = Number.parseInt(e.target.value, 10)
122
+ patchSettings({ ongoingLoopMaxRuntimeMinutes: Number.isFinite(n) ? n : DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES })
123
+ }}
124
+ className={inputClass}
125
+ style={{ fontFamily: 'inherit' }}
126
+ />
127
+ <p className="text-[11px] text-text-3/60 mt-2">Set to 0 to disable the runtime guard.</p>
128
+ </div>
129
+ </div>
130
+ )}
131
+
132
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Execution Timeouts (Seconds)</label>
133
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
134
+ <div>
135
+ <label className="block text-[11px] text-text-3 mb-2">Shell</label>
136
+ <input
137
+ type="number"
138
+ min={1}
139
+ max={600}
140
+ value={appSettings.shellCommandTimeoutSec ?? DEFAULT_SHELL_COMMAND_TIMEOUT_SEC}
141
+ onChange={(e) => {
142
+ const n = Number.parseInt(e.target.value, 10)
143
+ patchSettings({ shellCommandTimeoutSec: Number.isFinite(n) ? n : DEFAULT_SHELL_COMMAND_TIMEOUT_SEC })
144
+ }}
145
+ className={inputClass}
146
+ style={{ fontFamily: 'inherit' }}
147
+ />
148
+ </div>
149
+ <div>
150
+ <label className="block text-[11px] text-text-3 mb-2">Claude Code Tool</label>
151
+ <input
152
+ type="number"
153
+ min={5}
154
+ max={7200}
155
+ value={appSettings.claudeCodeTimeoutSec ?? DEFAULT_CLAUDE_CODE_TIMEOUT_SEC}
156
+ onChange={(e) => {
157
+ const n = Number.parseInt(e.target.value, 10)
158
+ patchSettings({ claudeCodeTimeoutSec: Number.isFinite(n) ? n : DEFAULT_CLAUDE_CODE_TIMEOUT_SEC })
159
+ }}
160
+ className={inputClass}
161
+ style={{ fontFamily: 'inherit' }}
162
+ />
163
+ </div>
164
+ <div>
165
+ <label className="block text-[11px] text-text-3 mb-2">CLI Provider Process</label>
166
+ <input
167
+ type="number"
168
+ min={10}
169
+ max={7200}
170
+ value={appSettings.cliProcessTimeoutSec ?? DEFAULT_CLI_PROCESS_TIMEOUT_SEC}
171
+ onChange={(e) => {
172
+ const n = Number.parseInt(e.target.value, 10)
173
+ patchSettings({ cliProcessTimeoutSec: Number.isFinite(n) ? n : DEFAULT_CLI_PROCESS_TIMEOUT_SEC })
174
+ }}
175
+ className={inputClass}
176
+ style={{ fontFamily: 'inherit' }}
177
+ />
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )
183
+ }
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
6
+ import { toast } from 'sonner'
7
+ import type { SettingsSectionProps } from './types'
8
+
9
+ export function SecretsSection({ appSettings, inputClass }: SettingsSectionProps) {
10
+ const secrets = useAppStore((s) => s.secrets)
11
+ const loadSecrets = useAppStore((s) => s.loadSecrets)
12
+ const agents = useAppStore((s) => s.agents)
13
+
14
+ const [addingSecret, setAddingSecret] = useState(false)
15
+ const [secretName, setSecretName] = useState('')
16
+ const [secretService, setSecretService] = useState('')
17
+ const [secretValue, setSecretValue] = useState('')
18
+ const [secretScope, setSecretScope] = useState<'global' | 'agent'>('global')
19
+ const [secretAgentIds, setSecretAgentIds] = useState<string[]>([])
20
+ const [deletingSecret, setDeletingSecret] = useState<string | null>(null)
21
+
22
+ const handleAddSecret = async () => {
23
+ if (!secretName.trim() || !secretValue.trim()) return
24
+ try {
25
+ await api('POST', '/secrets', {
26
+ name: secretName,
27
+ service: secretService || 'custom',
28
+ value: secretValue,
29
+ scope: secretScope,
30
+ agentIds: secretScope === 'agent' ? secretAgentIds : [],
31
+ })
32
+ await loadSecrets()
33
+ setAddingSecret(false)
34
+ setSecretName('')
35
+ setSecretService('')
36
+ setSecretValue('')
37
+ setSecretScope('global')
38
+ setSecretAgentIds([])
39
+ toast.success('Credential added')
40
+ } catch (err: unknown) {
41
+ toast.error(err instanceof Error ? err.message : 'Failed to add credential')
42
+ }
43
+ }
44
+
45
+ const handleDeleteSecret = async (id: string) => {
46
+ try {
47
+ await api('DELETE', `/secrets/${id}`)
48
+ await loadSecrets()
49
+ setDeletingSecret(null)
50
+ toast.success('Credential deleted')
51
+ } catch (err: unknown) {
52
+ toast.error(err instanceof Error ? err.message : 'Failed to delete credential')
53
+ }
54
+ }
55
+
56
+ const orchestrators = Object.values(agents).filter((p) => p.isOrchestrator)
57
+ const secretList = Object.entries(secrets).map(([rowId, secret]) => ({ ...secret, rowId }))
58
+
59
+ return (
60
+ <div className="mb-10">
61
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
62
+ Service Credentials
63
+ </h3>
64
+ <p className="text-[12px] text-text-3 mb-5">
65
+ Credentials for external services (Gmail, APIs, etc.) that orchestrators can use during task execution.
66
+ </p>
67
+
68
+ {secretList.length > 0 && (
69
+ <div className="space-y-2.5 mb-4">
70
+ {secretList.map((secret) => (
71
+ <div key={secret.rowId} className="flex items-center gap-3 py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
72
+ <div className="flex-1 min-w-0">
73
+ <div className="text-[14px] font-600 text-text truncate">{secret.name}</div>
74
+ <div className="flex items-center gap-2 mt-0.5">
75
+ <span className="text-[11px] font-mono text-text-3">{secret.service}</span>
76
+ <span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-[4px] ${
77
+ secret.scope === 'global'
78
+ ? 'bg-emerald-500/10 text-emerald-400'
79
+ : 'bg-amber-500/10 text-amber-400'
80
+ }`}>
81
+ {secret.scope === 'global' ? 'All orchestrators' : `${secret.agentIds.length} orchestrator(s)`}
82
+ </span>
83
+ </div>
84
+ </div>
85
+ {deletingSecret === secret.rowId ? (
86
+ <div className="flex gap-2">
87
+ <button onClick={() => setDeletingSecret(null)} className="px-3 py-1.5 text-[13px] font-600 bg-transparent border-none text-text-3 cursor-pointer hover:text-text-2 transition-colors" style={{ fontFamily: 'inherit' }}>Keep</button>
88
+ <button onClick={() => handleDeleteSecret(secret.rowId)} className="px-3 py-1.5 text-[13px] font-600 bg-danger text-white border-none cursor-pointer rounded-[8px] transition-colors hover:brightness-110" style={{ fontFamily: 'inherit' }}>Delete</button>
89
+ </div>
90
+ ) : (
91
+ <button onClick={() => setDeletingSecret(secret.rowId)} className="px-3 py-1.5 text-[13px] font-500 bg-transparent border-none text-text-3 cursor-pointer hover:text-danger transition-colors" style={{ fontFamily: 'inherit' }}>Remove</button>
92
+ )}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ )}
97
+
98
+ {addingSecret ? (
99
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06] space-y-4">
100
+ <div className="font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em]">New Secret</div>
101
+ <input type="text" value={secretName} onChange={(e) => setSecretName(e.target.value)} placeholder="Name (e.g. My Gmail)" className={inputClass} style={{ fontFamily: 'inherit' }} />
102
+ <input type="text" value={secretService} onChange={(e) => setSecretService(e.target.value)} placeholder="Service (e.g. gmail, ahrefs, custom)" className={inputClass} style={{ fontFamily: 'inherit' }} />
103
+ <input type="password" value={secretValue} onChange={(e) => setSecretValue(e.target.value)} placeholder="Value (API key, password, token...)" className={inputClass} style={{ fontFamily: 'inherit' }} />
104
+
105
+ <div>
106
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Scope</label>
107
+ <div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
108
+ {(['global', 'agent'] as const).map((s) => (
109
+ <button key={s} onClick={() => setSecretScope(s)} className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 capitalize ${secretScope === s ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`} style={{ fontFamily: 'inherit' }}>{s === 'global' ? 'All Orchestrators' : 'Specific'}</button>
110
+ ))}
111
+ </div>
112
+ </div>
113
+
114
+ {secretScope === 'agent' && orchestrators.length > 0 && (
115
+ <div className="flex flex-wrap gap-2">
116
+ {orchestrators.map((p) => (
117
+ <button key={p.id} onClick={() => setSecretAgentIds((prev) => prev.includes(p.id) ? prev.filter((x) => x !== p.id) : [...prev, p.id])} className={`px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border ${secretAgentIds.includes(p.id) ? 'bg-accent-soft border-accent-bright/25 text-accent-bright' : 'bg-bg border-white/[0.06] text-text-3 hover:text-text-2'}`} style={{ fontFamily: 'inherit' }}>{p.name}</button>
118
+ ))}
119
+ </div>
120
+ )}
121
+
122
+ <div className="flex gap-3 pt-2">
123
+ <button onClick={() => setAddingSecret(false)} 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" style={{ fontFamily: 'inherit' }}>Cancel</button>
124
+ <button onClick={handleAddSecret} disabled={!secretName.trim() || !secretValue.trim()} 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" style={{ fontFamily: 'inherit' }}>Save Secret</button>
125
+ </div>
126
+ </div>
127
+ ) : (
128
+ <button onClick={() => setAddingSecret(true)} className="w-full py-3 rounded-[12px] border border-dashed border-white/[0.1] bg-transparent text-text-3 text-[13px] font-600 cursor-pointer hover:border-accent-bright/30 hover:text-accent-bright hover:bg-accent-soft transition-all duration-200" style={{ fontFamily: 'inherit' }}>+ Add Service Credential</button>
129
+ )}
130
+ </div>
131
+ )
132
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ import type { SettingsSectionProps } from './types'
4
+
5
+ export function UserPreferencesSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ return (
7
+ <div className="mb-10">
8
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
9
+ User Preferences
10
+ </h3>
11
+ <p className="text-[12px] text-text-3 mb-5">
12
+ Global instructions injected into ALL agent system prompts. Define your style, rules, and preferences.
13
+ </p>
14
+ <textarea
15
+ value={appSettings.userPrompt || ''}
16
+ onChange={(e) => patchSettings({ userPrompt: e.target.value })}
17
+ placeholder="e.g. Always respond concisely. Use TypeScript over JavaScript. Prefer functional patterns. My timezone is PST."
18
+ rows={4}
19
+ className={`${inputClass} resize-y min-h-[100px]`}
20
+ style={{ fontFamily: 'inherit' }}
21
+ />
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,53 @@
1
+ 'use client'
2
+
3
+ import type { SettingsSectionProps } from './types'
4
+
5
+ export function VoiceSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ return (
7
+ <div className="mb-10">
8
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
9
+ Voice
10
+ </h3>
11
+ <p className="text-[12px] text-text-3 mb-5">
12
+ Configure voice playback (TTS) and speech-to-text input.
13
+ </p>
14
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
15
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
16
+ <div>
17
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs API Key</label>
18
+ <input
19
+ type="password"
20
+ value={appSettings.elevenLabsApiKey || ''}
21
+ onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
22
+ placeholder="sk_..."
23
+ className={inputClass}
24
+ style={{ fontFamily: 'inherit' }}
25
+ />
26
+ </div>
27
+ <div>
28
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs Voice ID</label>
29
+ <input
30
+ type="text"
31
+ value={appSettings.elevenLabsVoiceId || ''}
32
+ onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
33
+ placeholder="JBFqnCBsd6RMkjVDRZzb"
34
+ className={inputClass}
35
+ style={{ fontFamily: 'inherit' }}
36
+ />
37
+ </div>
38
+ </div>
39
+ <div>
40
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Speech Recognition Language</label>
41
+ <input
42
+ type="text"
43
+ value={appSettings.speechRecognitionLang || ''}
44
+ onChange={(e) => patchSettings({ speechRecognitionLang: e.target.value || null })}
45
+ placeholder="en-US (blank = browser default)"
46
+ className={inputClass}
47
+ style={{ fontFamily: 'inherit' }}
48
+ />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { BottomSheet } from '../bottom-sheet'
6
+ import { inputClass } from './utils'
7
+ import { UserPreferencesSection } from './section-user-preferences'
8
+ import { OrchestratorSection } from './section-orchestrator'
9
+ import { RuntimeLoopSection } from './section-runtime-loop'
10
+ import { CapabilityPolicySection } from './section-capability-policy'
11
+ import { VoiceSection } from './section-voice'
12
+ import { HeartbeatSection } from './section-heartbeat'
13
+ import { EmbeddingSection } from './section-embedding'
14
+ import { MemorySection } from './section-memory'
15
+ import { SecretsSection } from './section-secrets'
16
+ import { ProvidersSection } from './section-providers'
17
+ import { PluginManager } from './plugin-manager'
18
+
19
+ export function SettingsSheet() {
20
+ const open = useAppStore((s) => s.settingsOpen)
21
+ const setOpen = useAppStore((s) => s.setSettingsOpen)
22
+ const loadProviders = useAppStore((s) => s.loadProviders)
23
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
24
+ const appSettings = useAppStore((s) => s.appSettings)
25
+ const loadSettings = useAppStore((s) => s.loadSettings)
26
+ const updateSettings = useAppStore((s) => s.updateSettings)
27
+ const loadSecrets = useAppStore((s) => s.loadSecrets)
28
+ const loadAgents = useAppStore((s) => s.loadAgents)
29
+ const credentials = useAppStore((s) => s.credentials)
30
+
31
+ useEffect(() => {
32
+ if (open) {
33
+ loadProviders()
34
+ loadCredentials()
35
+ loadSettings()
36
+ loadSecrets()
37
+ loadAgents()
38
+ }
39
+ }, [open])
40
+
41
+ const credList = Object.values(credentials)
42
+ const patchSettings = updateSettings
43
+
44
+ return (
45
+ <BottomSheet open={open} onClose={() => setOpen(false)} wide>
46
+ {/* Header */}
47
+ <div className="mb-10">
48
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">Settings</h2>
49
+ <p className="text-[14px] text-text-3">Manage providers, API keys & orchestrator engine</p>
50
+ </div>
51
+
52
+ <UserPreferencesSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
53
+ <OrchestratorSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
54
+ <RuntimeLoopSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
55
+ <CapabilityPolicySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
56
+ <VoiceSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
57
+ <HeartbeatSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
58
+ <EmbeddingSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} credList={credList} />
59
+ <MemorySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
60
+ <SecretsSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
61
+ <ProvidersSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
62
+
63
+ {/* Plugins */}
64
+ <div className="mb-10">
65
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
66
+ Plugins
67
+ </h3>
68
+ <p className="text-[12px] text-text-3 mb-5">
69
+ Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
70
+ <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
71
+ </p>
72
+ <PluginManager />
73
+ </div>
74
+
75
+ {/* Done */}
76
+ <div className="pt-2 border-t border-white/[0.04]">
77
+ <button
78
+ onClick={() => setOpen(false)}
79
+ className="w-full py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer
80
+ hover:bg-surface-2 transition-all duration-200"
81
+ style={{ fontFamily: 'inherit' }}
82
+ >
83
+ Done
84
+ </button>
85
+ </div>
86
+ </BottomSheet>
87
+ )
88
+ }
@@ -0,0 +1,7 @@
1
+ import type { AppSettings } from '@/types'
2
+
3
+ export interface SettingsSectionProps {
4
+ appSettings: AppSettings
5
+ patchSettings: (patch: Partial<AppSettings>) => void
6
+ inputClass: string
7
+ }
@@ -0,0 +1,13 @@
1
+ import { useCallback } from 'react'
2
+ import { useAppStore } from '@/stores/use-app-store'
3
+ import type { AppSettings } from '@/types'
4
+
5
+ export const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-bg text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
6
+
7
+ export function usePatchSettings() {
8
+ const updateSettings = useAppStore((s) => s.updateSettings)
9
+ return useCallback(
10
+ (patch: Partial<AppSettings>) => updateSettings(patch),
11
+ [updateSettings],
12
+ )
13
+ }
@@ -0,0 +1 @@
1
+ export { SettingsSheet } from './settings/settings-sheet'
@@ -0,0 +1,19 @@
1
+ 'use client'
2
+
3
+ interface Props {
4
+ className?: string
5
+ width?: string | number
6
+ height?: string | number
7
+ }
8
+
9
+ export function Skeleton({ className = '', width, height }: Props) {
10
+ return (
11
+ <div
12
+ className={`bg-white/[0.06] animate-pulse rounded ${className}`}
13
+ style={{
14
+ width: typeof width === 'number' ? `${width}px` : width,
15
+ height: typeof height === 'number' ? `${height}px` : height,
16
+ }}
17
+ />
18
+ )
19
+ }
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ interface Props {
4
+ inputTokens: number
5
+ outputTokens: number
6
+ totalTokens: number
7
+ estimatedCost: number
8
+ }
9
+
10
+ export function UsageBadge({ totalTokens, estimatedCost }: Props) {
11
+ if (!totalTokens) return null
12
+
13
+ const costStr = estimatedCost < 0.001
14
+ ? '<$0.001'
15
+ : `$${estimatedCost.toFixed(3)}`
16
+
17
+ const tokenStr = totalTokens >= 1000
18
+ ? `${(totalTokens / 1000).toFixed(1)}k`
19
+ : String(totalTokens)
20
+
21
+ return (
22
+ <span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-[7px] bg-white/[0.04] text-[10px] font-mono text-text-3/60">
23
+ <span>{tokenStr} tok</span>
24
+ <span className="text-text-3/60">·</span>
25
+ <span className="text-emerald-400/60">{costStr}</span>
26
+ </span>
27
+ )
28
+ }