@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
@@ -45,6 +45,22 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
45
45
  const approvals = useApprovalStore((s) => s.approvals)
46
46
  const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
47
47
  const [heartbeatPulse, setHeartbeatPulse] = useState(false)
48
+ const monthlyBudget = typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0
49
+ ? agent.monthlyBudget
50
+ : null
51
+ const hasMonthlyBudget = monthlyBudget !== null
52
+ const spendWindows = [
53
+ {
54
+ key: '1h',
55
+ spend: agent.hourlySpend ?? 0,
56
+ budget: typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0 ? agent.hourlyBudget : null,
57
+ },
58
+ {
59
+ key: '24h',
60
+ spend: agent.dailySpend ?? 0,
61
+ budget: typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0 ? agent.dailyBudget : null,
62
+ },
63
+ ].filter((entry) => entry.budget !== null)
48
64
  useWs(`heartbeat:agent:${agent.id}`, () => {
49
65
  setHeartbeatPulse(true)
50
66
  setTimeout(() => setHeartbeatPulse(false), 1500)
@@ -226,28 +242,58 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
226
242
  <span>Cost: ${agent.totalCost.toFixed(2)}</span>
227
243
  )}
228
244
  </div>
229
- {typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0 && (
245
+ {hasMonthlyBudget && (
230
246
  <div className="mt-2">
231
247
  <div className="flex items-center justify-between text-[10px] text-text-3/60 mb-1">
232
- <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${agent.monthlyBudget.toFixed(2)}</span>
233
- <span className={`font-600 ${(agent.monthlySpend ?? 0) >= agent.monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
248
+ <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${monthlyBudget.toFixed(2)}</span>
249
+ <span className={`font-600 ${(agent.monthlySpend ?? 0) >= monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
234
250
  {agent.budgetAction === 'block' ? 'hard cap' : 'soft cap'}
235
251
  </span>
236
252
  </div>
237
- <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
253
+ <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden relative">
238
254
  <div
239
- className={`h-full rounded-full transition-all duration-300 ${
240
- (agent.monthlySpend ?? 0) >= agent.monthlyBudget
255
+ className={`h-full rounded-full transition-all duration-300 relative ${
256
+ (agent.monthlySpend ?? 0) >= monthlyBudget
241
257
  ? 'bg-red-400'
242
- : (agent.monthlySpend ?? 0) >= agent.monthlyBudget * 0.8
258
+ : (agent.monthlySpend ?? 0) >= monthlyBudget * 0.8
243
259
  ? 'bg-amber-400'
244
260
  : 'bg-accent'
245
261
  }`}
246
- style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / agent.monthlyBudget) * 100)}%` }}
247
- />
262
+ style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / monthlyBudget) * 100)}%` }}
263
+ >
264
+ {/* Shimmer overlay for active feel */}
265
+ <div
266
+ className="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-white/20 to-transparent"
267
+ style={{ animation: 'shimmer-bar 2s linear infinite' }}
268
+ />
269
+ </div>
248
270
  </div>
249
271
  </div>
250
272
  )}
273
+ {spendWindows.length > 0 && (
274
+ <div className="mt-2 flex flex-wrap gap-1.5">
275
+ {spendWindows.map((entry) => {
276
+ const budget = entry.budget as number
277
+ const ratio = budget > 0 ? (entry.spend / budget) : 0
278
+ const overCap = ratio >= 1
279
+ const nearCap = !overCap && ratio >= 0.8
280
+ return (
281
+ <span
282
+ key={entry.key}
283
+ className={`text-[10px] px-2 py-0.5 rounded-[6px] border ${
284
+ overCap
285
+ ? 'text-red-400 border-red-400/25 bg-red-400/[0.06]'
286
+ : nearCap
287
+ ? 'text-amber-400 border-amber-400/20 bg-amber-400/[0.06]'
288
+ : 'text-text-3/70 border-white/[0.08] bg-white/[0.03]'
289
+ }`}
290
+ >
291
+ {entry.key}: ${entry.spend.toFixed(2)} / ${budget.toFixed(2)}
292
+ </span>
293
+ )
294
+ })}
295
+ </div>
296
+ )}
251
297
  </div>
252
298
 
253
299
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -14,6 +14,7 @@ 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'
19
20
  import { HintTip } from '@/components/shared/hint-tip'
@@ -117,6 +118,8 @@ export function AgentSheet() {
117
118
  const [heartbeatModel, setHeartbeatModel] = useState('')
118
119
  const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
119
120
  const [budgetEnabled, setBudgetEnabled] = useState(false)
121
+ const [hourlyBudget, setHourlyBudget] = useState('')
122
+ const [dailyBudget, setDailyBudget] = useState('')
120
123
  const [monthlyBudget, setMonthlyBudget] = useState('')
121
124
  const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
122
125
  const [agentWallet, setAgentWallet] = useState<(Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null>(null)
@@ -201,7 +204,13 @@ export function AgentSheet() {
201
204
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
202
205
  setHeartbeatModel(editing.heartbeatModel || '')
203
206
  setHeartbeatPrompt(editing.heartbeatPrompt || '')
204
- 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) : '')
205
214
  setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
206
215
  setBudgetAction(editing.budgetAction || 'warn')
207
216
  // Load wallet if agent has one
@@ -245,6 +254,8 @@ export function AgentSheet() {
245
254
  setHeartbeatModel('')
246
255
  setHeartbeatPrompt('')
247
256
  setBudgetEnabled(false)
257
+ setHourlyBudget('')
258
+ setDailyBudget('')
248
259
  setMonthlyBudget('')
249
260
  setBudgetAction('warn')
250
261
  }
@@ -315,6 +326,9 @@ export function AgentSheet() {
315
326
  const url = normalizedEndpoint.trim().replace(/\/+$/, '')
316
327
  normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
317
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
318
332
  const data = {
319
333
  name: name.trim() || 'Unnamed Agent',
320
334
  description,
@@ -345,7 +359,9 @@ export function AgentSheet() {
345
359
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
346
360
  heartbeatModel: heartbeatModel.trim() || null,
347
361
  heartbeatPrompt: heartbeatPrompt.trim() || null,
348
- 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,
349
365
  budgetAction: budgetEnabled ? budgetAction : undefined,
350
366
  }
351
367
  if (editing) {
@@ -425,11 +441,14 @@ export function AgentSheet() {
425
441
  setTestStatus('fail')
426
442
  setTestMessage(result.message)
427
443
  setTestErrorCode(result.errorCode || null)
444
+ toast.error(result.message || 'Connection test failed')
428
445
  return false
429
446
  }
430
447
  } catch (err: unknown) {
448
+ const msg = err instanceof Error ? err.message : 'Connection test failed'
431
449
  setTestStatus('fail')
432
- setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
450
+ setTestMessage(msg)
451
+ toast.error(msg)
433
452
  return false
434
453
  }
435
454
  }
@@ -537,7 +556,10 @@ export function AgentSheet() {
537
556
  if (data.url) {
538
557
  setAvatarUrl(data.url)
539
558
  setAvatarSeed('')
559
+ toast.success('Avatar image uploaded')
540
560
  }
561
+ } catch (err: unknown) {
562
+ toast.error('Failed to upload image')
541
563
  } finally {
542
564
  setUploading(false)
543
565
  e.target.value = ''
@@ -763,10 +785,10 @@ export function AgentSheet() {
763
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>
764
786
  </div>
765
787
 
766
- {/* Monthly Budget */}
788
+ {/* Spend Limits */}
767
789
  <div className="mb-8">
768
790
  <div className="flex items-center justify-between mb-3">
769
- <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Monthly Budget <HintTip text="Set a spending limit for this agent's API usage" /></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>
770
792
  <button
771
793
  type="button"
772
794
  onClick={() => setBudgetEnabled(!budgetEnabled)}
@@ -777,20 +799,54 @@ export function AgentSheet() {
777
799
  </div>
778
800
  {budgetEnabled && (
779
801
  <div className="space-y-4 mt-3">
780
- <div>
781
- <label className="block text-[12px] text-text-3/70 mb-1.5">Budget cap (USD)</label>
782
- <div className="relative">
783
- <span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/50 text-[14px]">$</span>
784
- <input
785
- type="number"
786
- min="0.01"
787
- step="0.01"
788
- value={monthlyBudget}
789
- onChange={(e) => setMonthlyBudget(e.target.value)}
790
- placeholder="10.00"
791
- className={`${inputClass} pl-7`}
792
- style={{ fontFamily: 'inherit' }}
793
- />
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>
794
850
  </div>
795
851
  </div>
796
852
  <div>
@@ -826,8 +882,8 @@ export function AgentSheet() {
826
882
  )}
827
883
  <p className="text-[11px] text-text-3/70 mt-1.5">
828
884
  {budgetAction === 'block'
829
- ? 'Cap monthly spend for this agent. When exceeded, chat runs are blocked until the next month.'
830
- : '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.'}
831
887
  </p>
832
888
  </div>
833
889
 
@@ -1101,9 +1157,11 @@ export function AgentSheet() {
1101
1157
  <button
1102
1158
  type="button"
1103
1159
  onClick={() => {
1104
- navigator.clipboard.writeText((testDeviceId || openclawDeviceId)!)
1105
- setConfigCopied(true)
1106
- setTimeout(() => setConfigCopied(false), 2000)
1160
+ void copyTextToClipboard((testDeviceId || openclawDeviceId)!).then((copiedId) => {
1161
+ if (!copiedId) return
1162
+ setConfigCopied(true)
1163
+ setTimeout(() => setConfigCopied(false), 2000)
1164
+ })
1107
1165
  }}
1108
1166
  className="text-[12px] text-text-3/60 hover:text-text-3/80 transition-colors cursor-pointer bg-transparent border-none"
1109
1167
  >
@@ -1307,11 +1365,11 @@ export function AgentSheet() {
1307
1365
  </div>
1308
1366
  )}
1309
1367
 
1310
- {/* Tools — hidden for providers that manage capabilities outside LangGraph */}
1368
+ {/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
1311
1369
  {!hasNativeCapabilities && (
1312
1370
  <div className="mb-8">
1313
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Tools</label>
1314
- <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>
1315
1373
  <div className="space-y-3">
1316
1374
  {AVAILABLE_TOOLS.map((t) => (
1317
1375
  <label key={t.id} className="flex items-center gap-3 cursor-pointer">
@@ -1334,7 +1392,7 @@ export function AgentSheet() {
1334
1392
  {/* Platform — hidden for providers that manage capabilities outside LangGraph */}
1335
1393
  {!hasNativeCapabilities && (
1336
1394
  <div className="mb-8">
1337
- <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>
1338
1396
  <p className="text-[12px] text-text-3/60 mb-3">Allow this agent to manage platform resources directly.</p>
1339
1397
  <div className="space-y-3">
1340
1398
  {PLATFORM_TOOLS.map((t) => (
@@ -1622,4 +1680,3 @@ export function AgentSheet() {
1622
1680
  </>
1623
1681
  )
1624
1682
  }
1625
-
@@ -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">
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import { setStoredAccessKey } from '@/lib/api-client'
5
5
  import { fetchWithTimeout } from '@/lib/fetch-timeout'
6
+ import { copyTextToClipboard } from '@/lib/clipboard'
6
7
 
7
8
  interface AccessKeyGateProps {
8
9
  onAuthenticated: () => void
@@ -42,7 +43,8 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
42
43
 
43
44
  const handleCopyKey = async () => {
44
45
  try {
45
- await navigator.clipboard.writeText(generatedKey)
46
+ const copiedKey = await copyTextToClipboard(generatedKey)
47
+ if (!copiedKey) return
46
48
  setCopied(true)
47
49
  setTimeout(() => setCopied(false), 2000)
48
50
  } catch {
@@ -121,12 +123,9 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
121
123
  />
122
124
  </div>
123
125
 
124
- <div
125
- className="relative max-w-[440px] w-full text-center"
126
- style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}
127
- >
126
+ <div className="relative max-w-[440px] w-full text-center">
128
127
  {/* Lock / Key icon */}
129
- <div className="flex justify-center mb-6">
128
+ <div className="flex justify-center mb-6" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
130
129
  <div className="relative w-12 h-12 flex items-center justify-center">
131
130
  <svg
132
131
  width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@@ -151,15 +150,17 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
151
150
  {firstTime ? (
152
151
  /* ── First-time setup: show the generated key ── */
153
152
  <>
154
- <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
155
- Your Access Key
156
- </h1>
157
- <p className="text-[14px] text-text-2 mb-8">
158
- This key was generated for your server. Copy it somewhere safe — you&apos;ll need it to connect from other devices.
159
- </p>
153
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
154
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
155
+ Your Access Key
156
+ </h1>
157
+ <p className="text-[14px] text-text-2 mb-8">
158
+ This key was generated for your server. Copy it somewhere safe — you&apos;ll need it to connect from other devices.
159
+ </p>
160
+ </div>
160
161
 
161
162
  {/* Key display */}
162
- <div className="mb-3">
163
+ <div className="mb-3" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
163
164
  <div
164
165
  className="inline-flex items-center gap-3 px-5 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface
165
166
  cursor-pointer hover:border-accent-bright/20 transition-all duration-200"
@@ -185,7 +186,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
185
186
  </div>
186
187
  </div>
187
188
 
188
- <div className="relative h-5 mb-8">
189
+ <div className="relative h-5 mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
189
190
  <p
190
191
  className="absolute inset-x-0 text-[12px] transition-all duration-300"
191
192
  style={{
@@ -207,56 +208,64 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
207
208
  </p>
208
209
  </div>
209
210
 
210
- <button
211
- onClick={handleClaimKey}
212
- disabled={loading}
213
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
214
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
215
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
216
- >
217
- {loading ? 'Connecting...' : 'Continue'}
218
- </button>
211
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
212
+ <button
213
+ onClick={handleClaimKey}
214
+ disabled={loading}
215
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
216
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
217
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
218
+ >
219
+ {loading ? 'Connecting...' : 'Continue'}
220
+ </button>
221
+ </div>
219
222
  </>
220
223
  ) : (
221
224
  /* ── Returning user: enter key ── */
222
225
  <>
223
- <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
224
- Connect
225
- </h1>
226
- <p className="text-[14px] text-text-2 mb-2">
227
- Enter the access key to connect to this server.
228
- </p>
229
- <p className="text-[12px] text-text-3 mb-8">
230
- You can find it in <code className="text-text-2">.env.local</code> in the project root.
231
- </p>
226
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
227
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
228
+ Connect
229
+ </h1>
230
+ <p className="text-[14px] text-text-2 mb-2">
231
+ Enter the access key to connect to this server.
232
+ </p>
233
+ <p className="text-[12px] text-text-3 mb-8">
234
+ You can find it in <code className="text-text-2">.env.local</code> in the project root.
235
+ </p>
236
+ </div>
232
237
 
233
238
  <form onSubmit={handleSubmit} className="flex flex-col items-center gap-4">
234
- <input
235
- type="password"
236
- value={key}
237
- onChange={(e) => { setKey(e.target.value); setError('') }}
238
- placeholder="Access key"
239
- autoFocus
240
- autoComplete="off"
241
- className="w-full max-w-[320px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
242
- text-text text-[16px] text-center font-mono outline-none
243
- transition-all duration-200 placeholder:text-text-3/70
244
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
245
- />
239
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both', width: '100%', display: 'flex', justifyContent: 'center' }}>
240
+ <input
241
+ type="password"
242
+ value={key}
243
+ onChange={(e) => { setKey(e.target.value); setError('') }}
244
+ placeholder="Access key"
245
+ autoFocus
246
+ autoComplete="off"
247
+ className="w-full max-w-[320px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
248
+ text-text text-[16px] text-center font-mono outline-none
249
+ transition-all duration-200 placeholder:text-text-3/70
250
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
251
+ />
252
+ </div>
246
253
 
247
254
  {error && (
248
- <p className="text-[13px] text-red-400">{error}</p>
255
+ <p className="text-[13px] text-red-400" style={{ animation: 'ai-shake 0.5s' }}>{error}</p>
249
256
  )}
250
257
 
251
- <button
252
- type="submit"
253
- disabled={!key.trim() || loading}
254
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
255
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
256
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
257
- >
258
- {loading ? 'Connecting...' : 'Connect'}
259
- </button>
258
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
259
+ <button
260
+ type="submit"
261
+ disabled={!key.trim() || loading}
262
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
263
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
264
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
265
+ >
266
+ {loading ? 'Connecting...' : 'Connect'}
267
+ </button>
268
+ </div>
260
269
  </form>
261
270
  </>
262
271
  )}
@@ -39,11 +39,10 @@ export function UserPicker() {
39
39
  }} />
40
40
  </div>
41
41
 
42
- <div className="relative max-w-[420px] w-full text-center"
43
- style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}>
42
+ <div className="relative max-w-[420px] w-full text-center">
44
43
 
45
44
  {/* Sparkle icon */}
46
- <div className="flex justify-center mb-6">
45
+ <div className="flex justify-center mb-6" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
47
46
  <div className="relative w-12 h-12">
48
47
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none" className="text-accent-bright"
49
48
  style={{ animation: 'sparkle-spin 8s linear infinite' }}>
@@ -54,29 +53,33 @@ export function UserPicker() {
54
53
  </div>
55
54
  </div>
56
55
 
57
- <h1 className="font-display text-[42px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
58
- Welcome
59
- </h1>
60
- <p className="text-[15px] text-text-2 mb-10">
61
- What should we call you?
62
- </p>
56
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
57
+ <h1 className="font-display text-[42px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
58
+ Welcome
59
+ </h1>
60
+ <p className="text-[15px] text-text-2 mb-10">
61
+ What should we call you?
62
+ </p>
63
+ </div>
63
64
 
64
65
  <form onSubmit={handleSubmit} className="flex flex-col items-center gap-5">
65
- <input
66
- type="text"
67
- value={name}
68
- onChange={(e) => setName(e.target.value)}
69
- placeholder="Your name"
70
- autoFocus
71
- className="w-full max-w-[280px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
72
- text-text text-[18px] text-center font-display font-600 outline-none
73
- transition-all duration-200 placeholder:text-text-3/70
74
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
75
- style={{ fontFamily: 'inherit' }}
76
- />
66
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both', width: '100%', display: 'flex', justifyContent: 'center' }}>
67
+ <input
68
+ type="text"
69
+ value={name}
70
+ onChange={(e) => setName(e.target.value)}
71
+ placeholder="Your name"
72
+ autoFocus
73
+ className="w-full max-w-[280px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
74
+ text-text text-[18px] text-center font-display font-600 outline-none
75
+ transition-all duration-200 placeholder:text-text-3/70
76
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
77
+ style={{ fontFamily: 'inherit' }}
78
+ />
79
+ </div>
77
80
 
78
81
  {/* Avatar picker */}
79
- <div className="flex flex-col items-center gap-3">
82
+ <div className="flex flex-col items-center gap-3" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
80
83
  <AgentAvatar seed={avatarSeed || null} name={name || '?'} size={64} />
81
84
  <div className="flex items-center gap-2">
82
85
  <input
@@ -99,16 +102,18 @@ export function UserPicker() {
99
102
  </div>
100
103
  </div>
101
104
 
102
- <button
103
- type="submit"
104
- disabled={!name.trim()}
105
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
106
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
107
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
108
- style={{ fontFamily: 'inherit' }}
109
- >
110
- Get Started
111
- </button>
105
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
106
+ <button
107
+ type="submit"
108
+ disabled={!name.trim()}
109
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
110
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
111
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
112
+ style={{ fontFamily: 'inherit' }}
113
+ >
114
+ Get Started
115
+ </button>
116
+ </div>
112
117
  </form>
113
118
  </div>
114
119
  </div>
@@ -21,6 +21,7 @@ import { HeartbeatHistoryPanel } from './heartbeat-history-panel'
21
21
  import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
22
22
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
23
23
  import { speak } from '@/lib/tts'
24
+ import { api } from '@/lib/api-client'
24
25
 
25
26
  const PROMPT_SUGGESTIONS = [
26
27
  { text: 'What can you help me with?', icon: 'book', gradient: 'from-[#6366F1]/10 to-[#818CF8]/5' },
@@ -66,6 +67,15 @@ export function ChatArea() {
66
67
  const [heartbeatHistoryOpen, setHeartbeatHistoryOpen] = useState(false)
67
68
  const [messagesLoading, setMessagesLoading] = useState(true)
68
69
  const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
70
+ const [pluginChatActions, setPluginChatActions] = useState<Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>>([])
71
+
72
+ useEffect(() => {
73
+ if (sessionId) {
74
+ api<Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>>('GET', '/plugins/ui?type=chat_actions').then(actions => {
75
+ if (Array.isArray(actions)) setPluginChatActions(actions)
76
+ }).catch(() => {})
77
+ }
78
+ }, [sessionId])
69
79
 
70
80
  // Collect unique connector sources from messages for filter UI
71
81
  const { connectorSources, hasDirectMessages } = useMemo(() => {
@@ -421,6 +431,7 @@ export function ChatArea() {
421
431
  streaming={streamingForThisSession}
422
432
  onSend={sendMessage}
423
433
  onStop={stopStreaming}
434
+ pluginChatActions={pluginChatActions}
424
435
  />
425
436
 
426
437
  <Dropdown open={menuOpen} onClose={() => setMenuOpen(false)}>