@swarmclawai/swarmclaw 0.7.8 → 0.8.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 (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -4,6 +4,7 @@ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
4
4
  import { useCallback, useEffect, useState, type ReactNode } from 'react'
5
5
  import type { Agent } from '@/types'
6
6
  import { useAppStore } from '@/stores/use-app-store'
7
+ import { api } from '@/lib/api-client'
7
8
  import { AgentAvatar } from './agent-avatar'
8
9
  import { AgentFilesEditor } from './agent-files-editor'
9
10
  import { OpenClawSkillsPanel } from './openclaw-skills-panel'
@@ -11,6 +12,7 @@ import { PermissionPresetSelector } from './permission-preset-selector'
11
12
  import { ExecConfigPanel } from './exec-config-panel'
12
13
  import { SandboxEnvPanel } from './sandbox-env-panel'
13
14
  import { CronJobForm } from './cron-job-form'
15
+ import { toast } from 'sonner'
14
16
 
15
17
  interface Props {
16
18
  agent: Agent
@@ -87,6 +89,12 @@ export function InspectorPanel({ agent, onEditAgent, onClearHistory, onDeleteAge
87
89
  <div className="min-w-0 flex-1">
88
90
  <div className="flex items-center gap-2 min-w-0">
89
91
  <h3 className="font-display text-[16px] font-700 text-text truncate tracking-[-0.02em]">{agent.name}</h3>
92
+ {agent.disabled === true && (
93
+ <span className="inline-flex items-center gap-1 rounded-[7px] border border-amber-400/15 bg-amber-400/[0.1] px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-amber-300">
94
+ <span className="w-1.5 h-1.5 rounded-full bg-amber-300" />
95
+ Disabled
96
+ </span>
97
+ )}
90
98
  {agent.heartbeatEnabled && (
91
99
  <span className="inline-flex items-center gap-1 rounded-[7px] border border-emerald-400/15 bg-emerald-400/10 px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-emerald-300">
92
100
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
@@ -189,13 +197,32 @@ interface OverviewTabProps {
189
197
  }
190
198
 
191
199
  function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: OverviewTabProps) {
200
+ const loadAgents = useAppStore((s) => s.loadAgents)
201
+ const loadSessions = useAppStore((s) => s.loadSessions)
202
+ const [availabilitySaving, setAvailabilitySaving] = useState(false)
192
203
  const summaryStats = [
193
204
  { label: 'Provider', value: PROVIDER_LABELS[agent.provider] || agent.provider.replace(/-/g, ' ') },
194
205
  { label: 'Model', value: agent.model || 'Default' },
195
206
  { label: 'Plugins', value: String(agent.plugins?.length ?? 0) },
196
207
  { label: 'Heartbeat', value: agent.heartbeatEnabled ? `Every ${agent.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC}s` : 'Off' },
208
+ { label: 'Status', value: agent.disabled === true ? 'Disabled' : 'Enabled' },
197
209
  ]
198
210
 
211
+ const handleToggleAvailability = useCallback(async () => {
212
+ if (availabilitySaving) return
213
+ setAvailabilitySaving(true)
214
+ try {
215
+ const nextDisabled = agent.disabled !== true
216
+ await api('PUT', `/agents/${agent.id}`, { disabled: nextDisabled })
217
+ await Promise.all([loadAgents(), loadSessions()])
218
+ toast.success(nextDisabled ? `${agent.name} disabled` : `${agent.name} enabled`)
219
+ } catch (err: unknown) {
220
+ toast.error(err instanceof Error ? err.message : 'Failed to update agent availability')
221
+ } finally {
222
+ setAvailabilitySaving(false)
223
+ }
224
+ }, [agent.disabled, agent.id, agent.name, availabilitySaving, loadAgents, loadSessions])
225
+
199
226
  return (
200
227
  <div className="p-4 flex flex-col gap-4">
201
228
  <div className={panelCardClass('p-4 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]')}>
@@ -259,6 +286,20 @@ function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDele
259
286
  Edit Agent
260
287
  </button>
261
288
  )}
289
+ <button
290
+ onClick={() => void handleToggleAvailability()}
291
+ disabled={availabilitySaving}
292
+ className={`w-full px-3 py-2.5 rounded-[10px] text-[12px] font-700 border cursor-pointer transition-all text-left disabled:opacity-50 ${
293
+ agent.disabled === true
294
+ ? 'text-emerald-300 bg-emerald-400/[0.06] border-emerald-400/[0.12] hover:bg-emerald-400/[0.1]'
295
+ : 'text-amber-300 bg-amber-400/[0.06] border-amber-400/[0.12] hover:bg-amber-400/[0.1]'
296
+ }`}
297
+ style={{ fontFamily: 'inherit' }}
298
+ >
299
+ {availabilitySaving
300
+ ? (agent.disabled === true ? 'Enabling Agent...' : 'Disabling Agent...')
301
+ : (agent.disabled === true ? 'Enable Agent' : 'Disable Agent')}
302
+ </button>
262
303
  {(onClearHistory || onDeleteAgent || onDeleteChat) && (
263
304
  <>
264
305
  <SectionLabel>Danger Zone</SectionLabel>
@@ -1,8 +1,11 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useState, useCallback } from 'react'
4
+ import ReactMarkdown from 'react-markdown'
4
5
  import { useWs } from '@/hooks/use-ws'
5
6
  import { api } from '@/lib/api-client'
7
+ import { normalizeCanvasContent } from '@/lib/canvas-content'
8
+ import type { CanvasContent, CanvasDocument } from '@/types'
6
9
 
7
10
  interface CanvasPanelProps {
8
11
  sessionId: string
@@ -10,86 +13,254 @@ interface CanvasPanelProps {
10
13
  onClose: () => void
11
14
  }
12
15
 
16
+ const THEME_STYLES: Record<NonNullable<CanvasDocument['theme']>, { accent: string; chip: string }> = {
17
+ slate: { accent: 'text-sky-300', chip: 'bg-sky-500/10 text-sky-300 border-sky-500/20' },
18
+ sky: { accent: 'text-sky-300', chip: 'bg-sky-500/10 text-sky-300 border-sky-500/20' },
19
+ emerald: { accent: 'text-emerald-300', chip: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20' },
20
+ amber: { accent: 'text-amber-300', chip: 'bg-amber-500/10 text-amber-300 border-amber-500/20' },
21
+ rose: { accent: 'text-rose-300', chip: 'bg-rose-500/10 text-rose-300 border-rose-500/20' },
22
+ }
23
+
24
+ function toneClass(tone?: string): string {
25
+ switch (tone) {
26
+ case 'positive': return 'text-emerald-300'
27
+ case 'negative': return 'text-rose-300'
28
+ case 'warning': return 'text-amber-300'
29
+ default: return 'text-text'
30
+ }
31
+ }
32
+
33
+ function intentClass(intent?: string): string {
34
+ switch (intent) {
35
+ case 'primary': return 'bg-sky-500 text-white border-sky-400/30'
36
+ case 'success': return 'bg-emerald-500 text-white border-emerald-400/30'
37
+ case 'danger': return 'bg-rose-500 text-white border-rose-400/30'
38
+ default: return 'bg-white/[0.03] text-text-2 border-white/[0.08]'
39
+ }
40
+ }
41
+
42
+ function StructuredCanvasView({ document }: { document: CanvasDocument }) {
43
+ const theme = THEME_STYLES[document.theme || 'slate']
44
+ return (
45
+ <div className="h-full overflow-y-auto bg-bg px-5 py-5">
46
+ <div className="max-w-4xl mx-auto space-y-4">
47
+ {(document.title || document.subtitle) && (
48
+ <div className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
49
+ {document.title && <h2 className={`font-display text-[22px] font-700 tracking-[-0.03em] ${theme.accent}`}>{document.title}</h2>}
50
+ {document.subtitle && <p className="mt-1 text-[13px] text-text-3/70">{document.subtitle}</p>}
51
+ </div>
52
+ )}
53
+
54
+ {document.blocks.map((block, index) => {
55
+ if (block.type === 'markdown') {
56
+ return (
57
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
58
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
59
+ <div className="max-w-none text-[14px] leading-6 text-text-2/90 [&_h1]:font-display [&_h1]:text-[24px] [&_h1]:text-text [&_h2]:font-display [&_h2]:text-[20px] [&_h2]:text-text [&_h3]:font-display [&_h3]:text-[18px] [&_h3]:text-text [&_p]:my-3 [&_ul]:my-3 [&_ul]:pl-5 [&_li]:my-1 [&_code]:rounded [&_code]:bg-black/[0.2] [&_code]:px-1.5 [&_code]:py-0.5">
60
+ <ReactMarkdown>{block.markdown}</ReactMarkdown>
61
+ </div>
62
+ </section>
63
+ )
64
+ }
65
+
66
+ if (block.type === 'metrics') {
67
+ return (
68
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
69
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
70
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
71
+ {block.items.map((item) => (
72
+ <div key={item.label} className="rounded-[14px] border border-white/[0.08] bg-black/[0.14] px-4 py-3">
73
+ <div className="text-[11px] uppercase tracking-[0.08em] text-text-3/60">{item.label}</div>
74
+ <div className={`mt-1 text-[24px] font-display font-700 tracking-[-0.03em] ${toneClass(item.tone)}`}>{item.value}</div>
75
+ {item.detail && <div className="mt-1 text-[12px] text-text-3/65">{item.detail}</div>}
76
+ </div>
77
+ ))}
78
+ </div>
79
+ </section>
80
+ )
81
+ }
82
+
83
+ if (block.type === 'cards') {
84
+ return (
85
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
86
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
87
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
88
+ {block.items.map((item) => (
89
+ <div key={item.title} className="rounded-[14px] border border-white/[0.08] bg-black/[0.14] px-4 py-3">
90
+ <div className={`text-[15px] font-700 ${toneClass(item.tone)}`}>{item.title}</div>
91
+ {item.body && <p className="mt-2 text-[13px] leading-6 text-text-2/85 whitespace-pre-wrap">{item.body}</p>}
92
+ {item.meta && <div className="mt-3 text-[11px] text-text-3/60">{item.meta}</div>}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ </section>
97
+ )
98
+ }
99
+
100
+ if (block.type === 'table') {
101
+ return (
102
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4 overflow-hidden">
103
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
104
+ <div className="overflow-x-auto rounded-[12px] border border-white/[0.08]">
105
+ <table className="min-w-full text-left text-[13px]">
106
+ <thead className="bg-black/[0.18]">
107
+ <tr>
108
+ {block.table.columns.map((column) => (
109
+ <th key={column} className="px-3 py-2.5 font-700 text-text-2">{column}</th>
110
+ ))}
111
+ </tr>
112
+ </thead>
113
+ <tbody>
114
+ {block.table.rows.map((row, rowIndex) => (
115
+ <tr key={rowIndex} className="border-t border-white/[0.06]">
116
+ {row.map((cell, cellIndex) => (
117
+ <td key={cellIndex} className="px-3 py-2.5 text-text-3/80">{cell == null ? '—' : String(cell)}</td>
118
+ ))}
119
+ </tr>
120
+ ))}
121
+ </tbody>
122
+ </table>
123
+ </div>
124
+ {block.table.caption && <div className="mt-2 text-[11px] text-text-3/60">{block.table.caption}</div>}
125
+ </section>
126
+ )
127
+ }
128
+
129
+ if (block.type === 'code') {
130
+ return (
131
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
132
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
133
+ <pre className="overflow-x-auto rounded-[14px] border border-white/[0.08] bg-black/[0.25] p-4 text-[12px] leading-6 text-text-2">
134
+ <code>{block.code}</code>
135
+ </pre>
136
+ {block.language && <div className={`mt-2 inline-flex rounded-full border px-2 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${theme.chip}`}>{block.language}</div>}
137
+ </section>
138
+ )
139
+ }
140
+
141
+ if (block.type === 'actions') {
142
+ return (
143
+ <section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
144
+ {block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
145
+ <div className="flex flex-wrap gap-2">
146
+ {block.items.map((item) => (
147
+ item.href ? (
148
+ <a
149
+ key={item.label}
150
+ href={item.href}
151
+ target="_blank"
152
+ rel="noreferrer"
153
+ className={`inline-flex items-center rounded-[12px] border px-3 py-2 text-[12px] font-700 transition-all hover:brightness-110 ${intentClass(item.intent)}`}
154
+ >
155
+ {item.label}
156
+ </a>
157
+ ) : (
158
+ <div key={item.label} className={`inline-flex items-center rounded-[12px] border px-3 py-2 text-[12px] font-700 ${intentClass(item.intent)}`}>
159
+ {item.label}
160
+ </div>
161
+ )
162
+ ))}
163
+ </div>
164
+ {block.items.some((item) => item.note) && (
165
+ <div className="mt-3 space-y-1">
166
+ {block.items.filter((item) => item.note).map((item) => (
167
+ <div key={`${item.label}-note`} className="text-[11px] text-text-3/60">{item.label}: {item.note}</div>
168
+ ))}
169
+ </div>
170
+ )}
171
+ </section>
172
+ )
173
+ }
174
+
175
+ return null
176
+ })}
177
+ </div>
178
+ </div>
179
+ )
180
+ }
181
+
13
182
  export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
14
- const [content, setContent] = useState<string | null>(null)
183
+ const [content, setContent] = useState<CanvasContent>(null)
184
+ const [loaded, setLoaded] = useState(false)
15
185
 
16
186
  const loadCanvas = useCallback(async () => {
17
187
  try {
18
- const res = await api<{ content: string | null }>('GET', `/canvas/${sessionId}`)
19
- setContent(res.content)
20
- } catch { /* ignore */ }
188
+ const res = await api<{ content: CanvasContent }>('GET', `/canvas/${sessionId}`)
189
+ setContent(normalizeCanvasContent(res.content))
190
+ } catch {
191
+ setContent(null)
192
+ } finally {
193
+ setLoaded(true)
194
+ }
21
195
  }, [sessionId])
22
196
 
23
- useEffect(() => { loadCanvas() }, [loadCanvas]) // eslint-disable-line react-hooks/set-state-in-effect
197
+ useEffect(() => { loadCanvas() }, [loadCanvas])
24
198
  useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
25
199
 
26
- if (!content) return (
27
- <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
28
- <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
29
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
30
- <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
200
+ const header = (
201
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
202
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
203
+ <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
204
+ </svg>
205
+ <span className="text-[13px] font-600 text-text flex-1 truncate">
206
+ Canvas{agentName ? ` — ${agentName}` : ''}
207
+ </span>
208
+ <button
209
+ onClick={loadCanvas}
210
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
211
+ title="Refresh"
212
+ >
213
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
214
+ <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
31
215
  </svg>
32
- <span className="text-[13px] font-600 text-text flex-1 truncate">
33
- Canvas{agentName ? ` — ${agentName}` : ''}
34
- </span>
35
- <button
36
- onClick={onClose}
37
- className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
38
- title="Close canvas"
39
- aria-label="Close canvas"
40
- >
41
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
42
- <path d="M18 6L6 18M6 6l12 12" />
43
- </svg>
44
- </button>
45
- </div>
46
- <div className="flex-1 flex items-center justify-center">
47
- <div className="text-center">
48
- <div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
49
- <span className="text-[13px] text-text-3">Loading canvas...</span>
50
- </div>
51
- </div>
216
+ </button>
217
+ <button
218
+ onClick={onClose}
219
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
220
+ title="Close canvas"
221
+ >
222
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
223
+ <path d="M18 6L6 18M6 6l12 12" />
224
+ </svg>
225
+ </button>
52
226
  </div>
53
227
  )
54
228
 
55
- return (
56
- <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
57
- {/* Toolbar */}
58
- <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
59
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
60
- <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
61
- </svg>
62
- <span className="text-[13px] font-600 text-text flex-1 truncate">
63
- Canvas{agentName ? ` — ${agentName}` : ''}
64
- </span>
65
- <button
66
- onClick={loadCanvas}
67
- className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
68
- title="Refresh"
69
- >
70
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
71
- <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
72
- </svg>
73
- </button>
74
- <button
75
- onClick={onClose}
76
- className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
77
- title="Close canvas"
78
- >
79
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
80
- <path d="M18 6L6 18M6 6l12 12" />
81
- </svg>
82
- </button>
229
+ if (!loaded) {
230
+ return (
231
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
232
+ {header}
233
+ <div className="flex-1 flex items-center justify-center">
234
+ <div className="text-center">
235
+ <div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
236
+ <span className="text-[13px] text-text-3">Loading canvas...</span>
237
+ </div>
238
+ </div>
83
239
  </div>
240
+ )
241
+ }
84
242
 
85
- {/* Sandboxed iframe */}
243
+ return (
244
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
245
+ {header}
86
246
  <div className="flex-1 overflow-hidden">
87
- <iframe
88
- sandbox="allow-scripts allow-same-origin"
89
- srcDoc={content}
90
- className="w-full h-full border-none bg-white"
91
- title="Agent Canvas"
92
- />
247
+ {!content ? (
248
+ <div className="h-full flex items-center justify-center text-center px-6">
249
+ <div>
250
+ <div className="text-[14px] font-600 text-text-2">No canvas content yet</div>
251
+ <p className="mt-1 text-[12px] text-text-3/60">Agents can present HTML or structured documents here.</p>
252
+ </div>
253
+ </div>
254
+ ) : typeof content === 'string' ? (
255
+ <iframe
256
+ sandbox="allow-scripts allow-same-origin"
257
+ srcDoc={content}
258
+ className="w-full h-full border-none bg-white"
259
+ title="Agent Canvas"
260
+ />
261
+ ) : (
262
+ <StructuredCanvasView document={content} />
263
+ )}
93
264
  </div>
94
265
  </div>
95
266
  )
@@ -1,10 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
4
5
  import type { Session } from '@/types'
5
6
  import { api } from '@/lib/api-client'
6
7
  import { useAppStore } from '@/stores/use-app-store'
7
8
  import { useChatStore } from '@/stores/use-chat-store'
9
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
8
10
  import { ConnectorPlatformBadge, getSessionConnector } from '@/components/shared/connector-platform-icon'
9
11
  import { AgentAvatar } from '@/components/agents/agent-avatar'
10
12
  import { toast } from 'sonner'
@@ -44,18 +46,26 @@ export function ChatCard({ session, active, onClick }: Props) {
44
46
  const streamPhase = useChatStore((s) => s.streamPhase)
45
47
  const streamToolName = useChatStore((s) => s.streamToolName)
46
48
  const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
49
+ const [confirmDelete, setConfirmDelete] = useState(false)
50
+ const [deleting, setDeleting] = useState(false)
47
51
  const isTyping = streamingSessionId === session.id
48
52
 
49
- const handleDelete = async (e: React.MouseEvent) => {
53
+ const handleDeleteClick = (e: React.MouseEvent) => {
50
54
  e.stopPropagation()
51
- if (!confirm(`Delete chat session "${session.name}"?`)) return
52
-
55
+ setConfirmDelete(true)
56
+ }
57
+
58
+ const handleDelete = async () => {
59
+ setDeleting(true)
53
60
  try {
54
61
  await api('DELETE', `/chats/${session.id}`)
55
62
  removeSession(session.id)
56
63
  toast.success('Session deleted')
57
64
  } catch (err: unknown) {
58
65
  toast.error(err instanceof Error ? err.message : 'Failed to delete session')
66
+ } finally {
67
+ setDeleting(false)
68
+ setConfirmDelete(false)
59
69
  }
60
70
  }
61
71
 
@@ -84,14 +94,15 @@ export function ChatCard({ session, active, onClick }: Props) {
84
94
  && agent?.heartbeatEnabled !== false
85
95
 
86
96
  return (
87
- <div
88
- onClick={onClick}
89
- className={`group/card relative py-3.5 px-4 cursor-pointer rounded-[14px]
90
- transition-all duration-200 active:scale-[0.98]
91
- ${active
92
- ? 'bg-accent-soft border border-accent-bright/10'
93
- : 'bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03]'}`}
94
- >
97
+ <>
98
+ <div
99
+ onClick={onClick}
100
+ className={`group/card relative py-3.5 px-4 cursor-pointer rounded-[14px]
101
+ transition-all duration-200 active:scale-[0.98]
102
+ ${active
103
+ ? 'bg-accent-soft border border-accent-bright/10'
104
+ : 'bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03]'}`}
105
+ >
95
106
  {active && (
96
107
  <div className="absolute left-0 top-3.5 bottom-3.5 w-[2.5px] rounded-full bg-accent-bright" />
97
108
  )}
@@ -134,7 +145,7 @@ export function ChatCard({ session, active, onClick }: Props) {
134
145
  {timeAgo(session.lastActiveAt)}
135
146
  </span>
136
147
  <button
137
- onClick={handleDelete}
148
+ onClick={handleDeleteClick}
138
149
  className="shrink-0 opacity-0 group-hover/card:opacity-100 transition-opacity duration-150
139
150
  text-text-3 hover:text-red-400 p-0.5 -mr-1 cursor-pointer bg-transparent border-none"
140
151
  title="Delete chat"
@@ -163,6 +174,18 @@ export function ChatCard({ session, active, onClick }: Props) {
163
174
  ) : (
164
175
  <div className="text-[13px] text-text-2/50 truncate mt-1 leading-relaxed">{preview}</div>
165
176
  )}
166
- </div>
177
+ </div>
178
+ <ConfirmDialog
179
+ open={confirmDelete}
180
+ title="Delete Chat?"
181
+ message={`Delete chat session "${session.name}"?`}
182
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
183
+ confirmDisabled={deleting}
184
+ cancelDisabled={deleting}
185
+ danger
186
+ onConfirm={() => { void handleDelete() }}
187
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
188
+ />
189
+ </>
167
190
  )
168
191
  }
@@ -10,8 +10,8 @@ import { ChatToolToggles } from './chat-tool-toggles'
10
10
  import { api } from '@/lib/api-client'
11
11
  import {
12
12
  ConnectorPlatformIcon,
13
- CONNECTOR_PLATFORM_META,
14
13
  getSessionConnector,
14
+ resolveConnectorPlatformMeta,
15
15
  } from '@/components/shared/connector-platform-icon'
16
16
  import { AgentAvatar } from '@/components/agents/agent-avatar'
17
17
  import { ModelCombobox } from '@/components/shared/model-combobox'
@@ -34,6 +34,23 @@ function Tip({ label, children, side = 'bottom' }: { label: string; children: Re
34
34
  )
35
35
  }
36
36
 
37
+ function getAgentWalletIds(agent: { walletIds?: string[]; walletId?: string | null } | null | undefined): string[] {
38
+ const ids = Array.isArray(agent?.walletIds)
39
+ ? agent.walletIds.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
40
+ : []
41
+ const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
42
+ ? [agent.walletId.trim()]
43
+ : []
44
+ return [...new Set([...ids, ...legacy])]
45
+ }
46
+
47
+ function getAgentActiveWalletId(agent: { activeWalletId?: string | null; walletIds?: string[]; walletId?: string | null } | null | undefined): string | null {
48
+ const walletIds = getAgentWalletIds(agent)
49
+ if (typeof agent?.activeWalletId === 'string' && walletIds.includes(agent.activeWalletId)) return agent.activeWalletId
50
+ if (typeof agent?.walletId === 'string' && walletIds.includes(agent.walletId)) return agent.walletId
51
+ return walletIds[0] || null
52
+ }
53
+
37
54
  function HeaderChip({
38
55
  children,
39
56
  title,
@@ -129,7 +146,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
129
146
  const loadConnectors = useAppStore((s) => s.loadConnectors)
130
147
  const agent = session.agentId ? agents[session.agentId] : null
131
148
  const connector = getSessionConnector(session, connectors)
132
- const connectorMeta = connector ? CONNECTOR_PLATFORM_META[connector.platform] : null
149
+ const connectorMeta = connector ? resolveConnectorPlatformMeta(connector.platform) : null
133
150
  const connectorPresence = connector?.presence
134
151
  const providers = useAppStore((s) => s.providers)
135
152
  const loadProviders = useAppStore((s) => s.loadProviders)
@@ -152,8 +169,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
152
169
  const renameInputRef = useRef<HTMLInputElement>(null)
153
170
  const renameContainerRef = useRef<HTMLSpanElement>(null)
154
171
  const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
155
- const [walletBalance, setWalletBalance] = useState<number | null>(null)
172
+ const [walletBalance, setWalletBalance] = useState<{ formatted: string; symbol: string; assets?: number } | null>(null)
156
173
  const [headerWidgets, setHeaderWidgets] = useState<Array<{ id: string; label: string; icon?: string }>>([])
174
+ const agentWalletIds = useMemo(() => getAgentWalletIds(agent), [agent])
175
+ const activeWalletId = useMemo(() => getAgentActiveWalletId(agent), [agent])
157
176
 
158
177
  useEffect(() => {
159
178
  api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
@@ -162,17 +181,25 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
162
181
  }, [session.id])
163
182
 
164
183
  const fetchWalletBalance = useCallback(async () => {
165
- if (!agent?.walletId) {
184
+ if (!activeWalletId) {
166
185
  setWalletBalance(null)
167
186
  return
168
187
  }
169
188
  try {
170
- const data = await api<{ balanceSol?: number }>('GET', `/wallets/${agent.walletId}`)
171
- setWalletBalance(data.balanceSol ?? null)
189
+ const data = await api<{ balanceFormatted?: string; balanceSymbol?: string; portfolioSummary?: { nonZeroAssets?: number } }>('GET', `/wallets/${activeWalletId}`)
190
+ if (data.balanceFormatted && data.balanceSymbol) {
191
+ setWalletBalance({
192
+ formatted: data.balanceFormatted,
193
+ symbol: data.balanceSymbol,
194
+ assets: typeof data.portfolioSummary?.nonZeroAssets === 'number' ? data.portfolioSummary.nonZeroAssets : undefined,
195
+ })
196
+ } else {
197
+ setWalletBalance(null)
198
+ }
172
199
  } catch {
173
200
  setWalletBalance(null)
174
201
  }
175
- }, [agent?.walletId])
202
+ }, [activeWalletId])
176
203
 
177
204
  useEffect(() => {
178
205
  void fetchWalletBalance()
@@ -237,17 +264,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
237
264
  title: 'Open wallets',
238
265
  }
239
266
  }
240
- if (!agent.walletId) {
267
+ if (agentWalletIds.length === 0) {
241
268
  return {
242
269
  label: 'Create wallet',
243
270
  title: 'Create wallet',
244
271
  }
245
272
  }
246
273
  return {
247
- label: walletBalance !== null ? `${walletBalance.toFixed(3)} SOL` : 'Wallet',
248
- title: 'View wallet',
274
+ label: agentWalletIds.length > 1
275
+ ? (walletBalance ? `${walletBalance.formatted} ${walletBalance.symbol}${walletBalance.assets && walletBalance.assets > 1 ? ` +${walletBalance.assets - 1}` : ''} / ${agentWalletIds.length}` : `${agentWalletIds.length} wallets`)
276
+ : (walletBalance ? `${walletBalance.formatted} ${walletBalance.symbol}${walletBalance.assets && walletBalance.assets > 1 ? ` +${walletBalance.assets - 1}` : ''}` : 'Wallet'),
277
+ title: agentWalletIds.length > 1 ? 'View wallets' : 'View wallet',
249
278
  }
250
- }, [agent?.id, agent?.walletId, walletBalance])
279
+ }, [agent?.id, agentWalletIds, walletBalance])
251
280
 
252
281
  const handleHeaderWidgetClick = (widgetId: string) => {
253
282
  if (widgetId === 'wallet-status') {
@@ -389,17 +418,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
389
418
  setHeartbeatSaving(true)
390
419
  try {
391
420
  if (session.agentId) {
392
- // Save to agent with both formats so the cascade resolves correctly
421
+ // Save the cadence without implicitly toggling heartbeat on.
393
422
  await api('PUT', `/agents/${session.agentId}`, {
394
423
  heartbeatInterval: formatDuration(sec),
395
424
  heartbeatIntervalSec: sec,
396
- heartbeatEnabled: true,
397
425
  })
398
426
  // Clear stale session-level overrides
399
427
  await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
400
428
  await Promise.all([loadAgents(), loadSessions()])
401
429
  } else {
402
- await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
430
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec })
403
431
  await loadSessions()
404
432
  }
405
433
  } finally {
@@ -960,7 +988,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
960
988
  </button>
961
989
  {Array.from(connectorSources.entries()).map(([cid, info]) => {
962
990
  const active = connectorFilter === cid
963
- const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
991
+ const meta = resolveConnectorPlatformMeta(info.platform)
964
992
  return (
965
993
  <button
966
994
  key={cid}
@@ -969,7 +997,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
969
997
  active ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
970
998
  }`}
971
999
  >
972
- <ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
1000
+ <ConnectorPlatformIcon platform={info.platform} size={12} />
973
1001
  {info.connectorName || meta?.label || info.platform}
974
1002
  </button>
975
1003
  )