@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
@@ -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') {
@@ -280,12 +309,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
280
309
  const fromDelegateOpenCode = session.delegateResumeIds?.opencode
281
310
  ? { label: 'OpenCode', id: session.delegateResumeIds.opencode, command: `opencode run \"<task>\" --session ${session.delegateResumeIds.opencode}` }
282
311
  : null
312
+ const fromDelegateGemini = session.delegateResumeIds?.gemini
313
+ ? { label: 'Gemini', id: session.delegateResumeIds.gemini, command: `gemini --resume ${session.delegateResumeIds.gemini} --prompt \"<task>\"` }
314
+ : null
283
315
  return fromSessionClaude
284
316
  || fromSessionCodex
285
317
  || fromSessionOpenCode
286
318
  || fromDelegateClaude
287
319
  || fromDelegateCodex
288
320
  || fromDelegateOpenCode
321
+ || fromDelegateGemini
289
322
  || null
290
323
  }, [session.claudeSessionId, session.codexThreadId, session.opencodeSessionId, session.delegateResumeIds])
291
324
 
@@ -385,17 +418,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
385
418
  setHeartbeatSaving(true)
386
419
  try {
387
420
  if (session.agentId) {
388
- // Save to agent with both formats so the cascade resolves correctly
421
+ // Save the cadence without implicitly toggling heartbeat on.
389
422
  await api('PUT', `/agents/${session.agentId}`, {
390
423
  heartbeatInterval: formatDuration(sec),
391
424
  heartbeatIntervalSec: sec,
392
- heartbeatEnabled: true,
393
425
  })
394
426
  // Clear stale session-level overrides
395
427
  await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
396
428
  await Promise.all([loadAgents(), loadSessions()])
397
429
  } else {
398
- await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
430
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec })
399
431
  await loadSessions()
400
432
  }
401
433
  } finally {
@@ -956,7 +988,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
956
988
  </button>
957
989
  {Array.from(connectorSources.entries()).map(([cid, info]) => {
958
990
  const active = connectorFilter === cid
959
- const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
991
+ const meta = resolveConnectorPlatformMeta(info.platform)
960
992
  return (
961
993
  <button
962
994
  key={cid}
@@ -965,7 +997,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
965
997
  active ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
966
998
  }`}
967
999
  >
968
- <ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
1000
+ <ConnectorPlatformIcon platform={info.platform} size={12} />
969
1001
  {info.connectorName || meta?.label || info.platform}
970
1002
  </button>
971
1003
  )
@@ -9,6 +9,7 @@ import { toast } from 'sonner'
9
9
  import { Skeleton } from '@/components/shared/skeleton'
10
10
  import { EmptyState } from '@/components/shared/empty-state'
11
11
  import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
12
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
12
13
 
13
14
  interface Props {
14
15
  inSidebar?: boolean
@@ -38,6 +39,8 @@ export function ChatList({ inSidebar, onSelect }: Props) {
38
39
  const [sortMode, setSortMode] = useState<SortMode>('lastActive')
39
40
  const [loaded, setLoaded] = useState(Object.keys(sessions).length > 0)
40
41
  const [bulkMenuOpen, setBulkMenuOpen] = useState(false)
42
+ const [confirmClearIds, setConfirmClearIds] = useState<string[] | null>(null)
43
+ const [clearing, setClearing] = useState(false)
41
44
 
42
45
  useEffect(() => {
43
46
  if (Object.keys(sessions).length > 0 && !loaded) setLoaded(true)
@@ -116,6 +119,18 @@ export function ChatList({ inSidebar, onSelect }: Props) {
116
119
  onSelect?.()
117
120
  }
118
121
 
122
+ const handleClearFiltered = async () => {
123
+ if (!confirmClearIds || confirmClearIds.length === 0) return
124
+ setClearing(true)
125
+ try {
126
+ await clearSessions(confirmClearIds)
127
+ toast.success(`${confirmClearIds.length} chat${confirmClearIds.length === 1 ? '' : 's'} deleted`)
128
+ setConfirmClearIds(null)
129
+ } finally {
130
+ setClearing(false)
131
+ }
132
+ }
133
+
119
134
  // Truly empty — no sessions at all for this user
120
135
  if (!allUserSessions.length) {
121
136
  // Show skeleton cards while data is loading
@@ -178,11 +193,9 @@ export function ChatList({ inSidebar, onSelect }: Props) {
178
193
  </svg>
179
194
  </button>
180
195
  <Dropdown open={bulkMenuOpen} onClose={() => setBulkMenuOpen(false)}>
181
- <DropdownItem onClick={async () => {
196
+ <DropdownItem onClick={() => {
182
197
  setBulkMenuOpen(false)
183
- if (!window.confirm(`Delete ${filtered.length} chat${filtered.length === 1 ? '' : 's'}?`)) return
184
- await clearSessions(filtered.map((s) => s.id))
185
- toast.success(`${filtered.length} chat${filtered.length === 1 ? '' : 's'} deleted`)
198
+ setConfirmClearIds(filtered.map((s) => s.id))
186
199
  }}>
187
200
  Clear filtered chats
188
201
  </DropdownItem>
@@ -249,6 +262,17 @@ export function ChatList({ inSidebar, onSelect }: Props) {
249
262
  </p>
250
263
  </div>
251
264
  )}
265
+ <ConfirmDialog
266
+ open={!!confirmClearIds}
267
+ title="Clear Filtered Chats?"
268
+ message={confirmClearIds ? `Delete ${confirmClearIds.length} chat${confirmClearIds.length === 1 ? '' : 's'} from the current view?` : 'Delete filtered chats?'}
269
+ confirmLabel={clearing ? 'Deleting...' : 'Delete'}
270
+ confirmDisabled={clearing}
271
+ cancelDisabled={clearing}
272
+ danger
273
+ onConfirm={() => { void handleClearFiltered() }}
274
+ onCancel={() => { if (!clearing) setConfirmClearIds(null) }}
275
+ />
252
276
  </div>
253
277
  )
254
278
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
5
6
  import { useAppStore } from '@/stores/use-app-store'
6
7
  import { toast } from 'sonner'
7
8
 
@@ -21,6 +22,7 @@ export function CheckpointTimeline({ sessionId }: Props) {
21
22
  const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([])
22
23
  const [loading, setLoading] = useState(true)
23
24
  const [restoringId, setRestoringId] = useState<string | null>(null)
25
+ const [confirmRestore, setConfirmRestore] = useState<Checkpoint | null>(null)
24
26
  const loadSessions = useAppStore((s) => s.loadSessions)
25
27
 
26
28
  const load = async () => {
@@ -40,9 +42,9 @@ export function CheckpointTimeline({ sessionId }: Props) {
40
42
  // eslint-disable-next-line react-hooks/exhaustive-deps
41
43
  }, [sessionId])
42
44
 
43
- const handleRestore = async (checkpoint: Checkpoint) => {
44
- if (!confirm('Restore session to this point? This will delete all subsequent history.')) return
45
-
45
+ const handleRestore = async () => {
46
+ if (!confirmRestore) return
47
+ const checkpoint = confirmRestore
46
48
  setRestoringId(checkpoint.checkpointId)
47
49
  try {
48
50
  await api('POST', `/chats/${sessionId}/restore`, {
@@ -52,6 +54,7 @@ export function CheckpointTimeline({ sessionId }: Props) {
52
54
  toast.success('Session restored successfully')
53
55
  await loadSessions()
54
56
  await load()
57
+ setConfirmRestore(null)
55
58
  } catch (err) {
56
59
  toast.error('Failed to restore session')
57
60
  console.error(err)
@@ -74,39 +77,52 @@ export function CheckpointTimeline({ sessionId }: Props) {
74
77
  }
75
78
 
76
79
  return (
77
- <div className="flex flex-col gap-3 p-5">
78
- {checkpoints.map((cp, i) => (
79
- <div
80
- key={cp.checkpointId}
81
- className="group relative flex flex-col gap-2 p-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04] transition-all"
82
- >
83
- <div className="flex items-center justify-between">
84
- <div className="flex flex-col">
85
- <span className="text-[11px] font-700 text-accent-bright uppercase tracking-wider">
86
- {i === 0 ? 'Current State' : `Point ${checkpoints.length - i}`}
87
- </span>
88
- <span className="text-[10px] text-text-3 font-mono">
89
- {new Date(cp.createdAt).toLocaleString()}
90
- </span>
80
+ <>
81
+ <div className="flex flex-col gap-3 p-5">
82
+ {checkpoints.map((cp, i) => (
83
+ <div
84
+ key={cp.checkpointId}
85
+ className="group relative flex flex-col gap-2 p-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04] transition-all"
86
+ >
87
+ <div className="flex items-center justify-between">
88
+ <div className="flex flex-col">
89
+ <span className="text-[11px] font-700 text-accent-bright uppercase tracking-wider">
90
+ {i === 0 ? 'Current State' : `Point ${checkpoints.length - i}`}
91
+ </span>
92
+ <span className="text-[10px] text-text-3 font-mono">
93
+ {new Date(cp.createdAt).toLocaleString()}
94
+ </span>
95
+ </div>
96
+ {i > 0 && (
97
+ <button
98
+ onClick={() => setConfirmRestore(cp)}
99
+ disabled={!!restoringId}
100
+ className="px-3 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600 border-none cursor-pointer hover:brightness-110 disabled:opacity-50"
101
+ >
102
+ {restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
103
+ </button>
104
+ )}
91
105
  </div>
92
- {i > 0 && (
93
- <button
94
- onClick={() => handleRestore(cp)}
95
- disabled={!!restoringId}
96
- className="px-3 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600 border-none cursor-pointer hover:brightness-110 disabled:opacity-50"
97
- >
98
- {restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
99
- </button>
106
+
107
+ {cp.values && Array.isArray(cp.values.messages) && cp.values.messages.length > 0 && (
108
+ <div className="mt-1 p-2 rounded-[8px] bg-black/20 text-[11px] text-text-3 line-clamp-2 italic">
109
+ Last message: {String((cp.values.messages[cp.values.messages.length - 1] as Record<string, unknown>)?.content ?? 'Empty state')}
110
+ </div>
100
111
  )}
101
112
  </div>
102
-
103
- {cp.values && Array.isArray(cp.values.messages) && cp.values.messages.length > 0 && (
104
- <div className="mt-1 p-2 rounded-[8px] bg-black/20 text-[11px] text-text-3 line-clamp-2 italic">
105
- Last message: {String((cp.values.messages[cp.values.messages.length - 1] as Record<string, unknown>)?.content ?? 'Empty state')}
106
- </div>
107
- )}
108
- </div>
109
- ))}
110
- </div>
113
+ ))}
114
+ </div>
115
+ <ConfirmDialog
116
+ open={!!confirmRestore}
117
+ title="Restore Session?"
118
+ message="Restore session to this point? This will delete all subsequent history."
119
+ confirmLabel={restoringId ? 'Restoring...' : 'Restore'}
120
+ confirmDisabled={!!restoringId}
121
+ cancelDisabled={!!restoringId}
122
+ danger
123
+ onConfirm={() => { void handleRestore() }}
124
+ onCancel={() => { if (!restoringId) setConfirmRestore(null) }}
125
+ />
126
+ </>
111
127
  )
112
128
  }
@@ -23,5 +23,18 @@ describe('parseTaskCompletion', () => {
23
23
  assert.equal(parsed?.reportPath, 'data/task-reports/abc12345.md')
24
24
  assert.equal(parsed?.workingDir, '/tmp/work')
25
25
  })
26
- })
27
26
 
27
+ it('captures Gemini resume lines from task completion payloads', () => {
28
+ const text = [
29
+ 'Task completed: **[Ship follow-up](#task:task-gemini)**',
30
+ '',
31
+ 'Gemini session: `gemini-session-7`',
32
+ '',
33
+ 'All done.',
34
+ ].join('\n')
35
+ const parsed = parseTaskCompletion(text)
36
+
37
+ assert.ok(parsed)
38
+ assert.equal(parsed?.resumeInfo, 'Gemini session: `gemini-session-7`')
39
+ })
40
+ })
@@ -169,7 +169,7 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
169
169
  }
170
170
  } else if (section.startsWith('Task report: ')) {
171
171
  reportPath = section.replace('Task report: ', '').replace(/^`|`$/g, '')
172
- } else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
172
+ } else if (/^(Claude session|Codex thread|OpenCode session|Gemini session|CLI session):/.test(section)) {
173
173
  resumeInfo = section
174
174
  } else if (section.trim()) {
175
175
  resultParts.push(section)