@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
@@ -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'
@@ -63,6 +63,8 @@ import { CanvasPanel } from '@/components/canvas/canvas-panel'
63
63
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
64
64
  import { api } from '@/lib/api-client'
65
65
  import { safeStorageGet, safeStorageSet } from '@/lib/safe-storage'
66
+ import { useApprovalStore } from '@/stores/use-approval-store'
67
+ import { useWs } from '@/hooks/use-ws'
66
68
  import type { AppView } from '@/types'
67
69
 
68
70
  const RAIL_EXPANDED_KEY = 'sc_rail_expanded'
@@ -90,9 +92,37 @@ export function AppLayout() {
90
92
  const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
91
93
  const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
92
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)
93
100
  const isDesktop = useMediaQuery('(min-width: 768px)')
94
101
  const hasSelectedSession = !!(currentSessionId && sessions[currentSessionId])
95
- 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)
96
126
 
97
127
  const appSettings = useAppStore((s) => s.appSettings)
98
128
  const [agentViewMode, setAgentViewMode] = useState<'chat' | 'config'>('chat')
@@ -146,6 +176,14 @@ export function AppLayout() {
146
176
  }
147
177
  }, [appSettings.themeHue])
148
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
+
149
187
  const [railExpanded, setRailExpanded] = useState(() => {
150
188
  const stored = safeStorageGet(RAIL_EXPANDED_KEY)
151
189
  return stored === null ? true : stored === 'true'
@@ -199,10 +237,11 @@ export function AppLayout() {
199
237
 
200
238
  const swipeHandlers = useSwipe({
201
239
  onSwipe: (dir) => {
240
+ if (isDesktop) return
202
241
  if (dir === 'right') setSidebarOpen(true)
203
242
  else setSidebarOpen(false)
204
243
  },
205
- leftSwipeEnabled: sidebarOpen,
244
+ leftSwipeEnabled: !isDesktop && sidebarOpen,
206
245
  })
207
246
 
208
247
  const currentSession = currentSessionId ? sessions[currentSessionId] : null
@@ -223,15 +262,15 @@ export function AppLayout() {
223
262
  return (
224
263
  <div
225
264
  className="h-full flex overflow-hidden"
226
- onTouchStart={swipeHandlers.onTouchStart}
227
- onTouchMove={swipeHandlers.onTouchMove}
228
- onTouchEnd={swipeHandlers.onTouchEnd}
265
+ onTouchStart={isDesktop ? undefined : swipeHandlers.onTouchStart}
266
+ onTouchMove={isDesktop ? undefined : swipeHandlers.onTouchMove}
267
+ onTouchEnd={isDesktop ? undefined : swipeHandlers.onTouchEnd}
229
268
  >
230
269
  {/* Desktop: Navigation rail (expandable) */}
231
270
  {isDesktop && (
232
271
  <div
233
- className="shrink-0 bg-raised border-r border-white/[0.04] flex flex-col py-4 transition-all duration-200 overflow-visible"
234
- 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)' }}
235
274
  >
236
275
  {/* Logo + collapse toggle */}
237
276
  <div className={`flex items-center mb-4 shrink-0 ${railExpanded ? 'px-4 gap-3' : 'justify-center'}`}>
@@ -332,8 +371,9 @@ export function AppLayout() {
332
371
  </RailTooltip>
333
372
  )}
334
373
 
335
- {/* Nav items */}
336
- <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'}`}>
337
377
  <NavItem view="home" label="Home" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('home')}>
338
378
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
339
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" />
@@ -441,12 +481,12 @@ export function AppLayout() {
441
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" />
442
482
  </svg>
443
483
  </NavItem>
444
- </div>
484
+ </div>
445
485
 
446
- <div className="flex-1" />
486
+ <div className="flex-1" />
447
487
 
448
- {/* Bottom: Docs + Daemon + Settings + User */}
449
- <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'}`}>
450
490
  {railExpanded ? (
451
491
  <a
452
492
  href="https://swarmclaw.ai/docs"
@@ -543,19 +583,20 @@ export function AppLayout() {
543
583
  </button>
544
584
  </RailTooltip>
545
585
  )}
586
+ </div>
546
587
  </div>
547
588
  </div>
548
589
  )}
549
590
 
550
591
  {/* Desktop: Side panel (wallets has its own built-in sidebar) */}
551
- {isDesktop && sidebarOpen && activeView !== 'wallets' && (
592
+ {isDesktop && sidebarOpen && activeView !== 'wallets' && activeView !== 'approvals' && (
552
593
  <div
553
- className="w-[280px] shrink-0 bg-raised border-r border-white/[0.04] flex flex-col h-full"
554
- 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)' }}
555
596
  >
556
597
  <div className="flex items-center px-5 pt-5 pb-3 shrink-0">
557
598
  <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] capitalize flex-1">{activeView}</h2>
558
- {activeView === 'logs' || activeView === 'usage' || activeView === 'runs' || activeView === 'approvals' ? null : activeView === 'memory' ? (
599
+ {activeView === 'logs' || activeView === 'usage' || activeView === 'runs' ? null : activeView === 'memory' ? (
559
600
  <button
560
601
  onClick={() => useAppStore.getState().setMemorySheetOpen(true)}
561
602
  className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer"
@@ -618,7 +659,7 @@ export function AppLayout() {
618
659
  <div className="fixed inset-0 z-50">
619
660
  <div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={() => setSidebarOpen(false)} />
620
661
  <div
621
- 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"
622
663
  style={{ animation: 'slide-in-left 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }}
623
664
  >
624
665
  <div className="flex items-center gap-3 px-5 py-4 shrink-0">
@@ -659,8 +700,19 @@ export function AppLayout() {
659
700
  {v}
660
701
  </button>
661
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
+ ))}
662
714
  </div>
663
- {activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && activeView !== 'approvals' && (
715
+ {activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && (
664
716
  <div className="px-4 py-2.5 shrink-0">
665
717
  <button
666
718
  onClick={() => {
@@ -1170,7 +1222,7 @@ function NavItem({ view, label, expanded, active, sidebarOpen, onClick, badge, c
1170
1222
  </span>
1171
1223
  )}
1172
1224
  </span>
1173
- <span className="truncate">{label}</span>
1225
+ <span className="truncate" style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>{label}</span>
1174
1226
  </button>
1175
1227
  )
1176
1228
  }