@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useState, useCallback, useMemo } from 'react'
4
4
  import {
5
5
  LineChart, Line, BarChart, Bar, Cell,
6
6
  XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
@@ -29,12 +29,20 @@ interface ProviderHealthEntry {
29
29
  models: string[]
30
30
  }
31
31
 
32
+ interface PluginUsageEntry {
33
+ definitionTokens: number
34
+ invocationTokens: number
35
+ invocations: number
36
+ estimatedCost: number
37
+ }
38
+
32
39
  interface UsageResponse {
33
40
  records: unknown[]
34
41
  totalTokens: number
35
42
  totalCost: number
36
43
  byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }>
37
44
  byProvider: Record<string, { tokens: number; cost: number }>
45
+ byPlugin?: Record<string, PluginUsageEntry>
38
46
  timeSeries: TimePoint[]
39
47
  providerHealth?: Record<string, ProviderHealthEntry>
40
48
  }
@@ -150,6 +158,14 @@ export function MetricsDashboard() {
150
158
  useWs('tasks', loadTaskMetrics, 15_000)
151
159
 
152
160
  const completionRate = computeCompletionRate(tasks)
161
+ const pendingApprovals = useMemo(
162
+ () => Object.values(tasks).filter((task) => !!task.pendingApproval).length,
163
+ [tasks],
164
+ )
165
+ const failedTasks = useMemo(
166
+ () => Object.values(tasks).filter((task) => task.status === 'failed').length,
167
+ [tasks],
168
+ )
153
169
 
154
170
  const timeSeriesFormatted = (data?.timeSeries ?? []).map((pt) => ({
155
171
  ...pt,
@@ -170,6 +186,18 @@ export function MetricsDashboard() {
170
186
  cost: Math.round(v.cost * 10000) / 10000,
171
187
  }))
172
188
 
189
+ const pluginData = Object.entries(data?.byPlugin ?? {})
190
+ .filter(([id]) => id !== '_system' && id !== '_unknown')
191
+ .sort((a, b) => (b[1].definitionTokens + b[1].invocationTokens) - (a[1].definitionTokens + a[1].invocationTokens))
192
+ .slice(0, 12)
193
+ .map(([id, v]) => ({
194
+ name: id.length > 18 ? id.slice(0, 18) + '…' : id,
195
+ definitionTokens: v.definitionTokens,
196
+ invocationTokens: v.invocationTokens,
197
+ invocations: v.invocations,
198
+ estimatedCost: v.estimatedCost,
199
+ }))
200
+
173
201
  const tooltipStyle = {
174
202
  contentStyle: {
175
203
  background: 'var(--color-surface)',
@@ -182,6 +210,57 @@ export function MetricsDashboard() {
182
210
  labelStyle: { color: 'var(--color-text-2)' },
183
211
  }
184
212
 
213
+ const insightCards = useMemo(() => {
214
+ const series = data?.timeSeries ?? []
215
+ const latest = series[series.length - 1]
216
+ const previous = series.slice(0, -1)
217
+ const baselineCost = previous.length > 0
218
+ ? previous.reduce((sum, point) => sum + point.cost, 0) / previous.length
219
+ : 0
220
+ const costDeltaPct = latest && baselineCost > 0
221
+ ? Math.round(((latest.cost - baselineCost) / baselineCost) * 100)
222
+ : null
223
+
224
+ const providerHealthEntries = Object.entries(data?.providerHealth ?? {})
225
+ .filter(([, health]) => health.totalRequests > 0)
226
+ .sort(([, a], [, b]) => b.errorRate - a.errorRate)
227
+ const riskiestProvider = providerHealthEntries[0]
228
+
229
+ const topCostAgent = Object.values(data?.byAgent ?? {})
230
+ .sort((a, b) => b.cost - a.cost)[0]
231
+
232
+ return [
233
+ {
234
+ label: 'Cost Pulse',
235
+ value: latest
236
+ ? `${formatCost(latest.cost)}${costDeltaPct !== null ? ` · ${costDeltaPct >= 0 ? '+' : ''}${costDeltaPct}%` : ''}`
237
+ : 'No recent spend',
238
+ hint: latest ? `Latest bucket vs ${previous.length > 0 ? 'range average' : 'current range'}` : 'Waiting for usage data',
239
+ tone: costDeltaPct !== null && costDeltaPct > 40 ? 'text-red-400' : 'text-text',
240
+ },
241
+ {
242
+ label: 'Provider Risk',
243
+ value: riskiestProvider
244
+ ? `${riskiestProvider[0]} · ${(riskiestProvider[1].errorRate * 100).toFixed(1)}%`
245
+ : 'No provider issues',
246
+ hint: riskiestProvider ? `${riskiestProvider[1].totalRequests} requests in range` : 'No provider health records yet',
247
+ tone: riskiestProvider && riskiestProvider[1].errorRate > 0.1 ? 'text-red-400' : 'text-emerald-400',
248
+ },
249
+ {
250
+ label: 'Top Spend Agent',
251
+ value: topCostAgent ? `${topCostAgent.name} · ${formatCost(topCostAgent.cost)}` : 'No agent activity',
252
+ hint: topCostAgent ? `${formatTokens(topCostAgent.tokens)} tokens` : 'No per-agent usage in range',
253
+ tone: 'text-sky-400',
254
+ },
255
+ {
256
+ label: 'Workflow Friction',
257
+ value: `${pendingApprovals} approvals · ${failedTasks} failed`,
258
+ hint: pendingApprovals + failedTasks > 0 ? 'Operational overhead from live work' : 'No obvious workflow friction',
259
+ tone: pendingApprovals + failedTasks > 0 ? 'text-amber-400' : 'text-emerald-400',
260
+ },
261
+ ]
262
+ }, [data, failedTasks, pendingApprovals])
263
+
185
264
  return (
186
265
  <div className="flex-1 flex flex-col h-full overflow-y-auto">
187
266
  <div className="px-8 pt-6 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
@@ -226,6 +305,20 @@ export function MetricsDashboard() {
226
305
  <StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
227
306
  </div>
228
307
 
308
+ <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-4">
309
+ {insightCards.map((card, index) => (
310
+ <div
311
+ key={card.label}
312
+ className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] hover:bg-surface transition-all"
313
+ style={{ animation: 'spring-in 0.6s var(--ease-spring) both', animationDelay: `${0.12 + index * 0.04}s` }}
314
+ >
315
+ <p className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/60 mb-2">{card.label}</p>
316
+ <p className={`text-[15px] font-700 leading-tight ${card.tone}`}>{card.value}</p>
317
+ <p className="text-[11px] text-text-3/55 mt-2 leading-relaxed">{card.hint}</p>
318
+ </div>
319
+ ))}
320
+ </div>
321
+
229
322
  {/* Token usage over time */}
230
323
  <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
231
324
  <ChartCard title="Token Usage Over Time">
@@ -288,6 +381,59 @@ export function MetricsDashboard() {
288
381
  </ChartCard>
289
382
  </div>
290
383
 
384
+ {/* Plugin Usage */}
385
+ {pluginData.length > 0 && (
386
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.28s both' }}>
387
+ <ChartCard title="Plugin Token Usage">
388
+ <ResponsiveContainer width="100%" height={280}>
389
+ <BarChart data={pluginData} layout="vertical" margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
390
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
391
+ <XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
392
+ <YAxis type="category" dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={120} />
393
+ <Tooltip
394
+ {...tooltipStyle}
395
+ formatter={(value: number | undefined, name?: string) => [
396
+ formatTokens(value ?? 0),
397
+ name === 'definitionTokens' ? 'Context (definitions)' : 'Invocations',
398
+ ]}
399
+ />
400
+ <Bar dataKey="definitionTokens" fill="#818CF8" radius={[0, 0, 0, 0]} stackId="a" name="definitionTokens" />
401
+ <Bar dataKey="invocationTokens" fill="#34D399" radius={[0, 4, 4, 0]} stackId="a" name="invocationTokens" />
402
+ <Legend
403
+ verticalAlign="bottom"
404
+ iconType="circle"
405
+ iconSize={8}
406
+ formatter={(value: string) => (
407
+ <span style={{ color: '#a0a0b0', fontSize: 11 }}>
408
+ {value === 'definitionTokens' ? 'Context (definitions)' : 'Invocations'}
409
+ </span>
410
+ )}
411
+ />
412
+ </BarChart>
413
+ </ResponsiveContainer>
414
+ </ChartCard>
415
+
416
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mt-4">
417
+ {pluginData.filter((p) => p.invocations > 0).map((p, idx) => (
418
+ <div
419
+ key={p.name}
420
+ className="bg-surface-2 rounded-[10px] p-3 border border-white/[0.04] hover:bg-surface transition-all"
421
+ style={{ animation: 'spring-in 0.5s var(--ease-spring) both', animationDelay: `${0.3 + idx * 0.03}s` }}
422
+ >
423
+ <p className="text-[12px] font-600 text-text truncate">{p.name}</p>
424
+ <div className="flex items-baseline gap-2 mt-1">
425
+ <span className="text-[18px] font-display font-700 text-text">{p.invocations}</span>
426
+ <span className="text-[11px] text-text-3">calls</span>
427
+ </div>
428
+ <p className="text-[11px] text-text-3 mt-0.5">
429
+ {formatTokens(p.invocationTokens)} invocation tokens &middot; {formatCost(p.estimatedCost)}
430
+ </p>
431
+ </div>
432
+ ))}
433
+ </div>
434
+ </div>
435
+ )}
436
+
291
437
  {/* Task KPIs */}
292
438
  {taskMetrics && (
293
439
  <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
@@ -70,11 +70,7 @@ export function WalletPanel() {
70
70
  const data = await api<Record<string, SafeWallet>>('GET', '/wallets')
71
71
  setWallets(data)
72
72
 
73
- // Auto-select wallet for target agent
74
- if (walletPanelAgentId) {
75
- const match = Object.values(data).find((w) => w.agentId === walletPanelAgentId)
76
- if (match) setSelectedWalletId(match.id)
77
- } else if (!selectedWalletId && Object.keys(data).length > 0) {
73
+ if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
78
74
  setSelectedWalletId(Object.keys(data)[0])
79
75
  }
80
76
  } catch { /* ignore */ }
@@ -85,6 +81,22 @@ export function WalletPanel() {
85
81
  useEffect(() => { loadWallets() }, [loadWallets])
86
82
  useWs('wallets', loadWallets, 15000)
87
83
 
84
+ useEffect(() => {
85
+ if (!walletPanelAgentId) return
86
+ const match = Object.values(wallets).find((wallet) => wallet.agentId === walletPanelAgentId)
87
+ if (match) {
88
+ setSelectedWalletId(match.id)
89
+ setShowCreateForm(false)
90
+ setCreateError('')
91
+ return
92
+ }
93
+ if (!agents[walletPanelAgentId]) return
94
+ setSelectedWalletId(null)
95
+ setShowCreateForm(true)
96
+ setCreateAgentId(walletPanelAgentId)
97
+ setCreateError('')
98
+ }, [agents, walletPanelAgentId, wallets])
99
+
88
100
  // Load detail when wallet selected
89
101
  const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
90
102
 
@@ -64,8 +64,8 @@ export function WebhookSheet() {
64
64
 
65
65
  const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
66
66
  const endpoint = editing ? webhookUrl(editing.id) : ''
67
- const orchestrators = useMemo(
68
- () => Object.values(agents).filter((a) => a.isOrchestrator),
67
+ const eligibleAgents = useMemo(
68
+ () => Object.values(agents),
69
69
  [agents]
70
70
  )
71
71
 
@@ -127,7 +127,7 @@ export function WebhookSheet() {
127
127
 
128
128
  const handleSave = async () => {
129
129
  if (!agentId) {
130
- setError('An orchestrator agent is required.')
130
+ setError('Choose an eligible agent to handle this webhook.')
131
131
  return
132
132
  }
133
133
 
@@ -185,7 +185,7 @@ export function WebhookSheet() {
185
185
  <h2 className="font-display text-[24px] font-700 tracking-[-0.02em] mb-1">
186
186
  {editing ? 'Edit Webhook' : 'New Webhook'}
187
187
  </h2>
188
- <p className="text-[13px] text-text-3">Create an inbound endpoint that triggers an orchestrator</p>
188
+ <p className="text-[13px] text-text-3">Create an inbound endpoint that launches an agent workflow</p>
189
189
  </div>
190
190
 
191
191
  {editing && (
@@ -230,7 +230,7 @@ export function WebhookSheet() {
230
230
  <div className="text-[11px] text-red-300/80 mt-1">{entry.error}</div>
231
231
  )}
232
232
  {entry.sessionId && (
233
- <div className="text-[10px] text-text-3/50 mt-1 font-mono">Session: {entry.sessionId}</div>
233
+ <div className="text-[10px] text-text-3/50 mt-1 font-mono">Chat: {entry.sessionId}</div>
234
234
  )}
235
235
  </div>
236
236
  ))}
@@ -294,15 +294,15 @@ export function WebhookSheet() {
294
294
  </div>
295
295
 
296
296
  <div>
297
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Orchestrator</label>
297
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Agent</label>
298
298
  <select
299
299
  value={agentId}
300
300
  onChange={(e) => setAgentId(e.target.value)}
301
301
  className={`${inputClass} appearance-none cursor-pointer`}
302
302
  style={{ fontFamily: 'inherit' }}
303
303
  >
304
- <option value="">Select orchestrator...</option>
305
- {orchestrators.map((agent) => (
304
+ <option value="">Select agent...</option>
305
+ {eligibleAgents.map((agent) => (
306
306
  <option key={agent.id} value={agent.id}>{agent.name}</option>
307
307
  ))}
308
308
  </select>
@@ -0,0 +1,17 @@
1
+ export const AUTH_COOKIE_NAME = 'sc_auth'
2
+
3
+ export function getCookieValue(cookieHeader: string | null | undefined, name: string): string {
4
+ if (!cookieHeader) return ''
5
+ const parts = cookieHeader.split(';')
6
+ for (const part of parts) {
7
+ const [rawKey, ...rest] = part.split('=')
8
+ if (!rawKey || rest.length === 0) continue
9
+ if (rawKey.trim() !== name) continue
10
+ try {
11
+ return decodeURIComponent(rest.join('=').trim())
12
+ } catch {
13
+ return rest.join('=').trim()
14
+ }
15
+ }
16
+ return ''
17
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Message } from '@/types'
4
+ import {
5
+ mergeCompletedAssistantMessage,
6
+ messagesDiffer,
7
+ pruneStreamingAssistantArtifacts,
8
+ shouldHidePersistedStreamingAssistantMessage,
9
+ upsertStreamingAssistantArtifact,
10
+ } from './chat-streaming-state'
11
+
12
+ describe('chat-streaming-state', () => {
13
+ it('hides persisted streaming assistant artifacts while a local stream bubble is active', () => {
14
+ const message: Message = {
15
+ role: 'assistant',
16
+ text: 'partial',
17
+ time: 1,
18
+ streaming: true,
19
+ }
20
+
21
+ assert.equal(
22
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: 'live text' }),
23
+ true,
24
+ )
25
+ assert.equal(
26
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: '' }),
27
+ true,
28
+ )
29
+ })
30
+
31
+ it('replaces trailing streaming assistant messages with the completed assistant message', () => {
32
+ const messages: Message[] = [
33
+ { role: 'user', text: 'hello', time: 1 },
34
+ { role: 'assistant', text: 'partial 1', time: 2, streaming: true },
35
+ { role: 'assistant', text: 'partial 2', time: 3, streaming: true },
36
+ ]
37
+ const completed: Message = { role: 'assistant', text: 'final', time: 4 }
38
+
39
+ assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
40
+ { role: 'user', text: 'hello', time: 1 },
41
+ { role: 'assistant', text: 'final', time: 4 },
42
+ ])
43
+ })
44
+
45
+ it('prunes stale streaming artifacts without touching later system messages', () => {
46
+ const messages: Message[] = [
47
+ { role: 'user', text: 'hello', time: 1 },
48
+ { role: 'assistant', text: 'partial', time: 10, streaming: true },
49
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
50
+ { role: 'assistant', text: 'older partial', time: 12, streaming: true },
51
+ { role: 'assistant', text: 'previous run', time: 2, streaming: true },
52
+ ]
53
+
54
+ const changed = pruneStreamingAssistantArtifacts(messages, { minIndex: 1, minTime: 10 })
55
+
56
+ assert.equal(changed, true)
57
+ assert.deepEqual(messages, [
58
+ { role: 'user', text: 'hello', time: 1 },
59
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
60
+ { role: 'assistant', text: 'previous run', time: 2, streaming: true },
61
+ ])
62
+ })
63
+
64
+ it('replaces the current run partial with the latest artifact after system messages', () => {
65
+ const messages: Message[] = [
66
+ { role: 'user', text: 'hello', time: 1 },
67
+ { role: 'assistant', text: 'partial', time: 10, streaming: true },
68
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
69
+ ]
70
+
71
+ upsertStreamingAssistantArtifact(
72
+ messages,
73
+ { role: 'assistant', text: 'latest partial', time: 12, streaming: true },
74
+ { minIndex: 1, minTime: 10 },
75
+ )
76
+
77
+ assert.deepEqual(messages, [
78
+ { role: 'user', text: 'hello', time: 1 },
79
+ { role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
80
+ { role: 'assistant', text: 'latest partial', time: 12, streaming: true },
81
+ ])
82
+ })
83
+
84
+ it('reuses the previous assistant slot when the server already persisted the same final text', () => {
85
+ const messages: Message[] = [
86
+ { role: 'user', text: 'hello', time: 1 },
87
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
88
+ ]
89
+ const completed: Message = { role: 'assistant', text: 'final', time: 3, kind: 'chat' }
90
+
91
+ assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
92
+ { role: 'user', text: 'hello', time: 1 },
93
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
94
+ ])
95
+ })
96
+
97
+ it('detects same-length message updates during reconciliation', () => {
98
+ const previous: Message[] = [
99
+ { role: 'assistant', text: 'partial', time: 1, streaming: true },
100
+ ]
101
+ const next: Message[] = [
102
+ { role: 'assistant', text: 'final', time: 2, kind: 'chat' },
103
+ ]
104
+
105
+ assert.equal(messagesDiffer(next, previous), true)
106
+ assert.equal(messagesDiffer(next, next), false)
107
+ })
108
+ })
@@ -0,0 +1,108 @@
1
+ import type { Message } from '@/types'
2
+
3
+ interface StreamingArtifactWindow {
4
+ minIndex?: number
5
+ minTime?: number
6
+ }
7
+
8
+ function isStreamingAssistantMessage(
9
+ message: Message,
10
+ index: number,
11
+ opts: StreamingArtifactWindow,
12
+ ): boolean {
13
+ if (message.role !== 'assistant' || message.streaming !== true) return false
14
+ if (typeof opts.minIndex === 'number' && index < opts.minIndex) return false
15
+ if (typeof opts.minTime === 'number') {
16
+ if (typeof message.time !== 'number' || message.time < opts.minTime) return false
17
+ }
18
+ return true
19
+ }
20
+
21
+ export function shouldHidePersistedStreamingAssistantMessage(
22
+ message: Message,
23
+ opts: { localStreaming: boolean; displayText: string },
24
+ ): boolean {
25
+ return (
26
+ opts.localStreaming
27
+ && message.role === 'assistant'
28
+ && message.streaming === true
29
+ )
30
+ }
31
+
32
+ export function pruneStreamingAssistantArtifacts(
33
+ messages: Message[],
34
+ opts: StreamingArtifactWindow = {},
35
+ ): boolean {
36
+ const kept = messages.filter((message, index) => !isStreamingAssistantMessage(message, index, opts))
37
+ if (kept.length === messages.length) return false
38
+ messages.splice(0, messages.length, ...kept)
39
+ return true
40
+ }
41
+
42
+ export function upsertStreamingAssistantArtifact(
43
+ messages: Message[],
44
+ assistantMessage: Message,
45
+ opts: StreamingArtifactWindow = {},
46
+ ): boolean {
47
+ if (assistantMessage.role !== 'assistant' || assistantMessage.streaming !== true) {
48
+ throw new Error('upsertStreamingAssistantArtifact requires an assistant streaming message')
49
+ }
50
+ pruneStreamingAssistantArtifacts(messages, opts)
51
+ messages.push(assistantMessage)
52
+ return true
53
+ }
54
+
55
+ export function mergeCompletedAssistantMessage(messages: Message[], assistantMessage: Message): Message[] {
56
+ let end = messages.length
57
+ while (end > 0) {
58
+ const candidate = messages[end - 1]
59
+ if (candidate.role !== 'assistant' || candidate.streaming !== true) break
60
+ end -= 1
61
+ }
62
+ const base = messages.slice(0, end)
63
+ const last = base[base.length - 1]
64
+ if (
65
+ last
66
+ && last.role === 'assistant'
67
+ && (last.kind || 'chat') === (assistantMessage.kind || 'chat')
68
+ && last.text.trim() === assistantMessage.text.trim()
69
+ ) {
70
+ return [
71
+ ...base.slice(0, -1),
72
+ {
73
+ ...last,
74
+ ...assistantMessage,
75
+ time: last.time,
76
+ },
77
+ ]
78
+ }
79
+ return [...base, assistantMessage]
80
+ }
81
+
82
+ export function messageReconciliationKey(message: Message): string {
83
+ return JSON.stringify([
84
+ message.role,
85
+ message.kind || '',
86
+ message.text,
87
+ message.streaming === true,
88
+ message.replyToId || '',
89
+ message.bookmarked === true,
90
+ message.suggestions?.join('\u241f') || '',
91
+ (message.toolEvents || []).map((event) => [
92
+ event.name,
93
+ event.input,
94
+ event.output || '',
95
+ event.error === true,
96
+ ]),
97
+ ])
98
+ }
99
+
100
+ export function messagesDiffer(nextMessages: Message[], currentMessages: Message[]): boolean {
101
+ if (nextMessages.length !== currentMessages.length) return true
102
+ for (let i = 0; i < nextMessages.length; i += 1) {
103
+ if (messageReconciliationKey(nextMessages[i]) !== messageReconciliationKey(currentMessages[i])) {
104
+ return true
105
+ }
106
+ }
107
+ return false
108
+ }
package/src/lib/chat.ts CHANGED
@@ -27,7 +27,7 @@ export async function streamChat(
27
27
  }
28
28
 
29
29
  const key = getStoredAccessKey()
30
- const res = await fetch(`/api/sessions/${sessionId}/chat`, {
30
+ const res = await fetch(`/api/chats/${sessionId}/chat`, {
31
31
  method: 'POST',
32
32
  headers: {
33
33
  'Content-Type': 'application/json',
@@ -4,9 +4,11 @@ import type {
4
4
  ProviderInfo, Credential, Credentials, ProviderType, SessionType,
5
5
  } from '../types'
6
6
 
7
- export const fetchSessions = () => api<Sessions>('GET', '/sessions')
7
+ export const fetchChats = () => api<Sessions>('GET', '/chats')
8
+ /** @deprecated Use fetchChats */
9
+ export const fetchSessions = fetchChats
8
10
 
9
- export const createSession = (
11
+ export const createChat = (
10
12
  name: string,
11
13
  cwd: string,
12
14
  user: string,
@@ -16,23 +18,29 @@ export const createSession = (
16
18
  apiEndpoint?: string | null,
17
19
  sessionType?: SessionType,
18
20
  agentId?: string | null,
19
- tools?: string[],
21
+ plugins?: string[],
20
22
  file?: string | null,
21
23
  ) =>
22
- api<Session>('POST', '/sessions', {
24
+ api<Session>('POST', '/chats', {
23
25
  name, cwd: cwd || undefined, user,
24
26
  provider, model, credentialId, apiEndpoint,
25
- sessionType, agentId, tools, file: file || undefined,
27
+ sessionType, agentId, plugins, file: file || undefined,
26
28
  })
29
+ /** @deprecated Use createChat */
30
+ export const createSession = createChat
27
31
 
28
- export const updateSession = (id: string, updates: Partial<Pick<Session, 'name' | 'cwd'>>) =>
29
- api<Session>('PUT', `/sessions/${id}`, updates)
32
+ export const updateChat = (id: string, updates: Partial<Pick<Session, 'name' | 'cwd'>>) =>
33
+ api<Session>('PUT', `/chats/${id}`, updates)
34
+ /** @deprecated Use updateChat */
35
+ export const updateSession = updateChat
30
36
 
31
- export const deleteSession = (id: string) =>
32
- api<string>('DELETE', `/sessions/${id}`)
37
+ export const deleteChat = (id: string) =>
38
+ api<string>('DELETE', `/chats/${id}`)
39
+ /** @deprecated Use deleteChat */
40
+ export const deleteSession = deleteChat
33
41
 
34
42
  export const fetchMessages = (id: string) =>
35
- api<Message[]>('GET', `/sessions/${id}/messages`)
43
+ api<Message[]>('GET', `/chats/${id}/messages`)
36
44
 
37
45
  export interface PaginatedMessages {
38
46
  messages: Message[]
@@ -42,13 +50,15 @@ export interface PaginatedMessages {
42
50
  }
43
51
 
44
52
  export const fetchMessagesPaginated = (id: string, limit: number = 100) =>
45
- api<PaginatedMessages>('GET', `/sessions/${id}/messages?limit=${limit}`)
53
+ api<PaginatedMessages>('GET', `/chats/${id}/messages?limit=${limit}`)
46
54
 
47
55
  export const clearMessages = (id: string) =>
48
- api<string>('POST', `/sessions/${id}/clear`)
56
+ api<string>('POST', `/chats/${id}/clear`)
49
57
 
50
- export const stopSession = (id: string) =>
51
- api<string>('POST', `/sessions/${id}/stop`)
58
+ export const stopChat = (id: string) =>
59
+ api<string>('POST', `/chats/${id}/stop`)
60
+ /** @deprecated Use stopChat */
61
+ export const stopSession = stopChat
52
62
 
53
63
  export const fetchDirs = async () => {
54
64
  const data = await api<{ dirs: Directory[] }>('GET', '/dirs')
@@ -56,16 +66,16 @@ export const fetchDirs = async () => {
56
66
  }
57
67
 
58
68
  export const devServer = (id: string, action: 'start' | 'stop' | 'status') =>
59
- api<DevServerStatus>('POST', `/sessions/${id}/devserver`, { action })
69
+ api<DevServerStatus>('POST', `/chats/${id}/devserver`, { action })
60
70
 
61
71
  export const checkBrowser = (id: string) =>
62
- api<{ active: boolean }>('GET', `/sessions/${id}/browser`)
72
+ api<{ active: boolean }>('GET', `/chats/${id}/browser`)
63
73
 
64
74
  export const stopBrowser = (id: string) =>
65
- api<string>('DELETE', `/sessions/${id}/browser`)
75
+ api<string>('DELETE', `/chats/${id}/browser`)
66
76
 
67
77
  export const deploy = (id: string, message: string) =>
68
- api<DeployResult>('POST', `/sessions/${id}/deploy`, { message })
78
+ api<DeployResult>('POST', `/chats/${id}/deploy`, { message })
69
79
 
70
80
  export const fetchProviders = () => api<ProviderInfo[]>('GET', '/providers')
71
81
 
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { buildOpenClawMainSessionKey, normalizeOpenClawAgentId } from './openclaw-agent-id'
4
+
5
+ test('normalizeOpenClawAgentId mirrors gateway-style normalization', () => {
6
+ assert.equal(normalizeOpenClawAgentId('OpenClaw Ops'), 'openclaw-ops')
7
+ assert.equal(normalizeOpenClawAgentId(' Agent / Research '), 'agent-research')
8
+ assert.equal(normalizeOpenClawAgentId('main'), 'main')
9
+ })
10
+
11
+ test('buildOpenClawMainSessionKey uses normalized OpenClaw agent ids', () => {
12
+ assert.equal(buildOpenClawMainSessionKey('OpenClaw Ops'), 'agent:openclaw-ops:main')
13
+ assert.equal(buildOpenClawMainSessionKey(' '), null)
14
+ })
@@ -0,0 +1,31 @@
1
+ const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i
2
+ const INVALID_CHARS_RE = /[^a-z0-9_-]+/g
3
+ const LEADING_DASH_RE = /^-+/
4
+ const TRAILING_DASH_RE = /-+$/
5
+
6
+ export function normalizeOpenClawAgentId(value: string | undefined | null): string {
7
+ const trimmed = (value ?? '').trim()
8
+ if (!trimmed) {
9
+ return 'main'
10
+ }
11
+ if (VALID_ID_RE.test(trimmed)) {
12
+ return trimmed.toLowerCase()
13
+ }
14
+ return (
15
+ trimmed
16
+ .toLowerCase()
17
+ .replace(INVALID_CHARS_RE, '-')
18
+ .replace(LEADING_DASH_RE, '')
19
+ .replace(TRAILING_DASH_RE, '')
20
+ .slice(0, 64)
21
+ || 'main'
22
+ )
23
+ }
24
+
25
+ export function buildOpenClawMainSessionKey(agentNameOrId: string | undefined | null): string | null {
26
+ const trimmed = (agentNameOrId ?? '').trim()
27
+ if (!trimmed) {
28
+ return null
29
+ }
30
+ return `agent:${normalizeOpenClawAgentId(trimmed)}:main`
31
+ }