@swarmclawai/swarmclaw 0.6.0 → 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 (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. package/src/types/index.ts +32 -2
@@ -76,10 +76,57 @@ export function ConnectorPlatformIcon({
76
76
  return <BsMicrosoftTeams size={size} className={className} />
77
77
  case 'openclaw':
78
78
  return (
79
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
80
- <path d="M4 17l2-5 2 5" /><path d="M12 17l2-5 2 5" /><path d="M20 17l-2-5-2 5" />
81
- <path d="M2 7l4-4 3 3" /><path d="M22 7l-4-4-3 3" />
82
- <line x1="12" y1="3" x2="12" y2="8" />
79
+ <svg width={size} height={size} viewBox="0 0 16 16" aria-hidden className={className}>
80
+ {/* OpenClaw pixel lobster mark */}
81
+ <g fill="#3a0a0d">
82
+ <rect x="1" y="5" width="1" height="3" />
83
+ <rect x="2" y="4" width="1" height="1" />
84
+ <rect x="2" y="8" width="1" height="1" />
85
+ <rect x="3" y="3" width="1" height="1" />
86
+ <rect x="3" y="9" width="1" height="1" />
87
+ <rect x="4" y="2" width="1" height="1" />
88
+ <rect x="4" y="10" width="1" height="1" />
89
+ <rect x="5" y="2" width="6" height="1" />
90
+ <rect x="11" y="2" width="1" height="1" />
91
+ <rect x="12" y="3" width="1" height="1" />
92
+ <rect x="12" y="9" width="1" height="1" />
93
+ <rect x="13" y="4" width="1" height="1" />
94
+ <rect x="13" y="8" width="1" height="1" />
95
+ <rect x="14" y="5" width="1" height="3" />
96
+ <rect x="5" y="11" width="6" height="1" />
97
+ <rect x="4" y="12" width="1" height="1" />
98
+ <rect x="11" y="12" width="1" height="1" />
99
+ <rect x="3" y="13" width="1" height="1" />
100
+ <rect x="12" y="13" width="1" height="1" />
101
+ <rect x="5" y="14" width="6" height="1" />
102
+ </g>
103
+ <g fill="#ff4f40">
104
+ <rect x="5" y="3" width="6" height="1" />
105
+ <rect x="4" y="4" width="8" height="1" />
106
+ <rect x="3" y="5" width="10" height="1" />
107
+ <rect x="3" y="6" width="10" height="1" />
108
+ <rect x="3" y="7" width="10" height="1" />
109
+ <rect x="4" y="8" width="8" height="1" />
110
+ <rect x="5" y="9" width="6" height="1" />
111
+ <rect x="5" y="12" width="6" height="1" />
112
+ <rect x="6" y="13" width="4" height="1" />
113
+ </g>
114
+ <g fill="#ff775f">
115
+ <rect x="1" y="6" width="2" height="1" />
116
+ <rect x="2" y="5" width="1" height="1" />
117
+ <rect x="2" y="7" width="1" height="1" />
118
+ <rect x="13" y="6" width="2" height="1" />
119
+ <rect x="13" y="5" width="1" height="1" />
120
+ <rect x="13" y="7" width="1" height="1" />
121
+ </g>
122
+ <g fill="#081016">
123
+ <rect x="6" y="5" width="1" height="1" />
124
+ <rect x="9" y="5" width="1" height="1" />
125
+ </g>
126
+ <g fill="#f5fbff">
127
+ <rect x="6" y="4" width="1" height="1" />
128
+ <rect x="9" y="4" width="1" height="1" />
129
+ </g>
83
130
  </svg>
84
131
  )
85
132
  default:
@@ -1,15 +1,17 @@
1
1
  'use client'
2
2
 
3
3
  import type { ButtonHTMLAttributes, ReactNode } from 'react'
4
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
4
5
 
5
6
  interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
6
7
  children: ReactNode
7
8
  variant?: 'default' | 'accent' | 'danger'
8
9
  active?: boolean
9
10
  size?: 'sm' | 'md'
11
+ tooltip?: string
10
12
  }
11
13
 
12
- export function IconButton({ children, variant = 'default', active, size = 'md', className = '', ...props }: Props) {
14
+ export function IconButton({ children, variant = 'default', active, size = 'md', className = '', tooltip, ...props }: Props) {
13
15
  const sizeClass = size === 'sm' ? 'w-8 h-8 rounded-[9px]' : 'w-9 h-9 rounded-[10px]'
14
16
  const base = `${sizeClass} border-none bg-transparent flex items-center justify-center cursor-pointer shrink-0 transition-all duration-200 hover:bg-white/[0.06] active:scale-90`
15
17
  const color =
@@ -17,9 +19,21 @@ export function IconButton({ children, variant = 'default', active, size = 'md',
17
19
  variant === 'danger' ? 'text-danger' :
18
20
  active ? 'text-accent-bright bg-accent-soft' : 'text-text-3 hover:text-text-2'
19
21
 
20
- return (
22
+ const btn = (
21
23
  <button className={`${base} ${color} ${className}`} {...props}>
22
24
  {children}
23
25
  </button>
24
26
  )
27
+
28
+ if (!tooltip) return btn
29
+
30
+ return (
31
+ <Tooltip>
32
+ <TooltipTrigger asChild>{btn}</TooltipTrigger>
33
+ <TooltipContent side="bottom" sideOffset={6}
34
+ className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[8px] px-2.5 py-1.5 text-[11px]">
35
+ {tooltip}
36
+ </TooltipContent>
37
+ </Tooltip>
38
+ )
25
39
  }
@@ -77,7 +77,7 @@ export function KeyboardShortcutsDialog() {
77
77
  <Dialog open={open} onOpenChange={setOpen}>
78
78
  <DialogContent
79
79
  showCloseButton={false}
80
- className="sm:max-w-[420px] p-0 bg-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
80
+ className="sm:max-w-[420px] p-0 bg-surface/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
81
81
  >
82
82
  <DialogTitle className="sr-only">Keyboard shortcuts</DialogTitle>
83
83
  <div className="flex items-center justify-between px-5 py-3.5 border-b border-white/[0.06]">
@@ -96,12 +96,12 @@ export function SearchDialog() {
96
96
 
97
97
  // Reset on open
98
98
  useEffect(() => {
99
- if (open) {
100
- setQuery('')
101
- setResults([])
102
- setSelectedIdx(0)
103
- setTimeout(() => inputRef.current?.focus(), 50)
104
- }
99
+ if (!open) return
100
+ setQuery('')
101
+ setResults([])
102
+ setSelectedIdx(0)
103
+ const timer = setTimeout(() => inputRef.current?.focus(), 50)
104
+ return () => clearTimeout(timer)
105
105
  }, [open])
106
106
 
107
107
  // Debounced search
@@ -152,6 +152,12 @@ export function SearchDialog() {
152
152
  case 'message':
153
153
  setCurrentSession(result.id)
154
154
  setActiveView('agents')
155
+ // Scroll to the matched message after the chat renders
156
+ if (result.messageIndex != null) {
157
+ setTimeout(() => {
158
+ window.dispatchEvent(new CustomEvent('swarmclaw:scroll-to-message', { detail: { index: result.messageIndex } }))
159
+ }, 300)
160
+ }
155
161
  break
156
162
  case 'schedule':
157
163
  setEditingScheduleId(result.id)
@@ -194,7 +200,7 @@ export function SearchDialog() {
194
200
  <Dialog open={open} onOpenChange={setOpen}>
195
201
  <DialogContent
196
202
  showCloseButton={false}
197
- className="sm:max-w-[520px] p-0 bg-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
203
+ className="sm:max-w-[520px] p-0 bg-surface/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
198
204
  onKeyDown={handleKeyDown}
199
205
  >
200
206
  <DialogTitle className="sr-only">Search</DialogTitle>
@@ -208,6 +214,7 @@ export function SearchDialog() {
208
214
  value={query}
209
215
  onChange={(e) => handleQueryChange(e.target.value)}
210
216
  placeholder="Search agents, tasks, schedules..."
217
+ aria-label="Search"
211
218
  className="flex-1 bg-transparent border-none outline-none text-[14px] text-text placeholder:text-text-3/60 font-[inherit]"
212
219
  autoFocus
213
220
  />
@@ -235,10 +242,10 @@ export function SearchDialog() {
235
242
  )}
236
243
  {results.map((result, idx) => (
237
244
  <button
238
- key={`${result.type}-${result.id}`}
245
+ key={result.type === 'message' ? `${result.type}-${result.id}-${result.messageIndex}` : `${result.type}-${result.id}`}
239
246
  onClick={() => navigateTo(result)}
240
247
  onMouseEnter={() => setSelectedIdx(idx)}
241
- className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent
248
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent focus-visible:ring-1 focus-visible:ring-accent-bright/50 focus-visible:ring-inset
242
249
  ${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
243
250
  style={{ fontFamily: 'inherit' }}
244
251
  >
@@ -246,7 +253,7 @@ export function SearchDialog() {
246
253
  <div className={`w-8 h-8 rounded-[8px] flex items-center justify-center shrink-0
247
254
  ${idx === selectedIdx ? 'bg-accent-bright/20' : 'bg-white/[0.04]'}`}>
248
255
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
249
- className={idx === selectedIdx ? 'text-[#818CF8]' : 'text-text-3'}>
256
+ className={idx === selectedIdx ? 'text-accent-bright' : 'text-text-3'}>
250
257
  <path d={TYPE_ICONS[result.type]} />
251
258
  {TYPE_EXTRA_PATHS[result.type] && <path d={TYPE_EXTRA_PATHS[result.type]} />}
252
259
  </svg>
@@ -1,5 +1,9 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import toast from 'react-hot-toast'
3
7
  import type { SettingsSectionProps } from './types'
4
8
 
5
9
  interface EmbeddingSectionProps extends SettingsSectionProps {
@@ -7,6 +11,12 @@ interface EmbeddingSectionProps extends SettingsSectionProps {
7
11
  }
8
12
 
9
13
  export function EmbeddingSection({ appSettings, patchSettings, inputClass, credList }: EmbeddingSectionProps) {
14
+ const loadCredentials = useAppStore((s) => s.loadCredentials)
15
+ const [addingKey, setAddingKey] = useState(false)
16
+ const [newKeyName, setNewKeyName] = useState('')
17
+ const [newKeyValue, setNewKeyValue] = useState('')
18
+ const [savingKey, setSavingKey] = useState(false)
19
+
10
20
  return (
11
21
  <div className="mb-10">
12
22
  <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
@@ -60,20 +70,45 @@ export function EmbeddingSection({ appSettings, patchSettings, inputClass, credL
60
70
  </div>
61
71
  <div>
62
72
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
63
- {credList.filter((c) => c.provider === 'openai').length > 0 ? (
64
- <select
65
- value={appSettings.embeddingCredentialId || ''}
66
- onChange={(e) => patchSettings({ embeddingCredentialId: e.target.value || null })}
67
- className={`${inputClass} appearance-none cursor-pointer`}
68
- style={{ fontFamily: 'inherit' }}
69
- >
70
- <option value="">Select a key...</option>
71
- {credList.filter((c) => c.provider === 'openai').map((c) => (
72
- <option key={c.id} value={c.id}>{c.name}</option>
73
- ))}
74
- </select>
73
+ {credList.filter((c) => c.provider === 'openai').length > 0 && !addingKey ? (
74
+ <div className="flex gap-2 items-center">
75
+ <select
76
+ value={appSettings.embeddingCredentialId || ''}
77
+ onChange={(e) => patchSettings({ embeddingCredentialId: e.target.value || null })}
78
+ className={`${inputClass} appearance-none cursor-pointer flex-1`}
79
+ style={{ fontFamily: 'inherit' }}
80
+ >
81
+ <option value="">Select a key...</option>
82
+ {credList.filter((c) => c.provider === 'openai').map((c) => (
83
+ <option key={c.id} value={c.id}>{c.name}</option>
84
+ ))}
85
+ </select>
86
+ <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>
87
+ </div>
75
88
  ) : (
76
- <p className="text-[12px] text-text-3/60">No OpenAI API keys configured.</p>
89
+ <div className="space-y-2">
90
+ <input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="Key name (optional)" className={inputClass} style={{ fontFamily: 'inherit' }} />
91
+ <input type="password" value={newKeyValue} onChange={e => setNewKeyValue(e.target.value)} placeholder="sk-..." className={inputClass} style={{ fontFamily: 'inherit' }} />
92
+ <div className="flex gap-2">
93
+ <button type="button" disabled={savingKey || !newKeyValue.trim()} onClick={async () => {
94
+ setSavingKey(true)
95
+ try {
96
+ const cred = await api<{ id: string }>('POST', '/credentials', { provider: 'openai', name: newKeyName.trim() || 'OpenAI key', apiKey: newKeyValue.trim() })
97
+ await loadCredentials()
98
+ patchSettings({ embeddingCredentialId: cred.id })
99
+ setAddingKey(false)
100
+ setNewKeyName('')
101
+ setNewKeyValue('')
102
+ } catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
103
+ finally { setSavingKey(false) }
104
+ }} 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' }}>
105
+ {savingKey ? 'Saving...' : 'Save Key'}
106
+ </button>
107
+ {credList.filter(c => c.provider === 'openai').length > 0 && (
108
+ <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>
109
+ )}
110
+ </div>
111
+ </div>
77
112
  )}
78
113
  </div>
79
114
  </>
@@ -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>
@@ -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
+ }
@@ -25,6 +25,24 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
25
25
  style={{ fontFamily: 'inherit' }}
26
26
  />
27
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
+
28
46
  {/* Default agent */}
29
47
  <div className="mt-6">
30
48
  <label className="text-[12px] font-600 text-text-2 block mb-1.5">Default Agent</label>