@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -4,6 +4,7 @@ import {
4
4
  DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
5
5
  DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
6
6
  DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
7
+ DEFAULT_DELEGATION_MAX_DEPTH,
7
8
  DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
8
9
  DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
9
10
  DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
@@ -12,6 +13,7 @@ import {
12
13
  } from '@/lib/runtime-loop'
13
14
  import type { LoopMode } from '@/types'
14
15
  import type { SettingsSectionProps } from './types'
16
+ import { HintTip } from '@/components/shared/hint-tip'
15
17
 
16
18
  export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
17
19
  const loopMode: LoopMode = appSettings.loopMode === 'ongoing' ? 'ongoing' : 'bounded'
@@ -25,7 +27,7 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
25
27
  Choose bounded or ongoing agent loops and set safety guards for task execution.
26
28
  </p>
27
29
  <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
28
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Loop Mode</label>
30
+ <label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Loop Mode <HintTip text="Bounded = fixed max steps. Ongoing = runs until the task completes (with a safety cap)" /></label>
29
31
  <div className="grid grid-cols-2 gap-2 mb-5">
30
32
  {([
31
33
  { id: 'bounded' as const, name: 'Bounded' },
@@ -48,7 +50,7 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
48
50
  {loopMode === 'bounded' ? (
49
51
  <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
50
52
  <div>
51
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Agent Steps</label>
53
+ <label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Agent Steps <HintTip text="Maximum actions an agent can take before stopping — prevents infinite loops" /></label>
52
54
  <input
53
55
  type="number"
54
56
  min={1}
@@ -63,7 +65,7 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
63
65
  />
64
66
  </div>
65
67
  <div>
66
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Orchestrator Steps</label>
68
+ <label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Orchestrator Steps <HintTip text="Maximum tool calls the orchestrator can make when coordinating multiple agents" /></label>
67
69
  <input
68
70
  type="number"
69
71
  min={1}
@@ -78,7 +80,7 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
78
80
  />
79
81
  </div>
80
82
  <div>
81
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Legacy Turns</label>
83
+ <label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Legacy Turns <HintTip text="Max conversation turns for older orchestration mode — increase if agents get cut off mid-task" /></label>
82
84
  <input
83
85
  type="number"
84
86
  min={1}
@@ -129,6 +131,24 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
129
131
  </div>
130
132
  )}
131
133
 
134
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
135
+ <div>
136
+ <label className="flex items-center gap-1.5 font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Delegation Depth <HintTip text="Maximum delegation chain depth for delegate_to_agent and spawn_subagent to prevent runaway fan-out" /></label>
137
+ <input
138
+ type="number"
139
+ min={1}
140
+ max={12}
141
+ value={appSettings.delegationMaxDepth ?? DEFAULT_DELEGATION_MAX_DEPTH}
142
+ onChange={(e) => {
143
+ const n = Number.parseInt(e.target.value, 10)
144
+ patchSettings({ delegationMaxDepth: Number.isFinite(n) ? n : DEFAULT_DELEGATION_MAX_DEPTH })
145
+ }}
146
+ className={inputClass}
147
+ style={{ fontFamily: 'inherit' }}
148
+ />
149
+ </div>
150
+ </div>
151
+
132
152
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Execution Timeouts (Seconds)</label>
133
153
  <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
134
154
  <div>
@@ -177,6 +197,131 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
177
197
  />
178
198
  </div>
179
199
  </div>
200
+
201
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mt-6 mb-3">LLM Response Cache</label>
202
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
203
+ <div className="md:col-span-3 flex items-center gap-3">
204
+ <button
205
+ onClick={() => patchSettings({ responseCacheEnabled: !(appSettings.responseCacheEnabled ?? true) })}
206
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.responseCacheEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
207
+ >
208
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.responseCacheEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
209
+ </button>
210
+ <span className="text-[12px] text-text-2">Enable deterministic cache (TTL + LRU) for non-tool model responses</span>
211
+ </div>
212
+ <div>
213
+ <label className="block text-[11px] text-text-3 mb-2">TTL (seconds)</label>
214
+ <input
215
+ type="number"
216
+ min={5}
217
+ max={604800}
218
+ value={appSettings.responseCacheTtlSec ?? 900}
219
+ onChange={(e) => {
220
+ const n = Number.parseInt(e.target.value, 10)
221
+ patchSettings({ responseCacheTtlSec: Number.isFinite(n) ? n : 900 })
222
+ }}
223
+ className={inputClass}
224
+ style={{ fontFamily: 'inherit' }}
225
+ />
226
+ </div>
227
+ <div>
228
+ <label className="block text-[11px] text-text-3 mb-2">Max Entries</label>
229
+ <input
230
+ type="number"
231
+ min={1}
232
+ max={20000}
233
+ value={appSettings.responseCacheMaxEntries ?? 500}
234
+ onChange={(e) => {
235
+ const n = Number.parseInt(e.target.value, 10)
236
+ patchSettings({ responseCacheMaxEntries: Number.isFinite(n) ? n : 500 })
237
+ }}
238
+ className={inputClass}
239
+ style={{ fontFamily: 'inherit' }}
240
+ />
241
+ </div>
242
+ </div>
243
+
244
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Task Quality Gate Defaults</label>
245
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-5">
246
+ <div className="md:col-span-3 flex items-center gap-3">
247
+ <button
248
+ onClick={() => patchSettings({ taskQualityGateEnabled: !(appSettings.taskQualityGateEnabled ?? true) })}
249
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.taskQualityGateEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
250
+ >
251
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.taskQualityGateEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
252
+ </button>
253
+ <span className="text-[12px] text-text-2">Enable quality gate checks before tasks can be marked complete</span>
254
+ </div>
255
+ <div>
256
+ <label className="block text-[11px] text-text-3 mb-2">Min Result Chars</label>
257
+ <input
258
+ type="number"
259
+ min={10}
260
+ max={2000}
261
+ value={appSettings.taskQualityGateMinResultChars ?? 80}
262
+ onChange={(e) => {
263
+ const n = Number.parseInt(e.target.value, 10)
264
+ patchSettings({ taskQualityGateMinResultChars: Number.isFinite(n) ? n : 80 })
265
+ }}
266
+ className={inputClass}
267
+ style={{ fontFamily: 'inherit' }}
268
+ />
269
+ </div>
270
+ <div>
271
+ <label className="block text-[11px] text-text-3 mb-2">Min Evidence Signals</label>
272
+ <input
273
+ type="number"
274
+ min={0}
275
+ max={8}
276
+ value={appSettings.taskQualityGateMinEvidenceItems ?? 2}
277
+ onChange={(e) => {
278
+ const n = Number.parseInt(e.target.value, 10)
279
+ patchSettings({ taskQualityGateMinEvidenceItems: Number.isFinite(n) ? n : 2 })
280
+ }}
281
+ className={inputClass}
282
+ style={{ fontFamily: 'inherit' }}
283
+ />
284
+ </div>
285
+ <div className="md:col-span-3 grid grid-cols-1 md:grid-cols-3 gap-2">
286
+ <label className="flex items-center gap-2 text-[12px] text-text-2">
287
+ <input
288
+ type="checkbox"
289
+ checked={appSettings.taskQualityGateRequireVerification ?? false}
290
+ onChange={(e) => patchSettings({ taskQualityGateRequireVerification: e.target.checked })}
291
+ />
292
+ Require verification evidence
293
+ </label>
294
+ <label className="flex items-center gap-2 text-[12px] text-text-2">
295
+ <input
296
+ type="checkbox"
297
+ checked={appSettings.taskQualityGateRequireArtifact ?? false}
298
+ onChange={(e) => patchSettings({ taskQualityGateRequireArtifact: e.target.checked })}
299
+ />
300
+ Require artifact evidence
301
+ </label>
302
+ <label className="flex items-center gap-2 text-[12px] text-text-2">
303
+ <input
304
+ type="checkbox"
305
+ checked={appSettings.taskQualityGateRequireReport ?? false}
306
+ onChange={(e) => patchSettings({ taskQualityGateRequireReport: e.target.checked })}
307
+ />
308
+ Require task report
309
+ </label>
310
+ </div>
311
+ </div>
312
+
313
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Integrity Monitor</label>
314
+ <div className="flex items-center gap-3">
315
+ <button
316
+ onClick={() => patchSettings({ integrityMonitorEnabled: !(appSettings.integrityMonitorEnabled ?? true) })}
317
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.integrityMonitorEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
318
+ >
319
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.integrityMonitorEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
320
+ </button>
321
+ <span className="text-[12px] text-text-2">
322
+ Watch critical identity/config files for drift and raise alerts.
323
+ </span>
324
+ </div>
180
325
  </div>
181
326
  </div>
182
327
  )
@@ -23,6 +23,7 @@ interface SearchResponse {
23
23
  skills: ClawHubSkill[]
24
24
  total: number
25
25
  page: number
26
+ nextCursor?: string | null
26
27
  }
27
28
 
28
29
  interface ClawHubBrowserProps {
@@ -163,10 +163,14 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
163
163
 
164
164
  {hubSkills.length > 0 && (
165
165
  <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
166
- {hubSkills.map((skill) => (
166
+ {hubSkills.map((skill, idx) => (
167
167
  <div
168
168
  key={skill.id}
169
- className="p-4 rounded-[14px] border border-white/[0.06] bg-surface"
169
+ className="p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:border-white/[0.12] transition-all hover:scale-[1.01]"
170
+ style={{
171
+ animation: 'spring-in 0.5s var(--ease-spring) both',
172
+ animationDelay: `${idx * 0.03}s`
173
+ }}
170
174
  >
171
175
  <div className="flex items-start justify-between gap-2">
172
176
  <div className="min-w-0 flex-1">
@@ -225,21 +229,22 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
225
229
  <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
226
230
  {/* Sidebar: ClawHub button + Sheet */}
227
231
  {inSidebar && (
228
- <>
232
+ <div style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
229
233
  <button
230
234
  onClick={() => setClawHubOpen(true)}
231
- className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent"
235
+ className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent relative overflow-hidden group/hub"
232
236
  style={{ fontFamily: 'inherit' }}
233
237
  >
234
- Browse ClawHub Skills
238
+ <span className="relative z-10">Browse ClawHub Skills</span>
239
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover/hub:animate-[shimmer-bar_2s_infinite]" />
235
240
  </button>
236
241
  <ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
237
- </>
242
+ </div>
238
243
  )}
239
244
 
240
245
  {/* Full-width: tabs */}
241
246
  {!inSidebar && (
242
- <div className="flex gap-1 mb-4">
247
+ <div className="flex gap-1 mb-4" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
243
248
  <button onClick={() => setTab('skills')} className={tabClass('skills')} style={{ fontFamily: 'inherit' }}>
244
249
  My Skills
245
250
  </button>
@@ -251,7 +256,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
251
256
 
252
257
  {(!inSidebar && tab === 'clawhub') ? renderClawHub() : (
253
258
  skillList.length === 0 ? (
254
- <div className="text-center py-12">
259
+ <div className="text-center py-12" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
255
260
  <p className="text-[13px] text-text-3/60">No skills yet</p>
256
261
  <button
257
262
  onClick={() => setSkillSheetOpen(true)}
@@ -263,7 +268,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
263
268
  </div>
264
269
  ) : (
265
270
  <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
266
- {skillList.map((skill) => {
271
+ {skillList.map((skill, idx) => {
267
272
  const skillScope = skill.scope || 'global'
268
273
  const skillAgentIds = skill.agentIds || []
269
274
  const scopeLabel = skillScope === 'global' ? 'Global' : `${skillAgentIds.length} agent(s)`
@@ -271,10 +276,23 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
271
276
  ? skillAgentIds.map((id) => agents[id]).filter(Boolean)
272
277
  : []
273
278
  return (
274
- <button
279
+ <div
275
280
  key={skill.id}
276
281
  onClick={() => handleEdit(skill.id)}
277
- className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
282
+ onKeyDown={(e) => {
283
+ if (e.key === 'Enter' || e.key === ' ') {
284
+ e.preventDefault()
285
+ handleEdit(skill.id)
286
+ }
287
+ }}
288
+ role="button"
289
+ tabIndex={0}
290
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer hover:border-white/[0.12] hover:scale-[1.01]"
291
+ style={{
292
+ fontFamily: 'inherit',
293
+ animation: 'spring-in 0.5s var(--ease-spring) both',
294
+ animationDelay: `${idx * 0.05}s`
295
+ }}
278
296
  >
279
297
  <div className="flex items-center justify-between mb-1">
280
298
  <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
@@ -282,6 +300,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
282
300
  <span className="text-[10px] font-mono text-text-3/50">{skill.filename}</span>
283
301
  {!inSidebar && (
284
302
  <button
303
+ type="button"
285
304
  onClick={(e) => handleDelete(e, skill.id)}
286
305
  className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
287
306
  title="Delete"
@@ -317,7 +336,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
317
336
  )}
318
337
  </div>
319
338
  )}
320
- </button>
339
+ </div>
321
340
  )
322
341
  })}
323
342
  </div>
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { AgentAvatar } from '@/components/agents/agent-avatar'
7
7
  import { api } from '@/lib/api-client'
8
+ import { toast } from 'sonner'
8
9
 
9
10
  export function SkillSheet() {
10
11
  const open = useAppStore((s) => s.skillSheetOpen)
@@ -121,20 +122,32 @@ export function SkillSheet() {
121
122
  scope,
122
123
  agentIds: scope === 'agent' ? agentIds : [],
123
124
  }
124
- if (editing) {
125
- await api('PUT', `/skills/${editing.id}`, data)
126
- } else {
127
- await api('POST', '/skills', data)
125
+ try {
126
+ if (editing) {
127
+ await api('PUT', `/skills/${editing.id}`, data)
128
+ toast.success('Skill updated successfully')
129
+ } else {
130
+ await api('POST', '/skills', data)
131
+ toast.success('Skill created successfully')
132
+ }
133
+ await loadSkills()
134
+ onClose()
135
+ } catch (err: unknown) {
136
+ toast.error(err instanceof Error ? err.message : 'Failed to save skill')
128
137
  }
129
- await loadSkills()
130
- onClose()
131
138
  }
132
139
 
133
140
  const handleDelete = async () => {
134
- if (editing) {
141
+ if (!editing) return
142
+ if (!confirm(`Delete skill "${editing.name}"? This will remove it from all assigned agents.`)) return
143
+
144
+ try {
135
145
  await api('DELETE', `/skills/${editing.id}`)
146
+ toast.success('Skill deleted')
136
147
  await loadSkills()
137
148
  onClose()
149
+ } catch (err: unknown) {
150
+ toast.error(err instanceof Error ? err.message : 'Failed to delete skill')
138
151
  }
139
152
  }
140
153
 
@@ -0,0 +1,224 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { useApprovalStore } from '@/stores/use-approval-store'
6
+ import { api } from '@/lib/api-client'
7
+ import { toast } from 'sonner'
8
+ import { useWs } from '@/hooks/use-ws'
9
+ import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
10
+ import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
11
+ import type { ApprovalRequest } from '@/types'
12
+
13
+ const CATEGORY_LABELS: Record<string, string> = {
14
+ tool_access: 'Plugin Access',
15
+ wallet_transfer: 'Wallet Transfer',
16
+ plugin_scaffold: 'Plugin Creation',
17
+ plugin_install: 'Plugin Install',
18
+ task_tool: 'Task Plugin Call',
19
+ }
20
+
21
+ const CATEGORY_ICONS: Record<string, string> = {
22
+ tool_access: '🔑',
23
+ wallet_transfer: '💰',
24
+ plugin_scaffold: '🔌',
25
+ plugin_install: '📦',
26
+ task_tool: '🤖',
27
+ }
28
+
29
+ export function ApprovalsPanel() {
30
+ const tasks = useAppStore((s) => s.tasks)
31
+ const agents = useAppStore((s) => s.agents)
32
+ const serverApprovals = useAppStore((s) => s.approvals)
33
+ const loadTasks = useAppStore((s) => s.loadTasks)
34
+ const loadServerApprovals = useAppStore((s) => s.loadApprovals)
35
+
36
+ const execApprovals = useApprovalStore((s) => s.approvals)
37
+ const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
38
+ const pruneExecApprovals = useApprovalStore((s) => s.pruneExpired)
39
+
40
+ const refreshServerApprovals = useCallback(() => {
41
+ void loadServerApprovals()
42
+ }, [loadServerApprovals])
43
+
44
+ const refreshExecApprovals = useCallback(() => {
45
+ void loadExecApprovals()
46
+ pruneExecApprovals()
47
+ }, [loadExecApprovals, pruneExecApprovals])
48
+
49
+ useEffect(() => {
50
+ refreshServerApprovals()
51
+ refreshExecApprovals()
52
+ const interval = setInterval(() => {
53
+ refreshServerApprovals()
54
+ refreshExecApprovals()
55
+ }, 5000)
56
+ return () => clearInterval(interval)
57
+ }, [refreshServerApprovals, refreshExecApprovals])
58
+
59
+ useWs('approvals', refreshServerApprovals, 5000)
60
+ useWs('openclaw:approvals', refreshExecApprovals, 5000)
61
+
62
+ const taskApprovals = useMemo(() => {
63
+ return Object.values(tasks)
64
+ .filter((t) => t.pendingApproval)
65
+ .map((t) => ({
66
+ id: t.id,
67
+ category: 'task_tool' as const,
68
+ agentId: t.agentId,
69
+ sessionId: null,
70
+ taskId: t.id,
71
+ title: `Task Plugin Call: ${t.pendingApproval?.toolName || 'unknown'}`,
72
+ description: t.title,
73
+ data: t.pendingApproval?.args ?? {},
74
+ createdAt: t.updatedAt,
75
+ updatedAt: t.updatedAt,
76
+ status: 'pending' as const,
77
+ }))
78
+ }, [tasks])
79
+
80
+ const sessionApprovals = useMemo(() => {
81
+ return Object.values(serverApprovals)
82
+ .filter((a) => a.status === 'pending')
83
+ .sort((a, b) => b.updatedAt - a.updatedAt)
84
+ }, [serverApprovals])
85
+
86
+ const workflowApprovals = useMemo(() => {
87
+ return [...sessionApprovals, ...taskApprovals].sort((a, b) => b.updatedAt - a.updatedAt)
88
+ }, [sessionApprovals, taskApprovals])
89
+
90
+ const sortedExecApprovals = useMemo(() => {
91
+ return Object.values(execApprovals).sort((a, b) => b.createdAtMs - a.createdAtMs)
92
+ }, [execApprovals])
93
+
94
+ const pendingCount = sortedExecApprovals.length + workflowApprovals.length
95
+
96
+ const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
97
+ try {
98
+ if (req.category === 'task_tool') {
99
+ await api('POST', `/tasks/${req.id}/approve`, { approved })
100
+ void loadTasks()
101
+ } else {
102
+ await api('POST', '/approvals', { id: req.id, approved })
103
+ refreshServerApprovals()
104
+ }
105
+ toast.success(approved ? 'Action approved' : 'Action rejected')
106
+ } catch (err: unknown) {
107
+ toast.error(err instanceof Error ? err.message : 'Failed to submit decision')
108
+ }
109
+ }
110
+
111
+ if (pendingCount === 0) {
112
+ return (
113
+ <div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
114
+ <div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
115
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
116
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
117
+ <path d="m9 12 2 2 4-4"/>
118
+ </svg>
119
+ </div>
120
+ <h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
121
+ <p className="text-[13px] text-text-3/60 max-w-[320px]">
122
+ Your swarm is operating autonomously. Actions requiring oversight will appear here.
123
+ </p>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <div className="flex-1 overflow-y-auto px-6 py-8">
130
+ <div className="max-w-3xl mx-auto">
131
+ <div className="flex items-center justify-between mb-8">
132
+ <div>
133
+ <h1 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-1">Approvals</h1>
134
+ <p className="text-[13px] text-text-3">Execution and plugin governance requests pending review</p>
135
+ </div>
136
+ <div className="px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-[11px] font-600">
137
+ {pendingCount} Pending
138
+ </div>
139
+ </div>
140
+
141
+ {sortedExecApprovals.length > 0 && (
142
+ <div className="mb-6">
143
+ <h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Execution Approvals</h2>
144
+ <div className="grid grid-cols-1 gap-3">
145
+ {sortedExecApprovals.map((approval) => (
146
+ <ExecApprovalCard key={approval.id} approval={approval} />
147
+ ))}
148
+ </div>
149
+ </div>
150
+ )}
151
+
152
+ {workflowApprovals.length > 0 && (
153
+ <div>
154
+ <h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Plugin Workflow Approvals</h2>
155
+ <div className="grid grid-cols-1 gap-4">
156
+ {workflowApprovals.map((req) => {
157
+ const agent = req.agentId ? agents[req.agentId] : null
158
+ const icon = CATEGORY_ICONS[req.category] || '⚠️'
159
+ const categoryLabel = CATEGORY_LABELS[req.category] || req.category
160
+ const payload = getApprovalPayload(req)
161
+ const payloadText = JSON.stringify(payload, null, 2)
162
+
163
+ return (
164
+ <div key={req.id} className="bg-surface rounded-[16px] border border-white/[0.06] overflow-hidden">
165
+ <div className="px-5 py-3 border-b border-white/[0.04] flex items-center justify-between bg-surface-2/50">
166
+ <div className="flex items-center gap-3">
167
+ <div className="w-8 h-8 rounded-[8px] bg-white/[0.04] flex items-center justify-center">
168
+ <span className="text-[14px]">{icon}</span>
169
+ </div>
170
+ <div>
171
+ <div className="flex items-center gap-2">
172
+ <h3 className="text-[13px] font-600 text-text">{getApprovalTitle(req)}</h3>
173
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.04] text-[9px] font-600 text-text-3/60 uppercase tracking-wider">
174
+ {categoryLabel}
175
+ </span>
176
+ </div>
177
+ <p className="text-[11px] text-text-3">
178
+ {agent?.name || 'System'}
179
+ </p>
180
+ </div>
181
+ </div>
182
+ <span className="text-[10px] text-text-3/50 font-mono">
183
+ {new Date(req.updatedAt).toLocaleString()}
184
+ </span>
185
+ </div>
186
+
187
+ <div className="p-5">
188
+ {req.description && (
189
+ <p className="text-[13px] text-text-2/90 mb-4">{req.description}</p>
190
+ )}
191
+
192
+ <div className="bg-black/30 rounded-[10px] border border-white/[0.04] p-4 mb-5 overflow-x-auto max-h-[250px] overflow-y-auto">
193
+ <pre className="text-[12px] font-mono text-text-2/80 whitespace-pre-wrap break-all leading-relaxed">
194
+ {payloadText === '{}' ? 'No structured payload provided.' : payloadText}
195
+ </pre>
196
+ </div>
197
+
198
+ <div className="flex items-center justify-end gap-3 pt-4 border-t border-white/[0.04]">
199
+ <button
200
+ onClick={() => handleDecision(req, false)}
201
+ className="px-5 py-2 rounded-[10px] bg-transparent border border-red-500/30 text-red-400 text-[12px] font-600 hover:bg-red-500/10 transition-colors cursor-pointer"
202
+ style={{ fontFamily: 'inherit' }}
203
+ >
204
+ Reject
205
+ </button>
206
+ <button
207
+ onClick={() => handleDecision(req, true)}
208
+ className="px-5 py-2 rounded-[10px] bg-emerald-500 border border-emerald-400 text-[#000] text-[12px] font-700 hover:brightness-110 transition-all shadow-[0_0_15px_rgba(16,185,129,0.3)] cursor-pointer"
209
+ style={{ fontFamily: 'inherit' }}
210
+ >
211
+ Approve
212
+ </button>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )
217
+ })}
218
+ </div>
219
+ </div>
220
+ )}
221
+ </div>
222
+ </div>
223
+ )
224
+ }
@@ -231,7 +231,7 @@ export function TaskBoard() {
231
231
  }, [selectionMode])
232
232
 
233
233
  return (
234
- <div className="flex-1 flex flex-col h-full overflow-hidden">
234
+ <div className="flex-1 min-h-0 flex flex-col h-full overflow-hidden">
235
235
  <div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
236
236
  <div>
237
237
  <h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Task Board</h1>
@@ -399,7 +399,7 @@ export function TaskBoard() {
399
399
  </div>
400
400
  )}
401
401
 
402
- <div className="flex-1 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden">
402
+ <div className="flex-1 min-h-0 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x">
403
403
  {!loaded ? (
404
404
  ACTIVE_COLUMNS.map((status) => (
405
405
  <div key={status} className="flex flex-col gap-3 min-w-[260px] flex-1">
@@ -410,17 +410,25 @@ export function TaskBoard() {
410
410
  </div>
411
411
  ))
412
412
  ) : (
413
- columns.map((status) => (
414
- <TaskColumn
413
+ columns.map((status, idx) => (
414
+ <div
415
415
  key={status}
416
- status={status}
417
- tasks={tasksByStatus(status)}
418
- onDrop={handleDrop}
419
- selectionMode={selectionMode}
420
- selectedIds={selectedIds}
421
- onToggleSelect={toggleSelect}
422
- onSelectAll={() => selectAllInColumn(status)}
423
- />
416
+ className="flex flex-col gap-3 min-w-[260px] flex-1"
417
+ style={{
418
+ animation: 'fade-up 0.6s var(--ease-spring) both',
419
+ animationDelay: `${idx * 0.1}s`
420
+ }}
421
+ >
422
+ <TaskColumn
423
+ status={status}
424
+ tasks={tasksByStatus(status)}
425
+ onDrop={handleDrop}
426
+ selectionMode={selectionMode}
427
+ selectedIds={selectedIds}
428
+ onToggleSelect={toggleSelect}
429
+ onSelectAll={() => selectAllInColumn(status)}
430
+ />
431
+ </div>
424
432
  ))
425
433
  )}
426
434
  </div>