@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,6 +1,9 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
6
+ import toast from 'react-hot-toast'
4
7
  import { ModelCombobox } from '@/components/shared/model-combobox'
5
8
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
6
9
  import type { SettingsSectionProps } from './types'
@@ -8,7 +11,12 @@ import type { SettingsSectionProps } from './types'
8
11
  export function OrchestratorSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
9
12
  const providers = useAppStore((s) => s.providers)
10
13
  const credentials = useAppStore((s) => s.credentials)
14
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
11
15
  const credList = Object.values(credentials)
16
+ const [addingKey, setAddingKey] = useState(false)
17
+ const [newKeyName, setNewKeyName] = useState('')
18
+ const [newKeyValue, setNewKeyValue] = useState('')
19
+ const [savingKey, setSavingKey] = useState(false)
12
20
 
13
21
  const lgProviders = providers.filter((p) => !NON_LANGGRAPH_PROVIDER_IDS.has(String(p.id)))
14
22
  const hasConfiguredLgProvider = !!appSettings.langGraphProvider && lgProviders.some((p) => p.id === appSettings.langGraphProvider)
@@ -82,22 +90,45 @@ export function OrchestratorSection({ appSettings, patchSettings, inputClass }:
82
90
  {/* API Key picker */}
83
91
  <div>
84
92
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
85
- {lgCredentials.length > 0 ? (
86
- <select
87
- value={appSettings.langGraphCredentialId || ''}
88
- onChange={(e) => patchSettings({ langGraphCredentialId: e.target.value || null })}
89
- className={`${inputClass} appearance-none cursor-pointer`}
90
- style={{ fontFamily: 'inherit' }}
91
- >
92
- <option value="">Select a key...</option>
93
- {lgCredentials.map((c) => (
94
- <option key={c.id} value={c.id}>{c.name}</option>
95
- ))}
96
- </select>
93
+ {lgCredentials.length > 0 && !addingKey ? (
94
+ <div className="flex gap-2 items-center">
95
+ <select
96
+ value={appSettings.langGraphCredentialId || ''}
97
+ onChange={(e) => patchSettings({ langGraphCredentialId: e.target.value || null })}
98
+ className={`${inputClass} appearance-none cursor-pointer flex-1`}
99
+ style={{ fontFamily: 'inherit' }}
100
+ >
101
+ <option value="">Select a key...</option>
102
+ {lgCredentials.map((c) => (
103
+ <option key={c.id} value={c.id}>{c.name}</option>
104
+ ))}
105
+ </select>
106
+ <button type="button" onClick={() => setAddingKey(true)} className="text-accent-bright text-[11px] font-600 cursor-pointer bg-transparent border-none hover:brightness-110 transition-all" style={{ fontFamily: 'inherit' }}>+ New</button>
107
+ </div>
97
108
  ) : (
98
- <p className="text-[12px] text-text-3/60">
99
- No {lgProvider} API keys configured. Add one below in the Providers section.
100
- </p>
109
+ <div className="space-y-2">
110
+ <input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="Key name (optional)" className={inputClass} style={{ fontFamily: 'inherit' }} />
111
+ <input type="password" value={newKeyValue} onChange={e => setNewKeyValue(e.target.value)} placeholder="sk-..." className={inputClass} style={{ fontFamily: 'inherit' }} />
112
+ <div className="flex gap-2">
113
+ <button type="button" disabled={savingKey || !newKeyValue.trim()} onClick={async () => {
114
+ setSavingKey(true)
115
+ try {
116
+ const cred = await api<{ id: string }>('POST', '/credentials', { provider: lgProvider, name: newKeyName.trim() || `${lgProvider} key`, apiKey: newKeyValue.trim() })
117
+ await loadCredentials()
118
+ patchSettings({ langGraphCredentialId: cred.id })
119
+ setAddingKey(false)
120
+ setNewKeyName('')
121
+ setNewKeyValue('')
122
+ } catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
123
+ finally { setSavingKey(false) }
124
+ }} className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40" style={{ fontFamily: 'inherit' }}>
125
+ {savingKey ? 'Saving...' : 'Save Key'}
126
+ </button>
127
+ {lgCredentials.length > 0 && (
128
+ <button type="button" onClick={() => { setAddingKey(false); setNewKeyName(''); setNewKeyValue('') }} className="px-4 py-1.5 rounded-[8px] bg-surface-2 text-text-2 text-[12px] font-600 cursor-pointer border-none hover:bg-surface-3 transition-all" style={{ fontFamily: 'inherit' }}>Cancel</button>
129
+ )}
130
+ </div>
131
+ </div>
101
132
  )}
102
133
  </div>
103
134
  </div>
@@ -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,206 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
7
+ import { StorageBrowser } from './storage-browser'
8
+ import type { SettingsSectionProps } from './types'
9
+
10
+ interface UploadFile {
11
+ name: string
12
+ size: number
13
+ modified: number
14
+ category: string
15
+ url: string
16
+ }
17
+
18
+ interface UploadsResponse {
19
+ files: UploadFile[]
20
+ totalSize: number
21
+ count: number
22
+ }
23
+
24
+ function formatBytes(bytes: number): string {
25
+ if (bytes === 0) return '0 B'
26
+ const units = ['B', 'KB', 'MB', 'GB']
27
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
28
+ const value = bytes / Math.pow(1024, i)
29
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`
30
+ }
31
+
32
+ export function StorageSection(
33
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
34
+ _props: SettingsSectionProps,
35
+ ) {
36
+ const [data, setData] = useState<UploadsResponse | null>(null)
37
+ const [loading, setLoading] = useState(true)
38
+ const [browserOpen, setBrowserOpen] = useState(false)
39
+ const [confirmAction, setConfirmAction] = useState<'clearOld' | 'clearAll' | null>(null)
40
+ const [deleting, setDeleting] = useState(false)
41
+
42
+ const fetchFiles = useCallback(async () => {
43
+ try {
44
+ setLoading(true)
45
+ const res = await api<UploadsResponse>('GET', '/uploads')
46
+ setData(res)
47
+ } catch {
48
+ // silent — section just shows empty
49
+ } finally {
50
+ setLoading(false)
51
+ }
52
+ }, [])
53
+
54
+ useEffect(() => {
55
+ fetchFiles()
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [])
58
+
59
+ const handleBulkDelete = useCallback(async (filenames: string[]) => {
60
+ try {
61
+ await api('DELETE', '/uploads', { filenames })
62
+ await fetchFiles()
63
+ } catch {
64
+ // silent
65
+ }
66
+ }, [fetchFiles])
67
+
68
+ const handleConfirmAction = useCallback(async () => {
69
+ if (!confirmAction) return
70
+ setDeleting(true)
71
+ try {
72
+ if (confirmAction === 'clearOld') {
73
+ await api('DELETE', '/uploads', { olderThanDays: 30 })
74
+ } else {
75
+ await api('DELETE', '/uploads', { all: true })
76
+ }
77
+ await fetchFiles()
78
+ } catch {
79
+ // silent
80
+ } finally {
81
+ setDeleting(false)
82
+ setConfirmAction(null)
83
+ }
84
+ }, [confirmAction, fetchFiles])
85
+
86
+ // Breakdown by category
87
+ const breakdown = data?.files.reduce<Record<string, { count: number; size: number }>>((acc, f) => {
88
+ if (!acc[f.category]) acc[f.category] = { count: 0, size: 0 }
89
+ acc[f.category].count += 1
90
+ acc[f.category].size += f.size
91
+ return acc
92
+ }, {}) ?? {}
93
+
94
+ const CATEGORY_LABELS: Record<string, string> = {
95
+ image: 'Images',
96
+ video: 'Videos',
97
+ audio: 'Audio',
98
+ document: 'Documents',
99
+ archive: 'Archives',
100
+ other: 'Other',
101
+ }
102
+
103
+ return (
104
+ <div className="mb-10">
105
+ <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
106
+ Storage
107
+ </h3>
108
+ <p className="text-[12px] text-text-3 mb-5">
109
+ Uploaded files from agent tools (screenshots, images, documents). Manage disk usage.
110
+ </p>
111
+
112
+ <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
113
+ {/* Summary */}
114
+ {loading ? (
115
+ <div className="text-[13px] text-text-3/60 animate-pulse">Loading storage info...</div>
116
+ ) : (
117
+ <>
118
+ <div className="flex items-baseline gap-3 mb-4">
119
+ <span className="font-display text-[28px] font-700 tracking-[-0.02em] text-text">
120
+ {formatBytes(data?.totalSize ?? 0)}
121
+ </span>
122
+ <span className="text-[13px] text-text-3">
123
+ {data?.count ?? 0} file{data?.count !== 1 ? 's' : ''}
124
+ </span>
125
+ </div>
126
+
127
+ {/* Category breakdown */}
128
+ {Object.keys(breakdown).length > 0 && (
129
+ <div className="flex flex-wrap gap-x-4 gap-y-1 mb-5">
130
+ {Object.entries(breakdown).map(([cat, info]) => (
131
+ <span key={cat} className="text-[11px] text-text-3/70">
132
+ {CATEGORY_LABELS[cat] || cat}: {info.count} ({formatBytes(info.size)})
133
+ </span>
134
+ ))}
135
+ </div>
136
+ )}
137
+
138
+ {/* Actions */}
139
+ <div className="flex flex-wrap gap-2">
140
+ <button
141
+ onClick={() => setBrowserOpen(true)}
142
+ disabled={!data?.count}
143
+ className="px-4 py-2.5 rounded-[12px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer
144
+ hover:brightness-110 active:scale-[0.97] transition-all border border-accent-bright/20
145
+ disabled:opacity-40 disabled:cursor-not-allowed"
146
+ style={{ fontFamily: 'inherit' }}
147
+ >
148
+ Manage Files
149
+ </button>
150
+ <button
151
+ onClick={() => setConfirmAction('clearOld')}
152
+ disabled={!data?.count}
153
+ className="px-4 py-2.5 rounded-[12px] bg-white/[0.04] text-text-2 text-[12px] font-600 cursor-pointer
154
+ hover:bg-white/[0.06] active:scale-[0.97] transition-all border border-white/[0.06]
155
+ disabled:opacity-40 disabled:cursor-not-allowed"
156
+ style={{ fontFamily: 'inherit' }}
157
+ >
158
+ Clear Old Files
159
+ </button>
160
+ <button
161
+ onClick={() => setConfirmAction('clearAll')}
162
+ disabled={!data?.count}
163
+ className="px-4 py-2.5 rounded-[12px] bg-danger/10 text-danger text-[12px] font-600 cursor-pointer
164
+ hover:bg-danger/20 active:scale-[0.97] transition-all border border-danger/20
165
+ disabled:opacity-40 disabled:cursor-not-allowed"
166
+ style={{ fontFamily: 'inherit' }}
167
+ >
168
+ Clear All
169
+ </button>
170
+ </div>
171
+ </>
172
+ )}
173
+ </div>
174
+
175
+ {/* File browser sheet */}
176
+ <BottomSheet open={browserOpen} onClose={() => setBrowserOpen(false)} wide>
177
+ {data && (
178
+ <StorageBrowser
179
+ files={data.files}
180
+ onDelete={handleBulkDelete}
181
+ />
182
+ )}
183
+ </BottomSheet>
184
+
185
+ {/* Confirm dialogs */}
186
+ <ConfirmDialog
187
+ open={confirmAction === 'clearOld'}
188
+ title="Clear Old Files"
189
+ message="Delete all uploaded files older than 30 days? This cannot be undone."
190
+ confirmLabel={deleting ? 'Deleting...' : 'Delete Old Files'}
191
+ danger
192
+ onConfirm={handleConfirmAction}
193
+ onCancel={() => setConfirmAction(null)}
194
+ />
195
+ <ConfirmDialog
196
+ open={confirmAction === 'clearAll'}
197
+ title="Clear All Files"
198
+ message="Delete ALL uploaded files? This will free up all storage but cannot be undone."
199
+ confirmLabel={deleting ? 'Deleting...' : 'Delete All'}
200
+ danger
201
+ onConfirm={handleConfirmAction}
202
+ onCancel={() => setConfirmAction(null)}
203
+ />
204
+ </div>
205
+ )
206
+ }
@@ -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,58 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
19
24
  className={`${inputClass} resize-y min-h-[100px]`}
20
25
  style={{ fontFamily: 'inherit' }}
21
26
  />
27
+
28
+ {/* Suggested replies toggle */}
29
+ <div className="mt-6 flex items-center justify-between">
30
+ <div>
31
+ <label className="text-[12px] font-600 text-text-2 block">Suggested Replies</label>
32
+ <p className="text-[11px] text-text-3/60 mt-0.5">
33
+ Show follow-up suggestions after each agent response.
34
+ </p>
35
+ </div>
36
+ <button
37
+ type="button"
38
+ onClick={() => patchSettings({ suggestionsEnabled: appSettings.suggestionsEnabled === false })}
39
+ className={`relative w-9 h-5 rounded-full transition-colors ${appSettings.suggestionsEnabled !== false ? 'bg-accent-bright' : 'bg-white/[0.10]'}`}
40
+ style={{ fontFamily: 'inherit' }}
41
+ >
42
+ <span className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${appSettings.suggestionsEnabled !== false ? 'translate-x-4' : ''}`} />
43
+ </button>
44
+ </div>
45
+
46
+ {/* Default agent */}
47
+ <div className="mt-6">
48
+ <label className="text-[12px] font-600 text-text-2 block mb-1.5">Default Agent</label>
49
+ <p className="text-[11px] text-text-3/60 mb-2">
50
+ The agent that opens automatically when you start the app or click Main Chat.
51
+ </p>
52
+ <div className="flex flex-wrap gap-2">
53
+ <button
54
+ onClick={() => patchSettings({ defaultAgentId: null })}
55
+ className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
56
+ ${!appSettings.defaultAgentId
57
+ ? 'bg-white/[0.06] border-accent-bright/30 text-text'
58
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
59
+ style={{ fontFamily: 'inherit' }}
60
+ >
61
+ Auto (first agent)
62
+ </button>
63
+ {sortedAgents.map((agent) => (
64
+ <button
65
+ key={agent.id}
66
+ onClick={() => patchSettings({ defaultAgentId: agent.id })}
67
+ className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
68
+ ${appSettings.defaultAgentId === agent.id
69
+ ? 'bg-white/[0.06] border-accent-bright/30 text-text'
70
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
71
+ style={{ fontFamily: 'inherit' }}
72
+ >
73
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={18} />
74
+ {agent.name}
75
+ </button>
76
+ ))}
77
+ </div>
78
+ </div>
22
79
  </div>
23
80
  )
24
81
  }
@@ -3,6 +3,8 @@
3
3
  import type { SettingsSectionProps } from './types'
4
4
 
5
5
  export function VoiceSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ const enabled = appSettings.elevenLabsEnabled ?? false
7
+
6
8
  return (
7
9
  <div className="mb-10">
8
10
  <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
@@ -12,30 +14,49 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
12
14
  Configure voice playback (TTS) and speech-to-text input.
13
15
  </p>
14
16
  <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
15
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
16
- <div>
17
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs API Key</label>
18
- <input
19
- type="password"
20
- value={appSettings.elevenLabsApiKey || ''}
21
- onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
22
- placeholder="sk_..."
23
- className={inputClass}
24
- style={{ fontFamily: 'inherit' }}
25
- />
26
- </div>
17
+ {/* ElevenLabs toggle */}
18
+ <div className="flex items-center justify-between mb-5">
27
19
  <div>
28
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs Voice ID</label>
29
- <input
30
- type="text"
31
- value={appSettings.elevenLabsVoiceId || ''}
32
- onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
33
- placeholder="JBFqnCBsd6RMkjVDRZzb"
34
- className={inputClass}
35
- style={{ fontFamily: 'inherit' }}
36
- />
20
+ <label className="font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em]">ElevenLabs TTS</label>
21
+ <p className="text-[11px] text-text-3/60 mt-0.5">Enable text-to-speech for agent responses</p>
37
22
  </div>
23
+ <button
24
+ type="button"
25
+ onClick={() => patchSettings({ elevenLabsEnabled: !enabled })}
26
+ className={`relative w-10 h-[22px] rounded-full transition-colors cursor-pointer border-none ${enabled ? 'bg-accent-bright' : 'bg-surface-3'}`}
27
+ >
28
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform ${enabled ? 'translate-x-[18px]' : ''}`} />
29
+ </button>
38
30
  </div>
31
+
32
+ {enabled && (
33
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
34
+ <div>
35
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">API Key</label>
36
+ <input
37
+ type="password"
38
+ value={appSettings.elevenLabsApiKey || ''}
39
+ onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
40
+ placeholder="sk_..."
41
+ className={inputClass}
42
+ style={{ fontFamily: 'inherit' }}
43
+ />
44
+ </div>
45
+ <div>
46
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Default Voice ID</label>
47
+ <input
48
+ type="text"
49
+ value={appSettings.elevenLabsVoiceId || ''}
50
+ onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
51
+ placeholder="JBFqnCBsd6RMkjVDRZzb"
52
+ className={inputClass}
53
+ style={{ fontFamily: 'inherit' }}
54
+ />
55
+ <p className="text-[11px] text-text-3/60 mt-1.5">Fallback voice when an agent has no override set.</p>
56
+ </div>
57
+ </div>
58
+ )}
59
+
39
60
  <div>
40
61
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Speech Recognition Language</label>
41
62
  <input
@@ -26,8 +26,8 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
26
26
  <option value="google">Google (scraping, no key required)</option>
27
27
  <option value="bing">Bing (scraping, no key required)</option>
28
28
  <option value="searxng">SearXNG (self-hosted, no key required)</option>
29
- <option value="tavily">Tavily (requires API key in Secrets)</option>
30
- <option value="brave">Brave Search (requires API key in Secrets)</option>
29
+ <option value="tavily">Tavily (API key required)</option>
30
+ <option value="brave">Brave Search (API key required)</option>
31
31
  </select>
32
32
  </div>
33
33
 
@@ -45,10 +45,34 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
45
45
  </div>
46
46
  )}
47
47
 
48
- {(provider === 'tavily' || provider === 'brave') && (
49
- <p className="text-[11px] text-text-3/70">
50
- Add a secret named &quot;{provider}&quot; or &quot;{provider}_api_key&quot; in the Secrets section below.
51
- </p>
48
+ {provider === 'tavily' && (
49
+ <div>
50
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Tavily API Key</label>
51
+ <input
52
+ type="password"
53
+ value={appSettings.tavilyApiKey || ''}
54
+ onChange={(e) => patchSettings({ tavilyApiKey: e.target.value || null })}
55
+ placeholder="tvly-..."
56
+ className={inputClass}
57
+ style={{ fontFamily: 'inherit' }}
58
+ />
59
+ <p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://tavily.com" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">tavily.com</a></p>
60
+ </div>
61
+ )}
62
+
63
+ {provider === 'brave' && (
64
+ <div>
65
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Brave Search API Key</label>
66
+ <input
67
+ type="password"
68
+ value={appSettings.braveApiKey || ''}
69
+ onChange={(e) => patchSettings({ braveApiKey: e.target.value || null })}
70
+ placeholder="BSA..."
71
+ className={inputClass}
72
+ style={{ fontFamily: 'inherit' }}
73
+ />
74
+ <p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://brave.com/search/api/" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">brave.com/search/api</a></p>
75
+ </div>
52
76
  )}
53
77
  </div>
54
78
  </div>