@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
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
- import { AreaChart, Area, ResponsiveContainer } from 'recharts'
4
+ import { AreaChart, Area, ResponsiveContainer, Tooltip } from 'recharts'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
8
  import { api } from '@/lib/api-client'
9
9
  import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
10
+ import { HintTip } from '@/components/shared/hint-tip'
10
11
 
11
12
  function timeAgo(ts: number): string {
12
13
  const diff = Date.now() - ts
@@ -86,7 +87,7 @@ export function HomeView() {
86
87
  const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
87
88
  const setMessages = useChatStore((s) => s.setMessages)
88
89
  const [todayCost, setTodayCost] = useState(0)
89
- const [costTrend, setCostTrend] = useState<{ cost: number }[]>([])
90
+ const [costTrend, setCostTrend] = useState<{ cost: number; bucket: string }[]>([])
90
91
 
91
92
  const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
92
93
  const pinnedAgents = allAgents.filter((a) => a.pinned)
@@ -146,11 +147,13 @@ export function HomeView() {
146
147
  void loadSchedules()
147
148
  void loadNotifications()
148
149
  void loadConnectors()
149
- api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number }> }>('GET', '/usage?range=7d')
150
+ api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number; bucket: string }> }>('GET', '/usage?range=7d')
150
151
  .then((data) => {
151
- const total = (data.records || []).reduce((s, r) => s + (r.estimatedCost || 0), 0)
152
- setTodayCost(total)
153
- setCostTrend((data.timeSeries || []).map((pt) => ({ cost: pt.cost })))
152
+ const series = (data.timeSeries || []).map((pt: { cost: number; bucket?: string }) => ({ cost: pt.cost, bucket: pt.bucket || '' }))
153
+ setCostTrend(series)
154
+ const todayBucket = new Date().toISOString().slice(0, 10)
155
+ const todayPt = series.find((pt) => pt.bucket === todayBucket)
156
+ setTodayCost(todayPt?.cost || 0)
154
157
  })
155
158
  .catch(() => {})
156
159
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -189,7 +192,7 @@ export function HomeView() {
189
192
  <div className="flex-1 overflow-y-auto">
190
193
  <div className="max-w-[800px] mx-auto px-6 py-10">
191
194
  {/* Header */}
192
- <div className="mb-10">
195
+ <div className="mb-10" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
193
196
  <h1 className="font-display text-[28px] font-700 text-text tracking-[-0.03em]">
194
197
  SwarmClaw
195
198
  </h1>
@@ -200,25 +203,43 @@ export function HomeView() {
200
203
 
201
204
  {/* Quick Stats */}
202
205
  <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
203
- <StatCard label="Agents" value={String(agentCount)} />
204
- <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} />
205
- <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} />
206
- <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} />
206
+ <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" index={0} />
207
+ <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" index={1} />
208
+ <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" index={2} />
209
+ <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" index={3} />
207
210
  </div>
208
211
 
209
212
  {/* Cost trend sparkline */}
210
213
  {costTrend.length > 1 && (
211
- <div className="mb-10 px-1">
212
- <p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1">7-day cost trend</p>
214
+ <div className="mb-10 px-1" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
215
+ <p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1 flex items-center gap-1.5">
216
+ 7-day cost trend <HintTip text="Daily API spend over the past week — hover for details" />
217
+ </p>
213
218
  <ResponsiveContainer width="100%" height={60}>
214
- <AreaChart data={costTrend} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
219
+ <AreaChart data={costTrend} margin={{ top: 2, right: 0, bottom: 0, left: 0 }} style={{ cursor: 'crosshair' }}>
215
220
  <defs>
216
221
  <linearGradient id="costGrad" x1="0" y1="0" x2="0" y2="1">
217
222
  <stop offset="0%" stopColor="#818CF8" stopOpacity={0.3} />
218
223
  <stop offset="100%" stopColor="#818CF8" stopOpacity={0} />
219
224
  </linearGradient>
220
225
  </defs>
221
- <Area type="monotone" dataKey="cost" stroke="#818CF8" strokeWidth={1.5} fill="url(#costGrad)" dot={false} />
226
+ <Tooltip
227
+ content={({ active, payload }) => {
228
+ if (!active || !payload?.[0]) return null
229
+ const d = payload[0].payload as { cost: number; bucket: string }
230
+ const label = d.bucket
231
+ ? new Date(d.bucket + 'T00:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
232
+ : ''
233
+ return (
234
+ <div className="rounded-[8px] bg-surface border border-white/[0.1] px-3 py-2 shadow-lg">
235
+ <p className="text-[11px] text-text-3/70 m-0">{label}</p>
236
+ <p className="text-[14px] font-600 text-text m-0 mt-0.5">${d.cost.toFixed(4)}</p>
237
+ </div>
238
+ )
239
+ }}
240
+ cursor={{ stroke: '#818CF8', strokeWidth: 1, strokeDasharray: '3 3' }}
241
+ />
242
+ <Area type="monotone" dataKey="cost" stroke="#818CF8" strokeWidth={1.5} fill="url(#costGrad)" dot={false} activeDot={{ r: 3, fill: '#818CF8', stroke: '#818CF8' }} />
222
243
  </AreaChart>
223
244
  </ResponsiveContainer>
224
245
  </div>
@@ -226,7 +247,7 @@ export function HomeView() {
226
247
 
227
248
  {/* Notifications banner */}
228
249
  {unreadNotifications.length > 0 && (
229
- <section className="mb-8">
250
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
230
251
  <div className="rounded-[14px] border border-amber-400/20 bg-amber-400/[0.04] overflow-hidden">
231
252
  <div className="flex items-center gap-2 px-4 py-2.5 border-b border-amber-400/10">
232
253
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
@@ -262,7 +283,7 @@ export function HomeView() {
262
283
  )}
263
284
 
264
285
  {/* Connector Status */}
265
- <section className="mb-8">
286
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
266
287
  <SectionHeader label="Connectors" onViewAll={allConnectors.length > 0 ? () => setActiveView('connectors') : undefined} />
267
288
  {allConnectors.length > 0 ? (
268
289
  <div className="flex gap-2 flex-wrap">
@@ -286,7 +307,7 @@ export function HomeView() {
286
307
  </section>
287
308
 
288
309
  {/* Two-column layout: Running Tasks + Upcoming Schedules */}
289
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
310
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.45s both' }}>
290
311
  {/* Running Tasks */}
291
312
  <section>
292
313
  <SectionHeader label="Running Tasks" onViewAll={runningTasks.length > 0 ? () => setActiveView('tasks') : undefined} />
@@ -352,7 +373,7 @@ export function HomeView() {
352
373
  </div>
353
374
 
354
375
  {/* Pinned Agents */}
355
- <section className="mb-8">
376
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.5s both' }}>
356
377
  <SectionHeader label="Pinned Agents" />
357
378
  {pinnedAgents.length > 0 ? (
358
379
  <div className="flex gap-3 overflow-x-auto pb-2">
@@ -417,7 +438,7 @@ export function HomeView() {
417
438
  </section>
418
439
 
419
440
  {/* Recent Chats */}
420
- <section className="mb-8">
441
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.55s both' }}>
421
442
  <SectionHeader label="Recent Chats" />
422
443
  {recentChats.length > 0 ? (
423
444
  <div className="flex flex-col gap-1">
@@ -465,7 +486,7 @@ export function HomeView() {
465
486
 
466
487
  {/* Activity Feed */}
467
488
  {recentActivity.length > 0 && (
468
- <section className="mb-10">
489
+ <section className="mb-10" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.6s both' }}>
469
490
  <SectionHeader label="Recent Activity" />
470
491
  <div className="flex flex-col gap-0.5">
471
492
  {recentActivity.map((entry) => (
@@ -505,10 +526,19 @@ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () =>
505
526
  )
506
527
  }
507
528
 
508
- function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
529
+ function StatCard({ label, value, accent, hint, index = 0 }: { label: string; value: string; accent?: boolean; hint?: string; index?: number }) {
509
530
  return (
510
- <div className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
511
- <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1">{label}</p>
531
+ <div
532
+ className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06] hover:bg-white/[0.05] transition-all hover:scale-[1.02] active:scale-[0.98] cursor-default"
533
+ style={{
534
+ animation: 'spring-in 0.6s var(--ease-spring) both',
535
+ animationDelay: `${0.1 + index * 0.05}s`
536
+ }}
537
+ >
538
+ <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1 flex items-center gap-1.5">
539
+ {label}
540
+ {hint && <HintTip text={hint} />}
541
+ </p>
512
542
  <p className={`font-display text-[20px] font-700 tracking-[-0.02em] ${accent ? 'text-accent-bright' : 'text-text'}`}>{value}</p>
513
543
  </div>
514
544
  )
@@ -15,13 +15,14 @@ interface Props {
15
15
  streaming: boolean
16
16
  onSend: (text: string) => void
17
17
  onStop: () => void
18
+ pluginChatActions?: Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>
18
19
  }
19
20
 
20
21
  // FilePreview is now imported from @/components/shared/file-preview
21
22
 
22
23
  const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
23
24
 
24
- export function ChatInput({ streaming, onSend, onStop }: Props) {
25
+ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }: Props) {
25
26
  const [value, setValue] = useState('')
26
27
  const { ref: textareaRef, resize } = useAutoResize()
27
28
  const fileInputRef = useRef<HTMLInputElement>(null)
@@ -219,6 +220,26 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
219
220
  <span className="hidden sm:inline">Image</span>
220
221
  </button>
221
222
 
223
+ {/* Plugin Chat Actions */}
224
+ {pluginChatActions.map((action) => (
225
+ <Tooltip key={action.id}>
226
+ <TooltipTrigger asChild>
227
+ <button
228
+ onClick={() => {
229
+ if (action.action === 'message') onSend(action.value)
230
+ else if (action.action === 'link') window.open(action.value, '_blank')
231
+ }}
232
+ className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-emerald-500/[0.05]
233
+ text-emerald-400 text-[13px] cursor-pointer hover:text-emerald-300 hover:bg-emerald-500/[0.1] transition-all duration-200"
234
+ style={{ fontFamily: 'inherit' }}
235
+ >
236
+ {action.label}
237
+ </button>
238
+ </TooltipTrigger>
239
+ {action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
240
+ </Tooltip>
241
+ ))}
242
+
222
243
  {micSupported && (
223
244
  <button
224
245
  onClick={toggleRecording}
@@ -5,6 +5,7 @@ import { api } from '@/lib/api-client'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { Badge } from '@/components/ui/badge'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
+ import { EmptyState } from '@/components/shared/empty-state'
8
9
  import type { MemoryEntry } from '@/types'
9
10
 
10
11
  export function KnowledgeList() {
@@ -81,7 +82,7 @@ export function KnowledgeList() {
81
82
  <div className="flex-1 flex flex-col overflow-y-auto">
82
83
  {/* Search — only show when there are entries */}
83
84
  {entries.length > 0 && (
84
- <div className="px-5 py-2 shrink-0">
85
+ <div className="px-5 py-2 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
85
86
  <input
86
87
  type="text"
87
88
  value={search}
@@ -96,7 +97,7 @@ export function KnowledgeList() {
96
97
 
97
98
  {/* Tag filters */}
98
99
  {uniqueTags.length > 0 && (
99
- <div className="px-5 pb-1.5 shrink-0">
100
+ <div className="px-5 pb-1.5 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
100
101
  <div className="flex gap-1 flex-wrap">
101
102
  <button
102
103
  onClick={() => setActiveTag(null)}
@@ -124,7 +125,7 @@ export function KnowledgeList() {
124
125
  {/* Entries */}
125
126
  {entries.length > 0 ? (
126
127
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 px-5 pb-6">
127
- {entries.map((entry) => {
128
+ {entries.map((entry, idx) => {
128
129
  const meta = entry.metadata as { tags?: string[]; scope?: 'global' | 'agent'; agentIds?: string[] } | undefined
129
130
  const tags = meta?.tags || []
130
131
  const entryScope = meta?.scope || 'global'
@@ -136,7 +137,11 @@ export function KnowledgeList() {
136
137
  return (
137
138
  <div
138
139
  key={entry.id}
139
- className="p-3 rounded-[12px] border border-white/[0.04] bg-transparent hover:bg-surface-2 transition-all relative group"
140
+ className="p-3 rounded-[12px] border border-white/[0.04] bg-transparent hover:bg-surface-2 transition-all relative group hover:scale-[1.01] hover:border-white/[0.1]"
141
+ style={{
142
+ animation: 'spring-in 0.5s var(--ease-spring) both',
143
+ animationDelay: `${0.1 + idx * 0.03}s`
144
+ }}
140
145
  >
141
146
  <div className="flex items-start justify-between gap-2 mb-1">
142
147
  <span className="font-display text-[13px] font-600 text-text truncate">{entry.title}</span>
@@ -197,7 +202,7 @@ export function KnowledgeList() {
197
202
  })}
198
203
  </div>
199
204
  ) : error ? (
200
- <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
205
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
201
206
  <p className="font-display text-[14px] font-600 text-text-2">Couldn&apos;t load knowledge</p>
202
207
  <p className="text-[12px] text-text-3/60">{error}</p>
203
208
  <button
@@ -209,23 +214,17 @@ export function KnowledgeList() {
209
214
  </button>
210
215
  </div>
211
216
  ) : loaded ? (
212
- <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
213
- <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
217
+ <EmptyState
218
+ icon={
214
219
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
215
220
  <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
216
221
  <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
217
222
  </svg>
218
- </div>
219
- <p className="font-display text-[15px] font-600 text-text-2">No knowledge entries yet</p>
220
- <p className="text-[13px] text-text-3/50">Add shared knowledge for your agents</p>
221
- <button
222
- onClick={() => openSheet()}
223
- className="mt-1 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
224
- style={{ fontFamily: 'inherit' }}
225
- >
226
- + Add Knowledge
227
- </button>
228
- </div>
223
+ }
224
+ title="No knowledge entries yet"
225
+ subtitle="Add shared knowledge for your agents"
226
+ action={{ label: '+ Add Knowledge', onClick: () => openSheet() }}
227
+ />
229
228
  ) : null}
230
229
  </div>
231
230
  )
@@ -6,6 +6,7 @@ import { useAppStore } from '@/stores/use-app-store'
6
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
8
  import type { MemoryEntry } from '@/types'
9
+ import { toast } from 'sonner'
9
10
 
10
11
  const ACCEPTED_TYPES = '.txt,.md,.csv,.json,.jsonl,.html,.xml,.yaml,.yml,.toml,.py,.js,.ts,.tsx,.jsx,.go,.rs,.java,.c,.cpp,.h,.rb,.php,.sh,.sql,.log,.pdf'
11
12
 
@@ -91,21 +92,22 @@ export function KnowledgeSheet() {
91
92
  })
92
93
  if (!res.ok) {
93
94
  const err = await res.json().catch(() => ({ error: 'Upload failed' }))
94
- console.error('Upload failed:', err.error)
95
+ toast.error(err.error || 'Upload failed')
95
96
  return
96
97
  }
97
98
  const result: UploadResult = await res.json()
98
99
  if (!title.trim()) setTitle(result.title)
99
100
  setContent(result.content)
100
101
  setUploadedFile({ name: result.filename, url: result.url, size: result.size })
102
+ toast.success('Document content extracted')
101
103
 
102
104
  // Auto-tag based on file extension
103
105
  const ext = file.name.split('.').pop()?.toLowerCase() || ''
104
106
  if (ext && !tags.includes(ext)) {
105
107
  setTags((prev) => prev ? `${prev}, ${ext}` : ext)
106
108
  }
107
- } catch (err) {
108
- console.error('Upload error:', err)
109
+ } catch (err: unknown) {
110
+ toast.error(err instanceof Error ? err.message : 'Upload failed')
109
111
  } finally {
110
112
  setUploading(false)
111
113
  }
@@ -168,13 +170,15 @@ export function KnowledgeSheet() {
168
170
 
169
171
  if (editingId) {
170
172
  await api('PUT', `/knowledge/${editingId}`, payload)
173
+ toast.success('Knowledge entry updated')
171
174
  } else {
172
175
  await api('POST', '/knowledge', payload)
176
+ toast.success('Knowledge entry created')
173
177
  }
174
178
 
175
179
  onClose()
176
- } catch {
177
- // silent
180
+ } catch (err: unknown) {
181
+ toast.error(err instanceof Error ? err.message : 'Failed to save knowledge')
178
182
  } finally {
179
183
  setSaving(false)
180
184
  }
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { Component, useState, useEffect, useCallback } from 'react'
3
+ import { Component, useState, useEffect, useCallback, useMemo } from 'react'
4
4
  import type { ReactNode, ErrorInfo } from 'react'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useMediaQuery } from '@/hooks/use-media-query'
@@ -18,6 +18,7 @@ import { MemoryBrowser } from '@/components/memory/memory-browser'
18
18
  import { TaskList } from '@/components/tasks/task-list'
19
19
  import { TaskSheet } from '@/components/tasks/task-sheet'
20
20
  import { TaskBoard } from '@/components/tasks/task-board'
21
+ import { ApprovalsPanel } from '@/components/tasks/approvals-panel'
21
22
  import { SecretsList } from '@/components/secrets/secrets-list'
22
23
  import { SecretSheet } from '@/components/secrets/secret-sheet'
23
24
  import { ProviderList } from '@/components/providers/provider-list'
@@ -62,6 +63,8 @@ import { CanvasPanel } from '@/components/canvas/canvas-panel'
62
63
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
63
64
  import { api } from '@/lib/api-client'
64
65
  import { safeStorageGet, safeStorageSet } from '@/lib/safe-storage'
66
+ import { useApprovalStore } from '@/stores/use-approval-store'
67
+ import { useWs } from '@/hooks/use-ws'
65
68
  import type { AppView } from '@/types'
66
69
 
67
70
  const RAIL_EXPANDED_KEY = 'sc_rail_expanded'
@@ -89,9 +92,37 @@ export function AppLayout() {
89
92
  const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
90
93
  const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
91
94
  const tasks = useAppStore((s) => s.tasks)
95
+ const approvals = useAppStore((s) => s.approvals)
96
+ const loadApprovals = useAppStore((s) => s.loadApprovals)
97
+ const execApprovals = useApprovalStore((s) => s.approvals)
98
+ const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
99
+ const pruneExecApprovals = useApprovalStore((s) => s.pruneExpired)
92
100
  const isDesktop = useMediaQuery('(min-width: 768px)')
93
101
  const hasSelectedSession = !!(currentSessionId && sessions[currentSessionId])
94
- const pendingApprovalCount = Object.values(tasks).filter((t) => t.pendingApproval).length
102
+
103
+ const pendingApprovalCount = useMemo(() => {
104
+ const taskCount = Object.values(tasks).filter((t) => t.pendingApproval).length
105
+ const sessionCount = Object.values(approvals).filter((a) => a.status === 'pending').length
106
+ const execCount = Object.keys(execApprovals).length
107
+ return taskCount + sessionCount + execCount
108
+ }, [tasks, approvals, execApprovals])
109
+
110
+ useEffect(() => {
111
+ loadApprovals()
112
+ void loadExecApprovals()
113
+ const interval = setInterval(() => {
114
+ loadApprovals()
115
+ void loadExecApprovals()
116
+ pruneExecApprovals()
117
+ }, 10000)
118
+ return () => clearInterval(interval)
119
+ }, [loadApprovals, loadExecApprovals, pruneExecApprovals])
120
+
121
+ useWs('approvals', loadApprovals, 10000)
122
+ useWs('openclaw:approvals', () => {
123
+ void loadExecApprovals()
124
+ pruneExecApprovals()
125
+ }, 10000)
95
126
 
96
127
  const appSettings = useAppStore((s) => s.appSettings)
97
128
  const [agentViewMode, setAgentViewMode] = useState<'chat' | 'config'>('chat')
@@ -145,6 +176,14 @@ export function AppLayout() {
145
176
  }
146
177
  }, [appSettings.themeHue])
147
178
 
179
+ const [pluginSidebarItems, setPluginSidebarItems] = useState<Array<{ id: string; label: string; href: string }>>([])
180
+
181
+ useEffect(() => {
182
+ api<Array<{ id: string; label: string; href: string }>>('GET', '/plugins/ui?type=sidebar').then(items => {
183
+ if (Array.isArray(items)) setPluginSidebarItems(items)
184
+ }).catch(() => {})
185
+ }, [])
186
+
148
187
  const [railExpanded, setRailExpanded] = useState(() => {
149
188
  const stored = safeStorageGet(RAIL_EXPANDED_KEY)
150
189
  return stored === null ? true : stored === 'true'
@@ -198,10 +237,11 @@ export function AppLayout() {
198
237
 
199
238
  const swipeHandlers = useSwipe({
200
239
  onSwipe: (dir) => {
240
+ if (isDesktop) return
201
241
  if (dir === 'right') setSidebarOpen(true)
202
242
  else setSidebarOpen(false)
203
243
  },
204
- leftSwipeEnabled: sidebarOpen,
244
+ leftSwipeEnabled: !isDesktop && sidebarOpen,
205
245
  })
206
246
 
207
247
  const currentSession = currentSessionId ? sessions[currentSessionId] : null
@@ -222,15 +262,15 @@ export function AppLayout() {
222
262
  return (
223
263
  <div
224
264
  className="h-full flex overflow-hidden"
225
- onTouchStart={swipeHandlers.onTouchStart}
226
- onTouchMove={swipeHandlers.onTouchMove}
227
- onTouchEnd={swipeHandlers.onTouchEnd}
265
+ onTouchStart={isDesktop ? undefined : swipeHandlers.onTouchStart}
266
+ onTouchMove={isDesktop ? undefined : swipeHandlers.onTouchMove}
267
+ onTouchEnd={isDesktop ? undefined : swipeHandlers.onTouchEnd}
228
268
  >
229
269
  {/* Desktop: Navigation rail (expandable) */}
230
270
  {isDesktop && (
231
271
  <div
232
- className="shrink-0 bg-raised border-r border-white/[0.04] flex flex-col py-4 transition-all duration-200 overflow-visible"
233
- style={{ width: railExpanded ? 180 : 60 }}
272
+ className="shrink-0 bg-raised border-r border-white/[0.04] flex flex-col py-4 min-h-0 transition-all duration-300 overflow-visible"
273
+ style={{ width: railExpanded ? 180 : 60, transitionTimingFunction: 'var(--ease-spring)' }}
234
274
  >
235
275
  {/* Logo + collapse toggle */}
236
276
  <div className={`flex items-center mb-4 shrink-0 ${railExpanded ? 'px-4 gap-3' : 'justify-center'}`}>
@@ -331,8 +371,9 @@ export function AppLayout() {
331
371
  </RailTooltip>
332
372
  )}
333
373
 
334
- {/* Nav items */}
335
- <div className={`flex flex-col gap-0.5 ${railExpanded ? 'px-3' : 'items-center'}`}>
374
+ <div className="flex-1 min-h-0 flex flex-col overflow-y-auto overscroll-contain touch-pan-y">
375
+ {/* Nav items */}
376
+ <div className={`flex flex-col gap-0.5 ${railExpanded ? 'px-3' : 'items-center'}`}>
336
377
  <NavItem view="home" label="Home" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('home')}>
337
378
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
338
379
  <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><polyline points="9 22 9 12 15 12 15 22" />
@@ -369,6 +410,12 @@ export function AppLayout() {
369
410
  <path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" /><rect x="9" y="3" width="6" height="4" rx="1" /><path d="M9 14l2 2 4-4" />
370
411
  </svg>
371
412
  </NavItem>
413
+ <NavItem view="approvals" label="Approvals" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('approvals')} badge={pendingApprovalCount}>
414
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
415
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
416
+ <path d="m9 12 2 2 4-4"/>
417
+ </svg>
418
+ </NavItem>
372
419
  <NavItem view="secrets" label="Secrets" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('secrets')}>
373
420
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
374
421
  <rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
@@ -434,12 +481,12 @@ export function AppLayout() {
434
481
  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
435
482
  </svg>
436
483
  </NavItem>
437
- </div>
484
+ </div>
438
485
 
439
- <div className="flex-1" />
486
+ <div className="flex-1" />
440
487
 
441
- {/* Bottom: Docs + Daemon + Settings + User */}
442
- <div className={`flex flex-col gap-1 ${railExpanded ? 'px-3' : 'items-center'}`}>
488
+ {/* Bottom: Docs + Daemon + Settings + User */}
489
+ <div className={`flex flex-col gap-1 ${railExpanded ? 'px-3' : 'items-center'}`}>
443
490
  {railExpanded ? (
444
491
  <a
445
492
  href="https://swarmclaw.ai/docs"
@@ -536,15 +583,16 @@ export function AppLayout() {
536
583
  </button>
537
584
  </RailTooltip>
538
585
  )}
586
+ </div>
539
587
  </div>
540
588
  </div>
541
589
  )}
542
590
 
543
591
  {/* Desktop: Side panel (wallets has its own built-in sidebar) */}
544
- {isDesktop && sidebarOpen && activeView !== 'wallets' && (
592
+ {isDesktop && sidebarOpen && activeView !== 'wallets' && activeView !== 'approvals' && (
545
593
  <div
546
- className="w-[280px] shrink-0 bg-raised border-r border-white/[0.04] flex flex-col h-full"
547
- style={{ animation: 'panel-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)' }}
594
+ className="w-[280px] shrink-0 bg-raised border-r border-white/[0.04] flex flex-col h-full min-h-0 overflow-hidden touch-pan-y"
595
+ style={{ animation: 'panel-in 0.3s var(--ease-spring)' }}
548
596
  >
549
597
  <div className="flex items-center px-5 pt-5 pb-3 shrink-0">
550
598
  <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] capitalize flex-1">{activeView}</h2>
@@ -611,7 +659,7 @@ export function AppLayout() {
611
659
  <div className="fixed inset-0 z-50">
612
660
  <div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={() => setSidebarOpen(false)} />
613
661
  <div
614
- className="absolute inset-y-0 left-0 w-[300px] bg-raised shadow-[4px_0_60px_rgba(0,0,0,0.7)] flex flex-col"
662
+ className="absolute inset-y-0 left-0 w-[300px] bg-raised shadow-[4px_0_60px_rgba(0,0,0,0.7)] flex flex-col min-h-0 overflow-hidden touch-pan-y"
615
663
  style={{ animation: 'slide-in-left 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }}
616
664
  >
617
665
  <div className="flex items-center gap-3 px-5 py-4 shrink-0">
@@ -652,6 +700,17 @@ export function AppLayout() {
652
700
  {v}
653
701
  </button>
654
702
  ))}
703
+ {/* Dynamic Plugin Items */}
704
+ {pluginSidebarItems.map((item) => (
705
+ <button
706
+ key={item.id}
707
+ onClick={() => window.open(item.href, '_blank')}
708
+ className="py-2 px-2.5 rounded-[10px] text-[11px] font-600 capitalize cursor-pointer transition-all bg-emerald-500/[0.05] text-emerald-400/80 hover:text-emerald-400 border border-emerald-400/10"
709
+ style={{ fontFamily: 'inherit' }}
710
+ >
711
+ {item.label}
712
+ </button>
713
+ ))}
655
714
  </div>
656
715
  {activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && (
657
716
  <div className="px-4 py-2.5 shrink-0">
@@ -742,6 +801,8 @@ export function AppLayout() {
742
801
  </div>
743
802
  ) : activeView === 'tasks' && isDesktop ? (
744
803
  <TaskBoard />
804
+ ) : activeView === 'approvals' ? (
805
+ <ApprovalsPanel />
745
806
  ) : activeView === 'memory' ? (
746
807
  <MemoryBrowser />
747
808
  ) : activeView === 'activity' ? (
@@ -900,6 +961,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
900
961
  schedules: 'Automated task schedules',
901
962
  memory: 'Long-term agent memory store',
902
963
  tasks: 'Task board for orchestrator jobs',
964
+ approvals: 'Pending tool execution approvals',
903
965
  secrets: 'API keys & credentials for orchestrators',
904
966
  providers: 'LLM providers & custom endpoints',
905
967
  skills: 'Reusable instruction sets for agents',
@@ -1038,6 +1100,12 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: str
1038
1100
  description: 'Agent crypto wallets for autonomous financial operations on Solana.',
1039
1101
  features: ['Create Solana wallets for agents', 'Per-transaction and daily spending limits', 'User approval for transactions', 'Balance tracking and transaction history'],
1040
1102
  },
1103
+ approvals: {
1104
+ icon: 'check-circle',
1105
+ title: 'Approvals',
1106
+ description: 'Review and approve pending tool executions from agents.',
1107
+ features: ['Review tool calls before execution', 'Approve or reject agent actions', 'Full context for each pending approval'],
1108
+ },
1041
1109
  }
1042
1110
 
1043
1111
  function ViewEmptyState({ view }: { view: AppView }) {
@@ -1154,7 +1222,7 @@ function NavItem({ view, label, expanded, active, sidebarOpen, onClick, badge, c
1154
1222
  </span>
1155
1223
  )}
1156
1224
  </span>
1157
- <span className="truncate">{label}</span>
1225
+ <span className="truncate" style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>{label}</span>
1158
1226
  </button>
1159
1227
  )
1160
1228
  }