@swarmclawai/swarmclaw 0.7.7 → 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 (281) hide show
  1. package/README.md +12 -14
  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 +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -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
  )
@@ -136,10 +136,12 @@ export function ChatArea() {
136
136
  if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
137
137
  setMessagesLoading(false)
138
138
  })
139
- // If server reports session is still active, show streaming state
140
- if (session?.active) {
139
+
140
+ const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
141
+ if (sessionAtLoad?.active) {
141
142
  useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
142
143
  }
144
+
143
145
  // Refresh active state from server so returning to a session restores typing indicator.
144
146
  loadSessions().then(() => {
145
147
  if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
@@ -148,6 +150,7 @@ export function ChatArea() {
148
150
  useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
149
151
  }
150
152
  }).catch((err) => console.error('Failed to refresh messages:', err))
153
+
151
154
  devServer(requestedSessionId, 'status').then((r) => {
152
155
  if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
153
156
  setDevServer(r.running ? r : null)
@@ -155,23 +158,31 @@ export function ChatArea() {
155
158
  if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
156
159
  setDevServer(null)
157
160
  })
158
- // Check browser status
159
- if (sessionHasBrowserPlugin) {
160
- checkBrowser(requestedSessionId).then((r) => {
161
- if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
162
- setBrowserActive(r.active)
163
- }).catch((err) => {
164
- if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
165
- console.error('Browser check failed:', err)
166
- setBrowserActive(false)
167
- })
168
- } else {
161
+
162
+ return () => {
163
+ cancelled = true
164
+ }
165
+ }, [loadSessions, sessionId, setDevServer, setMessages])
166
+
167
+ useEffect(() => {
168
+ if (!sessionId) return
169
+ let cancelled = false
170
+ if (!sessionHasBrowserPlugin) {
169
171
  setBrowserActive(false)
172
+ return
170
173
  }
174
+ checkBrowser(sessionId).then((r) => {
175
+ if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
176
+ setBrowserActive(r.active)
177
+ }).catch((err) => {
178
+ if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
179
+ console.error('Browser check failed:', err)
180
+ setBrowserActive(false)
181
+ })
171
182
  return () => {
172
183
  cancelled = true
173
184
  }
174
- }, [loadSessions, session?.active, sessionHasBrowserPlugin, sessionId, setDevServer, setMessages])
185
+ }, [sessionHasBrowserPlugin, sessionId])
175
186
 
176
187
  // Auto-poll messages for sessions that are actively running on the server
177
188
  const isServerActive = session?.active === true
@@ -216,10 +227,16 @@ export function ChatArea() {
216
227
  shouldPollMessages ? 2000 : undefined,
217
228
  )
218
229
 
219
- // When server-active flag drops, stop the streaming indicator
230
+ // Keep the local typing indicator aligned with the server's active state
220
231
  useEffect(() => {
221
232
  if (!sessionId) return
222
233
  const state = useChatStore.getState()
234
+ if (isServerActive) {
235
+ if (!state.streaming && !state.streamText) {
236
+ useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamText: '' })
237
+ }
238
+ return
239
+ }
223
240
  if (
224
241
  !isServerActive
225
242
  && state.streaming
@@ -230,7 +247,7 @@ export function ChatArea() {
230
247
  fetchMessages(sessionId).then(setMessages).catch(() => {})
231
248
  useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '' })
232
249
  }
233
- }, [isServerActive, sessionId])
250
+ }, [isServerActive, sessionId, setMessages])
234
251
 
235
252
  // Poll browser status while session has browser tools
236
253
  const hasBrowserTool = session?.plugins?.includes('browser')
@@ -255,7 +272,7 @@ export function ChatArea() {
255
272
  if (!sessionId) return
256
273
  await devServer(sessionId, 'stop')
257
274
  setDevServer(null)
258
- }, [sessionId])
275
+ }, [sessionId, setDevServer])
259
276
 
260
277
  const handleClear = useCallback(async () => {
261
278
  setConfirmClear(false)
@@ -263,7 +280,7 @@ export function ChatArea() {
263
280
  await clearMessages(sessionId)
264
281
  setMessages([])
265
282
  loadSessions()
266
- }, [sessionId])
283
+ }, [loadSessions, sessionId, setMessages])
267
284
 
268
285
  const handleDelete = useCallback(async () => {
269
286
  setConfirmDelete(false)
@@ -271,7 +288,7 @@ export function ChatArea() {
271
288
  await deleteChat(sessionId)
272
289
  removeSessionFromStore(sessionId)
273
290
  setCurrentSession(null)
274
- }, [sessionId])
291
+ }, [removeSessionFromStore, sessionId, setCurrentSession])
275
292
 
276
293
  const handlePrompt = useCallback((text: string) => {
277
294
  sendMessage(text)
@@ -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
  }