@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -1,12 +1,14 @@
1
1
  'use client'
2
2
 
3
- import { useEffect } from 'react'
3
+ import { useEffect, useState, useRef, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { inputClass } from './utils'
6
6
  import { UserPreferencesSection } from './section-user-preferences'
7
+ import { ThemeSection } from './section-theme'
7
8
  import { OrchestratorSection } from './section-orchestrator'
8
9
  import { RuntimeLoopSection } from './section-runtime-loop'
9
10
  import { CapabilityPolicySection } from './section-capability-policy'
11
+ import { StorageSection } from './section-storage'
10
12
  import { VoiceSection } from './section-voice'
11
13
  import { WebSearchSection } from './section-web-search'
12
14
  import { HeartbeatSection } from './section-heartbeat'
@@ -16,6 +18,46 @@ import { SecretsSection } from './section-secrets'
16
18
  import { ProvidersSection } from './section-providers'
17
19
  import { PluginManager } from './plugin-manager'
18
20
 
21
+ interface Tab {
22
+ id: string
23
+ label: string
24
+ icon: React.ReactNode
25
+ keywords: string[]
26
+ }
27
+
28
+ const TABS: Tab[] = [
29
+ {
30
+ id: 'general',
31
+ label: 'General',
32
+ keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools', 'storage', 'uploads', 'disk', 'files', 'cleanup'],
33
+ icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>,
34
+ },
35
+ {
36
+ id: 'appearance',
37
+ label: 'Appearance',
38
+ keywords: ['theme', 'color', 'hue', 'palette', 'dark', 'light', 'style', 'swatch'],
39
+ icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="5" /><line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" /><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" /><line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" /><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" /></svg>,
40
+ },
41
+ {
42
+ id: 'agents',
43
+ label: 'Agents & Loops',
44
+ keywords: ['orchestrator', 'runtime', 'loop', 'heartbeat', 'delegation', 'agent', 'swarm', 'turns'],
45
+ icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>,
46
+ },
47
+ {
48
+ id: 'memory',
49
+ label: 'Memory & AI',
50
+ keywords: ['embedding', 'vector', 'voice', 'web search', 'memory', 'consolidation', 'tts', 'ai'],
51
+ icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>,
52
+ },
53
+ {
54
+ id: 'integrations',
55
+ label: 'Integrations',
56
+ keywords: ['provider', 'secret', 'plugin', 'api', 'key', 'openai', 'anthropic', 'ollama', 'credential'],
57
+ icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 2v4m0 12v4M2 12h4m12 0h4" /><circle cx="12" cy="12" r="4" /><path d="M8 8L5.5 5.5M16 8l2.5-2.5M8 16l-2.5 2.5M16 16l2.5 2.5" /></svg>,
58
+ },
59
+ ]
60
+
19
61
  export function SettingsPage() {
20
62
  const loadProviders = useAppStore((s) => s.loadProviders)
21
63
  const loadCredentials = useAppStore((s) => s.loadCredentials)
@@ -25,6 +67,22 @@ export function SettingsPage() {
25
67
  const loadSecrets = useAppStore((s) => s.loadSecrets)
26
68
  const loadAgents = useAppStore((s) => s.loadAgents)
27
69
  const credentials = useAppStore((s) => s.credentials)
70
+ const validTabIds = TABS.map((t) => t.id)
71
+ const [activeTab, setActiveTabRaw] = useState(() => {
72
+ if (typeof window === 'undefined') return 'general'
73
+ const params = new URLSearchParams(window.location.search)
74
+ const tab = params.get('tab')
75
+ return tab && validTabIds.includes(tab) ? tab : 'general'
76
+ })
77
+ const contentRef = useRef<HTMLDivElement>(null)
78
+
79
+ const setActiveTab = useCallback((tab: string) => {
80
+ setActiveTabRaw(tab)
81
+ const url = new URL(window.location.href)
82
+ if (tab === 'general') url.searchParams.delete('tab')
83
+ else url.searchParams.set('tab', tab)
84
+ window.history.replaceState(null, '', url.toString())
85
+ }, [])
28
86
 
29
87
  useEffect(() => {
30
88
  loadProviders()
@@ -35,38 +93,135 @@ export function SettingsPage() {
35
93
  // eslint-disable-next-line react-hooks/exhaustive-deps
36
94
  }, [])
37
95
 
96
+ // Scroll to top when switching tabs
97
+ useEffect(() => {
98
+ contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
99
+ }, [activeTab])
100
+
101
+ const [searchQuery, setSearchQuery] = useState('')
38
102
  const credList = Object.values(credentials)
39
103
  const patchSettings = updateSettings
104
+ const sectionProps = { appSettings, patchSettings, inputClass }
105
+
106
+ const matchingTabIds = searchQuery
107
+ ? new Set(TABS.filter((t) => {
108
+ const q = searchQuery.toLowerCase()
109
+ return t.label.toLowerCase().includes(q) || t.keywords.some((k) => k.includes(q))
110
+ }).map((t) => t.id))
111
+ : null
112
+
113
+ // Auto-switch to first matching tab when searching
114
+ useEffect(() => {
115
+ if (matchingTabIds && matchingTabIds.size > 0 && !matchingTabIds.has(activeTab)) {
116
+ const first = TABS.find((t) => matchingTabIds.has(t.id))
117
+ if (first) setActiveTab(first.id)
118
+ }
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [searchQuery])
40
121
 
41
122
  return (
42
- <div className="flex-1 flex flex-col h-full overflow-y-auto">
43
- <div className="w-full max-w-3xl mx-auto px-6 py-8">
44
- <div className="mb-10">
45
- <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">Settings</h2>
46
- <p className="text-[14px] text-text-3">Manage providers, API keys & orchestrator engine</p>
123
+ <div className="flex-1 flex h-full min-w-0">
124
+ {/* Tab sidebar */}
125
+ <div className="w-[200px] shrink-0 border-r border-white/[0.04] py-6 px-3 flex flex-col gap-1">
126
+ <h2 className="font-display text-[14px] font-700 text-text px-3 mb-3 tracking-[-0.01em]">Settings</h2>
127
+ <div className="px-2 mb-3">
128
+ <div className="relative">
129
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-3/50">
130
+ <circle cx="11" cy="11" r="8" />
131
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
132
+ </svg>
133
+ <input
134
+ type="text"
135
+ value={searchQuery}
136
+ onChange={(e) => setSearchQuery(e.target.value)}
137
+ placeholder="Search settings..."
138
+ className="w-full pl-8 pr-2 py-1.5 text-[12px] bg-white/[0.04] rounded-[8px] border border-white/[0.06] text-text placeholder:text-text-3/40 outline-none focus:border-white/[0.12] transition-colors"
139
+ style={{ fontFamily: 'inherit' }}
140
+ />
141
+ </div>
47
142
  </div>
143
+ {TABS.map((tab) => {
144
+ const dimmed = matchingTabIds && !matchingTabIds.has(tab.id)
145
+ return (
146
+ <button
147
+ key={tab.id}
148
+ onClick={() => setActiveTab(tab.id)}
149
+ className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all border-none text-left
150
+ ${dimmed ? 'opacity-30' : ''}
151
+ ${activeTab === tab.id
152
+ ? 'bg-accent-soft text-accent-bright'
153
+ : 'bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04]'}`}
154
+ style={{ fontFamily: 'inherit' }}
155
+ >
156
+ <span className="shrink-0">{tab.icon}</span>
157
+ {tab.label}
158
+ </button>
159
+ )
160
+ })}
161
+ </div>
162
+
163
+ {/* Content */}
164
+ <div ref={contentRef} className="flex-1 overflow-y-auto">
165
+ <div className="max-w-2xl px-8 py-8">
166
+ {/* Tab header */}
167
+ <div className="mb-8">
168
+ <h3 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">
169
+ {TABS.find((t) => t.id === activeTab)?.label}
170
+ </h3>
171
+ <p className="text-[13px] text-text-3 mt-1">
172
+ {activeTab === 'general' && 'User preferences and global behavior settings.'}
173
+ {activeTab === 'appearance' && 'Customize the look and feel of the interface.'}
174
+ {activeTab === 'agents' && 'Orchestrator, runtime loops, capabilities and heartbeat.'}
175
+ {activeTab === 'memory' && 'Embedding, memory governance, voice and web search.'}
176
+ {activeTab === 'integrations' && 'Providers, secrets and plugins.'}
177
+ </p>
178
+ </div>
179
+
180
+ {activeTab === 'general' && (
181
+ <>
182
+ <UserPreferencesSection {...sectionProps} />
183
+ <CapabilityPolicySection {...sectionProps} />
184
+ <StorageSection {...sectionProps} />
185
+ </>
186
+ )}
187
+
188
+ {activeTab === 'appearance' && (
189
+ <ThemeSection {...sectionProps} />
190
+ )}
191
+
192
+ {activeTab === 'agents' && (
193
+ <>
194
+ <OrchestratorSection {...sectionProps} />
195
+ <RuntimeLoopSection {...sectionProps} />
196
+ <HeartbeatSection {...sectionProps} />
197
+ </>
198
+ )}
199
+
200
+ {activeTab === 'memory' && (
201
+ <>
202
+ <EmbeddingSection {...sectionProps} credList={credList} />
203
+ <MemorySection {...sectionProps} />
204
+ <VoiceSection {...sectionProps} />
205
+ <WebSearchSection {...sectionProps} />
206
+ </>
207
+ )}
48
208
 
49
- <UserPreferencesSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
50
- <OrchestratorSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
51
- <RuntimeLoopSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
52
- <CapabilityPolicySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
53
- <WebSearchSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
54
- <VoiceSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
55
- <HeartbeatSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
56
- <EmbeddingSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} credList={credList} />
57
- <MemorySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
58
- <SecretsSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
59
- <ProvidersSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
60
-
61
- <div className="mb-10">
62
- <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
63
- Plugins
64
- </h3>
65
- <p className="text-[12px] text-text-3 mb-5">
66
- Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
67
- <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
68
- </p>
69
- <PluginManager />
209
+ {activeTab === 'integrations' && (
210
+ <>
211
+ <ProvidersSection {...sectionProps} />
212
+ <SecretsSection {...sectionProps} />
213
+ <div className="mb-10">
214
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
215
+ Plugins
216
+ </h3>
217
+ <p className="text-[12px] text-text-3 mb-5">
218
+ Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
219
+ <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
220
+ </p>
221
+ <PluginManager />
222
+ </div>
223
+ </>
224
+ )}
70
225
  </div>
71
226
  </div>
72
227
  </div>
@@ -2,87 +2,23 @@
2
2
 
3
3
  import { useEffect } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
- import { BottomSheet } from '../bottom-sheet'
6
- import { inputClass } from './utils'
7
- import { UserPreferencesSection } from './section-user-preferences'
8
- import { OrchestratorSection } from './section-orchestrator'
9
- import { RuntimeLoopSection } from './section-runtime-loop'
10
- import { CapabilityPolicySection } from './section-capability-policy'
11
- import { VoiceSection } from './section-voice'
12
- import { HeartbeatSection } from './section-heartbeat'
13
- import { EmbeddingSection } from './section-embedding'
14
- import { MemorySection } from './section-memory'
15
- import { SecretsSection } from './section-secrets'
16
- import { ProvidersSection } from './section-providers'
17
- import { PluginManager } from './plugin-manager'
18
5
 
6
+ /**
7
+ * Legacy settings sheet — redirects to the full settings page.
8
+ * Kept for backwards compat in case any code calls setSettingsOpen(true).
9
+ */
19
10
  export function SettingsSheet() {
20
11
  const open = useAppStore((s) => s.settingsOpen)
21
12
  const setOpen = useAppStore((s) => s.setSettingsOpen)
22
- const loadProviders = useAppStore((s) => s.loadProviders)
23
- const loadCredentials = useAppStore((s) => s.loadCredentials)
24
- const appSettings = useAppStore((s) => s.appSettings)
25
- const loadSettings = useAppStore((s) => s.loadSettings)
26
- const updateSettings = useAppStore((s) => s.updateSettings)
27
- const loadSecrets = useAppStore((s) => s.loadSecrets)
28
- const loadAgents = useAppStore((s) => s.loadAgents)
29
- const credentials = useAppStore((s) => s.credentials)
13
+ const setActiveView = useAppStore((s) => s.setActiveView)
30
14
 
31
15
  useEffect(() => {
32
16
  if (open) {
33
- loadProviders()
34
- loadCredentials()
35
- loadSettings()
36
- loadSecrets()
37
- loadAgents()
17
+ setActiveView('settings')
18
+ setOpen(false)
38
19
  }
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
21
  }, [open])
40
22
 
41
- const credList = Object.values(credentials)
42
- const patchSettings = updateSettings
43
-
44
- return (
45
- <BottomSheet open={open} onClose={() => setOpen(false)} wide>
46
- {/* Header */}
47
- <div className="mb-10">
48
- <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">Settings</h2>
49
- <p className="text-[14px] text-text-3">Manage providers, API keys & orchestrator engine</p>
50
- </div>
51
-
52
- <UserPreferencesSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
53
- <OrchestratorSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
54
- <RuntimeLoopSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
55
- <CapabilityPolicySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
56
- <VoiceSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
57
- <HeartbeatSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
58
- <EmbeddingSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} credList={credList} />
59
- <MemorySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
60
- <SecretsSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
61
- <ProvidersSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
62
-
63
- {/* Plugins */}
64
- <div className="mb-10">
65
- <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
66
- Plugins
67
- </h3>
68
- <p className="text-[12px] text-text-3 mb-5">
69
- Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
70
- <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
71
- </p>
72
- <PluginManager />
73
- </div>
74
-
75
- {/* Done */}
76
- <div className="pt-2 border-t border-white/[0.04]">
77
- <button
78
- onClick={() => setOpen(false)}
79
- className="w-full py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer
80
- hover:bg-surface-2 transition-all duration-200"
81
- style={{ fontFamily: 'inherit' }}
82
- >
83
- Done
84
- </button>
85
- </div>
86
- </BottomSheet>
87
- )
23
+ return null
88
24
  }
@@ -0,0 +1,259 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
5
+
6
+ interface UploadFile {
7
+ name: string
8
+ size: number
9
+ modified: number
10
+ category: string
11
+ url: string
12
+ }
13
+
14
+ type SortField = 'modified' | 'size' | 'name'
15
+
16
+ interface Props {
17
+ files: UploadFile[]
18
+ onDelete: (filenames: string[]) => void
19
+ }
20
+
21
+ function formatBytes(bytes: number): string {
22
+ if (bytes === 0) return '0 B'
23
+ const units = ['B', 'KB', 'MB', 'GB']
24
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
25
+ const value = bytes / Math.pow(1024, i)
26
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`
27
+ }
28
+
29
+ function formatDate(ms: number): string {
30
+ const d = new Date(ms)
31
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
32
+ }
33
+
34
+ const CATEGORY_ICONS: Record<string, string> = {
35
+ image: '\u{1F5BC}',
36
+ video: '\u{1F3AC}',
37
+ audio: '\u{1F3B5}',
38
+ document: '\u{1F4C4}',
39
+ archive: '\u{1F4E6}',
40
+ other: '\u{1F4CE}',
41
+ }
42
+
43
+ const CATEGORY_LABELS: Record<string, string> = {
44
+ image: 'Images',
45
+ video: 'Videos',
46
+ audio: 'Audio',
47
+ document: 'Docs',
48
+ archive: 'Archives',
49
+ other: 'Other',
50
+ }
51
+
52
+ export function StorageBrowser({ files, onDelete }: Props) {
53
+ const [selected, setSelected] = useState<Set<string>>(new Set())
54
+ const [sortBy, setSortBy] = useState<SortField>('modified')
55
+ const [filterCategory, setFilterCategory] = useState<string | null>(null)
56
+ const [confirmDelete, setConfirmDelete] = useState<string[] | null>(null)
57
+
58
+ const categories = useMemo(() => {
59
+ const cats = new Set<string>()
60
+ for (const f of files) cats.add(f.category)
61
+ return Array.from(cats).sort()
62
+ }, [files])
63
+
64
+ const filtered = useMemo(() => {
65
+ let list = filterCategory ? files.filter((f) => f.category === filterCategory) : files
66
+ list = [...list].sort((a, b) => {
67
+ if (sortBy === 'modified') return b.modified - a.modified
68
+ if (sortBy === 'size') return b.size - a.size
69
+ return a.name.localeCompare(b.name)
70
+ })
71
+ return list
72
+ }, [files, filterCategory, sortBy])
73
+
74
+ const totalSize = useMemo(() => files.reduce((s, f) => s + f.size, 0), [files])
75
+
76
+ const toggleSelect = (name: string) => {
77
+ setSelected((prev) => {
78
+ const next = new Set(prev)
79
+ if (next.has(name)) next.delete(name)
80
+ else next.add(name)
81
+ return next
82
+ })
83
+ }
84
+
85
+ const toggleSelectAll = () => {
86
+ if (selected.size === filtered.length) {
87
+ setSelected(new Set())
88
+ } else {
89
+ setSelected(new Set(filtered.map((f) => f.name)))
90
+ }
91
+ }
92
+
93
+ const handleDeleteSelected = () => {
94
+ const names = Array.from(selected)
95
+ if (names.length > 0) setConfirmDelete(names)
96
+ }
97
+
98
+ const executeDelete = () => {
99
+ if (confirmDelete) {
100
+ onDelete(confirmDelete)
101
+ setSelected((prev) => {
102
+ const next = new Set(prev)
103
+ for (const name of confirmDelete) next.delete(name)
104
+ return next
105
+ })
106
+ setConfirmDelete(null)
107
+ }
108
+ }
109
+
110
+ return (
111
+ <div>
112
+ {/* Header */}
113
+ <div className="flex items-center justify-between mb-5">
114
+ <div>
115
+ <h3 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">File Browser</h3>
116
+ <p className="text-[12px] text-text-3 mt-0.5">
117
+ {files.length} file{files.length !== 1 ? 's' : ''} &middot; {formatBytes(totalSize)}
118
+ </p>
119
+ </div>
120
+ <select
121
+ value={sortBy}
122
+ onChange={(e) => setSortBy(e.target.value as SortField)}
123
+ className="px-3 py-1.5 rounded-[10px] border border-white/[0.08] bg-bg text-text text-[12px] outline-none cursor-pointer"
124
+ style={{ fontFamily: 'inherit' }}
125
+ >
126
+ <option value="modified">Newest first</option>
127
+ <option value="size">Largest first</option>
128
+ <option value="name">Name A-Z</option>
129
+ </select>
130
+ </div>
131
+
132
+ {/* Category filters */}
133
+ <div className="flex gap-1.5 mb-4 flex-wrap">
134
+ <button
135
+ onClick={() => setFilterCategory(null)}
136
+ className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
137
+ ${!filterCategory
138
+ ? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
139
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
140
+ style={{ fontFamily: 'inherit' }}
141
+ >
142
+ All
143
+ </button>
144
+ {categories.map((cat) => (
145
+ <button
146
+ key={cat}
147
+ onClick={() => setFilterCategory(filterCategory === cat ? null : cat)}
148
+ className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
149
+ ${filterCategory === cat
150
+ ? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
151
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
152
+ style={{ fontFamily: 'inherit' }}
153
+ >
154
+ {CATEGORY_ICONS[cat] || ''} {CATEGORY_LABELS[cat] || cat}
155
+ </button>
156
+ ))}
157
+ </div>
158
+
159
+ {/* Select all */}
160
+ {filtered.length > 0 && (
161
+ <div className="flex items-center gap-2 mb-3">
162
+ <button
163
+ onClick={toggleSelectAll}
164
+ className="text-[11px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
165
+ style={{ fontFamily: 'inherit' }}
166
+ >
167
+ {selected.size === filtered.length ? 'Deselect all' : 'Select all'}
168
+ </button>
169
+ {selected.size > 0 && (
170
+ <span className="text-[11px] text-text-3">
171
+ {selected.size} selected
172
+ </span>
173
+ )}
174
+ </div>
175
+ )}
176
+
177
+ {/* File grid */}
178
+ {filtered.length === 0 ? (
179
+ <div className="py-12 text-center text-[13px] text-text-3/60">
180
+ {files.length === 0 ? 'No uploaded files.' : 'No files match this filter.'}
181
+ </div>
182
+ ) : (
183
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-[400px] overflow-y-auto pr-1">
184
+ {filtered.map((file) => (
185
+ <div
186
+ key={file.name}
187
+ onClick={() => toggleSelect(file.name)}
188
+ className={`relative p-3 rounded-[14px] border cursor-pointer transition-all
189
+ ${selected.has(file.name)
190
+ ? 'border-accent-bright/40 bg-accent-soft/30'
191
+ : 'border-white/[0.06] bg-surface hover:border-white/[0.12]'}`}
192
+ >
193
+ {/* Checkbox */}
194
+ <div className={`absolute top-2 right-2 w-4 h-4 rounded-[5px] border transition-all flex items-center justify-center
195
+ ${selected.has(file.name)
196
+ ? 'border-accent-bright bg-accent-bright'
197
+ : 'border-white/[0.15] bg-transparent'}`}
198
+ >
199
+ {selected.has(file.name) && (
200
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
201
+ <polyline points="20 6 9 17 4 12" />
202
+ </svg>
203
+ )}
204
+ </div>
205
+
206
+ {/* Thumbnail / icon */}
207
+ <div className="w-full aspect-square rounded-[10px] bg-white/[0.03] mb-2 flex items-center justify-center overflow-hidden">
208
+ {file.category === 'image' ? (
209
+ // eslint-disable-next-line @next/next/no-img-element
210
+ <img
211
+ src={file.url}
212
+ alt={file.name}
213
+ className="w-full h-full object-cover rounded-[10px]"
214
+ loading="lazy"
215
+ />
216
+ ) : (
217
+ <span className="text-[28px]">{CATEGORY_ICONS[file.category] || CATEGORY_ICONS.other}</span>
218
+ )}
219
+ </div>
220
+
221
+ {/* Meta */}
222
+ <p className="text-[11px] font-600 text-text truncate" title={file.name}>{file.name}</p>
223
+ <p className="text-[10px] text-text-3/60 mt-0.5">
224
+ {formatBytes(file.size)} &middot; {formatDate(file.modified)}
225
+ </p>
226
+ </div>
227
+ ))}
228
+ </div>
229
+ )}
230
+
231
+ {/* Bulk delete footer */}
232
+ {selected.size > 0 && (
233
+ <div className="mt-4 pt-4 border-t border-white/[0.06] flex items-center justify-between">
234
+ <span className="text-[12px] text-text-3">
235
+ {selected.size} file{selected.size !== 1 ? 's' : ''} selected
236
+ </span>
237
+ <button
238
+ onClick={handleDeleteSelected}
239
+ className="px-4 py-2 rounded-[10px] bg-danger text-white text-[12px] font-600 cursor-pointer
240
+ hover:brightness-110 active:scale-[0.97] transition-all border-none"
241
+ style={{ fontFamily: 'inherit' }}
242
+ >
243
+ Delete Selected
244
+ </button>
245
+ </div>
246
+ )}
247
+
248
+ <ConfirmDialog
249
+ open={!!confirmDelete}
250
+ title="Delete Files"
251
+ message={`Permanently delete ${confirmDelete?.length ?? 0} file${(confirmDelete?.length ?? 0) !== 1 ? 's' : ''}? This cannot be undone.`}
252
+ confirmLabel="Delete"
253
+ danger
254
+ onConfirm={executeDelete}
255
+ onCancel={() => setConfirmDelete(null)}
256
+ />
257
+ </div>
258
+ )
259
+ }
@@ -0,0 +1,33 @@
1
+ import type React from 'react'
2
+
3
+ interface Props {
4
+ onCancel: () => void
5
+ onSave: () => void
6
+ saveLabel?: string
7
+ saveDisabled?: boolean
8
+ /** Extra buttons rendered on the left (e.g. Archive, Delete) */
9
+ left?: React.ReactNode
10
+ }
11
+
12
+ export function SheetFooter({ onCancel, onSave, saveLabel = 'Save', saveDisabled, left }: Props) {
13
+ return (
14
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
15
+ {left}
16
+ <button
17
+ onClick={onCancel}
18
+ className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
19
+ style={{ fontFamily: 'inherit' }}
20
+ >
21
+ Cancel
22
+ </button>
23
+ <button
24
+ onClick={onSave}
25
+ disabled={saveDisabled}
26
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
27
+ style={{ fontFamily: 'inherit' }}
28
+ >
29
+ {saveLabel}
30
+ </button>
31
+ </div>
32
+ )
33
+ }