@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -39,7 +39,7 @@ function DialogOverlay({
39
39
  <DialogPrimitive.Overlay
40
40
  data-slot="dialog-overlay"
41
41
  className={cn(
42
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/72 backdrop-blur-md",
43
43
  className
44
44
  )}
45
45
  {...props}
@@ -71,7 +71,7 @@ function DialogContent({
71
71
  {showCloseButton && (
72
72
  <DialogPrimitive.Close
73
73
  data-slot="dialog-close"
74
- className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
74
+ className="absolute top-4 right-4 inline-flex h-9 w-9 items-center justify-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] text-text-3 transition-all hover:bg-white/[0.06] hover:text-text-2 focus:outline-none focus:ring-2 focus:ring-accent-bright/30 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
75
75
  >
76
76
  <XIcon />
77
77
  <span className="sr-only">Close</span>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useState, useCallback, useMemo } from 'react'
4
4
  import {
5
5
  LineChart, Line, BarChart, Bar, Cell,
6
6
  XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
@@ -158,6 +158,14 @@ export function MetricsDashboard() {
158
158
  useWs('tasks', loadTaskMetrics, 15_000)
159
159
 
160
160
  const completionRate = computeCompletionRate(tasks)
161
+ const pendingApprovals = useMemo(
162
+ () => Object.values(tasks).filter((task) => !!task.pendingApproval).length,
163
+ [tasks],
164
+ )
165
+ const failedTasks = useMemo(
166
+ () => Object.values(tasks).filter((task) => task.status === 'failed').length,
167
+ [tasks],
168
+ )
161
169
 
162
170
  const timeSeriesFormatted = (data?.timeSeries ?? []).map((pt) => ({
163
171
  ...pt,
@@ -202,6 +210,57 @@ export function MetricsDashboard() {
202
210
  labelStyle: { color: 'var(--color-text-2)' },
203
211
  }
204
212
 
213
+ const insightCards = useMemo(() => {
214
+ const series = data?.timeSeries ?? []
215
+ const latest = series[series.length - 1]
216
+ const previous = series.slice(0, -1)
217
+ const baselineCost = previous.length > 0
218
+ ? previous.reduce((sum, point) => sum + point.cost, 0) / previous.length
219
+ : 0
220
+ const costDeltaPct = latest && baselineCost > 0
221
+ ? Math.round(((latest.cost - baselineCost) / baselineCost) * 100)
222
+ : null
223
+
224
+ const providerHealthEntries = Object.entries(data?.providerHealth ?? {})
225
+ .filter(([, health]) => health.totalRequests > 0)
226
+ .sort(([, a], [, b]) => b.errorRate - a.errorRate)
227
+ const riskiestProvider = providerHealthEntries[0]
228
+
229
+ const topCostAgent = Object.values(data?.byAgent ?? {})
230
+ .sort((a, b) => b.cost - a.cost)[0]
231
+
232
+ return [
233
+ {
234
+ label: 'Cost Pulse',
235
+ value: latest
236
+ ? `${formatCost(latest.cost)}${costDeltaPct !== null ? ` · ${costDeltaPct >= 0 ? '+' : ''}${costDeltaPct}%` : ''}`
237
+ : 'No recent spend',
238
+ hint: latest ? `Latest bucket vs ${previous.length > 0 ? 'range average' : 'current range'}` : 'Waiting for usage data',
239
+ tone: costDeltaPct !== null && costDeltaPct > 40 ? 'text-red-400' : 'text-text',
240
+ },
241
+ {
242
+ label: 'Provider Risk',
243
+ value: riskiestProvider
244
+ ? `${riskiestProvider[0]} · ${(riskiestProvider[1].errorRate * 100).toFixed(1)}%`
245
+ : 'No provider issues',
246
+ hint: riskiestProvider ? `${riskiestProvider[1].totalRequests} requests in range` : 'No provider health records yet',
247
+ tone: riskiestProvider && riskiestProvider[1].errorRate > 0.1 ? 'text-red-400' : 'text-emerald-400',
248
+ },
249
+ {
250
+ label: 'Top Spend Agent',
251
+ value: topCostAgent ? `${topCostAgent.name} · ${formatCost(topCostAgent.cost)}` : 'No agent activity',
252
+ hint: topCostAgent ? `${formatTokens(topCostAgent.tokens)} tokens` : 'No per-agent usage in range',
253
+ tone: 'text-sky-400',
254
+ },
255
+ {
256
+ label: 'Workflow Friction',
257
+ value: `${pendingApprovals} approvals · ${failedTasks} failed`,
258
+ hint: pendingApprovals + failedTasks > 0 ? 'Operational overhead from live work' : 'No obvious workflow friction',
259
+ tone: pendingApprovals + failedTasks > 0 ? 'text-amber-400' : 'text-emerald-400',
260
+ },
261
+ ]
262
+ }, [data, failedTasks, pendingApprovals])
263
+
205
264
  return (
206
265
  <div className="flex-1 flex flex-col h-full overflow-y-auto">
207
266
  <div className="px-8 pt-6 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
@@ -246,6 +305,20 @@ export function MetricsDashboard() {
246
305
  <StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
247
306
  </div>
248
307
 
308
+ <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-4">
309
+ {insightCards.map((card, index) => (
310
+ <div
311
+ key={card.label}
312
+ className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] hover:bg-surface transition-all"
313
+ style={{ animation: 'spring-in 0.6s var(--ease-spring) both', animationDelay: `${0.12 + index * 0.04}s` }}
314
+ >
315
+ <p className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/60 mb-2">{card.label}</p>
316
+ <p className={`text-[15px] font-700 leading-tight ${card.tone}`}>{card.value}</p>
317
+ <p className="text-[11px] text-text-3/55 mt-2 leading-relaxed">{card.hint}</p>
318
+ </div>
319
+ ))}
320
+ </div>
321
+
249
322
  {/* Token usage over time */}
250
323
  <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
251
324
  <ChartCard title="Token Usage Over Time">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useCallback } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
5
6
  import type { WalletTransaction } from '@/types'
6
7
 
7
8
  interface WalletApprovalDialogProps {
@@ -35,65 +36,69 @@ export function WalletApprovalDialog({ transaction, walletAddress, onClose, onRe
35
36
  const amountSol = transaction.amountLamports / 1e9
36
37
 
37
38
  return (
38
- <div className="fixed inset-0 z-50 flex items-center justify-center">
39
- <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
40
- <div className="relative w-full max-w-md rounded-[16px] border border-white/[0.08] bg-surface-1 shadow-2xl p-6 space-y-5">
41
- <div className="flex items-center gap-2">
42
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
43
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
44
- <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
45
- </svg>
46
- <h3 className="font-display text-[15px] font-600 text-text-1">Transaction Approval</h3>
47
- </div>
39
+ <Dialog open onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
40
+ <DialogContent className="sm:max-w-[460px] rounded-[20px] border-white/[0.08] bg-surface/95 p-0 shadow-[0_24px_80px_rgba(0,0,0,0.6)]">
41
+ <div className="p-6 space-y-5">
42
+ <DialogHeader className="text-left">
43
+ <div className="flex items-center gap-2">
44
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
45
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
46
+ <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
47
+ </svg>
48
+ <DialogTitle className="font-display text-[16px] font-700 tracking-[-0.02em] text-text-1">
49
+ Transaction Approval
50
+ </DialogTitle>
51
+ </div>
52
+ <DialogDescription className="text-[12px] leading-relaxed text-text-3">
53
+ Crypto transactions are irreversible. Verify the recipient address carefully before approving.
54
+ </DialogDescription>
55
+ </DialogHeader>
48
56
 
49
- <div className="p-4 rounded-[12px] bg-black/20 border border-white/[0.06] space-y-3">
50
- <div className="flex items-center justify-between">
51
- <span className="text-[11px] text-text-3/70 uppercase tracking-wide">Amount</span>
52
- <span className="text-[16px] font-600 text-text-1">{amountSol.toFixed(4)} SOL</span>
53
- </div>
54
- <div>
55
- <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">From</span>
56
- <code className="text-[10px] text-text-3 font-mono break-all">{walletAddress}</code>
57
- </div>
58
- <div>
59
- <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">To</span>
60
- <code className="text-[10px] text-text-3 font-mono break-all">{transaction.toAddress}</code>
61
- </div>
62
- {transaction.memo && (
57
+ <div className="rounded-[14px] border border-white/[0.06] bg-black/20 p-4 space-y-3">
58
+ <div className="flex items-center justify-between">
59
+ <span className="text-[11px] uppercase tracking-wide text-text-3/70">Amount</span>
60
+ <span className="text-[16px] font-600 text-text-1">{amountSol.toFixed(4)} SOL</span>
61
+ </div>
63
62
  <div>
64
- <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">Reason</span>
65
- <p className="text-[12px] text-text-2">{transaction.memo}</p>
63
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">From</span>
64
+ <code className="text-[10px] text-text-3 font-mono break-all">{walletAddress}</code>
66
65
  </div>
67
- )}
68
- </div>
69
-
70
- <p className="text-[11px] text-amber-400/80">
71
- Crypto transactions are irreversible. Verify the recipient address carefully.
72
- </p>
66
+ <div>
67
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">To</span>
68
+ <code className="text-[10px] text-text-3 font-mono break-all">{transaction.toAddress}</code>
69
+ </div>
70
+ {transaction.memo && (
71
+ <div>
72
+ <span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">Reason</span>
73
+ <p className="text-[12px] text-text-2">{transaction.memo}</p>
74
+ </div>
75
+ )}
76
+ </div>
73
77
 
74
- {error && <p className="text-[11px] text-red-400">{error}</p>}
78
+ {error && <p className="text-[11px] text-red-400">{error}</p>}
75
79
 
76
- <div className="flex gap-3">
77
- <button
78
- type="button"
79
- onClick={() => handleDecision('deny')}
80
- disabled={submitting}
81
- className="flex-1 px-4 py-2.5 rounded-[10px] border border-white/[0.08] bg-surface text-text-3 text-[12px] font-600 hover:text-red-400 hover:border-red-400/30 transition-colors cursor-pointer disabled:opacity-50"
82
- style={{ fontFamily: 'inherit' }}
83
- >
84
- Deny
85
- </button>
86
- <button
87
- type="button"
88
- onClick={() => handleDecision('approve')}
89
- disabled={submitting}
90
- className="flex-1 px-4 py-2.5 rounded-[10px] bg-accent text-white text-[12px] font-600 hover:brightness-110 transition-all cursor-pointer disabled:opacity-50"
91
- style={{ fontFamily: 'inherit' }}
92
- >
93
- {submitting ? 'Processing...' : 'Approve & Send'}
94
- </button>
80
+ <DialogFooter>
81
+ <button
82
+ type="button"
83
+ onClick={() => handleDecision('deny')}
84
+ disabled={submitting}
85
+ className="flex-1 rounded-[12px] border border-white/[0.08] bg-surface px-4 py-2.5 text-[12px] font-600 text-text-3 transition-colors hover:border-red-400/30 hover:text-red-400 disabled:opacity-50"
86
+ style={{ fontFamily: 'inherit' }}
87
+ >
88
+ Deny
89
+ </button>
90
+ <button
91
+ type="button"
92
+ onClick={() => handleDecision('approve')}
93
+ disabled={submitting}
94
+ className="flex-1 rounded-[12px] bg-accent px-4 py-2.5 text-[12px] font-600 text-white transition-all hover:brightness-110 disabled:opacity-50"
95
+ style={{ fontFamily: 'inherit' }}
96
+ >
97
+ {submitting ? 'Processing...' : 'Approve & Send'}
98
+ </button>
99
+ </DialogFooter>
95
100
  </div>
96
- </div>
97
- </div>
101
+ </DialogContent>
102
+ </Dialog>
98
103
  )
99
104
  }
@@ -70,11 +70,7 @@ export function WalletPanel() {
70
70
  const data = await api<Record<string, SafeWallet>>('GET', '/wallets')
71
71
  setWallets(data)
72
72
 
73
- // Auto-select wallet for target agent
74
- if (walletPanelAgentId) {
75
- const match = Object.values(data).find((w) => w.agentId === walletPanelAgentId)
76
- if (match) setSelectedWalletId(match.id)
77
- } else if (!selectedWalletId && Object.keys(data).length > 0) {
73
+ if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
78
74
  setSelectedWalletId(Object.keys(data)[0])
79
75
  }
80
76
  } catch { /* ignore */ }
@@ -85,6 +81,22 @@ export function WalletPanel() {
85
81
  useEffect(() => { loadWallets() }, [loadWallets])
86
82
  useWs('wallets', loadWallets, 15000)
87
83
 
84
+ useEffect(() => {
85
+ if (!walletPanelAgentId) return
86
+ const match = Object.values(wallets).find((wallet) => wallet.agentId === walletPanelAgentId)
87
+ if (match) {
88
+ setSelectedWalletId(match.id)
89
+ setShowCreateForm(false)
90
+ setCreateError('')
91
+ return
92
+ }
93
+ if (!agents[walletPanelAgentId]) return
94
+ setSelectedWalletId(null)
95
+ setShowCreateForm(true)
96
+ setCreateAgentId(walletPanelAgentId)
97
+ setCreateError('')
98
+ }, [agents, walletPanelAgentId, wallets])
99
+
88
100
  // Load detail when wallet selected
89
101
  const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
90
102
 
@@ -64,8 +64,8 @@ export function WebhookSheet() {
64
64
 
65
65
  const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
66
66
  const endpoint = editing ? webhookUrl(editing.id) : ''
67
- const orchestrators = useMemo(
68
- () => Object.values(agents).filter((a) => a.isOrchestrator),
67
+ const eligibleAgents = useMemo(
68
+ () => Object.values(agents),
69
69
  [agents]
70
70
  )
71
71
 
@@ -127,7 +127,7 @@ export function WebhookSheet() {
127
127
 
128
128
  const handleSave = async () => {
129
129
  if (!agentId) {
130
- setError('An orchestrator agent is required.')
130
+ setError('Choose an eligible agent to handle this webhook.')
131
131
  return
132
132
  }
133
133
 
@@ -185,7 +185,7 @@ export function WebhookSheet() {
185
185
  <h2 className="font-display text-[24px] font-700 tracking-[-0.02em] mb-1">
186
186
  {editing ? 'Edit Webhook' : 'New Webhook'}
187
187
  </h2>
188
- <p className="text-[13px] text-text-3">Create an inbound endpoint that triggers an orchestrator</p>
188
+ <p className="text-[13px] text-text-3">Create an inbound endpoint that launches an agent workflow</p>
189
189
  </div>
190
190
 
191
191
  {editing && (
@@ -294,15 +294,15 @@ export function WebhookSheet() {
294
294
  </div>
295
295
 
296
296
  <div>
297
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Orchestrator</label>
297
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Agent</label>
298
298
  <select
299
299
  value={agentId}
300
300
  onChange={(e) => setAgentId(e.target.value)}
301
301
  className={`${inputClass} appearance-none cursor-pointer`}
302
302
  style={{ fontFamily: 'inherit' }}
303
303
  >
304
- <option value="">Select orchestrator...</option>
305
- {orchestrators.map((agent) => (
304
+ <option value="">Select agent...</option>
305
+ {eligibleAgents.map((agent) => (
306
306
  <option key={agent.id} value={agent.id}>{agent.name}</option>
307
307
  ))}
308
308
  </select>
@@ -0,0 +1,17 @@
1
+ export const AUTH_COOKIE_NAME = 'sc_auth'
2
+
3
+ export function getCookieValue(cookieHeader: string | null | undefined, name: string): string {
4
+ if (!cookieHeader) return ''
5
+ const parts = cookieHeader.split(';')
6
+ for (const part of parts) {
7
+ const [rawKey, ...rest] = part.split('=')
8
+ if (!rawKey || rest.length === 0) continue
9
+ if (rawKey.trim() !== name) continue
10
+ try {
11
+ return decodeURIComponent(rest.join('=').trim())
12
+ } catch {
13
+ return rest.join('=').trim()
14
+ }
15
+ }
16
+ return ''
17
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Message } from '@/types'
4
+ import {
5
+ mergeCompletedAssistantMessage,
6
+ messagesDiffer,
7
+ pruneStreamingAssistantArtifacts,
8
+ shouldHidePersistedStreamingAssistantMessage,
9
+ upsertStreamingAssistantArtifact,
10
+ } from './chat-streaming-state'
11
+
12
+ describe('chat-streaming-state', () => {
13
+ it('hides persisted streaming assistant artifacts while a local stream bubble is active', () => {
14
+ const message: Message = {
15
+ role: 'assistant',
16
+ text: 'partial',
17
+ time: 1,
18
+ streaming: true,
19
+ }
20
+
21
+ assert.equal(
22
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: 'live text' }),
23
+ true,
24
+ )
25
+ assert.equal(
26
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: '' }),
27
+ true,
28
+ )
29
+ })
30
+
31
+ it('replaces trailing streaming assistant messages with the completed assistant message', () => {
32
+ const messages: Message[] = [
33
+ { role: 'user', text: 'hello', time: 1 },
34
+ { role: 'assistant', text: 'partial 1', time: 2, streaming: true },
35
+ { role: 'assistant', text: 'partial 2', time: 3, streaming: true },
36
+ ]
37
+ const completed: Message = { role: 'assistant', text: 'final', time: 4 }
38
+
39
+ assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
40
+ { role: 'user', text: 'hello', time: 1 },
41
+ { role: 'assistant', text: 'final', time: 4 },
42
+ ])
43
+ })
44
+
45
+ it('prunes stale streaming artifacts without touching later system messages', () => {
46
+ const messages: Message[] = [
47
+ { role: 'user', text: 'hello', time: 1 },
48
+ { role: 'assistant', text: 'partial', time: 10, streaming: true },
49
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
50
+ { role: 'assistant', text: 'older partial', time: 12, streaming: true },
51
+ { role: 'assistant', text: 'previous run', time: 2, streaming: true },
52
+ ]
53
+
54
+ const changed = pruneStreamingAssistantArtifacts(messages, { minIndex: 1, minTime: 10 })
55
+
56
+ assert.equal(changed, true)
57
+ assert.deepEqual(messages, [
58
+ { role: 'user', text: 'hello', time: 1 },
59
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
60
+ { role: 'assistant', text: 'previous run', time: 2, streaming: true },
61
+ ])
62
+ })
63
+
64
+ it('replaces the current run partial with the latest artifact after system messages', () => {
65
+ const messages: Message[] = [
66
+ { role: 'user', text: 'hello', time: 1 },
67
+ { role: 'assistant', text: 'partial', time: 10, streaming: true },
68
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
69
+ ]
70
+
71
+ upsertStreamingAssistantArtifact(
72
+ messages,
73
+ { role: 'assistant', text: 'latest partial', time: 12, streaming: true },
74
+ { minIndex: 1, minTime: 10 },
75
+ )
76
+
77
+ assert.deepEqual(messages, [
78
+ { role: 'user', text: 'hello', time: 1 },
79
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
80
+ { role: 'assistant', text: 'latest partial', time: 12, streaming: true },
81
+ ])
82
+ })
83
+
84
+ it('reuses the previous assistant slot when the server already persisted the same final text', () => {
85
+ const messages: Message[] = [
86
+ { role: 'user', text: 'hello', time: 1 },
87
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
88
+ ]
89
+ const completed: Message = { role: 'assistant', text: 'final', time: 3, kind: 'chat' }
90
+
91
+ assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
92
+ { role: 'user', text: 'hello', time: 1 },
93
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
94
+ ])
95
+ })
96
+
97
+ it('detects same-length message updates during reconciliation', () => {
98
+ const previous: Message[] = [
99
+ { role: 'assistant', text: 'partial', time: 1, streaming: true },
100
+ ]
101
+ const next: Message[] = [
102
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
103
+ ]
104
+
105
+ assert.equal(messagesDiffer(next, previous), true)
106
+ assert.equal(messagesDiffer(next, next), false)
107
+ })
108
+ })
@@ -0,0 +1,108 @@
1
+ import type { Message } from '@/types'
2
+
3
+ interface StreamingArtifactWindow {
4
+ minIndex?: number
5
+ minTime?: number
6
+ }
7
+
8
+ function isStreamingAssistantMessage(
9
+ message: Message,
10
+ index: number,
11
+ opts: StreamingArtifactWindow,
12
+ ): boolean {
13
+ if (message.role !== 'assistant' || message.streaming !== true) return false
14
+ if (typeof opts.minIndex === 'number' && index < opts.minIndex) return false
15
+ if (typeof opts.minTime === 'number') {
16
+ if (typeof message.time !== 'number' || message.time < opts.minTime) return false
17
+ }
18
+ return true
19
+ }
20
+
21
+ export function shouldHidePersistedStreamingAssistantMessage(
22
+ message: Message,
23
+ opts: { localStreaming: boolean; displayText: string },
24
+ ): boolean {
25
+ return (
26
+ opts.localStreaming
27
+ && message.role === 'assistant'
28
+ && message.streaming === true
29
+ )
30
+ }
31
+
32
+ export function pruneStreamingAssistantArtifacts(
33
+ messages: Message[],
34
+ opts: StreamingArtifactWindow = {},
35
+ ): boolean {
36
+ const kept = messages.filter((message, index) => !isStreamingAssistantMessage(message, index, opts))
37
+ if (kept.length === messages.length) return false
38
+ messages.splice(0, messages.length, ...kept)
39
+ return true
40
+ }
41
+
42
+ export function upsertStreamingAssistantArtifact(
43
+ messages: Message[],
44
+ assistantMessage: Message,
45
+ opts: StreamingArtifactWindow = {},
46
+ ): boolean {
47
+ if (assistantMessage.role !== 'assistant' || assistantMessage.streaming !== true) {
48
+ throw new Error('upsertStreamingAssistantArtifact requires an assistant streaming message')
49
+ }
50
+ pruneStreamingAssistantArtifacts(messages, opts)
51
+ messages.push(assistantMessage)
52
+ return true
53
+ }
54
+
55
+ export function mergeCompletedAssistantMessage(messages: Message[], assistantMessage: Message): Message[] {
56
+ let end = messages.length
57
+ while (end > 0) {
58
+ const candidate = messages[end - 1]
59
+ if (candidate.role !== 'assistant' || candidate.streaming !== true) break
60
+ end -= 1
61
+ }
62
+ const base = messages.slice(0, end)
63
+ const last = base[base.length - 1]
64
+ if (
65
+ last
66
+ && last.role === 'assistant'
67
+ && (last.kind || 'chat') === (assistantMessage.kind || 'chat')
68
+ && last.text.trim() === assistantMessage.text.trim()
69
+ ) {
70
+ return [
71
+ ...base.slice(0, -1),
72
+ {
73
+ ...last,
74
+ ...assistantMessage,
75
+ time: last.time,
76
+ },
77
+ ]
78
+ }
79
+ return [...base, assistantMessage]
80
+ }
81
+
82
+ export function messageReconciliationKey(message: Message): string {
83
+ return JSON.stringify([
84
+ message.role,
85
+ message.kind || '',
86
+ message.text,
87
+ message.streaming === true,
88
+ message.replyToId || '',
89
+ message.bookmarked === true,
90
+ message.suggestions?.join('\u241f') || '',
91
+ (message.toolEvents || []).map((event) => [
92
+ event.name,
93
+ event.input,
94
+ event.output || '',
95
+ event.error === true,
96
+ ]),
97
+ ])
98
+ }
99
+
100
+ export function messagesDiffer(nextMessages: Message[], currentMessages: Message[]): boolean {
101
+ if (nextMessages.length !== currentMessages.length) return true
102
+ for (let i = 0; i < nextMessages.length; i += 1) {
103
+ if (messageReconciliationKey(nextMessages[i]) !== messageReconciliationKey(currentMessages[i])) {
104
+ return true
105
+ }
106
+ }
107
+ return false
108
+ }
@@ -0,0 +1,48 @@
1
+ export const DEFAULT_HEARTBEAT_INTERVAL_SEC = 1800
2
+ export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300
3
+ export const DEFAULT_HEARTBEAT_SHOW_OK = false
4
+ export const DEFAULT_HEARTBEAT_SHOW_ALERTS = true
5
+
6
+ function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
7
+ const parsed = typeof value === 'number'
8
+ ? value
9
+ : typeof value === 'string'
10
+ ? Number.parseInt(value, 10)
11
+ : Number.NaN
12
+ if (!Number.isFinite(parsed)) return fallback
13
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
14
+ }
15
+
16
+ function parseBoolSetting(value: unknown, fallback: boolean): boolean {
17
+ if (typeof value === 'boolean') return value
18
+ if (typeof value === 'string') {
19
+ const normalized = value.trim().toLowerCase()
20
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
21
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
22
+ }
23
+ return fallback
24
+ }
25
+
26
+ export interface NormalizedHeartbeatSettingFields {
27
+ heartbeatIntervalSec: number
28
+ heartbeatAckMaxChars: number
29
+ heartbeatShowOk: boolean
30
+ heartbeatShowAlerts: boolean
31
+ heartbeatTarget: string | null
32
+ heartbeatPrompt: string | null
33
+ }
34
+
35
+ export function normalizeHeartbeatSettingFields(settings: Record<string, unknown>): NormalizedHeartbeatSettingFields {
36
+ return {
37
+ heartbeatIntervalSec: parseIntSetting(settings.heartbeatIntervalSec, DEFAULT_HEARTBEAT_INTERVAL_SEC, 0, 86_400),
38
+ heartbeatAckMaxChars: parseIntSetting(settings.heartbeatAckMaxChars, DEFAULT_HEARTBEAT_ACK_MAX_CHARS, 0, 8_000),
39
+ heartbeatShowOk: parseBoolSetting(settings.heartbeatShowOk, DEFAULT_HEARTBEAT_SHOW_OK),
40
+ heartbeatShowAlerts: parseBoolSetting(settings.heartbeatShowAlerts, DEFAULT_HEARTBEAT_SHOW_ALERTS),
41
+ heartbeatTarget: typeof settings.heartbeatTarget === 'string' && settings.heartbeatTarget.trim()
42
+ ? settings.heartbeatTarget.trim()
43
+ : null,
44
+ heartbeatPrompt: typeof settings.heartbeatPrompt === 'string' && settings.heartbeatPrompt.trim()
45
+ ? settings.heartbeatPrompt.trim()
46
+ : null,
47
+ }
48
+ }