@swarmclawai/swarmclaw 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { toast } from 'sonner'
5
6
 
6
7
  interface CommandItem {
7
8
  id: string
@@ -129,10 +130,16 @@ export function CommandPalette() {
129
130
  result.push({
130
131
  id: `agent:${agent.id}`,
131
132
  label: agent.name,
132
- description: `Open ${agent.name}'s chat`,
133
+ description: agent.disabled === true
134
+ ? `${agent.name} is disabled`
135
+ : `Open ${agent.name}'s chat`,
133
136
  keywords: [agent.provider, agent.model, agent.description || ''].filter(Boolean),
134
137
  category: 'agent',
135
138
  onSelect: async () => {
139
+ if (agent.disabled === true && !agent.threadSessionId) {
140
+ toast.error(`${agent.name} is disabled. Re-enable it to start a new chat.`)
141
+ return
142
+ }
136
143
  await setCurrentAgent(agent.id)
137
144
  setActiveView('agents')
138
145
  setOpen(false)
@@ -14,14 +14,26 @@ interface Props {
14
14
  title: string
15
15
  message: string
16
16
  confirmLabel?: string
17
+ confirmDisabled?: boolean
18
+ cancelDisabled?: boolean
17
19
  danger?: boolean
18
20
  onConfirm: () => void
19
21
  onCancel: () => void
20
22
  }
21
23
 
22
- export function ConfirmDialog({ open, title, message, confirmLabel = 'Confirm', danger, onConfirm, onCancel }: Props) {
24
+ export function ConfirmDialog({
25
+ open,
26
+ title,
27
+ message,
28
+ confirmLabel = 'Confirm',
29
+ confirmDisabled = false,
30
+ cancelDisabled = false,
31
+ danger,
32
+ onConfirm,
33
+ onCancel,
34
+ }: Props) {
23
35
  return (
24
- <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onCancel() }}>
36
+ <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen && !cancelDisabled) onCancel() }}>
25
37
  <DialogContent
26
38
  className="sm:max-w-[400px] rounded-[20px] border-white/[0.06] bg-raised p-0 shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
27
39
  >
@@ -38,7 +50,8 @@ export function ConfirmDialog({ open, title, message, confirmLabel = 'Confirm',
38
50
  <button
39
51
  type="button"
40
52
  onClick={onCancel}
41
- className="flex-1 rounded-[12px] border border-white/[0.06] bg-transparent px-4 py-2.5 text-[13px] font-600 text-text-2 transition-all duration-200 hover:bg-surface"
53
+ disabled={cancelDisabled}
54
+ className="flex-1 rounded-[12px] border border-white/[0.06] bg-transparent px-4 py-2.5 text-[13px] font-600 text-text-2 transition-all duration-200 hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50"
42
55
  style={{ fontFamily: 'inherit' }}
43
56
  >
44
57
  Cancel
@@ -46,10 +59,12 @@ export function ConfirmDialog({ open, title, message, confirmLabel = 'Confirm',
46
59
  <button
47
60
  type="button"
48
61
  onClick={onConfirm}
62
+ disabled={confirmDisabled}
49
63
  className={`flex-1 rounded-[12px] border-none px-4 py-2.5 text-[13px] font-600 text-white transition-all duration-200 active:scale-[0.98]
50
64
  ${danger
51
65
  ? 'bg-danger shadow-[0_4px_20px_rgba(244,63,94,0.2)]'
52
- : 'bg-accent-bright shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}`}
66
+ : 'bg-accent-bright shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}
67
+ disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100`}
53
68
  style={{ fontFamily: 'inherit' }}
54
69
  >
55
70
  {confirmLabel}
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ getConnectorPlatformLabel,
6
+ resolveConnectorPlatformMeta,
7
+ } from './connector-platform-icon'
8
+
9
+ describe('connector platform metadata', () => {
10
+ it('resolves legacy connector platforms used by stored runtime data', () => {
11
+ assert.deepEqual(resolveConnectorPlatformMeta('webchat'), {
12
+ label: 'Web Chat',
13
+ color: '#0EA5E9',
14
+ })
15
+ assert.deepEqual(resolveConnectorPlatformMeta('mockmail'), {
16
+ label: 'MockMail',
17
+ color: '#7C3AED',
18
+ })
19
+ })
20
+
21
+ it('falls back safely for unknown connector platform strings', () => {
22
+ assert.deepEqual(resolveConnectorPlatformMeta('custom-bridge'), {
23
+ label: 'Custom Bridge',
24
+ color: '#64748B',
25
+ })
26
+ assert.equal(getConnectorPlatformLabel('custom-bridge'), 'Custom Bridge')
27
+ })
28
+ })
@@ -24,10 +24,35 @@ export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string;
24
24
  googlechat: { label: 'Google Chat', color: '#00AC47' },
25
25
  matrix: { label: 'Matrix', color: '#0DBD8B' },
26
26
  email: { label: 'Email', color: '#EA4335' },
27
+ webchat: { label: 'Web Chat', color: '#0EA5E9' },
28
+ mockmail: { label: 'MockMail', color: '#7C3AED' },
27
29
  }
28
30
 
29
- export function getConnectorPlatformLabel(platform: ConnectorPlatform): string {
30
- return CONNECTOR_PLATFORM_META[platform]?.label || platform
31
+ const FALLBACK_CONNECTOR_PLATFORM_META = { label: 'Connector', color: '#64748B' } as const
32
+
33
+ function formatUnknownConnectorPlatformLabel(platform: string): string {
34
+ const trimmed = platform.trim()
35
+ if (!trimmed) return FALLBACK_CONNECTOR_PLATFORM_META.label
36
+ return trimmed
37
+ .replace(/[_-]+/g, ' ')
38
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
39
+ .split(/\s+/)
40
+ .filter(Boolean)
41
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
42
+ .join(' ')
43
+ }
44
+
45
+ export function resolveConnectorPlatformMeta(platform: string): { label: string; color: string } {
46
+ const known = CONNECTOR_PLATFORM_META[platform as ConnectorPlatform]
47
+ if (known) return known
48
+ return {
49
+ label: formatUnknownConnectorPlatformLabel(platform),
50
+ color: FALLBACK_CONNECTOR_PLATFORM_META.color,
51
+ }
52
+ }
53
+
54
+ export function getConnectorPlatformLabel(platform: string): string {
55
+ return resolveConnectorPlatformMeta(platform).label
31
56
  }
32
57
 
33
58
  export function getConnectorIdFromSessionName(sessionName?: string | null): string | null {
@@ -46,7 +71,7 @@ export function getSessionConnector(
46
71
  }
47
72
 
48
73
  interface ConnectorPlatformIconProps {
49
- platform: ConnectorPlatform
74
+ platform: string
50
75
  size?: number
51
76
  className?: string
52
77
  }
@@ -131,12 +156,20 @@ export function ConnectorPlatformIcon({
131
156
  </svg>
132
157
  )
133
158
  default:
134
- return null
159
+ return (
160
+ <span
161
+ aria-hidden
162
+ className={cn('inline-flex items-center justify-center rounded-full font-700 uppercase', className)}
163
+ style={{ width: size, height: size, fontSize: Math.max(8, Math.floor(size * 0.5)), lineHeight: 1 }}
164
+ >
165
+ {getConnectorPlatformLabel(platform).charAt(0)}
166
+ </span>
167
+ )
135
168
  }
136
169
  }
137
170
 
138
171
  interface ConnectorPlatformBadgeProps {
139
- platform: ConnectorPlatform
172
+ platform: string
140
173
  size?: number
141
174
  iconSize?: number
142
175
  className?: string
@@ -152,7 +185,7 @@ export function ConnectorPlatformBadge({
152
185
  roundedClassName = 'rounded-[10px]',
153
186
  title,
154
187
  }: ConnectorPlatformBadgeProps) {
155
- const meta = CONNECTOR_PLATFORM_META[platform]
188
+ const meta = resolveConnectorPlatformMeta(platform)
156
189
  const glyphSize = iconSize ?? Math.max(12, Math.floor(size * 0.52))
157
190
 
158
191
  return (
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState, useCallback, useMemo } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
+ import { getPluginSourceLabel } from '@/lib/plugin-sources'
5
6
  import type { PluginMeta, MarketplacePlugin } from '@/types'
6
7
  import { toast } from 'sonner'
7
8
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
@@ -93,9 +94,12 @@ export function PluginManager() {
93
94
 
94
95
  const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
95
96
 
96
- await api('POST', '/plugins/install', {
97
- url: p.url,
98
- filename: safeFilename
97
+ await api('POST', '/plugins/install', {
98
+ url: p.url,
99
+ filename: safeFilename,
100
+ installMethod: 'marketplace',
101
+ sourceLabel: p.source,
102
+ installSource: p.catalogSource || p.source,
99
103
  })
100
104
 
101
105
  await loadPlugins()
@@ -154,6 +158,18 @@ export function PluginManager() {
154
158
  )}
155
159
  </div>
156
160
  <div className="text-[11px] font-mono text-text-3/40 truncate">{p.filename}</div>
161
+ <div className="flex items-center gap-1.5 mt-1.5 flex-wrap">
162
+ {p.sourceLabel && (
163
+ <span className="text-[9px] font-700 px-1.5 py-0.5 rounded uppercase tracking-wider bg-sky-500/10 text-sky-300">
164
+ {getPluginSourceLabel(p.sourceLabel)}
165
+ </span>
166
+ )}
167
+ {p.installSource && p.installSource !== p.sourceLabel && (
168
+ <span className="text-[9px] font-700 px-1.5 py-0.5 rounded uppercase tracking-wider bg-white/[0.04] text-text-3/65">
169
+ via {getPluginSourceLabel(p.installSource)}
170
+ </span>
171
+ )}
172
+ </div>
157
173
  {p.description && <div className="text-[12px] text-text-3/70 mt-1 line-clamp-1">{p.description}</div>}
158
174
  {p.hasDependencyManifest && (
159
175
  <div className="mt-1.5 flex items-center gap-2 text-[10px] font-700 uppercase tracking-[0.08em]">
@@ -320,9 +336,16 @@ export function PluginManager() {
320
336
  <div className="min-w-0 flex-1">
321
337
  <div className="flex items-center gap-3 mb-1.5">
322
338
  <span className="text-[16px] font-700 text-text tracking-tight">{p.name}</span>
323
- <span className={`text-[9px] font-800 uppercase px-2 py-0.5 rounded-[6px] border ${p.source === 'clawhub' ? 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20' : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'}`}>
324
- {p.source || 'swarmclaw'}
325
- </span>
339
+ {p.source && (
340
+ <span className="text-[9px] font-800 uppercase px-2 py-0.5 rounded-[6px] border bg-sky-500/10 text-sky-300 border-sky-500/20">
341
+ {getPluginSourceLabel(p.source)}
342
+ </span>
343
+ )}
344
+ {p.catalogSource && p.catalogSource !== p.source && (
345
+ <span className="text-[9px] font-800 uppercase px-2 py-0.5 rounded-[6px] border bg-white/[0.04] text-text-3/70 border-white/[0.08]">
346
+ via {getPluginSourceLabel(p.catalogSource)}
347
+ </span>
348
+ )}
326
349
  </div>
327
350
  <p className="text-[13px] text-text-3/80 leading-relaxed mb-4 line-clamp-2">{p.description}</p>
328
351
 
@@ -7,8 +7,10 @@ const APPROVAL_CATEGORY_OPTIONS: Array<{ id: ApprovalCategory; label: string; de
7
7
  { id: 'tool_access', label: 'Plugin Access', description: 'Auto-enable requested plugins for a chat.' },
8
8
  { id: 'plugin_scaffold', label: 'Plugin Scaffold', description: 'Auto-create plugin files requested by agents.' },
9
9
  { id: 'plugin_install', label: 'Plugin Install', description: 'Auto-install plugins from approved URLs.' },
10
+ { id: 'connector_sender', label: 'Connector Senders', description: 'Auto-approve new connector senders and add them to the allowlist.' },
10
11
  { id: 'human_loop', label: 'Human Approval Requests', description: 'Auto-approve ask-human approval prompts.' },
11
12
  { id: 'wallet_transfer', label: 'Wallet Transfers', description: 'Auto-approve wallet send requests. High risk.' },
13
+ { id: 'wallet_action', label: 'Wallet Actions', description: 'Auto-approve wallet signatures and arbitrary transaction requests. Very high risk.' },
12
14
  { id: 'task_tool', label: 'Task Tool Calls', description: 'Reserved for task-level approval flows.' },
13
15
  ]
14
16
 
@@ -93,11 +95,13 @@ export function CapabilityPolicySection({ appSettings, patchSettings, inputClass
93
95
  </p>
94
96
  </div>
95
97
  <button
96
- onClick={() => patchSettings({ approvalsEnabled: !(appSettings.approvalsEnabled ?? true) })}
97
- className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.approvalsEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
98
+ onClick={() => patchSettings({ approvalsEnabled: !(appSettings.approvalsEnabled ?? false) })}
99
+ className={`inline-flex h-[22px] w-10 shrink-0 items-center rounded-full border border-white/[0.08] p-[3px] transition-colors duration-200 cursor-pointer ${
100
+ (appSettings.approvalsEnabled ?? false) ? 'justify-end bg-accent' : 'justify-start bg-white/[0.16]'
101
+ }`}
98
102
  aria-label="Toggle platform approvals"
99
103
  >
100
- <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.approvalsEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
104
+ <span className="h-4 w-4 rounded-full bg-white shadow-[0_1px_4px_rgba(0,0,0,0.35)]" />
101
105
  </button>
102
106
  </div>
103
107
  </div>
@@ -275,6 +275,14 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
275
275
  const scopedAgents = skillScope === 'agent'
276
276
  ? skillAgentIds.map((id) => agents[id]).filter(Boolean)
277
277
  : []
278
+ const securityTone = skill.security?.level === 'high'
279
+ ? 'bg-red-500/10 text-red-300 border-red-500/20'
280
+ : skill.security?.level === 'medium'
281
+ ? 'bg-amber-500/10 text-amber-300 border-amber-500/20'
282
+ : 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20'
283
+ const requirementCount = (skill.skillRequirements?.env?.length || 0)
284
+ + (skill.skillRequirements?.bins?.length || 0)
285
+ + (skill.skillRequirements?.config?.length || 0)
278
286
  return (
279
287
  <div
280
288
  key={skill.id}
@@ -315,6 +323,23 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
315
323
  {skill.description && (
316
324
  <p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
317
325
  )}
326
+ <div className="mt-2 flex flex-wrap gap-1.5">
327
+ {skill.version && (
328
+ <span className="rounded-full border border-white/[0.08] px-2 py-1 text-[10px] font-700 text-text-3/70">
329
+ v{skill.version}
330
+ </span>
331
+ )}
332
+ {typeof requirementCount === 'number' && requirementCount > 0 && (
333
+ <span className="rounded-full border border-white/[0.08] px-2 py-1 text-[10px] font-700 text-text-3/70">
334
+ {requirementCount} reqs
335
+ </span>
336
+ )}
337
+ {skill.security && (
338
+ <span className={`rounded-full border px-2 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${securityTone}`}>
339
+ {skill.security.level}
340
+ </span>
341
+ )}
342
+ </div>
318
343
  <div className="flex items-center gap-2 mt-1.5">
319
344
  <span className="text-[11px] text-text-3/70">{skill.content.length} chars</span>
320
345
  <span className="text-[11px] text-text-3/60">·</span>
@@ -1,11 +1,14 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useRef } from 'react'
3
+ import { useEffect, useState, useRef, type ChangeEvent } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
6
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
7
8
  import { api } from '@/lib/api-client'
9
+ import { buildSkillSavePayload } from '@/lib/skill-save-payload'
8
10
  import { toast } from 'sonner'
11
+ import type { Skill, SkillSecuritySummary } from '@/types'
9
12
 
10
13
  export function SkillSheet() {
11
14
  const open = useAppStore((s) => s.skillSheetOpen)
@@ -28,6 +31,9 @@ export function SkillSheet() {
28
31
  const [importingUrl, setImportingUrl] = useState(false)
29
32
  const [importError, setImportError] = useState('')
30
33
  const [importNotice, setImportNotice] = useState('')
34
+ const [metadataPreview, setMetadataPreview] = useState<Partial<Skill> | null>(null)
35
+ const [confirmDelete, setConfirmDelete] = useState(false)
36
+ const [deleting, setDeleting] = useState(false)
31
37
 
32
38
  const editing = editingId ? skills[editingId] : null
33
39
  const agentList = Object.values(agents)
@@ -38,13 +44,14 @@ export function SkillSheet() {
38
44
  setImportError('')
39
45
  setImportNotice('')
40
46
  try {
41
- const result = await api<{ name: string; filename: string; description?: string; content: string; sourceFormat?: 'openclaw' | 'plain' }>('POST', '/skills/import', { url: importUrl.trim() })
47
+ const result = await api<Partial<Skill> & { name: string; filename: string; description?: string; content: string; sourceFormat?: 'openclaw' | 'plain' }>('POST', '/skills/import', { url: importUrl.trim() })
42
48
  setName(result.name || '')
43
49
  setFilename(result.filename || '')
44
50
  setDescription(result.description || '')
45
51
  setContent(result.content || '')
52
+ setMetadataPreview(result)
46
53
  if (result.sourceFormat === 'openclaw') {
47
- setImportNotice('Imported OpenClaw SKILL.md format and stripped frontmatter automatically.')
54
+ setImportNotice(`Imported OpenClaw SKILL.md format.${result.security ? ` Security review: ${result.security.level}.` : ''}`)
48
55
  } else {
49
56
  setImportNotice('Skill imported from URL.')
50
57
  }
@@ -73,6 +80,7 @@ export function SkillSheet() {
73
80
  setContent(editing.content)
74
81
  setScope(editing.scope || 'global')
75
82
  setAgentIds(editing.agentIds || [])
83
+ setMetadataPreview(editing)
76
84
  } else {
77
85
  setName('')
78
86
  setFilename('')
@@ -80,16 +88,19 @@ export function SkillSheet() {
80
88
  setContent('')
81
89
  setScope('global')
82
90
  setAgentIds([])
91
+ setMetadataPreview(null)
83
92
  }
84
93
  }
85
94
  }, [open, editingId])
86
95
 
87
96
  const onClose = () => {
97
+ setConfirmDelete(false)
98
+ setDeleting(false)
88
99
  setOpen(false)
89
100
  setEditingId(null)
90
101
  }
91
102
 
92
- const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
103
+ const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
93
104
  const file = e.target.files?.[0]
94
105
  if (!file) return
95
106
  const reader = new FileReader()
@@ -114,14 +125,14 @@ export function SkillSheet() {
114
125
  : `${agentIds.length} agent(s) selected`
115
126
 
116
127
  const handleSave = async () => {
117
- const data = {
118
- name: name.trim() || 'Unnamed Skill',
119
- filename: filename.trim() || `${name.trim().toLowerCase().replace(/\s+/g, '-')}.md`,
128
+ const data = buildSkillSavePayload({
129
+ name,
130
+ filename,
120
131
  description,
121
132
  content,
122
133
  scope,
123
- agentIds: scope === 'agent' ? agentIds : [],
124
- }
134
+ agentIds,
135
+ }, metadataPreview)
125
136
  try {
126
137
  if (editing) {
127
138
  await api('PUT', `/skills/${editing.id}`, data)
@@ -139,19 +150,25 @@ export function SkillSheet() {
139
150
 
140
151
  const handleDelete = async () => {
141
152
  if (!editing) return
142
- if (!confirm(`Delete skill "${editing.name}"? This will remove it from all assigned agents.`)) return
143
-
153
+ setDeleting(true)
144
154
  try {
145
155
  await api('DELETE', `/skills/${editing.id}`)
146
156
  toast.success('Skill deleted')
147
157
  await loadSkills()
158
+ setConfirmDelete(false)
148
159
  onClose()
149
160
  } catch (err: unknown) {
150
161
  toast.error(err instanceof Error ? err.message : 'Failed to delete skill')
162
+ } finally {
163
+ setDeleting(false)
151
164
  }
152
165
  }
153
166
 
154
167
  const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
168
+ const previewSecurity = metadataPreview?.security as SkillSecuritySummary | undefined
169
+ const requirementCount = (metadataPreview?.skillRequirements?.env?.length || 0)
170
+ + (metadataPreview?.skillRequirements?.bins?.length || 0)
171
+ + (metadataPreview?.skillRequirements?.config?.length || 0)
155
172
 
156
173
  return (
157
174
  <BottomSheet open={open} onClose={onClose} wide>
@@ -206,6 +223,50 @@ export function SkillSheet() {
206
223
  </div>
207
224
  )}
208
225
 
226
+ {metadataPreview && (
227
+ <div className="mb-8 rounded-[14px] border border-white/[0.08] bg-white/[0.03] p-4">
228
+ <div className="flex items-center justify-between gap-3">
229
+ <div>
230
+ <div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/60">Skill Metadata</div>
231
+ <p className="mt-1 text-[13px] text-text-2">
232
+ {metadataPreview.version ? `v${metadataPreview.version}` : 'Unversioned'}
233
+ {metadataPreview.sourceFormat ? ` · ${metadataPreview.sourceFormat}` : ''}
234
+ {requirementCount > 0 ? ` · ${requirementCount} declared requirement${requirementCount === 1 ? '' : 's'}` : ''}
235
+ </p>
236
+ </div>
237
+ {previewSecurity && (
238
+ <span className={`rounded-full px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${
239
+ previewSecurity.level === 'high'
240
+ ? 'bg-red-500/10 text-red-300 border border-red-500/20'
241
+ : previewSecurity.level === 'medium'
242
+ ? 'bg-amber-500/10 text-amber-300 border border-amber-500/20'
243
+ : 'bg-emerald-500/10 text-emerald-300 border border-emerald-500/20'
244
+ }`}>
245
+ {previewSecurity.level} risk
246
+ </span>
247
+ )}
248
+ </div>
249
+
250
+ {(metadataPreview.primaryEnv || metadataPreview.homepage || metadataPreview.skillKey) && (
251
+ <div className="mt-3 flex flex-wrap gap-2 text-[11px] text-text-3/70">
252
+ {metadataPreview.primaryEnv && <span className="rounded-full border border-white/[0.08] px-2 py-1">Primary env: {metadataPreview.primaryEnv}</span>}
253
+ {metadataPreview.skillKey && <span className="rounded-full border border-white/[0.08] px-2 py-1">Skill key: {metadataPreview.skillKey}</span>}
254
+ {metadataPreview.homepage && <span className="rounded-full border border-white/[0.08] px-2 py-1">Homepage linked</span>}
255
+ </div>
256
+ )}
257
+
258
+ {previewSecurity?.notes?.length ? (
259
+ <div className="mt-3 space-y-1">
260
+ {previewSecurity.notes.slice(0, 4).map((note) => (
261
+ <p key={note} className="text-[12px] text-text-3/75">- {note}</p>
262
+ ))}
263
+ </div>
264
+ ) : (
265
+ <p className="mt-3 text-[12px] text-text-3/65">No obvious requirement or security signals were detected.</p>
266
+ )}
267
+ </div>
268
+ )}
269
+
209
270
  <div className="mb-8">
210
271
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
211
272
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Frontend Design" className={inputClass} style={{ fontFamily: 'inherit' }} />
@@ -285,7 +346,7 @@ export function SkillSheet() {
285
346
 
286
347
  <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
287
348
  {editing && (
288
- <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
349
+ <button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
289
350
  Delete
290
351
  </button>
291
352
  )}
@@ -296,6 +357,17 @@ export function SkillSheet() {
296
357
  {editing ? 'Save' : 'Create'}
297
358
  </button>
298
359
  </div>
360
+ <ConfirmDialog
361
+ open={confirmDelete}
362
+ title="Delete Skill?"
363
+ message={editing ? `Delete "${editing.name}"? This will remove it from all assigned agents.` : 'Delete this skill?'}
364
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
365
+ confirmDisabled={deleting}
366
+ cancelDisabled={deleting}
367
+ danger
368
+ onConfirm={() => { void handleDelete() }}
369
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
370
+ />
299
371
  </BottomSheet>
300
372
  )
301
373
  }