@swarmclawai/swarmclaw 1.3.4 → 1.3.6

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 (73) hide show
  1. package/README.md +20 -76
  2. package/package.json +3 -2
  3. package/skills/swarmclaw.md +17 -0
  4. package/src/app/api/agents/[id]/dream/route.ts +45 -0
  5. package/src/app/api/knowledge/[id]/route.ts +48 -49
  6. package/src/app/api/knowledge/hygiene/route.ts +13 -0
  7. package/src/app/api/knowledge/route.ts +70 -42
  8. package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
  9. package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
  10. package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
  11. package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
  12. package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
  13. package/src/app/api/knowledge/sources/route.ts +1 -0
  14. package/src/app/api/knowledge/upload/route.ts +3 -51
  15. package/src/app/api/memory/dream/[id]/route.ts +19 -0
  16. package/src/app/api/memory/dream/route.ts +34 -0
  17. package/src/app/knowledge/layout.tsx +1 -1
  18. package/src/app/knowledge/page.tsx +2 -22
  19. package/src/app/protocols/page.tsx +21 -2
  20. package/src/cli/index.js +16 -0
  21. package/src/cli/spec.js +5 -0
  22. package/src/components/agents/agent-sheet.tsx +65 -0
  23. package/src/components/chat/message-bubble.tsx +10 -0
  24. package/src/components/knowledge/grounding-panel.tsx +99 -0
  25. package/src/components/knowledge/knowledge-detail.tsx +402 -0
  26. package/src/components/knowledge/knowledge-list.tsx +351 -126
  27. package/src/components/knowledge/knowledge-sheet.tsx +208 -119
  28. package/src/components/memory/dream-history.tsx +155 -0
  29. package/src/components/memory/memory-card.tsx +7 -0
  30. package/src/components/memory/memory-detail.tsx +46 -0
  31. package/src/components/runs/run-list.tsx +23 -0
  32. package/src/lib/server/api-routes.test.ts +43 -2
  33. package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
  34. package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
  35. package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +108 -0
  36. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
  37. package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
  38. package/src/lib/server/chat-execution/chat-execution.ts +1 -0
  39. package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
  40. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
  41. package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
  42. package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
  43. package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
  44. package/src/lib/server/execution-engine/task-attempt.ts +8 -2
  45. package/src/lib/server/knowledge-import.ts +159 -0
  46. package/src/lib/server/knowledge-sources.test.ts +261 -0
  47. package/src/lib/server/knowledge-sources.ts +1284 -0
  48. package/src/lib/server/memory/dream-cycles.ts +49 -0
  49. package/src/lib/server/memory/dream-idle-callback.ts +38 -0
  50. package/src/lib/server/memory/dream-service.ts +315 -0
  51. package/src/lib/server/memory/memory-db.ts +37 -2
  52. package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
  53. package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
  54. package/src/lib/server/protocols/protocol-service.test.ts +99 -0
  55. package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
  56. package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
  57. package/src/lib/server/protocols/protocol-types.ts +4 -0
  58. package/src/lib/server/runtime/daemon-state/core.ts +6 -1
  59. package/src/lib/server/runtime/run-ledger.test.ts +120 -0
  60. package/src/lib/server/runtime/run-ledger.ts +27 -1
  61. package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
  62. package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
  63. package/src/lib/server/storage-normalization.ts +5 -0
  64. package/src/lib/server/storage.ts +15 -0
  65. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
  66. package/src/stores/slices/ui-slice.ts +4 -0
  67. package/src/types/agent.ts +7 -0
  68. package/src/types/dream.ts +45 -0
  69. package/src/types/index.ts +1 -0
  70. package/src/types/message.ts +3 -0
  71. package/src/types/misc.ts +131 -0
  72. package/src/types/protocol.ts +4 -0
  73. package/src/types/run.ts +4 -1
@@ -5,7 +5,7 @@ import { api } from '@/lib/app/api-client'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
- import type { MemoryEntry } from '@/types'
8
+ import type { KnowledgeSourceDetail, KnowledgeSourceKind } from '@/types'
9
9
  import { toast } from 'sonner'
10
10
 
11
11
  const ACCEPTED_TYPES = '.txt,.md,.csv,.json,.jsonl,.html,.xml,.yaml,.yml,.toml,.py,.js,.ts,.tsx,.jsx,.go,.rs,.java,.c,.cpp,.h,.rb,.php,.sh,.sql,.log,.pdf'
@@ -20,195 +20,263 @@ interface UploadResult {
20
20
  }
21
21
 
22
22
  export function KnowledgeSheet() {
23
- const open = useAppStore((s) => s.knowledgeSheetOpen)
24
- const setOpen = useAppStore((s) => s.setKnowledgeSheetOpen)
25
- const editingId = useAppStore((s) => s.editingKnowledgeId)
26
- const agents = useAppStore((s) => s.agents)
27
- const loadAgents = useAppStore((s) => s.loadAgents)
28
-
23
+ const open = useAppStore((state) => state.knowledgeSheetOpen)
24
+ const setOpen = useAppStore((state) => state.setKnowledgeSheetOpen)
25
+ const editingId = useAppStore((state) => state.editingKnowledgeId)
26
+ const setEditingKnowledgeId = useAppStore((state) => state.setEditingKnowledgeId)
27
+ const setSelectedKnowledgeSourceId = useAppStore((state) => state.setSelectedKnowledgeSourceId)
28
+ const triggerKnowledgeRefresh = useAppStore((state) => state.triggerKnowledgeRefresh)
29
+ const agents = useAppStore((state) => state.agents)
30
+ const loadAgents = useAppStore((state) => state.loadAgents)
31
+
32
+ const [kind, setKind] = useState<KnowledgeSourceKind>('manual')
29
33
  const [title, setTitle] = useState('')
30
34
  const [content, setContent] = useState('')
31
35
  const [tags, setTags] = useState('')
32
36
  const [scope, setScope] = useState<'global' | 'agent'>('global')
33
37
  const [agentIds, setAgentIds] = useState<string[]>([])
38
+ const [sourceUrl, setSourceUrl] = useState('')
39
+ const [sourcePath, setSourcePath] = useState('')
40
+ const [sourceLabel, setSourceLabel] = useState('')
34
41
  const [saving, setSaving] = useState(false)
35
- const [uploadedFile, setUploadedFile] = useState<{ name: string; url: string; size: number } | null>(null)
36
42
  const [uploading, setUploading] = useState(false)
37
43
  const [isDragging, setIsDragging] = useState(false)
44
+ const [uploadedFile, setUploadedFile] = useState<{ name: string; url: string; size: number | null } | null>(null)
45
+
38
46
  const dragCounter = useRef(0)
39
47
  const fileInputRef = useRef<HTMLInputElement>(null)
40
48
  const agentList = Object.values(agents)
41
49
 
42
50
  useEffect(() => {
43
51
  if (open) loadAgents()
44
- // eslint-disable-next-line react-hooks/exhaustive-deps
45
- }, [open])
46
-
47
- useEffect(() => {
48
- if (!open) return
49
- if (editingId) {
50
- void api<MemoryEntry>('GET', `/knowledge/${editingId}`).then((entry) => {
51
- setTitle(entry.title)
52
- setContent(entry.content)
53
- const meta = entry.metadata as { tags?: string[]; scope?: 'global' | 'agent'; agentIds?: string[] } | undefined
54
- setTags(meta?.tags?.join(', ') || '')
55
- setScope(meta?.scope || 'global')
56
- setAgentIds(meta?.agentIds || [])
57
- }).catch(() => {
58
- setOpen(false)
59
- })
60
- } else {
61
- setTitle('')
62
- setContent('')
63
- setTags('')
64
- setScope('global')
65
- setAgentIds([])
66
- setUploadedFile(null)
67
- }
68
- }, [open, editingId, setOpen])
52
+ }, [loadAgents, open])
69
53
 
70
- const onClose = () => {
71
- setOpen(false)
54
+ const resetForm = useCallback(() => {
55
+ setKind('manual')
72
56
  setTitle('')
73
57
  setContent('')
74
58
  setTags('')
75
59
  setScope('global')
76
60
  setAgentIds([])
61
+ setSourceUrl('')
62
+ setSourcePath('')
63
+ setSourceLabel('')
77
64
  setUploadedFile(null)
78
65
  setIsDragging(false)
79
66
  dragCounter.current = 0
80
- }
67
+ }, [])
68
+
69
+ useEffect(() => {
70
+ if (!open) return
71
+ if (!editingId) {
72
+ resetForm()
73
+ return
74
+ }
75
+
76
+ resetForm()
77
+ void api<KnowledgeSourceDetail>('GET', `/knowledge/sources/${editingId}`).then((detail) => {
78
+ const { source } = detail
79
+ setKind(source.kind)
80
+ setTitle(source.title)
81
+ setContent(source.content || '')
82
+ setTags(source.tags.join(', '))
83
+ setScope(source.scope)
84
+ setAgentIds(source.agentIds)
85
+ setSourceUrl(source.sourceUrl || '')
86
+ setSourcePath(source.sourcePath || '')
87
+ setSourceLabel(source.sourceLabel || '')
88
+ setUploadedFile(source.kind === 'file'
89
+ ? { name: source.sourceLabel || source.title, url: source.sourceUrl || '', size: null }
90
+ : null)
91
+ }).catch(() => {
92
+ toast.error('Unable to load this knowledge source')
93
+ setOpen(false)
94
+ })
95
+ }, [editingId, open, resetForm, setOpen])
96
+
97
+ const onClose = useCallback(() => {
98
+ setOpen(false)
99
+ setEditingKnowledgeId(null)
100
+ resetForm()
101
+ }, [resetForm, setEditingKnowledgeId, setOpen])
81
102
 
82
103
  const parseTags = (raw: string): string[] =>
83
- raw.split(',').map((t) => t.trim()).filter(Boolean)
104
+ raw.split(',').map((tag) => tag.trim()).filter(Boolean)
105
+
106
+ const toggleAgent = (id: string) => {
107
+ setAgentIds((current) => current.includes(id) ? current.filter((agentId) => agentId !== id) : [...current, id])
108
+ }
84
109
 
85
110
  const handleUpload = useCallback(async (file: File) => {
86
111
  setUploading(true)
87
112
  try {
88
- const res = await fetch('/api/knowledge/upload', {
113
+ const response = await fetch('/api/knowledge/upload', {
89
114
  method: 'POST',
90
115
  headers: { 'X-Filename': file.name },
91
116
  body: file,
92
117
  })
93
- if (!res.ok) {
94
- const err = await res.json().catch(() => ({ error: 'Upload failed' }))
95
- toast.error(err.error || 'Upload failed')
118
+
119
+ if (!response.ok) {
120
+ const payload = await response.json().catch(() => ({ error: 'Upload failed' }))
121
+ toast.error(payload.error || 'Upload failed')
96
122
  return
97
123
  }
98
- const result: UploadResult = await res.json()
99
- if (!title.trim()) setTitle(result.title)
124
+
125
+ const result: UploadResult = await response.json()
126
+ setKind('file')
127
+ setTitle((current) => current.trim() || result.title)
100
128
  setContent(result.content)
129
+ setSourcePath(result.filePath)
130
+ setSourceUrl(result.url)
131
+ setSourceLabel(result.filename)
101
132
  setUploadedFile({ name: result.filename, url: result.url, size: result.size })
102
133
  toast.success('Document content extracted')
103
134
 
104
- // Auto-tag based on file extension
105
135
  const ext = file.name.split('.').pop()?.toLowerCase() || ''
106
- if (ext && !tags.includes(ext)) {
107
- setTags((prev) => prev ? `${prev}, ${ext}` : ext)
136
+ if (ext) {
137
+ setTags((current) => current.includes(ext) ? current : current ? `${current}, ${ext}` : ext)
108
138
  }
109
- } catch (err: unknown) {
110
- toast.error(err instanceof Error ? err.message : 'Upload failed')
139
+ } catch (error) {
140
+ toast.error(error instanceof Error ? error.message : 'Upload failed')
111
141
  } finally {
112
142
  setUploading(false)
113
143
  }
114
- }, [title, tags])
144
+ }, [])
115
145
 
116
- const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
117
- const file = e.target.files?.[0]
146
+ const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
147
+ const file = event.target.files?.[0]
118
148
  if (file) void handleUpload(file)
119
- e.target.value = ''
149
+ event.target.value = ''
120
150
  }, [handleUpload])
121
151
 
122
- const handleDragOver = useCallback((e: React.DragEvent) => {
123
- e.preventDefault()
124
- e.stopPropagation()
152
+ const handleDragOver = useCallback((event: React.DragEvent) => {
153
+ event.preventDefault()
154
+ event.stopPropagation()
125
155
  }, [])
126
156
 
127
- const handleDragEnter = useCallback((e: React.DragEvent) => {
128
- e.preventDefault()
129
- e.stopPropagation()
130
- dragCounter.current++
131
- if (e.dataTransfer.types.includes('Files')) setIsDragging(true)
157
+ const handleDragEnter = useCallback((event: React.DragEvent) => {
158
+ event.preventDefault()
159
+ event.stopPropagation()
160
+ dragCounter.current += 1
161
+ if (event.dataTransfer.types.includes('Files')) setIsDragging(true)
132
162
  }, [])
133
163
 
134
- const handleDragLeave = useCallback((e: React.DragEvent) => {
135
- e.preventDefault()
136
- e.stopPropagation()
137
- dragCounter.current--
164
+ const handleDragLeave = useCallback((event: React.DragEvent) => {
165
+ event.preventDefault()
166
+ event.stopPropagation()
167
+ dragCounter.current -= 1
138
168
  if (dragCounter.current === 0) setIsDragging(false)
139
169
  }, [])
140
170
 
141
- const handleDrop = useCallback((e: React.DragEvent) => {
142
- e.preventDefault()
143
- e.stopPropagation()
171
+ const handleDrop = useCallback((event: React.DragEvent) => {
172
+ event.preventDefault()
173
+ event.stopPropagation()
144
174
  dragCounter.current = 0
145
175
  setIsDragging(false)
146
- const file = e.dataTransfer.files?.[0]
176
+ const file = event.dataTransfer.files?.[0]
147
177
  if (file) void handleUpload(file)
148
178
  }, [handleUpload])
149
179
 
150
- const toggleAgent = (id: string) => {
151
- setAgentIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])
152
- }
153
-
154
- const scopeHelperText = scope === 'global'
155
- ? 'This knowledge will be accessible to all agents'
156
- : agentIds.length === 0
157
- ? 'Select which agents can access this knowledge'
158
- : `${agentIds.length} agent(s) selected`
159
-
160
180
  const handleSave = async () => {
181
+ if (kind === 'manual' && !content.trim()) {
182
+ toast.error('Manual knowledge needs content')
183
+ return
184
+ }
185
+ if (kind === 'file' && !sourcePath && !content.trim()) {
186
+ toast.error('Upload a file or provide extracted content')
187
+ return
188
+ }
189
+ if (kind === 'url' && !sourceUrl.trim()) {
190
+ toast.error('A URL is required for URL knowledge')
191
+ return
192
+ }
193
+
161
194
  setSaving(true)
162
195
  try {
163
196
  const payload = {
164
- title: title.trim() || 'Untitled',
197
+ kind,
198
+ title: title.trim(),
165
199
  content,
166
200
  tags: parseTags(tags),
167
201
  scope,
168
202
  agentIds: scope === 'agent' ? agentIds : [],
203
+ sourceUrl: sourceUrl.trim() || undefined,
204
+ sourcePath: sourcePath.trim() || undefined,
205
+ sourceLabel: sourceLabel.trim() || undefined,
206
+ metadata: uploadedFile?.size != null
207
+ ? { fileSize: uploadedFile.size }
208
+ : undefined,
169
209
  }
170
210
 
171
- if (editingId) {
172
- await api('PUT', `/knowledge/${editingId}`, payload)
173
- toast.success('Knowledge entry updated')
174
- } else {
175
- await api('POST', '/knowledge', payload)
176
- toast.success('Knowledge entry created')
177
- }
211
+ const detail = editingId
212
+ ? await api<KnowledgeSourceDetail>('PUT', `/knowledge/sources/${editingId}`, payload)
213
+ : await api<KnowledgeSourceDetail>('POST', '/knowledge/sources', payload)
178
214
 
215
+ setSelectedKnowledgeSourceId(detail.source.id)
216
+ triggerKnowledgeRefresh()
217
+ toast.success(editingId ? 'Knowledge source updated' : 'Knowledge source created')
179
218
  onClose()
180
- } catch (err: unknown) {
181
- toast.error(err instanceof Error ? err.message : 'Failed to save knowledge')
219
+ } catch (error) {
220
+ toast.error(error instanceof Error ? error.message : 'Failed to save knowledge')
182
221
  } finally {
183
222
  setSaving(false)
184
223
  }
185
224
  }
186
225
 
187
- const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
188
-
189
- const formatSize = (bytes: number) => {
226
+ const formatSize = (bytes: number | null) => {
227
+ if (bytes == null) return null
190
228
  if (bytes < 1024) return `${bytes} B`
191
229
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
192
230
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
193
231
  }
194
232
 
233
+ const inputClass = 'w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow'
234
+ const scopeHelperText = scope === 'global'
235
+ ? 'This source will be searchable across the whole fleet'
236
+ : agentIds.length === 0
237
+ ? 'Select which agents should receive this source during retrieval'
238
+ : `${agentIds.length} agent(s) selected`
239
+
240
+ const canSave = kind === 'manual'
241
+ ? Boolean(content.trim())
242
+ : kind === 'file'
243
+ ? Boolean(sourcePath || content.trim())
244
+ : Boolean(sourceUrl.trim())
245
+
195
246
  return (
196
247
  <BottomSheet open={open} onClose={onClose}>
197
248
  <div className="mb-10">
198
249
  <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
199
- {editingId ? 'Edit Knowledge' : 'New Knowledge'}
250
+ {editingId ? 'Edit Knowledge Source' : 'New Knowledge Source'}
200
251
  </h2>
201
252
  <p className="text-[14px] text-text-3">
202
- {editingId ? 'Update this knowledge entry' : 'Add shared knowledge for agents type or upload a document'}
253
+ Manual notes, uploaded files, and imported URLs all index into the same knowledge library.
203
254
  </p>
204
255
  </div>
205
256
 
206
- {/* Document upload zone — only show when creating new */}
207
- {!editingId && (
257
+ <div className="mb-8">
258
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Source Type</label>
259
+ <div className="grid grid-cols-3 gap-2">
260
+ {(['manual', 'file', 'url'] as const).map((sourceKind) => (
261
+ <button
262
+ key={sourceKind}
263
+ onClick={() => setKind(sourceKind)}
264
+ className={`py-3 rounded-[14px] text-[13px] font-600 border transition-all cursor-pointer ${
265
+ kind === sourceKind
266
+ ? 'border-accent-bright/25 bg-accent-soft text-accent-bright'
267
+ : 'border-white/[0.08] bg-white/[0.02] text-text-3 hover:text-text-2'
268
+ }`}
269
+ style={{ fontFamily: 'inherit' }}
270
+ >
271
+ {sourceKind === 'manual' ? 'Manual' : sourceKind === 'file' ? 'File' : 'URL'}
272
+ </button>
273
+ ))}
274
+ </div>
275
+ </div>
276
+
277
+ {kind === 'file' && (
208
278
  <div className="mb-8">
209
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
210
- Upload Document
211
- </label>
279
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Upload Document</label>
212
280
 
213
281
  {uploadedFile ? (
214
282
  <div className="flex items-center gap-3 px-4 py-3 rounded-[14px] border border-emerald-500/20 bg-emerald-500/[0.04]">
@@ -219,19 +287,24 @@ export function KnowledgeSheet() {
219
287
  </svg>
220
288
  <div className="flex-1 min-w-0">
221
289
  <p className="text-[13px] text-text font-500 truncate">{uploadedFile.name}</p>
222
- <p className="text-[11px] text-text-3/60">{formatSize(uploadedFile.size)} — content extracted</p>
290
+ <p className="text-[11px] text-text-3/60">
291
+ {formatSize(uploadedFile.size) ? `${formatSize(uploadedFile.size)} • ` : ''}content extracted
292
+ </p>
223
293
  </div>
224
294
  <button
225
295
  onClick={() => {
226
296
  setUploadedFile(null)
297
+ setSourcePath('')
298
+ setSourceUrl('')
299
+ setSourceLabel('')
227
300
  setContent('')
228
- setTitle('')
229
301
  }}
230
302
  className="p-1.5 rounded-[8px] text-text-3 hover:text-red-400 hover:bg-red-400/10 border-none bg-transparent cursor-pointer transition-colors"
231
303
  aria-label="Remove uploaded file"
232
304
  >
233
305
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
234
- <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
306
+ <line x1="18" y1="6" x2="6" y2="18" />
307
+ <line x1="6" y1="6" x2="18" y2="18" />
235
308
  </svg>
236
309
  </button>
237
310
  </div>
@@ -242,12 +315,11 @@ export function KnowledgeSheet() {
242
315
  onDragLeave={handleDragLeave}
243
316
  onDrop={handleDrop}
244
317
  onClick={() => fileInputRef.current?.click()}
245
- className={`flex flex-col items-center gap-3 px-6 py-8 rounded-[14px] border-2 border-dashed cursor-pointer transition-all duration-200
246
- ${isDragging
318
+ className={`flex flex-col items-center gap-3 px-6 py-8 rounded-[14px] border-2 border-dashed cursor-pointer transition-all duration-200 ${
319
+ isDragging
247
320
  ? 'border-accent-bright/50 bg-accent-soft/20'
248
321
  : 'border-white/[0.08] bg-white/[0.02] hover:border-white/[0.15] hover:bg-white/[0.03]'
249
- }
250
- ${uploading ? 'opacity-60 pointer-events-none' : ''}`}
322
+ } ${uploading ? 'opacity-60 pointer-events-none' : ''}`}
251
323
  >
252
324
  {uploading ? (
253
325
  <>
@@ -266,7 +338,7 @@ export function KnowledgeSheet() {
266
338
  {isDragging ? 'Drop document here' : 'Drop a document or click to browse'}
267
339
  </p>
268
340
  <p className="text-[11px] text-text-3/50 mt-1">
269
- Supports .txt, .md, .csv, .json, .pdf, code files, and more
341
+ Supports text, code, structured files, and PDFs
270
342
  </p>
271
343
  </div>
272
344
  </>
@@ -284,12 +356,29 @@ export function KnowledgeSheet() {
284
356
  </div>
285
357
  )}
286
358
 
359
+ {kind === 'url' && (
360
+ <div className="mb-8">
361
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Source URL</label>
362
+ <input
363
+ type="url"
364
+ value={sourceUrl}
365
+ onChange={(event) => setSourceUrl(event.target.value)}
366
+ placeholder="https://example.com/docs/article"
367
+ className={inputClass}
368
+ style={{ fontFamily: 'inherit' }}
369
+ />
370
+ <p className="text-[11px] text-text-3/55 mt-1.5 pl-1">
371
+ Save to fetch, clean, and index the page. You can also edit the extracted text below before saving again.
372
+ </p>
373
+ </div>
374
+ )}
375
+
287
376
  <div className="mb-8">
288
377
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
289
378
  <input
290
379
  type="text"
291
380
  value={title}
292
- onChange={(e) => setTitle(e.target.value)}
381
+ onChange={(event) => setTitle(event.target.value)}
293
382
  placeholder="Knowledge title"
294
383
  className={inputClass}
295
384
  style={{ fontFamily: 'inherit' }}
@@ -298,7 +387,7 @@ export function KnowledgeSheet() {
298
387
 
299
388
  <div className="mb-8">
300
389
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
301
- Content
390
+ Indexed Content
302
391
  {content.length > 0 && (
303
392
  <span className="ml-2 text-text-3/40 font-mono text-[10px] normal-case tracking-normal">
304
393
  {content.length.toLocaleString()} chars
@@ -307,10 +396,10 @@ export function KnowledgeSheet() {
307
396
  </label>
308
397
  <textarea
309
398
  value={content}
310
- onChange={(e) => setContent(e.target.value)}
311
- placeholder="Knowledge content..."
312
- rows={6}
313
- className={`${inputClass} resize-y min-h-[150px]`}
399
+ onChange={(event) => setContent(event.target.value)}
400
+ placeholder={kind === 'manual' ? 'Knowledge content...' : 'Extracted content appears here after import'}
401
+ rows={8}
402
+ className={`${inputClass} resize-y min-h-[180px]`}
314
403
  style={{ fontFamily: 'inherit' }}
315
404
  />
316
405
  </div>
@@ -320,7 +409,7 @@ export function KnowledgeSheet() {
320
409
  <input
321
410
  type="text"
322
411
  value={tags}
323
- onChange={(e) => setTags(e.target.value)}
412
+ onChange={(event) => setTags(event.target.value)}
324
413
  placeholder="api, docs, internal (comma-separated)"
325
414
  className={inputClass}
326
415
  style={{ fontFamily: 'inherit' }}
@@ -330,16 +419,16 @@ export function KnowledgeSheet() {
330
419
  <div className="mb-8">
331
420
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Scope</label>
332
421
  <div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
333
- {(['global', 'agent'] as const).map((s) => (
422
+ {(['global', 'agent'] as const).map((nextScope) => (
334
423
  <button
335
- key={s}
336
- onClick={() => setScope(s)}
424
+ key={nextScope}
425
+ onClick={() => setScope(nextScope)}
337
426
  className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
338
- scope === s ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
427
+ scope === nextScope ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
339
428
  }`}
340
429
  style={{ fontFamily: 'inherit' }}
341
430
  >
342
- {s === 'global' ? 'Global' : 'Specific'}
431
+ {nextScope === 'global' ? 'Global' : 'Specific'}
343
432
  </button>
344
433
  ))}
345
434
  </div>
@@ -389,7 +478,7 @@ export function KnowledgeSheet() {
389
478
  </button>
390
479
  <button
391
480
  onClick={() => { void handleSave() }}
392
- disabled={!title.trim() || saving}
481
+ disabled={!canSave || saving}
393
482
  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"
394
483
  style={{ fontFamily: 'inherit' }}
395
484
  >
@@ -0,0 +1,155 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import type { DreamCycle } from '@/types'
5
+
6
+ interface Props {
7
+ agentId: string
8
+ }
9
+
10
+ function formatDuration(ms: number): string {
11
+ if (ms < 1000) return `${ms}ms`
12
+ const sec = Math.round(ms / 1000)
13
+ if (sec < 60) return `${sec}s`
14
+ const min = Math.floor(sec / 60)
15
+ const remSec = sec % 60
16
+ return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`
17
+ }
18
+
19
+ function timeAgo(ts: number, now: number): string {
20
+ const diff = now - ts
21
+ if (diff < 60_000) return 'just now'
22
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
23
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
24
+ return `${Math.floor(diff / 86_400_000)}d ago`
25
+ }
26
+
27
+ const statusColors: Record<string, string> = {
28
+ completed: 'bg-emerald-400/10 text-emerald-300',
29
+ running: 'bg-amber-400/10 text-amber-300',
30
+ pending: 'bg-amber-400/10 text-amber-300',
31
+ failed: 'bg-red-400/10 text-red-300',
32
+ }
33
+
34
+ export function DreamHistory({ agentId }: Props) {
35
+ const [cycles, setCycles] = useState<DreamCycle[]>([])
36
+ const [loading, setLoading] = useState(true)
37
+ const [triggering, setTriggering] = useState(false)
38
+ const [error, setError] = useState<string | null>(null)
39
+ const [now] = useState(() => Date.now())
40
+
41
+ const load = useCallback(async () => {
42
+ try {
43
+ setLoading(true)
44
+ setError(null)
45
+ const res = await fetch(`/api/memory/dream?agentId=${encodeURIComponent(agentId)}&limit=10`)
46
+ const data = await res.json() as { ok: boolean; cycles?: DreamCycle[]; error?: string }
47
+ if (data.ok && Array.isArray(data.cycles)) {
48
+ setCycles(data.cycles)
49
+ } else {
50
+ setError(data.error || 'Failed to load dream cycles')
51
+ }
52
+ } catch {
53
+ setError('Unable to reach the server')
54
+ } finally {
55
+ setLoading(false)
56
+ }
57
+ }, [agentId])
58
+
59
+ useEffect(() => {
60
+ void load()
61
+ }, [load])
62
+
63
+ const handleTrigger = async () => {
64
+ try {
65
+ setTriggering(true)
66
+ setError(null)
67
+ const res = await fetch('/api/memory/dream', {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ agentId }),
71
+ })
72
+ const data = await res.json() as { ok: boolean; error?: string }
73
+ if (!data.ok) {
74
+ setError(data.error || 'Dream trigger failed')
75
+ }
76
+ await load()
77
+ } catch {
78
+ setError('Unable to trigger dream cycle')
79
+ } finally {
80
+ setTriggering(false)
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className="space-y-3">
86
+ <div className="flex items-center justify-between gap-3">
87
+ <h4 className="font-display text-[14px] font-600 text-text">Dream History</h4>
88
+ <div className="flex items-center gap-2">
89
+ <button
90
+ type="button"
91
+ onClick={() => { void load() }}
92
+ disabled={loading}
93
+ className="px-2.5 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-600 text-text-3 hover:bg-white/[0.04] hover:text-text-2 transition-all cursor-pointer disabled:opacity-50"
94
+ style={{ fontFamily: 'inherit' }}
95
+ >
96
+ Refresh
97
+ </button>
98
+ <button
99
+ type="button"
100
+ onClick={() => { void handleTrigger() }}
101
+ disabled={triggering}
102
+ className="px-2.5 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[11px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-50"
103
+ style={{ fontFamily: 'inherit' }}
104
+ >
105
+ {triggering ? 'Running...' : 'Trigger Dream'}
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ {error && (
111
+ <p className="text-[12px] text-red-400/80 leading-[1.5]">{error}</p>
112
+ )}
113
+
114
+ {loading && cycles.length === 0 ? (
115
+ <p className="text-[12px] text-text-3/60 py-4 text-center">Loading dream cycles...</p>
116
+ ) : cycles.length === 0 ? (
117
+ <p className="text-[12px] text-text-3/60 py-4 text-center">No dream cycles yet. Trigger one manually or enable dreaming in agent settings.</p>
118
+ ) : (
119
+ <div className="space-y-2">
120
+ {cycles.map((cycle) => (
121
+ <div
122
+ key={cycle.id}
123
+ className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3.5 py-3"
124
+ >
125
+ <div className="flex items-center gap-2 flex-wrap">
126
+ <span className={`px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] ${statusColors[cycle.status] || 'bg-white/[0.04] text-text-3/75'}`}>
127
+ {cycle.status}
128
+ </span>
129
+ <span className="px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] bg-white/[0.04] text-text-3/75">
130
+ {cycle.trigger}
131
+ </span>
132
+ {cycle.status === 'completed' && cycle.startedAt && cycle.completedAt && (
133
+ <span className="text-[10px] text-text-3/50 font-mono tabular-nums">
134
+ {formatDuration(cycle.completedAt - cycle.startedAt)}
135
+ </span>
136
+ )}
137
+ <span className="ml-auto text-[10px] text-text-3/50 tabular-nums font-mono">
138
+ {timeAgo(cycle.startedAt, now)}
139
+ </span>
140
+ </div>
141
+ {cycle.status === 'completed' && cycle.result && (
142
+ <p className="mt-1.5 text-[11px] text-text-3/70 leading-[1.5]">
143
+ {cycle.result.decayed} decayed, {cycle.result.pruned} pruned, {cycle.result.promoted} promoted, {cycle.result.consolidated} consolidated
144
+ </p>
145
+ )}
146
+ {cycle.status === 'failed' && cycle.error && (
147
+ <p className="mt-1.5 text-[11px] text-red-400/70 leading-[1.5] line-clamp-2">{cycle.error}</p>
148
+ )}
149
+ </div>
150
+ ))}
151
+ </div>
152
+ )}
153
+ </div>
154
+ )
155
+ }