@swarmclawai/swarmclaw 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -162,7 +162,7 @@ export function LogList() {
162
162
  <button
163
163
  key={i}
164
164
  onClick={() => { setLevelFilter(f.levels); setSearch(f.search) }}
165
- className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-[#6366F1]/15"
165
+ className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-accent-bright/15"
166
166
  >
167
167
  {f.name}
168
168
  <span
@@ -337,7 +337,7 @@ export function LogList() {
337
337
  <button
338
338
  onClick={handleCreateTask}
339
339
  disabled={creatingTask}
340
- className="px-5 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600
340
+ className="px-5 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600
341
341
  cursor-pointer active:scale-[0.97] disabled:opacity-40 transition-all
342
342
  shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110 shrink-0"
343
343
  style={{ fontFamily: 'inherit' }}
@@ -207,7 +207,7 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
207
207
  <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
208
208
  Cancel
209
209
  </button>
210
- <button onClick={handleSave} disabled={!canSave} className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] 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" style={{ fontFamily: 'inherit' }}>
210
+ <button onClick={handleSave} disabled={!canSave} 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" style={{ fontFamily: 'inherit' }}>
211
211
  {editing ? 'Save' : 'Create'}
212
212
  </button>
213
213
  </div>
@@ -0,0 +1,143 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
6
+ import { getMemoryCounts } from '@/lib/memory'
7
+
8
+ export function MemoryAgentList() {
9
+ const agents = useAppStore((s) => s.agents)
10
+ const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
11
+ const setMemoryAgentFilter = useAppStore((s) => s.setMemoryAgentFilter)
12
+ const setSelectedMemoryId = useAppStore((s) => s.setSelectedMemoryId)
13
+ const refreshKey = useAppStore((s) => s.memoryRefreshKey)
14
+
15
+ const [counts, setCounts] = useState<Record<string, number>>({})
16
+
17
+ useEffect(() => {
18
+ getMemoryCounts()
19
+ .then((data) => setCounts(data))
20
+ .catch(() => {/* ignore */})
21
+ }, [refreshKey])
22
+
23
+ const totalCount = Object.values(counts).reduce((a, b) => a + b, 0)
24
+ const globalCount = counts['_global'] || 0
25
+
26
+ const agentList = Object.values(agents).sort((a, b) =>
27
+ a.name.localeCompare(b.name),
28
+ )
29
+
30
+ const handleSelect = (agentId: string | null) => {
31
+ setMemoryAgentFilter(agentId)
32
+ setSelectedMemoryId(null)
33
+ }
34
+
35
+ return (
36
+ <div className="flex-1 flex flex-col overflow-y-auto">
37
+ {/* Header */}
38
+ <div className="px-4 pt-4 pb-2 shrink-0">
39
+ <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em]">Memory</h2>
40
+ </div>
41
+
42
+ {/* All Memories row */}
43
+ <div className="px-2 flex flex-col gap-0.5">
44
+ <button
45
+ onClick={() => handleSelect(null)}
46
+ className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
47
+ ${!memoryAgentFilter
48
+ ? 'bg-accent-soft'
49
+ : 'bg-transparent hover:bg-white/[0.02]'}`}
50
+ style={{ fontFamily: 'inherit' }}
51
+ >
52
+ {!memoryAgentFilter && (
53
+ <div className="absolute left-0 top-2.5 bottom-2.5 w-[2.5px] rounded-full bg-accent-bright" />
54
+ )}
55
+ <div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
56
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={!memoryAgentFilter ? 'text-accent-bright' : 'text-text-3'}>
57
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
58
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
59
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
60
+ </svg>
61
+ </div>
62
+ <span className={`text-[13px] font-600 flex-1 ${!memoryAgentFilter ? 'text-accent-bright' : 'text-text-2'}`}>
63
+ All Memories
64
+ </span>
65
+ {totalCount > 0 && (
66
+ <span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
67
+ {totalCount}
68
+ </span>
69
+ )}
70
+ </button>
71
+
72
+ {/* Global row */}
73
+ <button
74
+ onClick={() => handleSelect('_global')}
75
+ className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
76
+ ${memoryAgentFilter === '_global'
77
+ ? 'bg-accent-soft'
78
+ : 'bg-transparent hover:bg-white/[0.02]'}`}
79
+ style={{ fontFamily: 'inherit' }}
80
+ >
81
+ {memoryAgentFilter === '_global' && (
82
+ <div className="absolute left-0 top-2.5 bottom-2.5 w-[2.5px] rounded-full bg-accent-bright" />
83
+ )}
84
+ <div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
85
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={memoryAgentFilter === '_global' ? 'text-accent-bright' : 'text-text-3'}>
86
+ <circle cx="12" cy="12" r="10" />
87
+ <line x1="2" y1="12" x2="22" y2="12" />
88
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
89
+ </svg>
90
+ </div>
91
+ <span className={`text-[13px] font-600 flex-1 ${memoryAgentFilter === '_global' ? 'text-accent-bright' : 'text-text-2'}`}>
92
+ Global
93
+ </span>
94
+ {globalCount > 0 && (
95
+ <span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
96
+ {globalCount}
97
+ </span>
98
+ )}
99
+ </button>
100
+ </div>
101
+
102
+ {/* Divider */}
103
+ <div className="mx-4 my-2 border-t border-white/[0.04]" />
104
+
105
+ {/* Agent list */}
106
+ <div className="px-2 flex flex-col gap-0.5 pb-4">
107
+ {agentList.map((agent) => {
108
+ const count = counts[agent.id] || 0
109
+ const isActive = memoryAgentFilter === agent.id
110
+ return (
111
+ <button
112
+ key={agent.id}
113
+ onClick={() => handleSelect(agent.id)}
114
+ className={`relative flex items-center gap-3 px-3 py-2 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
115
+ ${isActive
116
+ ? 'bg-accent-soft'
117
+ : 'bg-transparent hover:bg-white/[0.02]'}`}
118
+ style={{ fontFamily: 'inherit' }}
119
+ >
120
+ {isActive && (
121
+ <div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
122
+ )}
123
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} />
124
+ <span className={`text-[13px] font-600 flex-1 truncate ${isActive ? 'text-accent-bright' : 'text-text-2'}`}>
125
+ {agent.name}
126
+ </span>
127
+ {count > 0 && (
128
+ <span className="text-[10px] font-mono tabular-nums text-text-3/60 bg-white/[0.04] px-1.5 py-0.5 rounded-[5px]">
129
+ {count}
130
+ </span>
131
+ )}
132
+ </button>
133
+ )
134
+ })}
135
+ {agentList.length === 0 && (
136
+ <div className="text-[12px] text-text-3/50 px-3 py-4 text-center">
137
+ No agents yet
138
+ </div>
139
+ )}
140
+ </div>
141
+ </div>
142
+ )
143
+ }
@@ -0,0 +1,205 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+ import { searchMemory } from '@/lib/memory'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { MemoryCard } from './memory-card'
7
+ import { MemoryDetail } from './memory-detail'
8
+ import type { MemoryEntry } from '@/types'
9
+
10
+ export function MemoryBrowser() {
11
+ const selectedMemoryId = useAppStore((s) => s.selectedMemoryId)
12
+ const setSelectedMemoryId = useAppStore((s) => s.setSelectedMemoryId)
13
+ const refreshKey = useAppStore((s) => s.memoryRefreshKey)
14
+ const agents = useAppStore((s) => s.agents)
15
+ const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
16
+
17
+ const [search, setSearch] = useState('')
18
+ const [entries, setEntries] = useState<MemoryEntry[]>([])
19
+ const [loaded, setLoaded] = useState(false)
20
+ const [error, setError] = useState<string | null>(null)
21
+ const [categoryFilter, setCategoryFilter] = useState<string>('')
22
+ const searchRef = useRef(search)
23
+
24
+ // Derive the API agentId from the filter
25
+ const apiAgentId = useMemo(() => {
26
+ if (!memoryAgentFilter) return undefined // all
27
+ if (memoryAgentFilter === '_global') return undefined // we'll filter client-side
28
+ return memoryAgentFilter
29
+ }, [memoryAgentFilter])
30
+
31
+ const load = useCallback(async (query: string) => {
32
+ try {
33
+ const results = await searchMemory({ q: query || undefined, agentId: apiAgentId })
34
+ setEntries(Array.isArray(results) ? results : [])
35
+ setError(null)
36
+ } catch {
37
+ setError('Unable to load memories right now.')
38
+ }
39
+ setLoaded(true)
40
+ }, [apiAgentId])
41
+
42
+ useEffect(() => {
43
+ searchRef.current = search
44
+ }, [search])
45
+
46
+ useEffect(() => {
47
+ const timer = setTimeout(() => { void load(searchRef.current) }, 0)
48
+ return () => clearTimeout(timer)
49
+ }, [refreshKey, load])
50
+
51
+ useEffect(() => {
52
+ const timer = setTimeout(() => { void load(search) }, 300)
53
+ return () => clearTimeout(timer)
54
+ }, [search, load])
55
+
56
+ // Reset selection when filter changes
57
+ useEffect(() => {
58
+ setSelectedMemoryId(null)
59
+ setCategoryFilter('')
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, [memoryAgentFilter])
62
+
63
+ const uniqueCategories = useMemo(() => {
64
+ const cats = new Set<string>()
65
+ for (const e of entries) cats.add(e.category || 'note')
66
+ return Array.from(cats).sort()
67
+ }, [entries])
68
+
69
+ const filtered = useMemo(() => {
70
+ return entries.filter((e) => {
71
+ // Client-side global filter
72
+ if (memoryAgentFilter === '_global' && e.agentId) return false
73
+ if (categoryFilter && (e.category || 'note') !== categoryFilter) return false
74
+ return true
75
+ })
76
+ }, [entries, memoryAgentFilter, categoryFilter])
77
+
78
+ const filterLabel = useMemo(() => {
79
+ if (!memoryAgentFilter) return 'All Memories'
80
+ if (memoryAgentFilter === '_global') return 'Global Memories'
81
+ return agents[memoryAgentFilter]?.name || 'Agent'
82
+ }, [memoryAgentFilter, agents])
83
+
84
+ // No agent selected prompt
85
+ if (!memoryAgentFilter && !loaded && entries.length === 0) {
86
+ return (
87
+ <div className="flex-1 flex items-center justify-center px-8">
88
+ <div className="text-center max-w-[380px]">
89
+ <div className="w-14 h-14 rounded-[16px] bg-white/[0.03] flex items-center justify-center mb-4 mx-auto">
90
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/60">
91
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
92
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
93
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
94
+ </svg>
95
+ </div>
96
+ <h2 className="font-display text-[15px] font-600 text-text-2 mb-2">Browse Memories</h2>
97
+ <p className="text-[13px] text-text-3/70">Select an agent from the sidebar to browse their memories, or view all.</p>
98
+ </div>
99
+ </div>
100
+ )
101
+ }
102
+
103
+ return (
104
+ <div className="flex-1 flex h-full min-w-0">
105
+ {/* Left: Memory card list */}
106
+ <div className="w-[360px] shrink-0 border-r border-white/[0.06] flex flex-col overflow-hidden">
107
+ {/* Header + search */}
108
+ <div className="px-3 pt-3 pb-1 shrink-0">
109
+ <div className="flex items-center gap-2 mb-2">
110
+ <h3 className="font-display text-[13px] font-600 text-text-2 tracking-[-0.01em] flex-1 truncate">{filterLabel}</h3>
111
+ <span className="text-[10px] font-mono tabular-nums text-text-3/50">{filtered.length}</span>
112
+ </div>
113
+ <input
114
+ type="text"
115
+ value={search}
116
+ onChange={(e) => setSearch(e.target.value)}
117
+ placeholder="Search memories..."
118
+ className="w-full px-4 py-2.5 rounded-[12px] border border-white/[0.04] bg-surface text-text
119
+ text-[13px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus-glow"
120
+ style={{ fontFamily: 'inherit' }}
121
+ />
122
+ </div>
123
+
124
+ {/* Category chips */}
125
+ {entries.length > 0 && uniqueCategories.length > 1 && (
126
+ <div className="px-3 py-1.5 shrink-0">
127
+ <div className="flex gap-1 flex-wrap">
128
+ <button
129
+ onClick={() => setCategoryFilter('')}
130
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border-none
131
+ ${!categoryFilter ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
132
+ style={{ fontFamily: 'inherit' }}
133
+ >
134
+ all
135
+ </button>
136
+ {uniqueCategories.map((c) => (
137
+ <button
138
+ key={c}
139
+ onClick={() => setCategoryFilter(categoryFilter === c ? '' : c)}
140
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border-none
141
+ ${categoryFilter === c ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
142
+ style={{ fontFamily: 'inherit' }}
143
+ >
144
+ {c}
145
+ </button>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ )}
150
+
151
+ {/* Cards */}
152
+ <div className="flex-1 overflow-y-auto">
153
+ {filtered.length > 0 ? (
154
+ <div className="flex flex-col gap-0.5 px-2 pb-4">
155
+ {filtered.map((e) => {
156
+ // Show agent info on cards when in "All Memories" view
157
+ const showAgent = !memoryAgentFilter
158
+ const agent = showAgent && e.agentId ? agents[e.agentId] : null
159
+ return (
160
+ <MemoryCard
161
+ key={e.id}
162
+ entry={e}
163
+ active={e.id === selectedMemoryId}
164
+ agentName={showAgent ? (agent?.name || null) : undefined}
165
+ agentAvatarSeed={showAgent ? (agent?.avatarSeed || null) : undefined}
166
+ onClick={() => setSelectedMemoryId(e.id)}
167
+ />
168
+ )
169
+ })}
170
+ </div>
171
+ ) : error ? (
172
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
173
+ <p className="font-display text-[14px] font-600 text-text-2">Couldn&apos;t load memories</p>
174
+ <p className="text-[12px] text-text-3/60">{error}</p>
175
+ <button
176
+ onClick={() => { void load(search) }}
177
+ className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer border-none"
178
+ style={{ fontFamily: 'inherit' }}
179
+ >
180
+ Retry
181
+ </button>
182
+ </div>
183
+ ) : loaded ? (
184
+ <div className="flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
185
+ <div className="w-10 h-10 rounded-[12px] bg-accent-soft flex items-center justify-center">
186
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
187
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
188
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
189
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
190
+ </svg>
191
+ </div>
192
+ <p className="font-display text-[14px] font-600 text-text-2">No memories yet</p>
193
+ <p className="text-[12px] text-text-3/50">Agents store knowledge here as they learn</p>
194
+ </div>
195
+ ) : null}
196
+ </div>
197
+ </div>
198
+
199
+ {/* Right: Detail */}
200
+ <div className="flex-1 flex flex-col min-w-0">
201
+ <MemoryDetail />
202
+ </div>
203
+ </div>
204
+ )
205
+ }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { MemoryEntry } from '@/types'
4
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
4
5
 
5
6
  function timeAgo(ts: number): string {
6
7
  if (!ts) return ''
@@ -15,10 +16,11 @@ interface Props {
15
16
  entry: MemoryEntry
16
17
  active?: boolean
17
18
  agentName?: string | null
19
+ agentAvatarSeed?: string | null
18
20
  onClick: () => void
19
21
  }
20
22
 
21
- export function MemoryCard({ entry, active, agentName, onClick }: Props) {
23
+ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, onClick }: Props) {
22
24
  return (
23
25
  <div
24
26
  onClick={onClick}
@@ -35,14 +37,33 @@ export function MemoryCard({ entry, active, agentName, onClick }: Props) {
35
37
  <span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
36
38
  {entry.category || 'note'}
37
39
  </span>
40
+ {entry.pinned && (
41
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" className="shrink-0 text-amber-400/80">
42
+ <path d="M16 2l-4 4-4-4-2 2 4 4-5 5v1h1l5-5 4 4 2-2-4-4 4-4z" transform="rotate(45 12 12)" />
43
+ </svg>
44
+ )}
38
45
  <span className="font-display text-[13px] font-600 truncate flex-1 tracking-[-0.01em]">{entry.title}</span>
39
46
  <span className="text-[10px] text-text-3/60 shrink-0 tabular-nums font-mono">
40
47
  {timeAgo(entry.updatedAt || entry.createdAt)}
41
48
  </span>
42
49
  </div>
43
- <div className="text-[12px] text-text-2/40 mt-1 truncate leading-relaxed">
50
+ <div className="text-[12px] text-text-2/40 mt-1 line-clamp-3 leading-relaxed">
44
51
  {entry.content || '(empty)'}
45
52
  </div>
53
+ {(entry.image?.path || entry.imagePath) && (
54
+ <div className="mt-2 w-10 h-10 rounded-[6px] overflow-hidden bg-white/[0.04] shrink-0">
55
+ {/* eslint-disable-next-line @next/next/no-img-element */}
56
+ <img
57
+ src={
58
+ (entry.image?.path || entry.imagePath || '').startsWith('data/memory-images/')
59
+ ? `/api/memory-images/${(entry.image?.path || entry.imagePath || '').split('/').pop()}`
60
+ : (entry.image?.path || entry.imagePath || '')
61
+ }
62
+ alt=""
63
+ className="w-full h-full object-cover"
64
+ />
65
+ </div>
66
+ )}
46
67
  {(entry.references?.length || entry.linkedMemoryIds?.length || entry.image?.path || entry.imagePath) && (
47
68
  <div className="flex items-center gap-2 mt-1.5 text-[10px] text-text-3/35">
48
69
  {entry.references?.length ? <span>{entry.references.length} ref{entry.references.length === 1 ? '' : 's'}</span> : null}
@@ -50,14 +71,20 @@ export function MemoryCard({ entry, active, agentName, onClick }: Props) {
50
71
  {(entry.image?.path || entry.imagePath) ? <span>image</span> : null}
51
72
  </div>
52
73
  )}
53
- {agentName && (
74
+ {agentName ? (
75
+ <div className="flex items-center gap-1.5 mt-1.5">
76
+ <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={16} />
77
+ <span className="text-[10px] text-text-3/60 truncate">{agentName}</span>
78
+ </div>
79
+ ) : !entry.agentId ? (
54
80
  <div className="flex items-center gap-1 mt-1.5">
55
- <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
56
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
81
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
82
+ <circle cx="12" cy="12" r="10" /><line x1="2" y1="12" x2="22" y2="12" />
83
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
57
84
  </svg>
58
- <span className="text-[10px] text-text-3/60 truncate">{agentName}</span>
85
+ <span className="text-[10px] text-text-3/50">Global</span>
59
86
  </div>
60
- )}
87
+ ) : null}
61
88
  </div>
62
89
  )
63
90
  }