@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
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { getMemory, updateMemory, deleteMemory } from '@/lib/memory'
6
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
6
7
  import type { MemoryEntry } from '@/types'
7
8
 
8
9
  const CATEGORIES = ['note', 'fact', 'preference', 'finding', 'learning', 'general']
@@ -17,17 +18,23 @@ export function MemoryDetail() {
17
18
  const setActiveView = useAppStore((s) => s.setActiveView)
18
19
 
19
20
  const [entry, setEntry] = useState<MemoryEntry | null>(null)
21
+ const [editing, setEditing] = useState(false)
20
22
  const [title, setTitle] = useState('')
21
23
  const [content, setContent] = useState('')
22
24
  const [category, setCategory] = useState('note')
23
- const [dirty, setDirty] = useState(false)
25
+ const [editAgentId, setEditAgentId] = useState<string | null>(null)
26
+ const [editSharedWith, setEditSharedWith] = useState<string[]>([])
24
27
  const [saving, setSaving] = useState(false)
25
28
  const [confirmDelete, setConfirmDelete] = useState(false)
29
+ const [linkedTitles, setLinkedTitles] = useState<Record<string, string>>({})
30
+ const [refsExpanded, setRefsExpanded] = useState(false)
31
+ const [metaExpanded, setMetaExpanded] = useState(false)
26
32
 
27
33
  // Load memory entry when selection changes
28
34
  useEffect(() => {
29
35
  if (!selectedId) {
30
36
  setEntry(null)
37
+ setEditing(false)
31
38
  return
32
39
  }
33
40
 
@@ -46,7 +53,11 @@ export function MemoryDetail() {
46
53
  setTitle(resolved.title)
47
54
  setContent(resolved.content)
48
55
  setCategory(resolved.category || 'note')
49
- setDirty(false)
56
+ setEditAgentId(resolved.agentId || null)
57
+ setEditSharedWith(resolved.sharedWith || [])
58
+ setEditing(false)
59
+ setRefsExpanded(false)
60
+ setMetaExpanded(false)
50
61
  })
51
62
  .catch((err) => console.error('Memory operation failed:', err))
52
63
 
@@ -55,29 +66,69 @@ export function MemoryDetail() {
55
66
  }
56
67
  }, [selectedId])
57
68
 
69
+ // Resolve linked memory titles
70
+ useEffect(() => {
71
+ if (!entry?.linkedMemoryIds?.length) {
72
+ setLinkedTitles({})
73
+ return
74
+ }
75
+ let cancelled = false
76
+ Promise.all(
77
+ entry.linkedMemoryIds.map((id) =>
78
+ getMemory(id, { depth: 0 }).then((m) => {
79
+ const resolved = Array.isArray(m) ? m[0] : m
80
+ return [id, resolved?.title || id] as const
81
+ }).catch(() => [id, id] as const),
82
+ ),
83
+ ).then((pairs) => {
84
+ if (cancelled) return
85
+ setLinkedTitles(Object.fromEntries(pairs))
86
+ })
87
+ return () => { cancelled = true }
88
+ }, [entry?.linkedMemoryIds])
89
+
58
90
  const handleSave = useCallback(async () => {
59
- if (!entry || !dirty) return
91
+ if (!entry) return
60
92
  setSaving(true)
61
93
  try {
62
- const updated = await updateMemory(entry.id, { title, content, category })
94
+ const updated = await updateMemory(entry.id, {
95
+ title,
96
+ content,
97
+ category,
98
+ agentId: editAgentId,
99
+ sharedWith: editSharedWith.length ? editSharedWith : undefined,
100
+ })
63
101
  setEntry(updated)
64
- setDirty(false)
102
+ setEditing(false)
65
103
  triggerRefresh()
66
104
  } catch { /* ignore */ }
67
105
  setSaving(false)
68
- }, [entry, title, content, category, dirty])
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [entry, title, content, category, editAgentId, editSharedWith])
69
108
 
70
109
  const handleDelete = useCallback(async () => {
71
110
  if (!entry) return
72
111
  await deleteMemory(entry.id)
73
112
  setSelectedId(null)
74
113
  triggerRefresh()
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [entry])
116
+
117
+ const handleTogglePin = useCallback(async () => {
118
+ if (!entry) return
119
+ try {
120
+ const updated = await updateMemory(entry.id, { pinned: !entry.pinned })
121
+ setEntry(updated)
122
+ triggerRefresh()
123
+ } catch { /* ignore */ }
124
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
125
  }, [entry])
76
126
 
77
127
  const handleNavigateToSession = useCallback(() => {
78
128
  if (!entry?.sessionId) return
79
129
  setActiveView('agents')
80
130
  setCurrentSession(entry.sessionId)
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
132
  }, [entry])
82
133
 
83
134
  if (!entry) {
@@ -90,9 +141,9 @@ export function MemoryDetail() {
90
141
  <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
91
142
  </svg>
92
143
  </div>
93
- <p className="font-display text-[17px] font-600 text-text-2">Memory</p>
144
+ <p className="font-display text-[17px] font-600 text-text-2">Select a Memory</p>
94
145
  <p className="text-[13px] text-text-3/70 max-w-[300px]">
95
- Select a memory from the sidebar to view and edit it
146
+ Choose a memory from the list to view its details
96
147
  </p>
97
148
  </div>
98
149
  )
@@ -108,6 +159,8 @@ export function MemoryDetail() {
108
159
  : null
109
160
 
110
161
  const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] text-text outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-accent-bright/20 focus:bg-white/[0.03]"
162
+ const refs = entry.references || []
163
+ const showRefsCollapse = refs.length > 3
111
164
 
112
165
  return (
113
166
  <div className="flex-1 flex flex-col h-full min-h-0">
@@ -116,9 +169,11 @@ export function MemoryDetail() {
116
169
  <div className="flex-1 min-w-0">
117
170
  <div className="flex items-center gap-2.5">
118
171
  <span className="shrink-0 text-[10px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-2 py-0.5 rounded-[6px]">
119
- {category}
172
+ {entry.category || 'note'}
120
173
  </span>
121
- <h2 className="font-display text-[16px] font-700 truncate tracking-[-0.02em]">{title || 'Untitled'}</h2>
174
+ {!editing && (
175
+ <h2 className="font-display text-[16px] font-700 truncate tracking-[-0.02em]">{entry.title || 'Untitled'}</h2>
176
+ )}
122
177
  </div>
123
178
  <div className="flex items-center gap-3 mt-1">
124
179
  {agentName && (
@@ -143,16 +198,55 @@ export function MemoryDetail() {
143
198
  </div>
144
199
 
145
200
  <div className="flex items-center gap-2 shrink-0">
146
- {dirty && (
201
+ {/* Pin/unpin toggle */}
202
+ <button
203
+ onClick={handleTogglePin}
204
+ className={`p-2 rounded-[8px] cursor-pointer transition-all bg-transparent border-none
205
+ ${entry.pinned ? 'text-amber-400 hover:text-amber-300' : 'text-text-3/40 hover:text-amber-400/70'}`}
206
+ title={entry.pinned ? 'Unpin memory' : 'Pin memory (always preloaded)'}
207
+ >
208
+ <svg width="15" height="15" viewBox="0 0 24 24" fill={entry.pinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
209
+ <path d="M12 17v5" /><path d="M9 2h6l-1.5 6H16l1 4H7l1-4h1.5z" />
210
+ </svg>
211
+ </button>
212
+ {editing ? (
213
+ <>
214
+ <button
215
+ onClick={() => {
216
+ setTitle(entry.title)
217
+ setContent(entry.content)
218
+ setCategory(entry.category || 'note')
219
+ setEditAgentId(entry.agentId || null)
220
+ setEditSharedWith(entry.sharedWith || [])
221
+ setEditing(false)
222
+ }}
223
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
224
+ style={{ fontFamily: 'inherit' }}
225
+ >
226
+ Cancel
227
+ </button>
228
+ <button
229
+ onClick={handleSave}
230
+ disabled={saving}
231
+ className="px-4 py-2 rounded-[10px] bg-accent-bright text-white text-[12px] font-600
232
+ cursor-pointer border-none transition-all hover:brightness-110 active:scale-[0.97]
233
+ disabled:opacity-50 shadow-[0_2px_10px_rgba(99,102,241,0.2)]"
234
+ style={{ fontFamily: 'inherit' }}
235
+ >
236
+ {saving ? 'Saving...' : 'Save'}
237
+ </button>
238
+ </>
239
+ ) : (
147
240
  <button
148
- onClick={handleSave}
149
- disabled={saving}
150
- className="px-4 py-2 rounded-[10px] bg-[#6366F1] text-white text-[12px] font-600
151
- cursor-pointer border-none transition-all hover:brightness-110 active:scale-[0.97]
152
- disabled:opacity-50 shadow-[0_2px_10px_rgba(99,102,241,0.2)]"
241
+ onClick={() => setEditing(true)}
242
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all flex items-center gap-1.5"
153
243
  style={{ fontFamily: 'inherit' }}
154
244
  >
155
- {saving ? 'Saving...' : 'Save'}
245
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
246
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
247
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
248
+ </svg>
249
+ Edit
156
250
  </button>
157
251
  )}
158
252
  <button
@@ -169,138 +263,283 @@ export function MemoryDetail() {
169
263
  </div>
170
264
  </div>
171
265
 
172
- {/* Edit form */}
266
+ {/* Content area */}
173
267
  <div className="flex-1 overflow-y-auto px-6 py-5">
174
- <div className="max-w-[640px] space-y-5">
175
- {/* Title */}
176
- <div>
177
- <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Title</label>
178
- <input
179
- type="text"
180
- value={title}
181
- onChange={(e) => { setTitle(e.target.value); setDirty(true) }}
182
- className={`${inputClass} text-[15px] font-600`}
183
- style={{ fontFamily: 'inherit' }}
184
- placeholder="Memory title"
185
- />
186
- </div>
268
+ <div className="max-w-[720px] space-y-5">
269
+ {editing ? (
270
+ <>
271
+ {/* Title input */}
272
+ <div>
273
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Title</label>
274
+ <input
275
+ type="text"
276
+ value={title}
277
+ onChange={(e) => setTitle(e.target.value)}
278
+ className={`${inputClass} text-[15px] font-600`}
279
+ style={{ fontFamily: 'inherit' }}
280
+ placeholder="Memory title"
281
+ />
282
+ </div>
283
+
284
+ {/* Category picker */}
285
+ <div>
286
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Category</label>
287
+ <div className="flex gap-1.5 flex-wrap">
288
+ {CATEGORIES.map((c) => (
289
+ <button
290
+ key={c}
291
+ onClick={() => setCategory(c)}
292
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all border-none
293
+ ${category === c
294
+ ? 'bg-accent-soft text-accent-bright'
295
+ : 'bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
296
+ style={{ fontFamily: 'inherit' }}
297
+ >
298
+ {c}
299
+ </button>
300
+ ))}
301
+ </div>
302
+ </div>
187
303
 
188
- {/* Category */}
189
- <div>
190
- <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Category</label>
191
- <div className="flex gap-1.5 flex-wrap">
192
- {CATEGORIES.map((c) => (
193
- <button
194
- key={c}
195
- onClick={() => { setCategory(c); setDirty(true) }}
196
- className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all border-none
197
- ${category === c
198
- ? 'bg-accent-soft text-accent-bright'
199
- : 'bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
304
+ {/* Agent assignment */}
305
+ <div>
306
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Assigned to</label>
307
+ <div className="flex gap-1.5 flex-wrap">
308
+ <button
309
+ onClick={() => setEditAgentId(null)}
310
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
311
+ ${!editAgentId
312
+ ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
313
+ : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
314
+ style={{ fontFamily: 'inherit' }}
315
+ >
316
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={!editAgentId ? 'text-accent-bright' : 'text-text-3/60'}>
317
+ <circle cx="12" cy="12" r="10" /><line x1="2" y1="12" x2="22" y2="12" />
318
+ <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" />
319
+ </svg>
320
+ Global
321
+ </button>
322
+ {Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((agent) => (
323
+ <button
324
+ key={agent.id}
325
+ onClick={() => setEditAgentId(agent.id)}
326
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
327
+ ${editAgentId === agent.id
328
+ ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
329
+ : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
330
+ style={{ fontFamily: 'inherit' }}
331
+ >
332
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={16} />
333
+ <span className="truncate max-w-[100px]">{agent.name}</span>
334
+ </button>
335
+ ))}
336
+ </div>
337
+ </div>
338
+
339
+ {/* Shared with */}
340
+ {editAgentId && (
341
+ <div>
342
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Share with</label>
343
+ <div className="flex gap-1.5 flex-wrap">
344
+ {Object.values(agents)
345
+ .filter((a) => a.id !== editAgentId)
346
+ .sort((a, b) => a.name.localeCompare(b.name))
347
+ .map((agent) => {
348
+ const isShared = editSharedWith.includes(agent.id)
349
+ return (
350
+ <button
351
+ key={agent.id}
352
+ onClick={() => {
353
+ setEditSharedWith(isShared
354
+ ? editSharedWith.filter((id) => id !== agent.id)
355
+ : [...editSharedWith, agent.id])
356
+ }}
357
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
358
+ ${isShared
359
+ ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
360
+ : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
361
+ style={{ fontFamily: 'inherit' }}
362
+ >
363
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={16} />
364
+ <span className="truncate max-w-[100px]">{agent.name}</span>
365
+ </button>
366
+ )
367
+ })}
368
+ </div>
369
+ {editSharedWith.length === 0 && (
370
+ <p className="text-[10px] text-text-3/40 mt-1.5">No agents selected — only the assigned agent can access this memory</p>
371
+ )}
372
+ </div>
373
+ )}
374
+
375
+ {/* Content textarea */}
376
+ <div>
377
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Content</label>
378
+ <textarea
379
+ value={content}
380
+ onChange={(e) => setContent(e.target.value)}
381
+ placeholder="Memory content..."
382
+ rows={12}
383
+ className={`${inputClass} text-[14px] resize-y min-h-[200px] leading-relaxed`}
200
384
  style={{ fontFamily: 'inherit' }}
201
- >
202
- {c}
203
- </button>
204
- ))}
205
- </div>
206
- </div>
385
+ />
386
+ </div>
387
+ </>
388
+ ) : (
389
+ <>
390
+ {/* Read-mode: Title as h1 */}
391
+ <h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text leading-tight">
392
+ {entry.title || 'Untitled'}
393
+ </h1>
207
394
 
208
- {/* Content */}
209
- <div>
210
- <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Content</label>
211
- <textarea
212
- value={content}
213
- onChange={(e) => { setContent(e.target.value); setDirty(true) }}
214
- placeholder="Memory content..."
215
- rows={12}
216
- className={`${inputClass} text-[14px] resize-y min-h-[200px] leading-relaxed`}
217
- style={{ fontFamily: 'inherit' }}
218
- />
219
- </div>
395
+ {/* Read-mode: Content as readable prose */}
396
+ <div className="text-[15px] leading-[1.7] text-text-2 whitespace-pre-wrap break-words">
397
+ {entry.content || '(empty)'}
398
+ </div>
399
+
400
+ {/* Shared with (read mode) */}
401
+ {entry.sharedWith && entry.sharedWith.length > 0 && (
402
+ <div>
403
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Shared with</label>
404
+ <div className="flex gap-1.5 flex-wrap">
405
+ {entry.sharedWith.map((aid) => {
406
+ const a = agents[aid]
407
+ return (
408
+ <span key={aid} className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.03] text-[11px] text-text-3">
409
+ <AgentAvatar seed={a?.avatarSeed || null} name={a?.name || aid} size={16} />
410
+ {a?.name || aid}
411
+ </span>
412
+ )
413
+ })}
414
+ </div>
415
+ </div>
416
+ )}
417
+ </>
418
+ )}
220
419
 
420
+ {/* Image (both modes) */}
221
421
  {imageUrl && (
222
422
  <div>
223
- <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Image</label>
423
+ {editing && <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Image</label>}
224
424
  <a href={imageUrl} target="_blank" rel="noreferrer" className="inline-block rounded-[12px] overflow-hidden border border-white/[0.08]">
225
- <img src={imageUrl} alt={entry.title} className="max-w-[320px] max-h-[220px] object-cover block" />
425
+ <img src={imageUrl} alt={entry.title} className="max-w-[600px] w-full max-h-[400px] object-cover block" />
226
426
  </a>
227
427
  </div>
228
428
  )}
229
429
 
230
- {entry.references?.length ? (
231
- <div>
232
- <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">References</label>
233
- <div className="space-y-2">
234
- {entry.references.map((ref, idx) => (
235
- <div key={`${ref.type}-${ref.path || ref.title || idx}`} className="text-[12px] rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
236
- <div className="text-text-2/70">
237
- <span className="uppercase text-[10px] tracking-[0.06em] mr-1">{ref.type}</span>
238
- {ref.path || ref.title || '(no path)'}
239
- </div>
240
- {(ref.projectName || ref.projectRoot || ref.note || typeof ref.exists === 'boolean') && (
241
- <div className="text-text-3/55 mt-1">
242
- {ref.projectName ? `project: ${ref.projectName} ` : ''}
243
- {ref.projectRoot ? `root: ${ref.projectRoot} ` : ''}
244
- {typeof ref.exists === 'boolean' ? (ref.exists ? 'exists' : 'missing') : ''}
245
- {ref.note ? ` — ${ref.note}` : ''}
246
- </div>
247
- )}
248
- </div>
249
- ))}
250
- </div>
251
- </div>
252
- ) : null}
253
-
430
+ {/* Linked Memories */}
254
431
  {entry.linkedMemoryIds?.length ? (
255
432
  <div>
256
433
  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Linked Memories</label>
257
- <div className="flex flex-wrap gap-1.5">
434
+ <div className="flex flex-col gap-1.5">
258
435
  {entry.linkedMemoryIds.map((id) => (
259
436
  <button
260
437
  key={id}
261
438
  onClick={() => setSelectedId(id)}
262
- className="px-2.5 py-1 rounded-[8px] text-[11px] font-mono bg-white/[0.04] border border-white/[0.08] text-accent-bright/70 hover:text-accent-bright cursor-pointer transition-colors"
439
+ className="flex items-center gap-2.5 px-3 py-2 rounded-[10px] bg-white/[0.02] border border-white/[0.06] hover:bg-white/[0.04] cursor-pointer transition-colors text-left w-full"
263
440
  >
264
- {id}
441
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright/60 shrink-0">
442
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
443
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
444
+ </svg>
445
+ <span className="text-[13px] text-text-2 truncate">
446
+ {linkedTitles[id] || id}
447
+ </span>
265
448
  </button>
266
449
  ))}
267
450
  </div>
268
451
  </div>
269
452
  ) : null}
270
453
 
271
- {/* Metadata */}
272
- <div className="pt-4 border-t border-white/[0.04]">
273
- <div className="grid grid-cols-2 gap-4 text-[11px]">
274
- <div>
275
- <span className="text-text-3/70 block mb-1">ID</span>
276
- <span className="text-text-3/60 font-mono">{entry.id}</span>
277
- </div>
278
- <div>
279
- <span className="text-text-3/70 block mb-1">Created</span>
280
- <span className="text-text-3/60 font-mono">{new Date(entry.createdAt).toLocaleString()}</span>
281
- </div>
282
- <div>
283
- <span className="text-text-3/70 block mb-1">Updated</span>
284
- <span className="text-text-3/60 font-mono">{new Date(entry.updatedAt).toLocaleString()}</span>
285
- </div>
286
- {entry.agentId && (
287
- <div>
288
- <span className="text-text-3/70 block mb-1">Agent</span>
289
- <span className="text-text-3/60 font-mono">{agentName}</span>
290
- </div>
291
- )}
292
- {entry.sessionId && (
293
- <div>
294
- <span className="text-text-3/70 block mb-1">Session</span>
295
- <button
296
- onClick={handleNavigateToSession}
297
- className="text-accent-bright/60 hover:text-accent-bright font-mono bg-transparent border-none cursor-pointer p-0 text-[11px] transition-colors"
298
- >
299
- {sessionName}
300
- </button>
454
+ {/* References (collapsible) */}
455
+ {refs.length > 0 && (
456
+ <div>
457
+ <button
458
+ onClick={() => setRefsExpanded(!refsExpanded)}
459
+ className="flex items-center gap-1.5 text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2 bg-transparent border-none cursor-pointer p-0 hover:text-text-3 transition-colors"
460
+ style={{ fontFamily: 'inherit' }}
461
+ >
462
+ <svg
463
+ width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
464
+ className={`transition-transform ${refsExpanded || !showRefsCollapse ? 'rotate-90' : ''}`}
465
+ >
466
+ <polyline points="9 18 15 12 9 6" />
467
+ </svg>
468
+ References ({refs.length})
469
+ </button>
470
+ {(refsExpanded || !showRefsCollapse) && (
471
+ <div className="space-y-2">
472
+ {refs.map((ref, idx) => (
473
+ <div key={`${ref.type}-${ref.path || ref.title || idx}`} className="text-[12px] rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
474
+ <div className="text-text-2/70">
475
+ <span className="uppercase text-[10px] tracking-[0.06em] mr-1">{ref.type}</span>
476
+ {ref.path || ref.title || '(no path)'}
477
+ </div>
478
+ {(ref.projectName || ref.projectRoot || ref.note || typeof ref.exists === 'boolean') && (
479
+ <div className="text-text-3/55 mt-1">
480
+ {ref.projectName ? `project: ${ref.projectName} ` : ''}
481
+ {ref.projectRoot ? `root: ${ref.projectRoot} ` : ''}
482
+ {typeof ref.exists === 'boolean' ? (ref.exists ? 'exists' : 'missing') : ''}
483
+ {ref.note ? ` — ${ref.note}` : ''}
484
+ </div>
485
+ )}
486
+ </div>
487
+ ))}
301
488
  </div>
302
489
  )}
303
490
  </div>
491
+ )}
492
+
493
+ {/* Metadata (disclosure) */}
494
+ <div className="pt-2">
495
+ <button
496
+ onClick={() => setMetaExpanded(!metaExpanded)}
497
+ className="flex items-center gap-1.5 text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] bg-transparent border-none cursor-pointer p-0 hover:text-text-3 transition-colors"
498
+ style={{ fontFamily: 'inherit' }}
499
+ >
500
+ <svg
501
+ width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
502
+ className={`transition-transform ${metaExpanded ? 'rotate-90' : ''}`}
503
+ >
504
+ <polyline points="9 18 15 12 9 6" />
505
+ </svg>
506
+ Details
507
+ </button>
508
+ {metaExpanded && (
509
+ <div className="mt-3 pt-3 border-t border-white/[0.04]">
510
+ <div className="grid grid-cols-2 gap-4 text-[11px]">
511
+ <div>
512
+ <span className="text-text-3/70 block mb-1">ID</span>
513
+ <span className="text-text-3/60 font-mono">{entry.id}</span>
514
+ </div>
515
+ <div>
516
+ <span className="text-text-3/70 block mb-1">Created</span>
517
+ <span className="text-text-3/60 font-mono">{new Date(entry.createdAt).toLocaleString()}</span>
518
+ </div>
519
+ <div>
520
+ <span className="text-text-3/70 block mb-1">Updated</span>
521
+ <span className="text-text-3/60 font-mono">{new Date(entry.updatedAt).toLocaleString()}</span>
522
+ </div>
523
+ {entry.agentId && (
524
+ <div>
525
+ <span className="text-text-3/70 block mb-1">Agent</span>
526
+ <span className="text-text-3/60 font-mono">{agentName}</span>
527
+ </div>
528
+ )}
529
+ {entry.sessionId && (
530
+ <div>
531
+ <span className="text-text-3/70 block mb-1">Chat</span>
532
+ <button
533
+ onClick={handleNavigateToSession}
534
+ className="text-accent-bright/60 hover:text-accent-bright font-mono bg-transparent border-none cursor-pointer p-0 text-[11px] transition-colors"
535
+ >
536
+ {sessionName}
537
+ </button>
538
+ </div>
539
+ )}
540
+ </div>
541
+ </div>
542
+ )}
304
543
  </div>
305
544
  </div>
306
545
  </div>