@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -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,
@@ -130,6 +131,24 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
130
131
  </div>
131
132
  )}
132
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
+
133
152
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Execution Timeouts (Seconds)</label>
134
153
  <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
135
154
  <div>
@@ -178,6 +197,131 @@ export function RuntimeLoopSection({ appSettings, patchSettings, inputClass }: S
178
197
  />
179
198
  </div>
180
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>
181
325
  </div>
182
326
  </div>
183
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
 
@@ -1,33 +1,114 @@
1
1
  'use client'
2
2
 
3
- import { useMemo } from 'react'
3
+ import { useCallback, useEffect, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useApprovalStore } from '@/stores/use-approval-store'
5
6
  import { api } from '@/lib/api-client'
6
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'
7
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
+ }
8
28
 
9
29
  export function ApprovalsPanel() {
10
30
  const tasks = useAppStore((s) => s.tasks)
11
31
  const agents = useAppStore((s) => s.agents)
32
+ const serverApprovals = useAppStore((s) => s.approvals)
12
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])
13
58
 
14
- const pendingApprovals = useMemo(() => {
59
+ useWs('approvals', refreshServerApprovals, 5000)
60
+ useWs('openclaw:approvals', refreshExecApprovals, 5000)
61
+
62
+ const taskApprovals = useMemo(() => {
15
63
  return Object.values(tasks)
16
64
  .filter((t) => t.pendingApproval)
17
- .sort((a, b) => b.updatedAt - a.updatedAt)
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
+ }))
18
78
  }, [tasks])
19
79
 
20
- const handleDecision = async (taskId: string, approved: boolean) => {
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) => {
21
97
  try {
22
- await api('POST', `/tasks/${taskId}/approve`, { approved })
23
- toast.success(approved ? 'Tool execution approved' : 'Tool execution rejected')
24
- loadTasks()
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')
25
106
  } catch (err: unknown) {
26
107
  toast.error(err instanceof Error ? err.message : 'Failed to submit decision')
27
108
  }
28
109
  }
29
110
 
30
- if (pendingApprovals.length === 0) {
111
+ if (pendingCount === 0) {
31
112
  return (
32
113
  <div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
33
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">
@@ -37,8 +118,8 @@ export function ApprovalsPanel() {
37
118
  </svg>
38
119
  </div>
39
120
  <h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
40
- <p className="text-[13px] text-text-3/60 max-w-[280px]">
41
- Your swarm is operating autonomously. Any actions requiring human oversight will appear here.
121
+ <p className="text-[13px] text-text-3/60 max-w-[320px]">
122
+ Your swarm is operating autonomously. Actions requiring oversight will appear here.
42
123
  </p>
43
124
  </div>
44
125
  )
@@ -46,74 +127,97 @@ export function ApprovalsPanel() {
46
127
 
47
128
  return (
48
129
  <div className="flex-1 overflow-y-auto px-6 py-8">
49
- <div className="max-w-4xl mx-auto">
130
+ <div className="max-w-3xl mx-auto">
50
131
  <div className="flex items-center justify-between mb-8">
51
132
  <div>
52
133
  <h1 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-1">Approvals</h1>
53
- <p className="text-[13px] text-text-3">Governance queue for manual tool interventions</p>
134
+ <p className="text-[13px] text-text-3">Execution and plugin governance requests pending review</p>
54
135
  </div>
55
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">
56
- {pendingApprovals.length} Pending
137
+ {pendingCount} Pending
57
138
  </div>
58
139
  </div>
59
140
 
60
- <div className="grid grid-cols-1 gap-4">
61
- {pendingApprovals.map((task) => {
62
- const agent = agents[task.agentId]
63
- const argsString = JSON.stringify(task.pendingApproval!.args, null, 2)
64
-
65
- return (
66
- <div key={task.id} className="bg-surface rounded-[16px] border border-white/[0.06] overflow-hidden">
67
- {/* Header */}
68
- <div className="px-5 py-3 border-b border-white/[0.04] flex items-center justify-between bg-surface-2/50">
69
- <div className="flex items-center gap-3">
70
- <div className="w-8 h-8 rounded-[8px] bg-white/[0.04] flex items-center justify-center">
71
- <span className="text-[14px]">{agent?.avatarSeed ? '🤖' : '🦞'}</span>
72
- </div>
73
- <div>
74
- <h3 className="text-[13px] font-600 text-text">{agent?.name || 'Unknown Agent'}</h3>
75
- <p className="text-[11px] text-text-3">Task: {task.title}</p>
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>
76
185
  </div>
77
- </div>
78
- <span className="text-[10px] text-text-3/50 font-mono">
79
- {new Date(task.updatedAt).toLocaleString()}
80
- </span>
81
- </div>
82
-
83
- {/* Body */}
84
- <div className="p-5">
85
- <div className="flex items-center gap-2 mb-3">
86
- <span className="px-2 py-0.5 rounded-[6px] bg-accent-soft text-accent-bright text-[10px] font-mono font-600">
87
- {task.pendingApproval!.toolName}
88
- </span>
89
- <span className="text-[12px] text-text-3">requested permission to execute.</span>
90
- </div>
91
186
 
92
- <div className="bg-black/30 rounded-[10px] border border-white/[0.04] p-4 mb-5 overflow-x-auto">
93
- <pre className="text-[12px] font-mono text-text-2/80">
94
- {argsString}
95
- </pre>
96
- </div>
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>
97
197
 
98
- <div className="flex items-center justify-end gap-3 pt-4 border-t border-white/[0.04]">
99
- <button
100
- onClick={() => handleDecision(task.id, false)}
101
- 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"
102
- >
103
- Reject
104
- </button>
105
- <button
106
- onClick={() => handleDecision(task.id, true)}
107
- 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)]"
108
- >
109
- Approve Execution
110
- </button>
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>
111
215
  </div>
112
- </div>
113
- </div>
114
- )
115
- })}
116
- </div>
216
+ )
217
+ })}
218
+ </div>
219
+ </div>
220
+ )}
117
221
  </div>
118
222
  </div>
119
223
  )