@swarmclawai/swarmclaw 0.5.2 → 0.6.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 (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -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 +155 -0
  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/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -167,7 +167,7 @@ export function ProvidersSection({ inputClass }: SettingsSectionProps) {
167
167
  <button
168
168
  onClick={handleAdd}
169
169
  disabled={!newKey.trim()}
170
- className="flex-1 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
170
+ className="flex-1 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
171
171
  style={{ fontFamily: 'inherit' }}
172
172
  >
173
173
  Save Key
@@ -121,7 +121,7 @@ export function SecretsSection({ appSettings, inputClass }: SettingsSectionProps
121
121
 
122
122
  <div className="flex gap-3 pt-2">
123
123
  <button onClick={() => setAddingSecret(false)} className="flex-1 py-3 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 transition-colors" style={{ fontFamily: 'inherit' }}>Cancel</button>
124
- <button onClick={handleAddSecret} disabled={!secretName.trim() || !secretValue.trim()} className="flex-1 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110" style={{ fontFamily: 'inherit' }}>Save Secret</button>
124
+ <button onClick={handleAddSecret} disabled={!secretName.trim() || !secretValue.trim()} className="flex-1 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110" style={{ fontFamily: 'inherit' }}>Save Secret</button>
125
125
  </div>
126
126
  </div>
127
127
  ) : (
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { toast } from 'sonner'
5
+ import type { SettingsSectionProps } from './types'
6
+
7
+ const PRESETS = [
8
+ { label: 'Default', color: '#1e1e30' },
9
+ { label: 'Midnight', color: '#1a1a3a' },
10
+ { label: 'Forest', color: '#1a2e1e' },
11
+ { label: 'Warm', color: '#2e1e1a' },
12
+ { label: 'Slate', color: '#1e2428' },
13
+ { label: 'Rose', color: '#2e1a24' },
14
+ ]
15
+
16
+ export function ThemeSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
17
+ const currentHue = appSettings.themeHue || PRESETS[0].color
18
+ const [customHex, setCustomHex] = useState(
19
+ PRESETS.some((p) => p.color === currentHue) ? '' : currentHue,
20
+ )
21
+
22
+ const applyHue = (color: string) => {
23
+ patchSettings({ themeHue: color })
24
+ document.documentElement.style.setProperty('--neutral-tint', color)
25
+ toast.success('Theme updated')
26
+ }
27
+
28
+ const handleCustomChange = (value: string) => {
29
+ setCustomHex(value)
30
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) {
31
+ applyHue(value)
32
+ }
33
+ }
34
+
35
+ return (
36
+ <div className="mb-10">
37
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
38
+ Theme
39
+ </h3>
40
+ <p className="text-[12px] text-text-3 mb-5">
41
+ Shift the UI color palette. Pick a preset or enter a custom hex color.
42
+ </p>
43
+
44
+ {/* Preset swatches */}
45
+ <div className="flex flex-wrap gap-3 mb-4">
46
+ {PRESETS.map((preset) => {
47
+ const isActive = currentHue === preset.color && !customHex
48
+ return (
49
+ <button
50
+ key={preset.color}
51
+ onClick={() => { setCustomHex(''); applyHue(preset.color) }}
52
+ className={`group flex flex-col items-center gap-1.5 cursor-pointer bg-transparent border-none p-0`}
53
+ title={preset.label}
54
+ >
55
+ <div
56
+ className={`w-9 h-9 rounded-full transition-all duration-200 ${
57
+ isActive
58
+ ? 'ring-2 ring-accent-bright ring-offset-2 ring-offset-bg scale-110'
59
+ : 'hover:scale-105'
60
+ }`}
61
+ style={{ backgroundColor: preset.color }}
62
+ />
63
+ <span className={`text-[10px] font-500 ${isActive ? 'text-text' : 'text-text-3'}`}>
64
+ {preset.label}
65
+ </span>
66
+ </button>
67
+ )
68
+ })}
69
+ </div>
70
+
71
+ {/* Custom color picker + hex input */}
72
+ <div className="flex items-center gap-3">
73
+ <label className="text-[12px] text-text-3 shrink-0">Custom</label>
74
+ <div className="relative shrink-0">
75
+ <input
76
+ type="color"
77
+ value={customHex || currentHue}
78
+ onChange={(e) => handleCustomChange(e.target.value)}
79
+ className="w-9 h-9 rounded-full cursor-pointer border-2 border-white/[0.1] bg-transparent appearance-none [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-full [&::-webkit-color-swatch]:border-none [&::-moz-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none"
80
+ title="Pick a custom color"
81
+ />
82
+ </div>
83
+ <input
84
+ type="text"
85
+ value={customHex}
86
+ onChange={(e) => handleCustomChange(e.target.value)}
87
+ placeholder="#2a1f3d"
88
+ maxLength={7}
89
+ className={`${inputClass} max-w-[120px] font-mono`}
90
+ style={{ fontFamily: 'inherit' }}
91
+ />
92
+ </div>
93
+ </div>
94
+ )
95
+ }
@@ -1,8 +1,13 @@
1
1
  'use client'
2
2
 
3
+ import { useAppStore } from '@/stores/use-app-store'
4
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
3
5
  import type { SettingsSectionProps } from './types'
4
6
 
5
7
  export function UserPreferencesSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
8
+ const agents = useAppStore((s) => s.agents)
9
+ const sortedAgents = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
10
+
6
11
  return (
7
12
  <div className="mb-10">
8
13
  <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
@@ -19,6 +24,40 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
19
24
  className={`${inputClass} resize-y min-h-[100px]`}
20
25
  style={{ fontFamily: 'inherit' }}
21
26
  />
27
+
28
+ {/* Default agent */}
29
+ <div className="mt-6">
30
+ <label className="text-[12px] font-600 text-text-2 block mb-1.5">Default Agent</label>
31
+ <p className="text-[11px] text-text-3/60 mb-2">
32
+ The agent that opens automatically when you start the app or click Main Chat.
33
+ </p>
34
+ <div className="flex flex-wrap gap-2">
35
+ <button
36
+ onClick={() => patchSettings({ defaultAgentId: null })}
37
+ className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
38
+ ${!appSettings.defaultAgentId
39
+ ? 'bg-white/[0.06] border-accent-bright/30 text-text'
40
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
41
+ style={{ fontFamily: 'inherit' }}
42
+ >
43
+ Auto (first agent)
44
+ </button>
45
+ {sortedAgents.map((agent) => (
46
+ <button
47
+ key={agent.id}
48
+ onClick={() => patchSettings({ defaultAgentId: agent.id })}
49
+ className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
50
+ ${appSettings.defaultAgentId === agent.id
51
+ ? 'bg-white/[0.06] border-accent-bright/30 text-text'
52
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
53
+ style={{ fontFamily: 'inherit' }}
54
+ >
55
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={18} />
56
+ {agent.name}
57
+ </button>
58
+ ))}
59
+ </div>
60
+ </div>
22
61
  </div>
23
62
  )
24
63
  }
@@ -1,9 +1,10 @@
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'
@@ -16,6 +17,46 @@ import { SecretsSection } from './section-secrets'
16
17
  import { ProvidersSection } from './section-providers'
17
18
  import { PluginManager } from './plugin-manager'
18
19
 
20
+ interface Tab {
21
+ id: string
22
+ label: string
23
+ icon: React.ReactNode
24
+ keywords: string[]
25
+ }
26
+
27
+ const TABS: Tab[] = [
28
+ {
29
+ id: 'general',
30
+ label: 'General',
31
+ keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools'],
32
+ 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>,
33
+ },
34
+ {
35
+ id: 'appearance',
36
+ label: 'Appearance',
37
+ keywords: ['theme', 'color', 'hue', 'palette', 'dark', 'light', 'style', 'swatch'],
38
+ 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>,
39
+ },
40
+ {
41
+ id: 'agents',
42
+ label: 'Agents & Loops',
43
+ keywords: ['orchestrator', 'runtime', 'loop', 'heartbeat', 'delegation', 'agent', 'swarm', 'turns'],
44
+ 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>,
45
+ },
46
+ {
47
+ id: 'memory',
48
+ label: 'Memory & AI',
49
+ keywords: ['embedding', 'vector', 'voice', 'web search', 'memory', 'consolidation', 'tts', 'ai'],
50
+ 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>,
51
+ },
52
+ {
53
+ id: 'integrations',
54
+ label: 'Integrations',
55
+ keywords: ['provider', 'secret', 'plugin', 'api', 'key', 'openai', 'anthropic', 'ollama', 'credential'],
56
+ 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>,
57
+ },
58
+ ]
59
+
19
60
  export function SettingsPage() {
20
61
  const loadProviders = useAppStore((s) => s.loadProviders)
21
62
  const loadCredentials = useAppStore((s) => s.loadCredentials)
@@ -25,6 +66,22 @@ export function SettingsPage() {
25
66
  const loadSecrets = useAppStore((s) => s.loadSecrets)
26
67
  const loadAgents = useAppStore((s) => s.loadAgents)
27
68
  const credentials = useAppStore((s) => s.credentials)
69
+ const validTabIds = TABS.map((t) => t.id)
70
+ const [activeTab, setActiveTabRaw] = useState(() => {
71
+ if (typeof window === 'undefined') return 'general'
72
+ const params = new URLSearchParams(window.location.search)
73
+ const tab = params.get('tab')
74
+ return tab && validTabIds.includes(tab) ? tab : 'general'
75
+ })
76
+ const contentRef = useRef<HTMLDivElement>(null)
77
+
78
+ const setActiveTab = useCallback((tab: string) => {
79
+ setActiveTabRaw(tab)
80
+ const url = new URL(window.location.href)
81
+ if (tab === 'general') url.searchParams.delete('tab')
82
+ else url.searchParams.set('tab', tab)
83
+ window.history.replaceState(null, '', url.toString())
84
+ }, [])
28
85
 
29
86
  useEffect(() => {
30
87
  loadProviders()
@@ -35,38 +92,134 @@ export function SettingsPage() {
35
92
  // eslint-disable-next-line react-hooks/exhaustive-deps
36
93
  }, [])
37
94
 
95
+ // Scroll to top when switching tabs
96
+ useEffect(() => {
97
+ contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
98
+ }, [activeTab])
99
+
100
+ const [searchQuery, setSearchQuery] = useState('')
38
101
  const credList = Object.values(credentials)
39
102
  const patchSettings = updateSettings
103
+ const sectionProps = { appSettings, patchSettings, inputClass }
104
+
105
+ const matchingTabIds = searchQuery
106
+ ? new Set(TABS.filter((t) => {
107
+ const q = searchQuery.toLowerCase()
108
+ return t.label.toLowerCase().includes(q) || t.keywords.some((k) => k.includes(q))
109
+ }).map((t) => t.id))
110
+ : null
111
+
112
+ // Auto-switch to first matching tab when searching
113
+ useEffect(() => {
114
+ if (matchingTabIds && matchingTabIds.size > 0 && !matchingTabIds.has(activeTab)) {
115
+ const first = TABS.find((t) => matchingTabIds.has(t.id))
116
+ if (first) setActiveTab(first.id)
117
+ }
118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
+ }, [searchQuery])
40
120
 
41
121
  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>
122
+ <div className="flex-1 flex h-full min-w-0">
123
+ {/* Tab sidebar */}
124
+ <div className="w-[200px] shrink-0 border-r border-white/[0.04] py-6 px-3 flex flex-col gap-1">
125
+ <h2 className="font-display text-[14px] font-700 text-text px-3 mb-3 tracking-[-0.01em]">Settings</h2>
126
+ <div className="px-2 mb-3">
127
+ <div className="relative">
128
+ <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">
129
+ <circle cx="11" cy="11" r="8" />
130
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
131
+ </svg>
132
+ <input
133
+ type="text"
134
+ value={searchQuery}
135
+ onChange={(e) => setSearchQuery(e.target.value)}
136
+ placeholder="Search settings..."
137
+ 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"
138
+ style={{ fontFamily: 'inherit' }}
139
+ />
140
+ </div>
47
141
  </div>
142
+ {TABS.map((tab) => {
143
+ const dimmed = matchingTabIds && !matchingTabIds.has(tab.id)
144
+ return (
145
+ <button
146
+ key={tab.id}
147
+ onClick={() => setActiveTab(tab.id)}
148
+ 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
149
+ ${dimmed ? 'opacity-30' : ''}
150
+ ${activeTab === tab.id
151
+ ? 'bg-accent-soft text-accent-bright'
152
+ : 'bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04]'}`}
153
+ style={{ fontFamily: 'inherit' }}
154
+ >
155
+ <span className="shrink-0">{tab.icon}</span>
156
+ {tab.label}
157
+ </button>
158
+ )
159
+ })}
160
+ </div>
161
+
162
+ {/* Content */}
163
+ <div ref={contentRef} className="flex-1 overflow-y-auto">
164
+ <div className="max-w-2xl px-8 py-8">
165
+ {/* Tab header */}
166
+ <div className="mb-8">
167
+ <h3 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">
168
+ {TABS.find((t) => t.id === activeTab)?.label}
169
+ </h3>
170
+ <p className="text-[13px] text-text-3 mt-1">
171
+ {activeTab === 'general' && 'User preferences and global behavior settings.'}
172
+ {activeTab === 'appearance' && 'Customize the look and feel of the interface.'}
173
+ {activeTab === 'agents' && 'Orchestrator, runtime loops, capabilities and heartbeat.'}
174
+ {activeTab === 'memory' && 'Embedding, memory governance, voice and web search.'}
175
+ {activeTab === 'integrations' && 'Providers, secrets and plugins.'}
176
+ </p>
177
+ </div>
178
+
179
+ {activeTab === 'general' && (
180
+ <>
181
+ <UserPreferencesSection {...sectionProps} />
182
+ <CapabilityPolicySection {...sectionProps} />
183
+ </>
184
+ )}
185
+
186
+ {activeTab === 'appearance' && (
187
+ <ThemeSection {...sectionProps} />
188
+ )}
189
+
190
+ {activeTab === 'agents' && (
191
+ <>
192
+ <OrchestratorSection {...sectionProps} />
193
+ <RuntimeLoopSection {...sectionProps} />
194
+ <HeartbeatSection {...sectionProps} />
195
+ </>
196
+ )}
197
+
198
+ {activeTab === 'memory' && (
199
+ <>
200
+ <EmbeddingSection {...sectionProps} credList={credList} />
201
+ <MemorySection {...sectionProps} />
202
+ <VoiceSection {...sectionProps} />
203
+ <WebSearchSection {...sectionProps} />
204
+ </>
205
+ )}
48
206
 
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 />
207
+ {activeTab === 'integrations' && (
208
+ <>
209
+ <ProvidersSection {...sectionProps} />
210
+ <SecretsSection {...sectionProps} />
211
+ <div className="mb-10">
212
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
213
+ Plugins
214
+ </h3>
215
+ <p className="text-[12px] text-text-3 mb-5">
216
+ 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>.
217
+ <span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
218
+ </p>
219
+ <PluginManager />
220
+ </div>
221
+ </>
222
+ )}
70
223
  </div>
71
224
  </div>
72
225
  </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,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
+ }