@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
@@ -14,8 +14,10 @@ import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/li
14
14
  import { AgentAvatar } from './agent-avatar'
15
15
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
16
16
  import { randomSoul } from '@/lib/soul-suggestions'
17
+ import { copyTextToClipboard } from '@/lib/clipboard'
17
18
  import { SectionLabel } from '@/components/shared/section-label'
18
19
  import { SoulLibraryPicker } from './soul-library-picker'
20
+ import { HintTip } from '@/components/shared/hint-tip'
19
21
 
20
22
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
21
23
 
@@ -109,12 +111,15 @@ export function AgentSheet() {
109
111
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
110
112
  const [uploading, setUploading] = useState(false)
111
113
  const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
114
+ const [autoRecovery, setAutoRecovery] = useState(false)
112
115
  const [voiceId, setVoiceId] = useState('')
113
116
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
114
117
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
115
118
  const [heartbeatModel, setHeartbeatModel] = useState('')
116
119
  const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
117
120
  const [budgetEnabled, setBudgetEnabled] = useState(false)
121
+ const [hourlyBudget, setHourlyBudget] = useState('')
122
+ const [dailyBudget, setDailyBudget] = useState('')
118
123
  const [monthlyBudget, setMonthlyBudget] = useState('')
119
124
  const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
120
125
  const [agentWallet, setAgentWallet] = useState<(Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null>(null)
@@ -193,12 +198,19 @@ export function AgentSheet() {
193
198
  setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
194
199
  setAvatarUrl(editing.avatarUrl || null)
195
200
  setThinkingLevel(editing.thinkingLevel || '')
201
+ setAutoRecovery(editing.autoRecovery || false)
196
202
  setVoiceId(editing.elevenLabsVoiceId || '')
197
203
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
198
204
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
199
205
  setHeartbeatModel(editing.heartbeatModel || '')
200
206
  setHeartbeatPrompt(editing.heartbeatPrompt || '')
201
- setBudgetEnabled(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0)
207
+ setBudgetEnabled(
208
+ (typeof editing.hourlyBudget === 'number' && editing.hourlyBudget > 0)
209
+ || (typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0)
210
+ || (typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0),
211
+ )
212
+ setHourlyBudget(typeof editing.hourlyBudget === 'number' && editing.hourlyBudget > 0 ? String(editing.hourlyBudget) : '')
213
+ setDailyBudget(typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0 ? String(editing.dailyBudget) : '')
202
214
  setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
203
215
  setBudgetAction(editing.budgetAction || 'warn')
204
216
  // Load wallet if agent has one
@@ -235,12 +247,15 @@ export function AgentSheet() {
235
247
  setProjectId(undefined)
236
248
  setAvatarSeed('')
237
249
  setThinkingLevel('')
250
+ setAutoRecovery(false)
238
251
  setVoiceId('')
239
252
  setHeartbeatEnabled(false)
240
253
  setHeartbeatIntervalSec('')
241
254
  setHeartbeatModel('')
242
255
  setHeartbeatPrompt('')
243
256
  setBudgetEnabled(false)
257
+ setHourlyBudget('')
258
+ setDailyBudget('')
244
259
  setMonthlyBudget('')
245
260
  setBudgetAction('warn')
246
261
  }
@@ -311,6 +326,9 @@ export function AgentSheet() {
311
326
  const url = normalizedEndpoint.trim().replace(/\/+$/, '')
312
327
  normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
313
328
  }
329
+ const parsedHourlyBudget = budgetEnabled && hourlyBudget ? Number(hourlyBudget) : null
330
+ const parsedDailyBudget = budgetEnabled && dailyBudget ? Number(dailyBudget) : null
331
+ const parsedMonthlyBudget = budgetEnabled && monthlyBudget ? Number(monthlyBudget) : null
314
332
  const data = {
315
333
  name: name.trim() || 'Unnamed Agent',
316
334
  description,
@@ -334,13 +352,16 @@ export function AgentSheet() {
334
352
  avatarSeed: avatarSeed.trim() || undefined,
335
353
  avatarUrl: avatarUrl || null,
336
354
  thinkingLevel: thinkingLevel || undefined,
355
+ autoRecovery,
337
356
  elevenLabsVoiceId: voiceId.trim() || null,
338
357
  heartbeatEnabled,
339
358
  heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
340
359
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
341
360
  heartbeatModel: heartbeatModel.trim() || null,
342
361
  heartbeatPrompt: heartbeatPrompt.trim() || null,
343
- monthlyBudget: budgetEnabled && monthlyBudget ? Number(monthlyBudget) : null,
362
+ hourlyBudget: parsedHourlyBudget && parsedHourlyBudget > 0 ? parsedHourlyBudget : null,
363
+ dailyBudget: parsedDailyBudget && parsedDailyBudget > 0 ? parsedDailyBudget : null,
364
+ monthlyBudget: parsedMonthlyBudget && parsedMonthlyBudget > 0 ? parsedMonthlyBudget : null,
344
365
  budgetAction: budgetEnabled ? budgetAction : undefined,
345
366
  }
346
367
  if (editing) {
@@ -420,11 +441,14 @@ export function AgentSheet() {
420
441
  setTestStatus('fail')
421
442
  setTestMessage(result.message)
422
443
  setTestErrorCode(result.errorCode || null)
444
+ toast.error(result.message || 'Connection test failed')
423
445
  return false
424
446
  }
425
447
  } catch (err: unknown) {
448
+ const msg = err instanceof Error ? err.message : 'Connection test failed'
426
449
  setTestStatus('fail')
427
- setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
450
+ setTestMessage(msg)
451
+ toast.error(msg)
428
452
  return false
429
453
  }
430
454
  }
@@ -532,7 +556,10 @@ export function AgentSheet() {
532
556
  if (data.url) {
533
557
  setAvatarUrl(data.url)
534
558
  setAvatarSeed('')
559
+ toast.success('Avatar image uploaded')
535
560
  }
561
+ } catch (err: unknown) {
562
+ toast.error('Failed to upload image')
536
563
  } finally {
537
564
  setUploading(false)
538
565
  e.target.value = ''
@@ -653,8 +680,9 @@ export function AgentSheet() {
653
680
 
654
681
  {/* Thinking Level */}
655
682
  <div className="mb-8">
656
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
683
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
657
684
  Thinking Level <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
685
+ <HintTip text="Higher levels produce more thoughtful responses but cost more tokens" />
658
686
  </label>
659
687
  <select
660
688
  value={thinkingLevel}
@@ -671,6 +699,20 @@ export function AgentSheet() {
671
699
  <p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
672
700
  </div>
673
701
 
702
+ {/* Auto-Recovery */}
703
+ <div className="mb-8">
704
+ <div className="flex items-center justify-between mb-1.5">
705
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Guardian Auto-Recovery <HintTip text="Automatically resets the agent's workspace if it gets into a broken state" /></label>
706
+ <div
707
+ onClick={() => setAutoRecovery(!autoRecovery)}
708
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer ${autoRecovery ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
709
+ >
710
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${autoRecovery ? 'left-[18px]' : 'left-0.5'}`} />
711
+ </div>
712
+ </div>
713
+ <p className="text-[11px] text-text-3/70">If this agent critically fails a task that modifies the workspace, SwarmClaw Guardian will automatically perform a <code className="text-[10px] bg-white/[0.05] px-1 rounded">git reset --hard</code> to restore the last known good state.</p>
714
+ </div>
715
+
674
716
  {/* ElevenLabs Voice ID */}
675
717
  {appSettings.elevenLabsEnabled && (
676
718
  <div className="mb-8">
@@ -692,7 +734,7 @@ export function AgentSheet() {
692
734
  {/* Heartbeat Configuration */}
693
735
  <div className="mb-8">
694
736
  <div className="flex items-center justify-between mb-3">
695
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Heartbeat</label>
737
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Heartbeat <HintTip text="Periodically runs a background prompt to keep the agent active and aware" /></label>
696
738
  <button
697
739
  type="button"
698
740
  onClick={() => setHeartbeatEnabled(!heartbeatEnabled)}
@@ -704,7 +746,7 @@ export function AgentSheet() {
704
746
  {heartbeatEnabled && (
705
747
  <div className="space-y-4 mt-3">
706
748
  <div>
707
- <label className="block text-[12px] text-text-3/70 mb-1.5">Interval</label>
749
+ <label className="flex items-center gap-1.5 text-[12px] text-text-3/70 mb-1.5">Interval <HintTip text="Minutes between each heartbeat check" /></label>
708
750
  <select
709
751
  value={heartbeatIntervalSec}
710
752
  onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
@@ -743,10 +785,10 @@ export function AgentSheet() {
743
785
  <p className="text-[11px] text-text-3/70 mt-1.5">Periodic check-in runs on idle sessions using this agent. Processes pending events and monitors status.</p>
744
786
  </div>
745
787
 
746
- {/* Monthly Budget */}
788
+ {/* Spend Limits */}
747
789
  <div className="mb-8">
748
790
  <div className="flex items-center justify-between mb-3">
749
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Monthly Budget</label>
791
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Spend Limits <HintTip text="Set hourly, daily, and monthly API spend limits for this agent" /></label>
750
792
  <button
751
793
  type="button"
752
794
  onClick={() => setBudgetEnabled(!budgetEnabled)}
@@ -757,24 +799,58 @@ export function AgentSheet() {
757
799
  </div>
758
800
  {budgetEnabled && (
759
801
  <div className="space-y-4 mt-3">
760
- <div>
761
- <label className="block text-[12px] text-text-3/70 mb-1.5">Budget cap (USD)</label>
762
- <div className="relative">
763
- <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
764
- <input
765
- type="number"
766
- min="0.01"
767
- step="0.01"
768
- value={monthlyBudget}
769
- onChange={(e) => setMonthlyBudget(e.target.value)}
770
- placeholder="10.00"
771
- className={`${inputClass} pl-7`}
772
- style={{ fontFamily: 'inherit' }}
773
- />
802
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
803
+ <div>
804
+ <label className="block text-[12px] text-text-3/70 mb-1.5">Hourly cap (USD)</label>
805
+ <div className="relative">
806
+ <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
807
+ <input
808
+ type="number"
809
+ min="0.01"
810
+ step="0.01"
811
+ value={hourlyBudget}
812
+ onChange={(e) => setHourlyBudget(e.target.value)}
813
+ placeholder="0.50"
814
+ className={`${inputClass} pl-7`}
815
+ style={{ fontFamily: 'inherit' }}
816
+ />
817
+ </div>
818
+ </div>
819
+ <div>
820
+ <label className="block text-[12px] text-text-3/70 mb-1.5">Daily cap (USD)</label>
821
+ <div className="relative">
822
+ <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
823
+ <input
824
+ type="number"
825
+ min="0.01"
826
+ step="0.01"
827
+ value={dailyBudget}
828
+ onChange={(e) => setDailyBudget(e.target.value)}
829
+ placeholder="5.00"
830
+ className={`${inputClass} pl-7`}
831
+ style={{ fontFamily: 'inherit' }}
832
+ />
833
+ </div>
834
+ </div>
835
+ <div>
836
+ <label className="block text-[12px] text-text-3/70 mb-1.5">Monthly cap (USD)</label>
837
+ <div className="relative">
838
+ <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
839
+ <input
840
+ type="number"
841
+ min="0.01"
842
+ step="0.01"
843
+ value={monthlyBudget}
844
+ onChange={(e) => setMonthlyBudget(e.target.value)}
845
+ placeholder="20.00"
846
+ className={`${inputClass} pl-7`}
847
+ style={{ fontFamily: 'inherit' }}
848
+ />
849
+ </div>
774
850
  </div>
775
851
  </div>
776
852
  <div>
777
- <label className="block text-[12px] text-text-3/70 mb-1.5">When exceeded</label>
853
+ <label className="flex items-center gap-1.5 text-[12px] text-text-3/70 mb-1.5">When exceeded <HintTip text="Warn shows an alert but keeps running; Block stops the agent from making API calls" /></label>
778
854
  <div className="flex gap-2">
779
855
  <button
780
856
  type="button"
@@ -806,8 +882,8 @@ export function AgentSheet() {
806
882
  )}
807
883
  <p className="text-[11px] text-text-3/70 mt-1.5">
808
884
  {budgetAction === 'block'
809
- ? 'Cap monthly spend for this agent. When exceeded, chat runs are blocked until the next month.'
810
- : 'Cap monthly spend for this agent. When exceeded, a warning is shown but runs continue.'}
885
+ ? 'When any configured cap is exceeded, runs are blocked until spend drops below that cap window.'
886
+ : 'When a configured cap is exceeded, a warning is shown but runs continue.'}
811
887
  </p>
812
888
  </div>
813
889
 
@@ -835,6 +911,7 @@ export function AgentSheet() {
835
911
  <div className="mb-8">
836
912
  <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
837
913
  Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
914
+ <HintTip text="The agent's voice and tone — how it talks, not what it knows" />
838
915
  {soul !== soulInitial && soulSaveState === 'idle' && (
839
916
  <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-amber-400 font-600">
840
917
  <span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
@@ -889,7 +966,7 @@ export function AgentSheet() {
889
966
  {provider !== 'openclaw' && (
890
967
  <div className="mb-8">
891
968
  <div className="flex items-center gap-2 mb-3">
892
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt</label>
969
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt <HintTip text="Instructions that tell the agent what it can do, what tools to use, and how to behave" /></label>
893
970
  <button onClick={() => promptFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
894
971
  <input ref={promptFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSystemPrompt)} className="hidden" />
895
972
  </div>
@@ -1080,9 +1157,11 @@ export function AgentSheet() {
1080
1157
  <button
1081
1158
  type="button"
1082
1159
  onClick={() => {
1083
- navigator.clipboard.writeText((testDeviceId || openclawDeviceId)!)
1084
- setConfigCopied(true)
1085
- setTimeout(() => setConfigCopied(false), 2000)
1160
+ void copyTextToClipboard((testDeviceId || openclawDeviceId)!).then((copiedId) => {
1161
+ if (!copiedId) return
1162
+ setConfigCopied(true)
1163
+ setTimeout(() => setConfigCopied(false), 2000)
1164
+ })
1086
1165
  }}
1087
1166
  className="text-[12px] text-text-3/60 hover:text-text-3/80 transition-colors cursor-pointer bg-transparent border-none"
1088
1167
  >
@@ -1286,11 +1365,11 @@ export function AgentSheet() {
1286
1365
  </div>
1287
1366
  )}
1288
1367
 
1289
- {/* Tools — hidden for providers that manage capabilities outside LangGraph */}
1368
+ {/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
1290
1369
  {!hasNativeCapabilities && (
1291
1370
  <div className="mb-8">
1292
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Tools</label>
1293
- <p className="text-[12px] text-text-3/60 mb-3">Enable tools for LangGraph agent sessions.</p>
1371
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Plugins</label>
1372
+ <p className="text-[12px] text-text-3/60 mb-3">Enable capabilities and plugins for this agent.</p>
1294
1373
  <div className="space-y-3">
1295
1374
  {AVAILABLE_TOOLS.map((t) => (
1296
1375
  <label key={t.id} className="flex items-center gap-3 cursor-pointer">
@@ -1313,7 +1392,7 @@ export function AgentSheet() {
1313
1392
  {/* Platform — hidden for providers that manage capabilities outside LangGraph */}
1314
1393
  {!hasNativeCapabilities && (
1315
1394
  <div className="mb-8">
1316
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Platform</label>
1395
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Platform Plugins</label>
1317
1396
  <p className="text-[12px] text-text-3/60 mb-3">Allow this agent to manage platform resources directly.</p>
1318
1397
  <div className="space-y-3">
1319
1398
  {PLATFORM_TOOLS.map((t) => (
@@ -1601,4 +1680,3 @@ export function AgentSheet() {
1601
1680
  </>
1602
1681
  )
1603
1682
  }
1604
-
@@ -173,7 +173,7 @@ function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDele
173
173
  )}
174
174
  {agent.tools && agent.tools.length > 0 && (
175
175
  <div>
176
- <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Tools</label>
176
+ <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Plugins</label>
177
177
  <div className="flex flex-wrap gap-1">
178
178
  {agent.tools.map((tool) => (
179
179
  <span key={tool} className="px-2 py-0.5 rounded-[6px] text-[11px] font-600 bg-sky-400/[0.08] text-sky-400/70">
@@ -1,8 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { useState, useMemo } from 'react'
3
+ import { useState, useMemo, useEffect } from 'react'
4
4
  import { BottomSheet } from '@/components/shared/bottom-sheet'
5
5
  import { SOUL_LIBRARY, SOUL_ARCHETYPES, searchSouls, type SoulTemplate } from '@/lib/soul-library'
6
+ import { api } from '@/lib/api-client'
6
7
 
7
8
  interface SoulLibraryPickerProps {
8
9
  open: boolean
@@ -13,8 +14,48 @@ interface SoulLibraryPickerProps {
13
14
  export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPickerProps) {
14
15
  const [query, setQuery] = useState('')
15
16
  const [archetype, setArchetype] = useState('All')
17
+ const [source, setSource] = useState<'library' | 'forge'>('library')
18
+ const [customSouls, setCustomSouls] = useState<SoulTemplate[]>([])
19
+ const [loading, setLoading] = useState(false)
16
20
 
17
- const results = useMemo(() => searchSouls(query, archetype), [query, archetype])
21
+ const results = useMemo(() => {
22
+ if (source === 'library') {
23
+ return searchSouls(query, archetype)
24
+ } else {
25
+ let filtered = customSouls
26
+ if (archetype && archetype !== 'All') {
27
+ filtered = filtered.filter(s => s.archetype === archetype)
28
+ }
29
+ if (query) {
30
+ const q = query.toLowerCase()
31
+ filtered = filtered.filter(s =>
32
+ s.name.toLowerCase().includes(q) ||
33
+ s.soul.toLowerCase().includes(q) ||
34
+ s.tags.some(t => t.toLowerCase().includes(q))
35
+ )
36
+ }
37
+ return filtered
38
+ }
39
+ }, [query, archetype, source, customSouls])
40
+
41
+ useEffect(() => {
42
+ if (open && source === 'forge') {
43
+ const load = async () => {
44
+ setLoading(true)
45
+ try {
46
+ const res = await api<SoulTemplate[]>('GET', '/souls')
47
+ // Filter out the built-in ones from the API result since we show them in 'library' tab
48
+ const libraryIds = new Set(SOUL_LIBRARY.map(s => s.id))
49
+ setCustomSouls(res.filter(s => !libraryIds.has(s.id)))
50
+ } catch (err) {
51
+ console.error('Failed to load custom souls', err)
52
+ } finally {
53
+ setLoading(false)
54
+ }
55
+ }
56
+ load()
57
+ }
58
+ }, [open, source])
18
59
 
19
60
  const handleSelect = (template: SoulTemplate) => {
20
61
  onSelect(template.soul)
@@ -23,9 +64,25 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
23
64
 
24
65
  return (
25
66
  <BottomSheet open={open} onClose={onClose}>
26
- <div className="mb-6">
27
- <h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
28
- <p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
67
+ <div className="mb-6 flex items-center justify-between">
68
+ <div>
69
+ <h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
70
+ <p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
71
+ </div>
72
+ <div className="flex bg-white/[0.04] p-1 rounded-[12px] border border-white/[0.04]">
73
+ <button
74
+ onClick={() => setSource('library')}
75
+ className={`px-3 py-1.5 rounded-[10px] text-[12px] font-600 transition-all ${source === 'library' ? 'bg-white/[0.08] text-text shadow-sm' : 'text-text-3 hover:text-text-2'}`}
76
+ >
77
+ Verified
78
+ </button>
79
+ <button
80
+ onClick={() => setSource('forge')}
81
+ className={`px-3 py-1.5 rounded-[10px] text-[12px] font-600 transition-all ${source === 'forge' ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:text-text-2'}`}
82
+ >
83
+ SwarmForge
84
+ </button>
85
+ </div>
29
86
  </div>
30
87
 
31
88
  {/* Search */}
@@ -34,7 +91,7 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
34
91
  type="text"
35
92
  value={query}
36
93
  onChange={(e) => setQuery(e.target.value)}
37
- placeholder="Search personalities..."
94
+ placeholder={source === 'library' ? "Search verified personalities..." : "Search SwarmForge / Custom..."}
38
95
  className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none focus-glow"
39
96
  style={{ fontFamily: 'inherit' }}
40
97
  />
@@ -59,31 +116,45 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
59
116
 
60
117
  {/* Results grid */}
61
118
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto pb-4">
62
- {results.map((template) => (
119
+ {loading ? (
120
+ <div className="col-span-2 py-12 flex flex-col items-center gap-3">
121
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accent-bright" />
122
+ <p className="text-[13px] text-text-3">Stoking the forge...</p>
123
+ </div>
124
+ ) : results.map((template) => (
63
125
  <button
64
126
  key={template.id}
65
127
  onClick={() => handleSelect(template)}
66
- className="text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-accent-bright/20 transition-all cursor-pointer group"
128
+ className={`text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer group
129
+ ${source === 'forge' ? 'hover:border-accent-bright/20' : 'hover:border-white/[0.12]'}`}
67
130
  style={{ fontFamily: 'inherit' }}
68
131
  >
69
132
  <div className="flex items-start gap-2 mb-2">
70
- <h4 className="text-[14px] font-600 text-text group-hover:text-accent-bright transition-colors">
133
+ <h4 className={`text-[14px] font-600 text-text transition-colors ${source === 'forge' ? 'group-hover:text-accent-bright' : ''}`}>
71
134
  {template.name}
72
135
  </h4>
73
136
  <span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] text-text-3 text-[10px] font-600 shrink-0">
74
137
  {template.archetype}
75
138
  </span>
76
139
  </div>
77
- <p className="text-[12px] text-text-3 mb-2">{template.description}</p>
140
+ <p className="text-[12px] text-text-3 mb-2 line-clamp-2">{template.description}</p>
78
141
  <p className="text-[11px] text-text-3/60 line-clamp-2 italic">{template.soul}</p>
79
142
  </button>
80
143
  ))}
81
- {results.length === 0 && (
82
- <p className="text-[13px] text-text-3 col-span-2 text-center py-8">No personalities match your search</p>
144
+ {!loading && results.length === 0 && (
145
+ <div className="col-span-2 text-center py-12">
146
+ <div className="w-12 h-12 rounded-full bg-white/[0.03] flex items-center justify-center mx-auto mb-3">
147
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40"><path d="M12 2a4 4 0 0 1 4 4v2a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4Z"/><path d="M16 14H8a4 4 0 0 0-4 4v2h16v-2a4 4 0 0 0-4-4Z"/></svg>
148
+ </div>
149
+ <p className="text-[14px] font-600 text-text-2">No personalities match</p>
150
+ <p className="text-[12px] text-text-3/50 mt-1">{source === 'forge' ? 'Be the first to forge a custom soul in this category!' : 'Try a different search term.'}</p>
151
+ </div>
83
152
  )}
84
153
  </div>
85
154
 
86
- <p className="text-[11px] text-text-3/50 mt-4 text-center">{SOUL_LIBRARY.length} personalities available</p>
155
+ <p className="text-[11px] text-text-3/50 mt-4 text-center">
156
+ {source === 'library' ? `${SOUL_LIBRARY.length} verified templates` : `${customSouls.length} custom souls in your forge`}
157
+ </p>
87
158
  </BottomSheet>
88
159
  )
89
160
  }